ホーム >プログラム >Delphi 6 ローテクTips

Windows APIメモ CommandLineToArgW

Windows APIのメモ。
CommandLineToArgW。

CommandLineToArgW APIはコマンドライン引数を分割してくれる便利なAPIです。
引数は半角スペースとタブで分割されます。
引数を二重引用符で囲うことで引数に半角スペースを含めることもできます。

function gfniCommandLineToArg(sStr: WideString; AList: TStrings): Integer;
//与えられた引数を半角スペースとタブで分割してAListにセットする。
//AList[0]はプログラムのフルパスになる
//戻り値はParamCountと違いプログラムのフルパスも含めたリストの数。
var
  i: Integer;
  lpp_Arg, lpp_Param: PPWideChar;
begin
  AList.Clear;
  if (sStr <> '') then
  begin
    lpp_Param := CommandLineToArgvW(PWideChar(sStr), Result);
    try
      lpp_Arg := lpp_Param;
      for i := 0 to Result-1 do
      begin
        AList.Add(WideString(lpp_Arg^));
        Inc(lpp_Arg);
      end;
    finally
      LocalFree(Cardinal(lpp_Param));
    end;
  end;
end;

CommandLineToArgW APIは二重引用符で区切られた引数の場合、引数の最後の文字が「\」であるとおかしな動作になってしまうことがあります。
具体的には「\」が消えて「"」が残ります。

"C:\Windows\"C:\Windows"

おそらく「\」や「"」は特殊な扱いになっていて、「\」が次の「"」をエスケープして引数の区切りとしてではなく引数内の文字だということになっているのだと思われます。
その結果二重引用符で囲んだ引数が期待通りに分割されなくなってしまいます。

ただ、最初の分割は期待通りに「\」が最後について分割されます。
それ以降の分割がおかしくなります。
また二重引用符で囲わなければ期待通りに「\」が最後について分割されます。
ちょっとややこしい動作になっています。
そのため引数の中の「\"」をStringReplace関数で全部「\\"」に変換するという手は使えません。

"Documents and Settings\" "Program Files\" "WINDOWS\"

は、

Documents and Settings\
Program Files\
WINDOWS\

とはならず

Documents and Settings\
Program Files" WINDOWS"

となります。

エクスプローラーからフォルダをドロップした場合は終わりの「\」はつかないのでそれほど問題にならないと思いますが、ショートカットファイルの引数を自前で書く場合や半角スペース区切りのデータを分割するのにこのAPIを利用する場合などには気をつける必要があります。

自作関数

上記のような引数の最後の文字が「\」である場合にも対処したければ分割する関数を自作することになると思います。

function gfniStrCmdLineDiv(const sWStr: WideString; AList: TStrings): Integer;
//sWStrを半角スペースもしくはタブで区切ってAListにセットし、引数の数を返す関数。
//返す値はParamCountに合わせてリストの数-1。

const
  lcs_WQUOT = WideString('"');
var
  i: Integer;
  ls_WStr, ls_WChar: WideString;
  lb_Quot, lb_Skip: Boolean;
begin
  AList.Clear;
  lb_Quot := False;
  lb_Skip := False;
  ls_WStr := '';
  for i := 1 to Length(sWStr) do
  begin
    ls_WChar := sWStr[i];
    if (ls_WChar = lcs_WQUOT) then
    begin
      if not(lb_Quot) then
      begin
        //最初にみつかった引用符は区切り
        lb_Quot := True;
        if (ls_WStr <> '') then
        begin
          AList.Add(ls_WStr);
          ls_WStr := '';
        end;
      end else
      if (lb_Skip) then
      begin
        //引用符内引用符
        lb_Skip := False;
        ls_WStr := ls_WStr + ls_WChar;
      end else
      if (i < Length(sWStr)) and (sWStr[i +1] = lcs_WQUOT) then
      begin
        //この引用符のすぐ後にも引用符があるので引用符内引用符であるので引数の取り出しにはならない
        //例) "引用符を含めるには""とする"
        lb_Skip := True;
      end else
      if (lb_Quot) then
      begin
        //引用符の閉じ⇒引数取り出し
        lb_Quot := False;
//        if (ls_WStr <> '') then begin //空文字も返すためコメントアウト。
          AList.Add(ls_WStr);
          ls_WStr := '';
//        end;
      end;
    end else
    if (ls_WChar = WideString(' ')) or (ls_WChar = WideString(#9)) then
    begin
      //引数の区切り
      if (lb_Quot) then
      begin
        //引用符つき引数であるので引数の区切りではない⇒引数の文字列に加える
        ls_WStr := ls_WStr + ls_WChar;
      end else 
      begin
        //引用符つき引数ではないので引数の区切り⇒引数の取り出し
        if (ls_WStr <> '') then
        begin
          AList.Add(ls_WStr);
          ls_WStr := '';
        end;
      end;
    end else
    begin
      ls_WStr := ls_WStr + ls_WChar;
    end;
  end;
  if (ls_WStr <> '') then
  begin
    AList.Add(ls_WStr);
  end;
  Result := AList.Count-1;
end;

ParamStrとまったく同じ要領で分割されるわけではありません。
大まかには一緒なのですが二重引用符をイレギュラーな書き方で囲んだ場合の処理がParamStrと違います。

ABC DEF 'ABC'
'DEF'

"ABC DEF" 'ABC DEF'
"ABC""DEF" 'ABC"DEF' 間の""は区切りではなく引用符中に引用符を入れるため二つ続けているもの。
"ABC" "DEF" 'ABC'
'DEF'
上記の場合と違い、CとDの間の"と"の間にスペースがあるので区切り。
ABC""DEF 'ABC'
''
'DEF'
間の""は区切りとみなすので ABC"DEF とはならない。
空文字も一つの引数として返す。
ABC "" DEF 'ABC'
''
'DEF'
上と同じ。
こちらの方が分かりやすい書き方。
ABC"""DEF 'ABC'
'"DEF'
一つ目の"は区切り。
二つ目と三つ目の"で引用符内の引用符。
この例の場合二つ目の引数は最後がきちんと"で終了していない。
'ABC"""DEF"' か 'ABC """DEF"' が最後がきちんと終了している例。
ABC""""DEF 'ABC'
'"'
'DEF'
間の""""は引用符でかこまれた引用符ということ。
ABC """" DEF 'ABC'
'"'
'DEF'
上と同じ。
こちらの方が分かりやすい書き方。