web-dev-qa-db-ja.com

Windowsコマンドインタープリター(CMD.EXE)はどのようにスクリプトを解析しますか?

ss64.com に遭遇しました。これは、Windowsコマンドインタープリターが実行するバッチスクリプトの作成方法に関する優れたヘルプを提供します。

ただし、バッチスクリプトの文法、物事がどのように展開するか、または展開しないか、および物事をエスケープする方法についての良い説明を見つけることができませんでした。

ここに、私が解決できなかった質問の例を示します:

  • 見積システムはどのように管理されていますか? TinyPerl スクリプトを作成しました
    foreach $i (@ARGV) { print '*' . $i ; })、コンパイルしてこの方法で呼び出しました:
    • my_script.exe "a ""b"" c"→出力は*a "b*cです
    • my_script.exe """a b c"""→出力*"a*b*c"
  • 内部echoコマンドはどのように機能しますか?そのコマンド内で何が展開されますか?
  • ファイルスクリプトではfor [...] %%Iを使用する必要がありますが、インタラクティブセッションではfor [...] %Iを使用する必要があるのはなぜですか?
  • エスケープ文字とはどのようなコンテキストですか?パーセント記号をエスケープする方法は?たとえば、%PROCESSOR_ARCHITECTURE%を文字通りエコーするにはどうすればよいですか? echo.exe %""PROCESSOR_ARCHITECTURE%が機能することがわかりましたが、より良い解決策はありますか?
  • %のペアはどのように一致しますか?例:
    • set b=aecho %a %b% c%%a a c%
    • set a =becho %a %b% c%bb c%
  • この変数に二重引用符が含まれている場合、変数が単一の引数としてコマンドに渡されるようにするにはどうすればよいですか?
  • setコマンドを使用すると、変数はどのように保存されますか?たとえば、set a=a" bを実行してからecho.%a%を実行すると、a" bを取得します。ただし、UnxUtilsからecho.exeを使用すると、a bが返されます。 %a%が別の方法で拡張するのはなぜですか?

あなたのライトをありがとう。

130
Benoit

コマンドウィンドウからコマンドを呼び出す場合、コマンドライン引数のトークン化はcmd.exe(別名「シェル」)によって行われません。ほとんどの場合、トークン化は新しく形成されたプロセスのC/C++ランタイムによって行われますが、これは必ずしもそうではありません-たとえば、新しいプロセスがC/C++で記述されていない場合、または新しいプロセスがargvそして、それ自体の生のコマンドラインを処理します(例: GetCommandLine() )。 OSレベルでは、Windowsはトークン化されていないコマンドラインを単一の文字列として新しいプロセスに渡します。これは、シェルが引数を新しく形成されたプロセスに渡す前に一貫性のある予測可能な方法でトークン化するほとんどの* nixシェルとは対照的です。これは、個々のプログラムが引数のトークン化を自分の手に委ねることが多いため、Windows上の異なるプログラム間で引数のトークン化の動作が大きく異なることを意味します。

それが無秩序のように聞こえるなら、それは一種です。ただし、多数のWindowsプログラムdoはMicrosoft C/C++ランタイムのargvを利用するため、一般的に理解するのに役立つ場合があります- MSVCRTがトークン化する方法 引数。以下は抜粋です。

  • 引数は空白で区切られます。空白はスペースまたはタブです。
  • 二重引用符で囲まれた文字列は、含まれる空白に関係なく、単一の引数として解釈されます。引用符で囲まれた文字列を引数に埋め込むことができます。キャレット(^)はエスケープ文字または区切り文字として認識されないことに注意してください。
  • バックスラッシュ\ "が前に付いた二重引用符は、リテラルの二重引用符(")として解釈されます。
  • バックスラッシュは、二重引用符の直前にない限り、文字どおりに解釈されます。
  • 偶数個のバックスラッシュの後に二重引用符が続く場合、バックスラッシュ(\)のペアごとに1つのバックスラッシュ()がargv配列に配置され、二重引用符( ")は文字列区切り文字として解釈されます。
  • 奇数のバックスラッシュの後に二重引用符が続く場合、バックスラッシュ(\)のペアごとに1つのバックスラッシュ()がargv配列に配置され、二重引用符は残りのバックスラッシュによってエスケープシーケンスとして解釈されます。 argvに置かれるリテラルの二重引用符( ")。

Microsoftの「バッチ言語」(.bat)はこの無秩序な環境の例外ではなく、トークン化とエスケープのための独自の独自のルールを開発しました。また、cmd.exeのコマンドプロンプトは、コマンドライン引数の前処理(主に変数の置換とエスケープ)を行ってから、新しく実行するプロセスに引数を渡すように見えます。このページでは、jebとdbenhamによる優れた回答で、バッチ言語とcmdエスケープの低レベルの詳細について詳しく読むことができます。


Cで簡単なコマンドラインユーティリティを作成し、テストケースについての説明を見てみましょう。

int main(int argc, char* argv[]) {
    int i;
    for (i = 0; i < argc; i++) {
        printf("argv[%d][%s]\n", i, argv[i]);
    }
    return 0;
}

(注:argv [0]は常に実行可能ファイルの名前であり、簡潔にするために以下では省略されています。Windowsでテスト済みXPSP3。VisualStudio 2005でコンパイルされています。)

> test.exe "a ""b"" c"
argv[1][a "b" c]

> test.exe """a b c"""
argv[1]["a b c"]

> test.exe "a"" b c
argv[1][a" b c]

そして、私自身のテストのいくつか:

> test.exe a "b" c
argv[1][a]
argv[2][b]
argv[3][c]

> test.exe a "b c" "d e
argv[1][a]
argv[2][b c]
argv[3][d e]

> test.exe a \"b\" c
argv[1][a]
argv[2]["b"]
argv[3][c]
60
Mike Clark

パーセント拡張ルール

jeb's answer (バッチモードとコマンドラインモードの両方で有効)のフェーズ1の詳細な説明を次に示します。

フェーズ1)拡張率左から順に、各文字をスキャンして%または<LF>を探します。見つかったら

  • 1.05(<LF>の行を切り捨てます)
    • 文字が<LF>の場合、
      • <LF>以降の行の残りをドロップ(無視)します
      • フェーズ1.5に移動(ストリップ<CR>
    • それ以外の場合、文字は%でなければならないため、1.1に進んでください
  • 1.1(エスケープ%コマンドラインモードの場合はスキップ
    • バッチモードの後に​​別の%が続く場合
      %%を単一の%に置き換えて、スキャンを続行します
  • 1.2(引数を展開)コマンドラインモードの場合はスキップ
    • それ以外の場合、バッチモードの場合は
      • その後に*が続き、コマンド拡張が有効になっている場合
        %*をすべてのコマンドライン引数のテキストで置き換え(引数がない場合は何も置き換えない)、スキャンを続行します。
      • そうでない場合、その後に<digit>が続く場合
        %<digit>を引数値に置き換え(未定義の場合は何も置き換えない)、スキャンを続行します。
      • そうでない場合、その後に~が続き、コマンド拡張が有効になっている場合は
        • 引数修飾子のオプションの有効なリストの後に必須の<digit>が続く場合、
          %~[modifiers]<digit>を変更された引数値に置き換え(定義されていない場合、または指定された$ PATH:修飾子が定義されていない場合は何も置き換えない)、スキャンを続行します。
          注:修飾子は大文字と小文字を区別せず、任意の順序で複数回出現できます。ただし、$ PATH:修飾子は1回しか出現できず、<digit>の前の最後の修飾子でなければなりません
        • それ以外の無効な変更された引数構文は、致命的エラーを発生させます。すべての解析されたコマンドは中止され、バッチモードの場合、バッチ処理は中止されます。 /
  • 1.3(展開変数)
    • それ以外の場合、コマンド拡張機能が無効になっている場合
      次の文字列を見て、%またはバッファーの終わりの前で区切り、それらをVAR(空のリストの場合があります)と呼びます。
      • 次の文字が%の場合、
        • VARが定義されている場合
          %VAR%をVARの値に置き換えてスキャンを続行します
        • それ以外の場合はバッチモード
          %VAR%を削除してスキャンを続行します
        • その他のgoto 1.4
      • その他のgoto 1.4
    • それ以外の場合は、コマンド拡張機能が有効になっている場合
      次の文字列を見て、%:またはバッファーの終わりの前で中断し、それらをVAR(空のリストの場合があります)と呼びます。 VARが:の前で中断し、後続の文字が%である場合、VARの最後の文字として:を含め、%。の前で中断します。
      • 次の文字が%の場合、
        • VARが定義されている場合
          %VAR%をVARの値に置き換えてスキャンを続行します
        • それ以外の場合はバッチモード
          %VAR%を削除してスキャンを続行します
        • その他のgoto 1.4
      • それ以外の場合、次の文字が:である場合は
        • VARが未定義の場合、
          • バッチモードの場合
            %VAR:を削除して、スキャンを続行します。
          • その他のgoto 1.4
        • それ以外の場合、次の文字が~である場合は
          • 次の文字列が[integer][,[integer]]%のパターンに一致する場合
            %VAR:~[integer][,[integer]]%をVARの値のサブストリングに置き換え(空のストリングになる可能性がある)、スキャンを続行します。
          • その他のgoto 1.4
        • そうでない場合、その後に=または*=が続く場合
          無効な変数検索および置換構文は、致命的エラーを発生させます:解析されたすべてのコマンドは中止され、バッチモードの場合、バッチ処理は中止されます! /
        • それ以外の場合、次の文字列が[*]search=[replace]%のパターンと一致する場合。検索では=を除く任意の文字セットを含めることができ、replaceでは%を除く任意の文字セットを含めることができます。
          %VAR:[*]search=[replace]%は、検索と置換(空の文字列になる可能性がある)を実行した後、VARの値でスキャンを続行します
        • その他のgoto 1.4
  • 1.4(ストリップ%)
    • それ以外の場合、バッチモード
      %を削除し、%の後の次の文字からスキャンを続行します
    • そうでない場合は%を保持し、%の後の次の文字からスキャンを続行します

上記は、このバッチの理由を説明するのに役立ちます

@echo off
setlocal enableDelayedExpansion
set "1var=varA"
set "~f1var=varB"
call :test "arg1"
exit /b  
::
:test "arg1"
echo %%1var%% = %1var%
echo ^^^!1var^^^! = !1var!
echo --------
echo %%~f1var%% = %~f1var%
echo ^^^!~f1var^^^! = !~f1var!
exit /b

次の結果が得られます。

%1var% = "arg1"var
!1var! = varA
--------
%~f1var% = P:\arg1var
!~f1var! = varB

注1-フェーズ1は、REMステートメントの認識前に発生します。これは、発言でさえ致命的なエラーを生成できるため、非常に重要です。無効な引数展開構文または無効な変数検索と置換構文があります!

@echo off
rem %~x This generates a fatal argument expansion error
echo this line is never reached

注2-%解析ルールの別の興味深い結果:名前に:を含む変数は定義できますが、コマンド拡張機能が無効になっていない限り展開できません。例外が1つあります。最後に1つのコロンを含む変数名は、コマンド拡張が有効になっているときに展開できます。ただし、コロンで終わる変数名に対して部分文字列または検索および置換操作を実行することはできません。以下のバッチファイル(jeb提供)は、この動作を示しています

@echo off
setlocal
set var=content
set var:=Special
set var::=double colon
set var:~0,2=tricky
set var::~0,2=unfortunate
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%
echo Now with DisableExtensions
setlocal DisableExtensions
echo %var%
echo %var:%
echo %var::%
echo %var:~0,2%
echo %var::~0,2%

注3-投稿でjebがレイアウトする構文解析ルールの順序の興味深い結果:検索を実行して通常の展開に置き換える場合、特殊文字はエスケープしないでください(ただし、引用される)。ただし、検索を実行して遅延展開に置き換える場合、特殊文字はエスケープする必要があります(引用符で囲まれていない場合)。

@echo off
setlocal enableDelayedExpansion
set "var=this & that"
echo %var:&=and%
echo "%var:&=and%"
echo !var:^&=and!
echo "!var:&=and!"

遅延拡張ルール

jeb's answer (バッチモードとコマンドラインモードの両方で有効)のフェーズ5の拡張されたより正確な説明を次に示します。

フェーズ5)拡張の遅延

次の条件のいずれかが当てはまる場合、このフェーズはスキップされます。

  • 遅延拡張は無効です。
  • コマンドは、パイプの両側の括弧で囲まれたブロック内にあります。
  • 着信コマンドトークンは「裸の」バッチスクリプトです。つまり、CALL、括弧付きブロック、コマンド連結(&&&または||)、またはパイプ|に関連付けられていません。

遅延拡張プロセスは、トークンに個別に適用されます。コマンドには複数のトークンが含まれる場合があります。

  • コマンドトークン。ほとんどのコマンドでは、コマンド名自体がトークンです。ただし、いくつかのコマンドには、フェーズ5のトークンと見なされる特殊な領域があります。
    • for ... in(TOKEN) do
    • if defined TOKEN
    • if exists TOKEN
    • if errorlevel TOKEN
    • if cmdextversion TOKEN
    • if TOKEN comparison TOKEN、比較は==equneqlssleqgtr、またはgeqのいずれか
  • 引数トークン
  • リダイレクトの宛先トークン(リダイレクトごとに1つ)

!を含まないトークンは変更されません。

少なくとも1つの!を含む各トークンについて、各文字を左から右に^または!をスキャンし、見つかった場合は、

  • 5.1(キャレットエスケープ)!または^リテラルに必要です
    • 文字がキャレットの場合^ then
      • ^を削除します
      • 次の文字をスキャンし、リテラルとして保存します
      • スキャンを続ける
  • 5.2(展開変数)
    • 文字が!の場合、
      • コマンド拡張機能が無効になっている場合
        次の文字列を見て、!または<LF>の前で区切り、それらをVAR(空のリストである可能性があります)と呼びます
        • 次の文字が!の場合、
          • VARが定義されている場合、
            !VAR!をVARの値に置き換えてスキャンを続行します
          • それ以外の場合はバッチモード
            !VAR!を削除してスキャンを続行します
          • それ以外の場合5.2.1
        • それ以外の場合5.2.1
      • それ以外の場合は、コマンド拡張機能が有効になっている場合
        次の文字列を見て、!:、または<LF>の前で区切り、それらをVAR(空のリストの場合があります)と呼びます。 VARが:の前で中断し、後続の文字が!の場合、VARの最後の文字として:を含め、! の前で中断します。
        • 次の文字が!の場合、
          • VARが存在する場合、
            !VAR!をVARの値に置き換えてスキャンを続行します
          • それ以外の場合はバッチモード
            !VAR!を削除してスキャンを続行します
          • それ以外の場合5.2.1
        • それ以外の場合、次の文字が:である場合は
          • VARが未定義の場合、
            • バッチモードの場合
              !VAR:を削除してスキャンを続行します
            • それ以外の場合5.2.1
          • それ以外の場合、次の文字列がパターン
            ~[integer][,[integer]]!その後
            !VAR:~[integer][,[integer]]!をVARの値のサブストリングに置き換え(おそらく空のストリングになる)、スキャンを続行します
          • それ以外の場合、次の文字列が[*]search=[replace]!のパターンと一致する場合。検索では=を除く任意の文字セットを含むことができ、replaceは!を除く任意の文字セットを含むことができます。
            検索と置換を実行した後に!VAR:[*]search=[replace]!をVARの値に置き換え(場合によっては空の文字列になる)、スキャンを続行
          • それ以外の場合5.2.1
        • それ以外の場合5.2.1
      • 5.2.1
        • バッチモードの場合は、!を削除します
          それ以外の場合は!を保存します
        • !の後の次の文字からスキャンを続行します
44
dbenham

指摘したように、コマンドはμSoftランドの引数文字列全体に渡され、これを独自の使用のために個別の引数に解析するのは彼らの責任です。異なるプログラム間でこれに一貫性はないため、このプロセスを記述するためのルールのセットはありません。プログラムで使用するCライブラリが存在するかどうか、各コーナーケースを確認する必要があります。

システム.batファイルに関する限り、ここにそのテストがあります。

c> type args.cmd
@echo off
echo cmdcmdline:[%cmdcmdline%]
echo 0:[%0]
echo *:[%*]
set allargs=%*
if not defined allargs goto :eof
setlocal
@rem Wot about a Nice for loop?
@rem Then we are in the land of delayedexpansion, !n!, call, etc.
@rem Plays havoc with args like %t%, a"b etc. ugh!
set n=1
:loop
    echo %n%:[%1]
    set /a n+=1
    shift
    set param=%1
    if defined param goto :loop
endlocal

これで、いくつかのテストを実行できます。 μSoftが何をしようとしているかを把握できるかどうかを確認します。

C>args a b c
cmdcmdline:[cmd.exe ]
0:[args]
*:[a b c]
1:[a]
2:[b]
3:[c]

今のところ結構です。 (これからは面白くない%cmdcmdline%%0を省きます。)

C>args *.*
*:[*.*]
1:[*.*]

ファイル名の展開はありません。

C>args "a b" c
*:["a b" c]
1:["a b"]
2:[c]

引用符は引数の分割を防ぎますが、引用符の除去はありません。

c>args ""a b" c
*:[""a b" c]
1:[""a]
2:[b" c]

連続した二重引用符があると、特殊な解析機能が失われます。 @Beniotの例:

C>args "a """ b "" c"""
*:["a """ b "" c"""]
1:["a """]
2:[b]
3:[""]
4:[c"""]

クイズ:環境変数の値をsingle引数(つまり、%1)としてbatファイルに渡すにはどうすればよいですか?

c>set t=a "b c
c>set t
t=a "b c
c>args %t%
1:[a]
2:["b c]
c>args "%t%"
1:["a "b]
2:[c"]
c>Aaaaaargh!

正常な解析は永遠に壊れているようです。

娯楽のために、これらの例にその他の^\'&(&c。)文字を追加してみてください。

7
bobbogo

編集:受け入れられた答えを参照してください。以下は間違っており、コマンドラインをTinyPerlに渡す方法のみを説明しています。


引用に関して、私はその振る舞いは次のように感じています:

  • "が見つかると、文字列のグロビングが始まります
  • 文字列のグロビングが発生した場合:
    • "以外の文字はすべてグロブされます
    • "が見つかった場合:
      • 後に""が続く場合(したがって、トリプル")、文字列に二重引用符が追加されます。
      • 後に"が続く場合(したがって、二重")、二重引用符が文字列に追加され、文字列のグロビングが終了します
      • 次の文字が"でない場合、文字列のグロビングは終了します
    • 行が終了すると、ストリングのグロビングが終了します。

要するに:

"a """ b "" c"""は2つの文字列で構成されます:a " b "およびc"

"a"""a"""および"a""""は、行末にある場合はすべて同じ文字列です

0
Benoit