2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiitaでバッチファイルの謎なシンタクスハイライトを直したい2~バッチファイルの文法を整理する

Posted at

バッチファイルの文法を整理する

この記事を書いている2024年11月現在,Qiita のシンタクスハイライトは Ruby の rouge によって実現されている。rouge では単なるキーワードマッチングではなく,ステートマシンを用いて文法解析を行っており,精度の高い文法解析を行うことができると言われている。

とはいえ,バッチファイルの構文自体が謎ではどうしようもないので,勉強がてらバッチファイルの文法について整理してみる。

@マーク

行頭に @ マークを付けると,その行を画面にエコー出力しない。echo off とすれば,その次の行からエコー出力しなくなるが,echo off とエコー出力されてしまう。これを防ぐには @echo off とする。なお,行頭から @ マークまで,および@ マーク以降に空白文字を挟んでもよい。

ここで注意すべきは @ マークの位置である。下記は OK という文字列を標準エラー出力に出力するというものでリダイレクト記号を用いている。下記の例では行頭に @ マークを置いているが,コマンドの前とも解釈できる。

@echo OK 1>&2

バッチファイルの文法上リダイレクト記号はどこに置いても良いらしく,行頭に持ってくることも可能だ。この場合も行頭に @ マークを置く必要がある。下記の例でコマンドの前に置いて @echo と書くとエラーとなる。すなわち @ マークを置くのはコマンドの前ではなく,実行可能なブロックの先頭なのだ。ちなみに @ マークと 1 の間にスペースを入れないとエラーになる。

@ 1>&2 echo OK 

ただし,下記の例ではスペースは不要である。

@>nul echo OK

例外として,下記の例では @ マークは行頭に加えて do 文の後にも付ける。

@for /L %%I in (1 1 10) do @echo %%I

複数行に渡る場合には下記のようにする。

@for /L %%I in (1 1 10) do @(
  echo %%I
)

要するに実行可能なブロックの先頭に @ マークを付けることができ,このブロックには (...) によるブロックも含まれる。for 文以外では ifelse 文,論理演算子 &&&|| の次のブロックの先頭で使用することができる。

コメント

rem から始まる行はコメントとして扱われる。次行に繋げるエスケープ文字 ^ を行末に置いても無効であり,当該行しかコメントにならない。ちなみに 複数行をコメントにするテクニックとしては下記のような用法がある。

rem/||(
  この行は絶対に実行されないのでコメントが書けます。
  ただし,カッコ閉じの記号は書けません。
)

なお rem||( ではなく何故 rem/||( なのかというと単に rem||( だと ||( もコメントとして扱われてしまうので次行以降も実行されてしまうからだ。一方 rem/ は不完全なオプション指定となり,コメント指定としては扱われないうえ,エラーメッセージも表示されない。ただし errorlevel は 0 なので ||(...) の文(ブロック)は実行されない。なので rem/ 以外にも rem/a など存在しないオプションであれば OK であるが,rem/? のように存在するオプションだとヘルプメッセージが表示されてしまう。将来,オプションが追加される可能性を考えると不完全なオプション指定である rem/ が良いのだろう。

ラベル

コロン : から始まる文字列はラベルとして扱われる。goto 文でラベルにジャンプしたり,call 文で呼び出すことができる。なお,行頭からコロン : まで,およびコロン : とラベル文字列の間に空白文字を挟んでもよい。ラベルとして使用可能な文字列は明確に定義されていないが,等号記号 = やセミコロン ; などの区切り文字は禁止されている。ラベル文字列以降の文字列は行末まで無視され,コメント扱いとなる。コメント代わりに使う場合はコロンの二度打ち :: してもよい。この際,二つのコロン : の間に空白文字を挟んでもよい。

ちなみに call 文の場合はラベルにコロン : を付ける必要があるが,goto 文の場合はコロン : を付けても付けなくてもよいようだ。一般的には goto 文の場合にはコロン : は不要とされているが,別に付けても OK のようである。

変数

一時変数

一時変数の種類

for 文のループ変数(%% + 英字一文字)とバッチファイルやサブルーチンの引数(% + 数字一桁)がある。後者のほうは加えて全引数の %* がある。後者の場合,呼び出されたバッチファイル名またはサブルーチン名が %0 になる。サブルーチン名の場合,%0 の先頭にはコロン : が入る。全引数の %*%0 を含まない。

オプション修飾子

オプション修飾子としてチルダ ~ があり,以下の用法はループ変数もサブルーチン引数も同様であるため,代表として %1 に適用した例を示す。なお,これら複数の修飾子を %~dp1%~nx1 のように組み合わせて使用することもできる。

修飾子 修飾子の意味
%~1 二重引用符を外して展開する。
%~f1 完全修飾パス名に展開する。
%~d1 ドライブ文字だけに展開する。
%~p1 パスだけに展開する。
%~n1 ファイル名だけに展開する。
%~x1 ファイル拡張子だけに展開する。
%~s1 短い名前に展開する。
%~a1 ファイル属性に展開する。
%~t1 ファイルの日付・時刻に展開する。
%~z1 ファイルのサイズに展開する。
%~$PATH:1 環境変数 PATH に指定されているディレクトリを検索し,最初に見つかった完全修飾名に展開する。見つからなかった場合は空文字列となる。

環境変数のディレクトリ検索機能

実は環境変数のディレクトリ検索は PATH 以外の環境変数でも使用できる。たとえば Visual C++ のコマンドライン開発環境を立ち上げると,環境変数 INCLUDE および LIB にヘッダファイルやライブラリのパスがセットされる。このとき以下のように使用できる。

c:\>for %I in ( stdio.h ) do @echo %~$INCLUDE:I
C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\ucrt\stdio.h

c:\>for %I in ( windows.h ) do @echo %~$INCLUDE:I
C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\Windows.h

環境変数

変数名に使用可能な文字

set 文で環境変数を設定・表示・削除できる。参照する際には変数名の前後をパーセント % で囲む。環境変数名として許可されている文字は明らかにされていないが,少なくとも先頭文字では数字とスペースは NG である。逆に言うと先頭文字以外であれば数字もスペースも変数名に使ってよい。

文法上の曖昧さ

なお,ループ変数ではパーセント記号 % を閉じないので文法上曖昧な点が残ってしまう。たとえば %I%I と記述した際に,これを環境変数 %I% + I と解釈するのか,あるいはループ変数 %I + %I と解釈するのか?という問題である。試しに実験すると下記のようになった。この結果より,おそらく環境変数の展開が優先されて行われ,該当する環境変数が存在しなければループ変数として解釈されるのだろう。これは単なる文字列なのか,それとも変数なのか単純な文法解析では判別不可能であることを意味している。

c:\>set I=X
c:\>@for /L %I in (1 1 5) do @echo %I%I
XI
XI
XI
XI
XI

c:\>set I=
c:\>@for /L %I in (1 1 5) do @echo %I%I
11
22
33
44
55

部分文字列の参照機能

環境変数は %STR:~開始位置,文字列の長さ% のように部分文字列を参照することができる。開始位置は 0-origin である。開始位置および文字列の長さには負の値を設定することもできる。後述する遅延環境変数展開を用いれば,部分文字列の開始位置および文字列の長さにループ変数や環境変数を用いることができる。

cmd /v:on
Microsoft Windows [Version 10.0.19045.5131]
(c) Microsoft Corporation. All rights reserved.

c:\>set STR=abcdefghijklmnopqrstuvwxyz
c:\>set LEN=3
c:\>@for /L %I in (0 %LEN% 25) do @echo !STR:~%I,%LEN%!
abc
def
ghi
jkl
mno
pqr
stu
vwx
yz

文字列置換機能

また %STR:変更前=変更後% のように文字列置換もできる。変更前の文字列にはワイルドカード * を使用することもできる。ただし,ワイルドカード文字 * 自体を置換することができない。エスケープ記号も無効なので,どうしてもワイルドカード文字 * を置換したい場合は下記記事のようなテクニックを使うしかない。

ファイル/フォルダ名の大文字・小文字&全角・半角文字を変換するバッチファイルをささっと作る(完結編)

なお,文字列比較の際に英字の大文字・小文字の区別を行わないことに加え,置換前後の文字列にはループ変数や環境変数を用いることができるので,以下のようにすれば小文字を大文字に変換することができる。

cmd /v:on
Microsoft Windows [Version 10.0.19045.5131]
(c) Microsoft Corporation. All rights reserved.

c:\>set STR=hello world
c:\>@for %C in ( A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ) do @(
More? set "STR=!STR:%C=%C!"
More? )

c:\>echo %STR%
HELLO WORLD

遅延環境変数展開

参照する際に変数名の前後を感嘆符 ! で囲む。部分文字列の参照機能や,文字列置換機能についても通常の環境変数と同様に使用できる。

なお,遅延環境変数展開はデフォルトでは OFF になっており,ON にするためにはコマンドプロンプトを cmd /v:on として起動するか,あるいは setlocal enabledelayedexpansion とするか,下記のレジストリを変更して 0x1 にする等の手段がある。

HKEY_CURRENT_USER\Software\Microsoft\Command Processor\EnableExtensions

遅延環境変数展開には未定義変数に関する副作用(後述)があるので使用場所を限定したほうが良いかもしれない。例えば下記のバッチファイル中の一文を考えよう。

echo error!!

遅延環境変数展開が OFF のとき二つの感嘆符は表示されるが,

遅延環境変数展開が OFF のとき
error!!

遅延環境変数展開が ON のときは二つの感嘆符が消えてしまう。

遅延環境変数展開が ON のとき
error

遅延環境変数展開その2

call 文を用いる方法もある。JavaScript でいう eval 関数のようなものだろうか?

TEST001.CMD
@echo off
setlocal
set STR=abcdefghijklmnopqrstuvwxyz
set LEN=3
for /L %%I in (0 %LEN% 25) do call echo %%STR:~%%I,%LEN%%%
TEST001.CMD の実行結果
c:\>test001
abc
def
ghi
jkl
mno
pqr
stu
vwx
yz

未定義変数の取り扱い

未定義変数については,コマンドプロンプト上での動作とバッチファイル上での動作は異なる。コマンドプロンプト上では以下のように未定義変数は展開されない。

cmd /v:on
Microsoft Windows [Version 10.0.19045.5131]
(c) Microsoft Corporation. All rights reserved.

c:\>set STR=
c:\>echo %STR%
%STR%
c:\>echo !STR!
!STR!

一方,バッチファイルでは未定義変数は空の文字列に展開される。なお,デフォルトでは遅延環境変数展開は無効なので,!STR! は展開されずにそのまま残る。

TEST002.CMD
@echo off
setlocal
set STR=
echo %STR%
echo !STR!
TEST002.CMD の実行結果
c:\>test002
ECHO は <OFF> です。
!STR!

遅延環境変数展開を有効にすると !STR! も空の文字列に展開される。

TEST003.CMD
@echo off
setlocal enabledelayedexpansion
set STR=
echo %STR%
echo !STR!
TET003.CMD の実行結果
c:\>test003
ECHO は <OFF> です。
ECHO は <OFF> です。

set 文

環境変数は set 文を用いて値を設定する。原則として一文につき一つの変数しか設定できないが,値が数値のときオプション /A を指定することにより,コンマ演算子 , を用いて複数の変数を設定できる。

set /a A=1, B=2

制御構文

丸括弧 (~) ブロック

基本的にバッチファイルは一行に一つのコマンドを記述し,行単位で実行される。それ故に,後述する if 文や for 文等の制御構文でブロックの概念が必要になり,バッチファイルでは丸括弧 (...) で表される。

if 文

if 文の構文

if 文は以下の5種類である。コマンドはコマンド単体に限らず,前述した (...) ブロックを用いて複数コマンドを記述したり,後述する論理演算子を用いて複数コマンドを連結することもできる。

if [/I] [not] 文字列1 比較演算子 文字列2 コマンド
if [not] exist パス名 コマンド
if [not] defined 変数名 コマンド
if [not] cmdextversion 番号 コマンド
if [not] errorlevel 番号 コマンド

else 文

if 文に続いて else 文も用意されている。

else コマンド

else 文は if 文のコマンドの最終行と同じ行で始まらなくてはならない。ただし,たいていのコマンドは改行で終了するため下記の例は正しく動作しない。

if exist %FILENAME% del %FILENAME% else echo %FILENAME% は存在しません

どうしても一行で記述したい場合は下記のように (...) ブロックを使う。

if exist %FILENAME% (del %FILENAME%) else echo %FILENAME% は存在しません

if 文クイズ

さて,以下のバッチファイルは if 文字列1 equ 文字列2 という構文で解釈されるだろうか,それとも if defined 変数名 という構文で解釈されるだろうか?

TEST004.CMD
@echo off
setlocal
set DEFINED=defined
set EMPTY=
set EQU=equ
if %DEFINED% EQU %EMPTY% echo DEFINED!!

上記のバッチファイルの実行結果を以下に示す。正解は if defined 変数名 のほうだ。要するにバッチファイルではいったん環境変数を展開した後に構文解析が行われるようだ。

TEST004.CMD の実行結果
c:\>test004
DEFINED!!

比較演算子

比較演算子は以下の7種類ある。

演算子 意味
== 等しい
EQU 等しい
NEQ 等しくない
LSS より小さい
LEQ 以下
GTR より大きい
GEQ 以上

いずれも比較する二つの文字列が数字だけを含む場合,文字列は数値に変換されて数値の比較が行われる。

TEST005.CMD
@echo off
if  99  GTR  100  (echo YES) else echo NO
if "99" GTR "100" (echo YES) else echo NO
if  99  GTR "100" (echo YES) else echo NO

試しに上記のバッチファイルを実行すると下記のようになった。確かに比較する文字列が二つとも二重引用符を含まない場合に限り,数値で比較しているようだ。

TEST005.CMD の実行結果
c:\>test005
NO
YES
YES

ちなみに比較演算子は数値以外の文字・文字列の比較にも使用できる。文字の比較順序は Shift-JIS でもなく Unicode でもなく,SORT コマンドの照合順番に近いと思われる。

  1. 記号
  2. 数字(アラビア数字,丸付き数字,ローマ数字,無限大)
  3. アルファベット(ラテン文字,ギリシャ文字,キリル文字,英字の単位記号)
  4. 仮名文字(ひらがな,カタカナ,カタカナの単位記号)
  5. 一般漢字
  6. 踊り字・長音記号(単独で用いた場合)

加えて日本語特有の踊り字(繰り返し記号)に関しては独特のルールが存在する。試しに踊り字を用いた文字列の比較をしてみよう。

TEST006.CMD
@echo off
if "いゝ" LEQ "いあ" (echo YES) else echo NO
if "いゝ" LEQ "いい" (echo YES) else echo NO
if "いゝ" LEQ "いう" (echo YES) else echo NO

if "うゝ" LEQ "うい" (echo YES) else echo NO
if "うゝ" LEQ "うう" (echo YES) else echo NO
if "うゝ" LEQ "うえ" (echo YES) else echo NO

上記のバッチファイルを実行してみると下記のようになった。踊り字 は固有の照合順番を持つ訳ではなく,直前の文字に準じる照合順番を持つことが分かる。これは直前の文字と同じ読みになるという踊り字の意味から考えて妥当であろう。

TEST006.CMD の実行結果
c:\>test006
NO
NO
YES
NO
NO
YES

Windows の SORT コマンドのアルゴリズムについて詳しくは下記記事を参照されたい。

Windowsのsortコマンドのアルゴリズムを探る(照合順番の闇)- Qiita

for 文

強力で便利な for 文には有用なオプションがいくつもあるが,下記が最も基本的なパターンである。

基本パターン

for [/R [パス]] [/D] 変数 in ( ファイルセット ) do コマンド

上記では (...) 内にファイルセットを指定すると書いたが,実はファイルに限らず任意の文字列を複数指定できる。文字列の区切り文字は空白以外にコンマ , やセミコロン ; 等も許されている。

c:\>@for %I in ( alpha beta gamma ) do @echo %I
alpha
beta
gamma

何故ファイルセットなのかというとワイルドカードを使用できるからだ。この場合,パターンにマッチするファイル名のみがリストアップされる。オプション /D を指定すればディレクトリ名のみリストアップされる。オプション /R を指定すれば指定したパスを基準として再帰的に検索する。

c:\>@for %I in ( *.txt ) do @echo %I

オプション /L

ループ範囲を数値で指定する場合にはオプション /L を用いる。

for /L 変数 in ( 開始 ステップ 終了 ) do コマンド

開始とステップ,終了の間の区切り記号はスペースやコンマ , あるいはセミコロン ; 等を使用できる。

c:\>@for /L %I in (1 1 5) do @echo %I
1
2
3
4
5

オプション /F 用例その1

オプション /F を用いるとファイル名ではなく,ファイルを読み込んでオプション /F の解析結果が変数にセットされる。下記の場合,ファイルセットには複数のファイル名を指定できるが,ワイルドカードは使えない。

for /F "オプション" 変数 in ( ファイルセット ) do コマンド

ちなみにファイルセットのファイル名を二重引用符で囲むとファイルの内容ではなく,ファイル名がそのまま文字列として変数にセットされてしまう。ファイル名に空白や括弧 () などの記号が含まれていて二重引用符で囲まざるを得ない場合はオプション "usebackq" を使う。

for /F "usebackq" %I in ( "test(1).txt" ) do @echo %I

オプション /F 用例その2

前項で説明したが,括弧 (...) 内に指定した文字列に対してオプション /F による解析結果が変数にセットされる。

for /F "オプション" 変数 in ( "文字列" ) do コマンド
for /F "usebackq オプション" 変数 in ( '文字列' ) do コマンド

オプション /F 用例その3

コマンドの実行結果に対してオプション /F による解析結果が変数にセットされる。

for /F "オプション" 変数 in ( 'コマンド' ) do コマンド
for /F "usebackq オプション" 変数 in ( `コマンド` ) do コマンド

goto 文

goto 文はラベルにジャンプする。ラベルはコロン : から始まる非スペースの文字列であり,ジャンプ先を指定するときはコロン : を省略することができる。ただし,後述する call 文ではコロン : が必要なので goto 文でもコロン : を付けたら対称性が増して文法的に美しいのにと思っていたら,goto 文でもコロン : を付けても良いようだ。

ラベルが重複する場合

バッチファイルは(おそらく)全文をパースして実行している訳ではないので,ラベルが重複していてもエラーにならない。ならばラベルが重複する場合,どうなるのだろうか?テスト用のバッチファイルを以下に示す。

TEST007.CMD
@echo off
goto SKIP

:LABEL
echo LABEL 1st
exit /b

:SKIP
goto LABEL

:LABEL
echo LABEL 2nd
exit /b

:LABEL
echo LABEL 3rd
exit /b

上記のバッチファイルを実行してみると下記の結果となった。

TEST007.CMD の実行結果
c:\>test007
LABEL 2nd

それでは下記のバッチファイルの場合はどうなるだろうか?

TEST008.CMD
@echo off
goto SKIP

:LABEL
echo LABEL 1st
exit /b

:LABEL
echo LABEL 2nd
exit /b

:LABEL
echo LABEL 3rd
exit /b

:SKIP
goto LABEL

実行してみると下記のようになった。

TEST008.CMD の実行結果
c:\>test008
LABEL 1st

要するに,現在実行している行からファイルの末尾方向に検索し,最初に見つかったラベルへジャンプする。ファイルの末尾に達しても見つからない場合はファイルの先頭から検索し直す,というアルゴリズムのようだ。

goto :EOF の謎

goto :EOF はバッチファイルの最終行へ跳ぶので一般的にはバッチファイル終了と等価だと思われているが,厳密には異なる。例えば下記のバッチファイルを実行したらどうなるだろうか?

TEST009.CMD
@echo off
call :FUNC
echo RETURN
exit /b

:FUNC
goto :EOF

実は呼び出し元に戻ってくるのだ。

TEST009.CMD の実行結果
c:\>test009
RETURN

実はバッチファイルの最終行へ跳ぶというのは(終了コードを戻せないという点を除けば)exit /b の実行と等価であり,呼び出し元が存在すればそこに戻ってくるようだ。呼び出し元が存在しなければバッチファイルを終了する。ちなみにエラー処理等によりサブルーチン先でバッチファイルを途中終了させたい場合は exit を使えばよい。

call 文

大きく分けて下記三種類の用法がある。

外部バッチファイルの呼び出し

call 文を用いなくても外部バッチファイルを呼び出すこと自体は可能である。ただし,外部バッチファイルが終了すると戻ってこないので,処理を続けたい場合は call 文を使う必要がある。

バッチファイル内のサブルーチンコール

サブルーチン名(ラベル)が重複する場合のルールは goto 文と同じである。

JavaScript の eval 関数代わり

公式サイトには記載されていない隠し機能的な存在である。遅延環境変数展開には副作用があることを考えると非常に有用な機能である。ただし,どんな文でも実行できる訳ではないようだ。set 文や echo 文は使えるが,if 文や for 文のような制御構文は使えない。

exit 文

サブルーチンから戻る場合は exit /b,バッチファイルを終了したい場合は単に exit とする。いずれも終了コードを引き渡すことができる。終了コードは符号付き 32bit 整数の範囲である。終了コードは環境変数 %errorlevel% で参照することができる。

連結演算子(コマンドセパレータ)

連結演算子は以下の三種類ある。これらの演算子は複数のコマンドを一行に並べて記述する際に用いられる。

演算子 内容
& 直前のコマンドの結果によらず,コマンドを実行する。
&& 直前のコマンドの終了コードが 0 のとき,コマンドを実行する。
|| 直前のコマンドの終了コードが 0 以外のとき,コマンドを実行する。

パイプ・リダイレクト

基本機能

標準入出力先をファイルや他のコマンドの標準入出力に割り当てることができる。なお下記表にはファイルと記載しているが,ファイル以外にも NULCON などのデバイスにも割り当てることができる。

記号 内容
> ファイル
1> ファイル
コマンドの標準出力をファイルに書き込む。※上書き
2> ファイル コマンドの標準エラー出力をファイルに書き込む。※上書き
< ファイル ファイルの内容を標準入力として読み込む。
>> ファイル
1>> ファイル
コマンドの標準出力をファイルに追加書き込みする。
2>> ファイル コマンドの標準エラー出力をファイルに追加書き込みする。
>&2 または 1>&2
<&2 または 1<&2
標準出力を標準エラー出力に複製する。
2>&1 または 2<&1 標準エラー出力を標準出力に複製する。
| コマンド コマンドの標準出力を次のコマンドの標準入力に繋げる。

標準入出力のファイルディスクリプタ(ハンドル)番号は下記の通り。

番号 内容
0 標準入力
1 標準出力
2 標準エラー出力

ちなみにファイルハンドルの「複製」とは,たとえば下記のバッチファイルを考える。

TEST010.CMD
@echo off
setlocal
echo OK

一例として標準出力をファイル data にリダイレクトした場合,画面上には何も表示されなくなる。

TEST010.CMD の実行例その1
c:\>test010 > data
c:\>type data
OK

そこで標準出力を標準エラー出力に「複製」してやると画面上にも表示される。

TEST010.CMD の実行例その2
c:\>test010 > data 1>&2
OK
c:\>type data
OK

ただし,標準出力をファイルにリダイレクトするのを止めると文字列が複数回表示されるという訳ではないようだ。

TEST010.CMD の実行例その3
c:\>test010 1>&2
OK

リダイレクト記号の位置について

リダイレクト記号 < および > は区切り文字でもあるのでスペースを入れないで続けて記述することができる。

echo OK>data

ところが標準出力を標準エラー出力に複製するときの 1 の前には必ずスペースを入れなくてはならない。しかし,こうすると出力される文字列にスペースが入ってしまう。

echo OK 1>&2

これが嫌な場合,リダイレクト記号を先頭に持ってくればよい。

1>&2 echo OK

参考文献

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?