知られざるバッチファイル

概要

この記事は Windows の「バッチファイル」ついて、 あまり一般的でない変な切り口で説明しています。

バッチファイルは MS-DOS の頃から存在し、 互換性を維持したまま Windows とともに進化しています。 この記事では、過去と互換性の無い新しい機能をむしろ積極的に使って、 書きやすく読みやすいバッチファイルを書くための知識を紹介したいと思います。 動作に必要な条件は正確には調べていませんが、 Windows 2000 以降で動作すると思います。

バッチファイルとは

バッチファイル (Batch File) とは、 一括して実行したい処理の内容を書いたファイルを指します。 バッチファイルに連続して実行するべき命令をあらかじめ記しておくと、 後で記述した命令を「再生」できます。 この仕組みは定型的な命令を手軽にかつ確実に連続実行するのに役立ちます。

MS-DOS は UNIX のようなコマンド型のインタフェーイスを備える OS であり、 キーボードから命令(コマンド)を入力する事であらゆる操作を行います。 こういった環境では決まりきったコマンドの流れで操作を行う事がよくあります。 たとえば「C ドライブの temp というディレクトリに移動して、 その中にある拡張子が tmp であるファイルを削除する」といった具合です。 MS-DOS でこれを実行する場合、普通は 3 つの命令を実行します。 このような処理をたとえば起動直後に実行しなければならないとすると、 毎回起動するたびに同じ 3 つの命令を打ち込まなければならず面倒です。 そこで、毎回実行すべき命令をバッチファイルに書いておけば 毎回起動時にそのファイルを再生するだけでその処理を簡単に実行できます。 たとえばこういった場面で、バッチファイルは役に立つのです。

バッチファイルの書き方の基本は、 一行ごとに実行すべき一命令を書く事です。 たとえば先程の例に挙げた定型処理は次のようなものでした。

C ドライブの temp というディレクトリに移動して、 その中にある拡張子が tmp であるファイルを削除する

この処理は、次の 3 命令で実行できます。

  1. c:
  2. cd \temp
  3. del *.tmp

これを実行するバッチファイルは次のようになります。

c:
cd \temp
del *.tmp

前述の通りコマンドプロンプト上で実行する命令を単純に一行ずつ書けば良いので、 ただ命令を連続実行するだけのバッチファイルは非常に単純に書く事ができます。 バッチファイルを実行するには、 コマンドプロンプト上でそのファイル名を入力して実行します (Windows ではファイルアイコンをダブルクリックしても実行できます)。 つまり、新しいコマンドが追加されたような感覚で実行できると言えます。 また実行するに先立ってコンパイルするなどの必要はありませんので、 バッチファイルは非常に単純なスクリプト言語の一種と考えて良いでしょう。

コメントの書式

多くのプログラミング言語では ソースコードに書いてある処理が何をしているのか説明したり、 注意しなければならない点を補足するなどの目的で プログラムの処理に影響しない「コメント」の記述をサポートしています。 バッチファイルにも、同じようにコメントを書き入れる事ができます。

バッチファイル中にコメントを書くには、 行頭にコロンを 2 つ書くか行頭に rem と書きます。 コロン 2 つ、あるいは rem で始まる行は実行中に無視されるため、 コメントとして自由にヒントを書き込めます。 次に例を挙げます。

:: hoge と表示する
echo hoge

rem piyo と表示する
echo piyo

私個人としては、 rem によるコメントは読みにくいと考えています。 なぜなら rem のスペルを読み終わるまでの間、 それが命令なのかどうかを判断できないからです。 この間はほぼ一瞬です。 しかし一瞬でも意識を奪われる可能性があるのであれば良い記法とは言えません。 一方でコロンは記号ですから命令の名前ではありえません (MS-DOSやWindowsでは)。 したがってバッチファイル中に現れるコロンはラベルの宣言かコメント開始のどちらかであり、 プログラム的な意味が無いものだとすぐに理解できます。 この点で、私はコロン二つの方法が良いと思っています。

ところでバッチファイルにおいてコロンで始まる行はラベル (GOTO の章にて後述) の宣言です。 すると、 コロン二つで始まる行は「コロンで始まるラベル」を宣言する行となり、 結果的に何も起こっていないように見えるのではないか、 と疑うこともできます。 とはいえ、仮にそうだとしても、 現在のコンピュータ性能を考えれば意味の無い心配でしょう。 無駄になるリソースも問題にならないほど少ないですし、 ラベルの名前がバッティングする事も普通はありません。 むしろコメントの読みやすさがもたらす利益の方が大きい、 というのが私の考えです。

(なお、コロン二つは恐らくラベル定義です。 後述する if や for 中でのコロン二つによるコメントが上手に動作しない場合があるのはそのためだと思います。 しかしラベルとして考えた場合、 後述の goto 命令でジャンプしようとしてもジャンプできないなど不可解な点もあります。 結局のところ、厳格な仕様も無いバッチファイルの細かい点はあまり深追いしない方が良いのではないでしょうか。)

変数

バッチファイルではシステム共通の「環境変数」を扱えるため、 これを使ってバッチファイル中で値を保存・参照する事ができます。 次に、環境変数 MyVar に文字列を設定して表示するバッチファイルの例を示します。

set MyVar=Hello, my name is YAMAMOTO Suguru. Nice to meet you :)
echo %MyVar%

環境変数に値を設定するには SET 命令を使います。 具体的には SET に続けて変数名を書き、 その後ろに = 記号、そして設定する値を書きます。 環境変数に設定された値を参照するには変数名を % 記号で挟んで書きます。

ところで環境変数はシステム共有であり、 不用意に変更すると影響がシステム全体に及んでしまいます。 そこで、実行中のバッチファイルの中だけで有効な環境変数を作る仕組みが用意されています。 この仕組みを利用するには、まず SETLOCAL 命令を実行し、 その変数を使い終わったら ENDLOCAL 命令を実行します。 次に例を示します。

:: システム共通の変数を表示(出力は "" と "Windows_NT" など)
echo "%Hoge%"
echo "%OS%"

:: 環境変数のローカル化を開始
setlocal

:: ローカルな変数に値を設定
set Hoge=piyo
set OS=Linux Is Not UNix

:: ローカルな変数を表示(出力は "piyo" と "Linux Is Not UNix...")
echo "%Hoge%"
echo "%OS%"

:: 環境変数のローカル化を終了
endlocal

:: システム共通の変数を表示(出力は "" と "Windows_NT" など)
echo "%Hoge%"
echo "%OS%"

実際に使用する場合は、 バッチファイルの最初で SETLOCAL を呼ぶ形になると思います。 なお ENDLOCAL はバッチファイルを終了するタイミングで自動的に実行されるため、 省略可能です。

変数使用時の注意点

バッチファイル中で変数を使用するにあたって一つ重要な注意点があります。 多くのプログラミング言語では、プログラム中の変数の値はアクセスするたびに評価されるものです。 しかしバッチファイルでは、 各行ごとに、実行の直前に 「行中にある環境変数の部分を変数の値にテキスト置換」 した上で実行するような結果になります。 実装がどのようになっているかは知りませんが、 これは値が設定されていない変数を参照する場合にバッチファイル特有の問題を引き起こします。 例として、次のようにプログラムのパスを環境変数に記録しておき、 その変数を使ってプログラムを実行する場合を考えます。

%MyProgram% some arguments

もし MyProgram という環境変数に値が定義されていなかった場合、 %MyProgram% の部分がコード上から削除された上で実行されると考えられます。 すると、この一行は次のような文と解釈されてしまいます。

 some arguments

結果、本来は第一引数だった some というトークンが命令と誤って解釈され、 実行されてしまいます。 このように、参照する変数が空だった場合に処理の内容が変化してしまう事があるため、 この点はよく注意する必要があります。 なおこれについては IF の章で改めて触れます。

IF による条件分岐

条件に応じて処理を分岐させるには、IF を使います。 書式の例は次のようになります。

IF 条件 (
    条件が真の場合に実行する処理1
    条件が真の場合に実行する処理2
    ...
)

IF は文字列の比較と、ファイルの存在確認に使えます。

IF の条件を記述する

(まだ書いていません)

文字列比較による分岐

たとえば環境変数 OS が Windows_NT という値かどうか判定、 結果に応じて違うメッセージを表示するバッチファイルを書くと次のようになります。

if "%OS%" == "Windows_NT" (
    echo This system is
    echo running on NT technology.
) else (
    echo This system is NOT
    echo running on NT technology.
)

なお書式にはあまり自由が無く、たとえば else の直後に空白を入れずに開き括弧を続けて書くとエラーになったりします。 また C 言語などとは違って改行に意味がありますので、 C のつもりで次のように書いたりすると文法エラーになります。。

:: 文法エラーになる例
if "%OS%" == "Windows_NT"
(
    echo This system is
    echo running on NT technology.
)
else
(
    echo This system is NOT
    echo running on NT technology.
)

プログラムの終了コードを判定

Windows で動作するすべてのプログラムは終了する際に数値を Windows に返します。これはプログラムの終了コードなどと呼ばれ、 プログラムが終了した理由を表すために使われます。 もしバッチファイルの途中で実行したプログラムが何らかの理由で失敗したら、 その後の処理は成功した場合と異なる事がほとんどです。 そこで、 バッチファイルでは直前に実行したプログラムの終了コードを判定する手段が用意されています。

バッチファイルで実行したプログラムの終了コードは、 自動的に ERRORLEVEL という名前の環境変数に格納されます。 そのため、プログラムの終了コードの判定には 通常の環境変数に対する操作と同じものが使えます。 例として、動作が成功した時に終了コード 0 を返すプログラム Hoge.exe があるとしましょう。 これを実行し、成功したかどうかを判定する例を次に示します。

Hoge
if not "%ERRORLEVEL%" == "0" (
    echo 成功
) else (
    echo 失敗
)

ファイルの存在確認による分岐

(まだ書いていません)

FOR によるループ

バッチファイルには純粋なループという概念はありませんが、 FOR を使うとあらかじめ決められた対象にのみループを回す事ができます。 FOR は結構多くの事を実現できます。 ここでは、私がバッチファイルで使えると便利だと思うものだけ紹介します。 すべての機能を調べたい方は FOR /? と実行すると詳細な解説が表示されます。 そちらをご覧ください。

FOR はファイルシステム上のファイルやディレクトリの名前をループで処理できます。 その場合、次のような書式を使います。

FOR オプション %%変数 IN (検索パターン) do (
    処理
)

検索パターンはワイルドカードをカッコで囲って指定します。 なお、FOR で使う変数の名前は一文字である必要があります

FOR を実行すると、 検索パターンにしたがって検索されたファイルの名前が変数へと格納され、 見つかったファイル名ごとに処理部が実行されていきます。 続いて実例を挙げていきたいと思います。

ファイル検索

カレントディレクトリにある全ファイルの名前を表示するバッチファイルは次のようになります。

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

少し応用して、 親ディレクトリにあるすべての .txt ファイルの中身を表示するバッチファイルを書くと、 次のようになります。

for %%i in (..\*.txt) do  (
    type %%i
)

ディレクトリ検索

オプション /D を使うと検索対象がファイルではなくディレクトリになります。 たとえば親ディレクトリにある a で始まるディレクトリを表示するバッチファイルは次のようになります。

for /D %%i in (..\a*) do  (
    echo %%i
)

少し応用して、カレントディレクトリの各サブディレクトリに入って _build.bat というバッチファイルを次々に実行していくバッチファイルを書くと、 次のようになります (この例では _build.bat が存在しなければスキップするようになっている)。

for /D  %%d in (*) do (
    if exist %%d\_build.bat (
        cd %%d
        call _build
        cd ..
    )
)

ディレクトリ構造の再帰検索

オプション /R に続けてディレクトリ パスを指定すると、 そのディレクトリ以下のディレクトリ エントリを再帰的に検索できるようになります。 親ディレクトリ以下にある .tmp ファイルを再帰的に検索して削除するバッチファイルは次のようになります。

for /R .. %%i in (*.tmp) do  (
    del %%i
)

なお、カレントディレクトリ以下を検索する場合には 検索ルートの指定は省略できます。 たとえば、カレントディレクトリ以下にある temp という名前のディレクトリをすべて削除するバッチファイルを書くと、 次のようになります。

for /D /R %%i in (temp) do  (
    rmdir %%i
)

検索結果への参照

FOR で取得する検索結果は指定した変数に格納されますが、 環境変数とは違って中身がただの文字列ではありません。 参照の書式を変更する事で、 検索結果のファイルに関する多くの情報を取得できます。

参照用変数を %%i とした場合、 FOR による検索結果への参照方法には次のようなものがあります(一部)。

たとえば、C:\Program Files 以下にあるすべての readme.txt のフルパスを表示するバッチファイルを書くと、 次のようになります (厳密には readme で始まるテキストファイルを検索)。

for /R "C:\Program Files" %%i in (readme*.txt) do  (
    echo %%i
)

ワイルドカードを含まない検索パターンを指定すると FOR の意味が変わります。 その場合、次節で紹介する文字列についてのループに変化してしまいます。 注意してください。

PATH の通ったディレクトリからファイルを検索

本題に入る前に、FOR のもう一つの使い方を紹介しておきます。 先ほどまで検索パターンを書いていた部分に複数の文字列をカンマ区切りで書くと、 その各文字列に対してループを回す事ができます。書式は次のとおりです。

for %%変数 in (文字列, 文字列, ...) do (
    処理
)

たとえば、hoge、foo、barと続けて表示するバッチファイルは次のように書けます。

for %%i in (hoge, foo, bar) do (
    echo %%i
)

また、 ファイル検索の際にワイルドカードを含まず既知のファイル名を指定すると、 こちらの構文と受け取られてしまってまるで違う結果が得られます。 その点は注意してください。

では本題に入ります。 FOR の検索結果への参照には一つだけ非常に特殊な方法があります。 それは、「FOR の検索結果を参照する際、 結果の文字列をファイル名と見立てて PATH 環境変数で指定された各ディレクトリから同名ファイルを検索、 最初に見つかったものを返す」というものです。 分かりにくいので、例を挙げます。

for %%i in (javac.exe) do (
    echo The path of javac.exe in this system is:
    echo %%~$PATH:i
)

これを実行すると、パスが通った javac.exe のフルパスが表示されます。 仕組みは次のようになっています。

  1. javac.exe という文字列が %%i に代入される
  2. 1行目が表示される
  3. javac.exe というファイルをパスの通ったディレクトリから検索し、 最初に見つかったもののフルパスで %%~$PATH:i が置き換えられる
  4. そのパスが2行目に表示される

これを応用すると、 特定言語のコンパイラがあるかどうかを判定する、 といった処理も可能になります。 たとえば、javac.exe がパスの通った場所にある場合にのみコンパイルを実行するバッチファイルを書くと、 次のようになります。

@echo off

:: javac.exe が使えるか判定
for %%i in (javac.exe) do (
    :: %%~$PATH:i が空文字かどうか判定
    if _%%~$PATH:i == _ (
        :: JDK の環境が整っていない
        echo javac.exe not found.
        echo failed to compile.
        goto :EOF
    )
)

:: コンパイル実行
javac *.java

GOTO による制御のジャンプ

GOTO は、「実行している行」を指定した行にジャンプさせる命令です (バッチファイルはファイル先頭から末尾に向かって 一行ずつ実行する事を思い出してください)。 昔からバッチファイルでは GOTO 命令を使って処理の流れを制御してきました。 なお、プログラミングの世界では GOTO は悪玉であり使うべきでないなどと言われますが、 バッチファイルで実現できる制御は貧弱なので (苦笑) 高級言語とは話が違っています。 つまり、IF および FOR では書ききれない場面が多くあるため、 ある程度複雑な処理をするバッチファイルでは GOTO の使用を避けられません。 もっとも、可能であれば、そのように複雑な処理は Perl、 Python、 Ruby などのスクリプト言語を使うべきだとは思います。

GOTO は「実行している行」をジャンプさせる命令で、 一部の処理を実行させずに無視する目的などに利用します。 あらかじめ「ラベル」と呼ばれる目印をジャンプしたい行に書いておき、 GOTO に続けてそのラベル名を書きます。 ラベルの書き方は、コロンに続けてラベル名を書きます。 次に例を示します。

echo この行は実行されますが、
goto END
echo この行は実行されません。
:END
echo 終了

特殊なラベル

GOTO のジャンプ先に指定できるラベルには、一つだけ特殊なラベルがあります。 そのラベルは「:EOF」という名前で、 実行中バッチファイルの最後の位置を指すものです。 これは、たとえばエラー発生後に残りの処理を飛ばして終了する場合などで手軽に利用できます。 次に例を示します。

:: Hoge る
Hoge
if not "%ERRORLEVEL%" == "0" (
    echo Hoge れませんでした。終了します。
    goto :EOF
)

:: Piyo る
Piyo
if not "%ERRORLEVEL%" == "0" (
    echo Piyo れませんでした。終了します。
    goto :EOF
)

...

この例では C 言語の return 文と同じように使っていますが、 実質 "goto :EOF" は C 言語の return とほぼ同じと考えて問題ありません。

コラム - 昔ながらの制御構文

(この章の内容には個人的な予想が含まれています。 間違ってるかもしれませんのでご注意ください。)

昔のバッチファイルでは IF による条件分岐が可能でしたが、 分岐後の内容を一行で記述する必要がありました。 これは、IF が制御構文ではなくコマンドである事に由来しています。 IF のある行の実行は、 次のような内容のコマンドラインプログラム IF.EXE の実行と同じ、 と言い換えても大きく間違ってはいません。

  1. 引数の前半を条件として評価
  2. 条件が真であれば、引数の後ろを C 言語の shell 関数などでそのまま実行

もし処理を違う行に書いたなら、 それは引数として IF に渡されません。 こう考えると、 IF の条件が真であった場合の処理を同じ行内に書く必要があるのは当然だと分かります。 このような制約があるため、 分岐後に複雑な処理を実行するには次のように面倒な手順を踏んでいました。

  1. 分岐後の処理を別の場所に書く
  2. その場所にラベルを付ける
  3. IF 文では条件に合致すると GOTO でそのラベルにジャンプするように

たとえば、先ほどと同じように、 環境変数 OS が Windows_NT という値かどうか判定、 結果に応じて違うメッセージを表示するバッチファイルを書くとしましょう。 昔ながらのバッチファイルの世界では、次のように書きます。

rem 環境変数OSの値に応じて処理を分岐
if  %OS% == Windows_NT  goto nt_system
else  goto non_nt_system

rem NT系の場合
:nt_system
echo This batch file is
echo running on NT technology.
goto endif_os_check

rem 非NT系の場合
:non_nt_system
echo This batch file is NOT
echo running on NT technology.
goto endif_os_check

:endif_os_check

このバッチファイルに対し、私が嫌だと感じる点は次のように3つあります。

これらの「嫌な点」が現れるのは、昔の文法では処理を構造化して記述できないためです。 言い換えると、複数の処理を一つの単位としてまとめられない事が原因です。 もし複数の処理を一つの単位にまとめられるのであれば、 「条件に合致した場合は『この処理』を実行する」 という記述ができます。 しかし、それができないので、 結果的にそれと同等の結果になるようなロジックを書いて解決するしかありません。 ですから、読みにくいのは当然と言えます。