Windowsアプリケーションにおけるコマンドライン引数の扱い [Win]
Windowsアプリケーションにおいてコマンドライン引数はどのように渡されるのか。非Unicodeアプリケーションで注意すべき点など。
コマンドライン引数の仕様
コマンドライン引数のパラメータは基本的にはスペース(空白)で区切るが、二重引用符で括った個所は一つのパラメータとして扱うことになっている。これについて厳密には次のような規則が定められている。(Cランゲージリファレンス「Microsoft固有の仕様」、マイクロソフト株式会社、1996年 より)
- 引数は、空白文字(スペースまたはタブ)で区切ります。
- ダブルクォーテーションマークで囲まれている文字列は、空白文字が含まれていても、1 つの引数と解釈されます。この文字列は、引数に埋め込むこともできます。カレット(^)はエスケープ文字にもデリミタにも認識されません。
- ダブルクォーテーションマークの前に円記号を付ける(\“)と、ダブルクォーテーション(”)そのものとして解釈されます。
- ダブルクォーテーションマークの直前にある円記号以外は、円記号として解釈されます。
- 偶数個の円記号の後ろにダブルクォーテーションマークを付けると、それぞれの円記号のペア(\)は 1つの円記号()と解釈されてargvに渡されます。ダブルクォーテーションマーク(“)は文字列のデリミタと解釈されます。
- 奇数個の円記号の後ろにダブルクォーテーションマークを付けると、それぞれの円記号のペア(\)は 1つの円記号()と解釈されてargvに渡されます。また、残りの円記号とダブルクォーテーションはエスケープシーケンスと解釈されて、ダブルクォーテーションマーク(“)そのものがargvに渡されます。
MS-Cの標準Cライブラリにはコマンドライン引数をパースする処理のソースコードが収録されていて、そのコメントには次のような例が記されている。(Microsoft Visual C++ 1.5のSTDARGV.ASMより)
[""] ==> []
[\"] ==> ["]
[" \" "] == [ " ]
[" \\"] == [ \]
[" \\ "] == [ \\ ]
[" \\\" "] == [ \" ]
etc.
["one two three"] ==> [one two three]
[one" two "three] ==> [one two three]
[o"ne two t"hree] ==> [one two three]
["one \"two\" three"] ==> [one "two" three]
["x\\\"x"] ==> [x\"x]
ここで、バックスラッシュ(半角)と円記号(半角)は文字コードが同じなので意味も同じ。
アプリケーション側で引数を受け取る仕組みについて
Windowsアプリケーションはコマンドライン引数を次のいずれかの方法で受け取っている。
- Win32 APIのGetCommandLine関数 - Windowsアプリケーションの標準的な方法
- main関数の引数 argc, argvやグローバル変数__argc, __argv - コンパイラに付属するライブラリを使った方法
- WinMain関数の引数 lpzCmdLine変数 - 一部の古いWindowsアプリケーションで利用されていた
2. main関数の引数 argc, argvやグローバル変数__argc, __argv
例えばVisual C++でコンソールアプリケーションを作るとなると、次のようなサンプルコードから始まる。
int _tmain(int argc, TCHAR* argv[]);
argc変数には引数の数、argv変数には引数の文字列がパラメータごとに配列に振り分けられている。これは実際には、main関数を実行する前にVisual C++付属の標準Cライブラリで行われる前処理によって用意されたもので、Windows標準機能でargcやargvが用意されているわけではない。.net Frameworkアプリケーションも同様と思われる。
3. WinMain関数の引数 lpCmdLine変数
WindowsアプリケーションのエントリポイントWinMain関数の3番目の引数にコマンドライン文字列が格納されることを利用したもので、Windows 3.1以前のアプリケーションで利用されることがあった。windows.hで次のように定義している。
int PASCAL WinMain(HINSTANCE hinstCurrent, HINSTANCE hinstPrevious, LPSTR lpzCmdLine, int nCmdShow);
ここでLPSTR=char FAR*、つまり一つの文字列変数。これをパラメータに分解する機能はWindowsには用意されていないため、アプリケーション側でパース処理を組む必要がある。この方法は現在のWindowsでも利用できるが、Unicodeに対応していないため使うべきではない。lpzCmdLineはGetCommandLineやargvで得られるものとは異なり、実行パスは含まず引数のみが渡される。
1. Win32 APIのGetCommandLine関数
Windows NT3.1, Windows 95以降で利用できるWin32 APIの関数。windows.hで次のように定義している。
LPTSTR WINAPI GetCommandLine(void);//ANSIまたはUnicode対応(ビルド時のプリプロセッサ指定で決定)
LPSTR WINAPI GetCommandLineA(void);//ANSI(日本語環境なら俗にいうShift-JIS)
LPWSTR WINAPI GetCommandLineW(void);//Unicode(UTF-16)
こちらもコマンドライン引数は一つの文字列変数として渡される。これを正しくパラメータに分解するためにWin32 APIにCommandLineToArgvW関数が用意されている。アプリケーションはこれを用いるか、自前でパース処理を行うことになる。
コマンドライン文字列の扱い
上記のいずれの方法でも、コマンドライン引数はもともとは一つ続きの文字列データとしてWindowsから渡されることがわかる。この時点で受け渡されるコマンドライン文字列は二重引用符やエスケープ文字もそのままで、これをアプリケーション自前のルーチン、ライブラリ、CommandLineToArgvW関数などを使ってパラメータとして分解・解釈する。
実際にどのようにコマンドライン引数がパースされるのか。Cで次のようなプログラムを作り、いくつかの処理系に通して実行してみた。
#include <stdio.h>
int main(int argc, char* argv[])
{
int i;
printf("argc: %d\n", argc);
for(i=0;i<argc;++i) //>
{
printf("argv[%d]: %s\n", i, argv[i]);
}
getchar();
return 0;
}
次はVisual C++ 4.0でビルドしたWin32コンソールアプリケーションの実行例。Cランタイム内ではWin32 APIのGetCommandLine関数でコマンドライン文字列を取得し、自前のルーチンでパース処理しているようである。CommandLineToArgvW関数でも同様のデータが得られる。
C:\workspace>parsecmd arg1 "引数2ソ" "arg 3" "\"arg4\""
argc: 5
argv[0]: parsecmd
argv[1]: arg1
argv[2]: 引数2ソ
argv[3]: arg 3
argv[4]: "arg4"
次はVisual C++ 1.5でビルドしたMS-DOS/Win16アプリケーションの実行例。先と異なり、argv[0]
の実行パスは常に(短いファイル名の)完全パスになる。Cランタイム内ではコマンド引数はDOSのPSP(Program Segment Prefix)、実行パスは環境変数領域のセグメントから取得している。
C:\workspace>parsecmd arg1 "引数2ソ" "arg 3" "\"arg4\""
argc: 5
argv[0]: C:\WORKSP~1\PARSECMD.EXE
argv[1]: arg1
argv[2]: 引数2ソ
argv[3]: arg 3
argv[4]: "arg4"
次はBorland C++ Builder 5.5でビルドしたWin32コンソールアプリケーションの実行例。argv[0]
に格納される実行パスは常に完全パスになる。
C:\workspace>parsecmd arg1 "引数2ソ" "arg 3" "\"arg4\""
argc: 5
argv[0]: C:\workspace\parsecmd.exe
argv[1]: arg1
argv[2]: 引数2ソ
argv[3]: arg 3
argv[4]: "arg4"
非Unicodeアプリケーションで起こりうる問題
主にUnicodeに対応していない古いアプリケーションで、コマンドライン引数を正しく受け取れない例として次のような問題が考えられる。
異なるロケールの文字列が文字化けする
コマンドライン引数をパラメータごとに配列に振り分けるためにWin32 APIにCommandLineToArgvW関数が用意されているが、これはUnicode版しか存在しない。そのため、非UnicodeアプリケーションではGetCommandLineW→CommandLineToArgvWで得られたUnicode文字列の配列をANSI文字列の配列に変換する場合がある。この時、既定ではシステムロケールの文字コードに変換されるため、その文字コードに変換できない文字列は文字化けすることがある。
例:Windows日本語環境においてタイ語の文字を含む引数を指定した場合、その箇所はアプリケーション内で文字化けする。
PARSECMD.EXE เมษายน
arg1: ??????
日本語などマルチバイト文字が含まれていると正しく処理できない
主に1バイト文字のみを扱う西洋言語圏で開発されたアプリケーションで、2バイト文字の2バイト目の0x5c(円記号、バックスラッシュ)をエスケープ文字として誤認識してしまう、いわゆる円記号問題が起きる。
例:この例は問題のアプリケーションでは「・ ラ」(「ソ」の第2バイトが切り詰められて化ける)という一つのパラメータとして認識される。
PARSECMD.EXE "ソ" ラ
arg1: ・ ラ
二重引用符のパース処理が正しくない
コマンドライン引数のパースをアプリケーション独自で行っている場合で、先述の二重引用符の扱いをしっかり考慮していないことにより不具合が起きる。
例:lpCmdLineの文字列を単純にスペース区切りでパースしている場合、二重引用符が呼び出し側の意図と異なって解釈される。
PARSECMD.EXE "引数 1"
lpCmdLine: "引数 1"
arg1: "引数
arg2: 1"
特にWindows 3.1以前のアプリケーションでは二重引用符が考慮されていないことが多い。というのも、MS-DOSのファイルシステムではパスにスペースを含めることが許されていないため、二重引用符の必要性が薄かった。LFNをサポートするWindows 95以降ではパスにスペースを含むことができるが、MS-DOSとの互換性のためか短いファイル名では短縮形で表すようになっている。(ただし全角スペースは短縮形にならない。)
C:\workspace\ソ ラ>PARSECMD.EXE
argc: 1
argv[0]: C:\WORKSP~1\ソラ~1\PARSECMD.EXE
C:\workspace\ソ ラ>PARSECMD.EXE
argc: 1
argv[0]: C:\WORKSP~1\ソ ラ\PARSECMD.EXE
コマンドラインで渡された長いファイル名はMS-DOS/Win16アプリケーションで開けない
MS-DOS/Win16アプリケーションに対して関連付け設定したファイルを開いたりエクスプローラー上でファイルをドラッグアンドドロップ入力したりと、ShellExecuteでファイルを開く場合に引数に渡すファイルパスは自動で短いファイル名に変換される。しかし、コマンドラインで渡す引数についてはそのまま渡される。この時、パスに長いファイル名が含まれていると意図したとおりに処理されない。
C:\workspace\ソ ラ>typefile "C:\WORKSP~1\ソ ラ\file.txt"
argc: 2
argv[0]: C:\WORKSP~1\ソ ラ\TYPEFILE.EXE
argv[1]: C:\WORKSP~1\ソ ラ\file.txt
fopen ok.
C:\workspace\ソ ラ>typefile "C:\workspace\ソ ラ\file.txt"
argc: 2
argv[0]: C:\WORKSP~1\ソ ラ\TYPEFILE.EXE
argv[1]: C:\workspace\ソ ラ\file.txt
fopen failed.
コマンドライン引数でファイルパスを扱うのであればこれらのことを念頭に置き、特に自前で引数のパース処理を組むならいろんなテストデータを用意することが必要。ここでは触れていないが、バッチファイルではコマンドプロンプト独自の制限があって、これもまたややこしい問題を引き起こすことがある。