batの書き方メモ

はじめに

新しい言語とかフレームワークとかを勉強するとき、いつもこういった自分用ノートを書きます。 そういうわけでローカルPCにはこの手のノートをしこたま溜め込んでいるのですが、公開できない(出典をメモってないので剽窃になっちゃう)のはちょっと勿体無いのかなと思っていたりしました。

今回ちょっと思い立って、しっかり参考文献をメモりながらノートを取ったので試しに公開してみます。 勉強用ノートですので目新しいことは書いていませんし、間違ったことが書いてあるかもしれません。 もしお気づきの点があったら教えていただけると嬉しいです。

参考

目次

本編

batとは何か、どう実行するのかみたいなレベルのことは省略。

コメント

行頭にremを置くと1行分がコメントアウトする。

:から始まる行がラベルになることを利用して:をコメントとして用いることもある。ラベルと区別するために::から始める文化もあるらしい。

出力

echo <string>で1行出力。echo.(またはecho,, echo:, echo;)で空行出力。<string>に長い文字列を入れたいときには以下の通り:

rem hogehugapiyo が1行に出力される
echo ^
hoge^
huga^
piyo

<string>中で特殊文字(&<>[]{}^=;!'+,`~)を使うときには^エスケープする。

@echo off

batを実行すると各行に書かれたコマンドが逐一コンソールに表示される(echo機能)。この挙動が鬱陶しい場合にはbatの先頭に@echo offのおまじないを置くとよいとされる。

echo offが以後のecho機能をオフにするためのコマンドで、行頭の@は続くコマンドのecho機能を一時的にオフにするprefixである。

ECHO は <OFF> です。

echoを単体で使用するとecho機能のオンオフを確認できる。

この動作はechoに存在しない変数を渡したときにも見られる。予期しない出力ECHO は <OFF> です。が得られたときにはこの手のバグが疑われる。

変数

変数はset <variable>=<string>で宣言/代入し、変数名を%で囲って展開することで使用する。<string>が空白を含んだ文字列だったとしてもダブルクォートなどは必要ない。

代入演算子=の前後には 空白を入れてはいけない ので注意。正確に言えば、前に入れた空白は変数名の、後ろに入れた空白は文字列の一部とみなされるだけで、別に入れてはいけないわけでない。

rem 宣言と代入は同時に行う
set a=hoge
set b=huga

rem hogahuga
echo %a%%b%

rem 再代入にも set を用いる
set a=hoge2

rem hoge2huga
echo %a%%b%

変数と言っているが、これは正確にはcmdのプロセス終了までオーバーライドされる一時的な 環境変数 である。したがって:

  • 変数の型は文字列である。
  • bat中でsetされてなくても元々環境に存在する環境変数は展開することができる。
  • bat1で予めsetした変数は、bat1がcallで呼び出したbat2の中からも展開できる。

参照ではなくしつこく"展開"と言っているのは、%x%が行う処理が「ある1つのコマンドの実行前にプレースホルダを変数値で置換する」という処理であるからで、例えば次のような真似が可能だからである。

set hoge=a

rem %hoge% が a に展開されるので `set a=100` と等価
set %hoge%=100

rem 100
echo %a%

数値

/aオプションを使うと、右辺に数値演算表現を使うことができる。つまり、set /a <variable>=<expr><expr>部分に数値、%を省略した変数名 、演算記号(+, -, *, /, %, (), <<, >>)を含んだ式を書けるようになる。

特筆すべき点は:

  • 変数に代入されるのはあくまで<expr>が評価された結果の 文字列 である。
  • =の代わりに複合代入演算子*1も使用できる。
  • /aを適用しているときには代入演算子の後に空白を置いても機能する*2
  • 数値演算できるのは32ビット符号付き整数の範囲だけ。範囲外の数値や小数値は文字列として扱うことならできる。
  • 0x0から始めて16進数、8進数を扱うこともできる。
set /a a = 1 + 2

rem 3
echo %a%

rem 複合代入 (set /a a = a * 5 と等価)
set /a a *= 5

rem 15
echo %a%

標準入力を変数に格納する

set /p <variable>=<message><message>を表示しながら標準入力を受け付ける状態になり、入力内容が<variable>に格納される。

set /p a = "> "

変数の消去

set <variable>=で変数を消去する。

ローカルスコープ

setlocalコマンドの実行からendlocalコマンドの実行まで間で変数のスコープを区切ることができる。このスコープはネストできる。

set a=0
setlocal
    set a=1
    setlocal
        set a=2
        echo inner local scope: %a%
    endlocal
    echo outer local scope: %a%
endlocal
echo global scope: %a%

出力は:

inner local scope: 2
outer local scope: 1
global scope: 0

部分文字列

%<variable>:~<n>,<m>で変数<variable>の「先頭から数えて<n>番目までを無視した上で、(無視した文字を含まない文字列に関して)先頭から数えて<m>番目までを読む」という意味になる。

  • ,<m>は省略可能。
  • <n>, <m>には負数も設定可能。

配列

ない。代わりにa[i]という名前の変数を作れば似たようなことができる。

遅延変数展開

グループの項で後述。

特殊な値

コマンドライン引数

%0に実行ファイル名、%1, %2, …, %9コマンドライン引数、%*にすべてのコマンドライン引数が格納されている。

I番目のコマンドライン引数を%~<options>Iのオプション構文で取り出すことで、例えばパス文字列からファイル名のみを取り出したりといったことができる。以下はfor /?で参照できるヘルプからの引用:

次のオプション構文を使うことができます:
    %~I         - すべての引用句 (") を削除して、%I を展開します。
    %~fI        - %I を完全修飾パス名に展開します。
    %~dI        - %I をドライブ文字だけに展開します。
    %~pI        - %I をパス名だけに展開します。
    %~nI        - %I をファイル名だけに展開します。
    %~xI        - %I をファイル拡張子だけに展開します。
    %~sI        - 展開されたパスは短い名前だけを含みます。
    %~aI        - %I をファイルの属性に展開します。
    %~tI        - %I ファイルの日付/時刻に展開します。
    %~zI        - %I ファイルのサイズに展開します。
    %~$PATH:I   - PATH 環境変数に指定されているディレクトリを
                   検索し、最初に見つかった完全修飾名に %I を
                   展開します。
                   環境変数名が定義されていない場合、または検索
                   してもファイルが見つからなかった場合は、この
                   修飾子を指定すると空の文字列に展開されます。

修飾子を組み合わせて、複合結果を得ることもできます:

    %~dpI       - %I をドライブ文字とパスだけに展開します。
    %~nxI       - %I をファイル名と拡張子だけに展開します。
    %~fsI       - %I を完全なパスと短い名前だけに展開します。
    %~dp$PATH:I - PATH 環境変数に指定されているディレクトリを
                   検索して %I を探し、最初に見つかったファイル
                   のドライブ文字とパスだけに展開します。
    %~ftzaI     - %I を DIR コマンドの出力行のように展開します。

上の例の %I と PATH は、他の有効な値で置き換えることができます。

終了ステータス

  • 最後に実行したコマンドの終了ステータスが%ERRORLEVEL%に格納される。
  • exit /b <int32-value>で終了ステータスを設定しつつbatを終了する。
    • 成功時には0を返すべき。
    • /bは(cmdでなく)batを終了させるためのオプション

日付と時刻

%date%, %time%が利用可能。

制御構文

グループ

ifforでは処理本体が1行で記述されなければならないので、多くの言語と同じようにグループ*3を作って複数行の処理を1行に束ねる。グループは()で囲って記述する。

グループの実行にあたっては、実行前に一度だけ変数展開が行われる。したがって、次のスクリプトは期待通りに動作しない:

if 1 == 1 (
    set a=10
    set b=%a%

    rem "10" ではなく "echo は <OFF> です" と表示される
    echo %b%
)

これを防ぐには遅延変数展開を利用する。 setlocal enabledelayedexpansion~endlocalの中では、%の代わりに!で変数を囲むことができ、この変数は実際にその行が実行されるまで展開されない。したがって、先のスクリプトは次のように修正すれば期待通り動作する:

setlocal enabledelayedexpansion
if 1 == 1 (
    set a=10
    set b=!a!

    rem "10" と表示される
    echo !b!
)
endlocal

if

ifコマンドと()によるグループを使って記述する。 else if, else節を任意に付け加えることもできるが、その際には続くelse if, else節の始まりは直前の節の終わりと同じ行になければならない。

if 1 == 1 (
    echo True
rem ") else (" は同じ行に置かなければならない 
) else (
    echo False
)

if /?によると、==の代わりに以下の演算子を使うこともできる:

比較演算子は、次のいずれかです:

    EQU - 等しい
    NEQ - 等しくない
    LSS - より小さい
    LEQ - 以下
    GTR - より大きい
    GEQ - 以上

また、条件部には以下のような表現を使うこともできる:

if <string1> == <string2>
if exist <file>
if errorlevel <value>
if defined <variable> 

また、if notから始めることで条件が偽のときにグループを実行できる。

for

for <option> %%<loop-variable> in (<set>) do <command>で、適当な方法でコマンド(またはグループ)<command>を繰り返し実行する。詳細はケース別に後述。%%<loop-variable>1文字の変数 で、 <command>の中でのみ有効 である。

for /l

(<set>)(<start>, <step>, <end>)の形式を取る。一般的なfor文と同様に使う。

オプション無し

(<set>),区切りのパス文字列の集合。

パス文字列にはワイルドカード(*または?)を使うこともでき、その場合検索対象はカレントディレクトリ直下のファイルになる。例えば次の例では、カレントディレクトリ直下のファイル名を列挙する:

for %%i in (*) do (
    echo %%i
)

ファイルセットと言うものの、別にパス文字列の集合である必要はまったくなく、任意の文字列を並べて書いてよい。例えば:

rem 0 から 4 までが改行区切りで出力される
for %%i in (0, 1, 2, 3, 4) do (
    echo %%i
)

なお、%%<loop-variable>を展開するときには、コマンドライン引数の展開時と同様のオプションを使うことができる。

for /d

ワイルドカードを使った際の検索対象がカレントディレクトリ直下のディレクトリとなるだけで、他はオプション無しと同じ。

for /r

ワイルドカードを使った際の検索対象がカレントディレクトリ以下のすべてのファイルとなるだけで、他はオプション無しと同じ。

for /f

半角スペースと改行区切りの2次元マトリクスをトークナイズしながら各値について何らかの処理を行いたいときなどに用いる。デリミタは後述のオプションで自由に指定可能。詳細は以下の通り。

(<set>),区切りの

  • "で囲われた通常の文字列
  • 'で囲われたコマンド
  • パス文字列

のいずれか。それぞれ、文字列そのもの、コマンドの標準出力、ファイルの中身がトークナイズの対象になる。

%%<loop-variable>%%iとした場合には、%%i, %%j, %%k, … に各行をトークナイズした結果が格納される。

for /f "<options>"<options>に次のオプションを,または;区切りで指定することができる。面倒なのでfor /?からそのまま引用:

eol=c           - 行末のコメント文字を指定します (1 文字)。
skip=n          - ファイルの先頭でスキップする行数を指定します。
delims=xxx      - 区切り文字のセットを指定します。
                    これは、既定の区切り文字であるスペースとタブを
                    置き換えます。
tokens=x,y,m-n  - 各繰り返しに対して、各行から for 本体に渡す
                    トークンを指定します。これにより、追加の変数名が
                    割り当てられます。
                    m-n の形式は範囲で、m 番目から n 番目の
                    トークンを指定します。
                    tokens= 文字列の最後の文字がアスタリスクである場合は、
                    追加の変数が割り当てられ、最後のトークンが解析された
                    後、行に含まれている残りのテキストを受け取ります。
usebackq        - 次の新しい表示形式を指定します。
                    逆引用符で囲まれた文字列がコマンドとして実行され、
                    一重引用符で囲まれた文字列がリテラル文字列コマンドに
                    なり、ファイル セットのファイル名を二重引用符で
                    囲めるようになります。

goto

goto <label>:<label>行までジャンプする。

if "%x%" == "" goto blank

:filled
@some-command
goto end

:blank
@some-command
goto end

:end

サブルーチン

call <label> [args...]:<label>行からgoto :EOF行までを、あたかも[args...]コマンドライン引数として渡したかのように実行できる。

[args...],または;区切りで記述する。

戻り値

グローバルスコープの変数にセットして戻り値の代わりとする。または、次のようにサブルーチンに変数名を渡すことでグローバルスコープを汚染せずに戻り値を得ることもできる:

@echo off

rem bat終了後にcmdの環境変数を汚さないためのスコープ
setlocal
  call :main %*
endlocal
goto :EOF


rem 処理本体
:main
call :get10 x
echo %x%
goto :EOF


rem 10を得るサブルーチン
:get10
set %1=10
goto :EOF

batの呼び出し

  • start <bat-file> [args...]で別プロセスのcmdの中で非同期的に<bat-file>を実行する。
  • call <bat-file> [args...]で同プロセスのcmdの中で同期的に<bat-file>を実行する。

一時停止

pauseが実行されると続行するには何かキーを押してください . . .と表示され、実行が一時停止する。

nul

すべてのディレクトリに中身のないファイルnulが仮想的に存在する。nulとリダイレクトを組み合わせると色々小ネタじみた処理ができる。

  • pause > nulで何も表示せずに一時停止する。
    • pauseの出力をゴミ箱にリダイレクトしている。
  • set \p dummy=<message> < nulで改行なしで<message>を表示する。
    • set \p dummy=<message>の入力にnulを利用することで、入力受付時のメッセージだけ表示して処理を終了している。

*1:……と色々な場所でよく呼ばれるもの。batの世界でどう呼ぶのかは知らない。

*2:が、敢えて入れる理由もない気がする

*3:batの世界での正式名称は分からないので、便宜上こう呼ぶことにする