DOSバッチファイルを書くときに気をつけていること

この記事では、私自身がバッチファイルを書く時に気をつけていることについて書きます。業務でバッチファイルと関わると「不可解な挙動に心が削られる」「試行錯誤で時間を溶かす」などしますので本来ならバッチファイルに近づかないことの方が先なのですが。

そうはいってもSIerの方などで、不本意にもバッチファイルを書かざるを得ない場面に遭遇してしまうことは少なからずあると思います。そんな人がバッチファイルの落とし穴にハマって貴重な工数を溶かさないことを願ってこの記事を書きました。

なお「環境変数の遅延展開」という言葉にピンとこない方は、@sawa_tsukaさんの素敵な記事「[バッチファイル界の魔境『遅延環境変数』に挑む（おまけもあるよ）](https://qiita.com/sawa_tsuka/items/c7c477cacf8c97792e17)」を一読されることをオススメします。

#まずは記号をおさえよう
バッチファイルでは記号の扱いでドはまりすることが多いです。まずはじめに、どんな記号があるか押さえておきます。

* ファイル名に使用できない記号。→　```\ / : * ? " < > |```
* ファイル名に使用できる記号。　→　```! # $ % & ' ( ) = ~ ` {} + _ - ^ @ [ ; ] , .```

といっても、たいていの文字は`"..."`のようにダブルクォートで囲めばエスケープできるので、ダブルクォートで囲ってさえいれば特に何か注意すべきことはないのですが、例外的に`%`と`"`、遅延展開有効時の`!`は`"..."`でエスケープができません。

以下では`%`と`"`の扱い方について説明します。

##「%」について

`%`は構文解析の冒頭で以下の展開がおこなわれます。

* `%%`は`%`に展開される。
* `%変数名%`または`%%単一文字変数名`は、変数がもつ値に展開される。
* 単独の`%`は消滅する。

なので、`%`をエスケープしたいときは`%%`とすれば疑似的にエスケープできます。例えば、文字列`%foo%`をママ表示したいときは`echo %%foo%%`と書きます。このテクニックは`call`の引数に含まれる変数の事前展開を先送りしたいときにも使います。

`%`の展開は構文解析の最初に行われ、それ以降に展開後文字列に含まれる特殊記号の発動が行われるので、`%変数名%`を展開した結果の文字列にエスケープされていない特殊記号（`^``!``&``|`など）が含まれていれば、その処理が発動します。

##「"」について

文字列の値を変数に代入するときは、ダブルクォテーションを囲って代入することに決めています。変数を取りまわすとき、特殊記号のエスケープに心を砕きたくないためです。また「文字列はすべてダブルクォートで囲まれている」という前提があると精神的にも楽です。

```bat:ダブルクォテーションで囲って代入
set foo="Hello, world"
```

##文字列中に「"」は含めない
文字列を囲むものとコメント部分を除き、文字列中にダブルクォテーション（`"`）を含めることを、私的には禁止しています。なぜなら、バッチファイルでのダブルクォテーションの取り扱いは異常に難しいためです。ただ1個の`"`があるだけで、それを含む文全体のシンタックスを崩壊させる力をもつのです。

どれぐらい難しいかというと、、、
例えば、一個の`"`を含む文字列を扱ってみましょう。

```bat:×シンタックスエラー
set foo="aaa"bbb"
if %foo%=="aaa"bbb" (
    echo matched
)
```

```bat:実行結果
( の使い方が誤っています。

>if "aaa"bbb"=="aaa"bbb" (
```

上記では「`if`文の条件部に`"aaa"bbb"=="aaa"bbb"`という一続きの文字列が置かれている」と解釈されたようす。では、文字列中のダブルクォテーションを`^"`とエスケープすれば解決するかというと…

```bat:×シンタックスエラー
if %foo%=="aaa^"bbb" (
    echo matched
)
```

```bat:実行結果
コマンドの構文が誤っています。

>if "aaa^"bbb"=="aaa^"bbb" (
```

解決しないです。上記では行末の`(`まで含めて文字列リテラルとして扱われているみたいですが。

…とまあ「〇〇をしたい時、どう書くのが正解なのか」を探り当てるのに非常に時間がかかるわけです。

バッチファイルに熟練すればダブルクォテーションを自在に取りまわすやり方はあるのかもしれないですが、仮にあるとしてもトリッキーな方法であることは予想されるし、そうすると必ず事故を起こすでしょう。保守のことも考えると（文字列囲みやコメント部分以外の）ダブルクォテーションの使用は禁止としたほうが精神的に楽かと思います。

##バッチファイルのプロローグ

```bat:△よくある書き方
@echo off
cd %~dp0
```

```bat:△よくある書き方
@echo off
pushd %~dp0
```

```bat:〇罠を回避する書き方
@echo off
setlocal
set CurrentDirectory="%~dp0"
```

###解説
* 冒頭で`setlocal`しておけば、バッチ終了時までに対応する`endlocal`が実行されない場合、バッチ終了時に暗黙的に`endlocal`が呼ばれます。つまり、バッチファイル内の変数スコープが外に漏れることがありません。
* パスに`&`が含まれている場合、`set`コマンドの行で`&`より後の文字列をコマンドとみなして実行してしまいます。そのため`"%~dp0"`のようにパス全体をダブルクォテーションで囲むことにしています。
* バッチファイル自体がUNCパス（`\\ServerName\SharedFolder`形式のパス）上に配置して実行された場合、`cd /d`ではUNCパスには移動できませんので、`pushd`を使うという手法があります…が、これですとバッチファイルが異常終了したときにドライブレターの割り当てがゴミとして残ってしまう問題があります。ユーザの実行環境を汚したくない場合は`pushd`は使わず、ファイルパスは絶対パス指定で取りまわすようにしています。

##関数呼出し

```bat:△よくある書き方
    set fileName="C:\folder\%%xxx%%.txt"
    call :func %fileName%
```

`call`文の実行にさいしては構文解析が2回行われます。1回目の構文解析では変数`%fileName%`が`C:\folder\%xxx%.txt`に展開されますが、さらに2回目の構文解析で`%xxx%`が展開されます。しかし`xxx`という名前の変数は存在しないため`%xxx%`はブランクに展開され、最終的に`call :func "C:\folder\.txt"`というコマンドが実行されることになります。

そこで`call`文で引数に変数を指定する場合、`%変数名%`ではなく`%%変数名%%`とします。

```bat:〇罠を回避する書き方
    set fileName="C:\folder\%%xxx%%.txt"
    call :func %%fileName%%
```

`call`文の1回目の構文解析で`%fileName%`を展開させないため、`%%`と2つにしています。こうすれば、1回目の構文解析は`%%`が`%`に展開されるのみにとどまり、2回目の構文解析で`%fileName%`が`"C:\folder\%xxx%.txt`に展開されます。

また、call文の引数に「ダブルクォートで囲まれた文字列」を指定したとき、その文字列にサーカムフレックス（`^`）が含まれると、そのサーカムフレックス1個につき2個に増える（`^^`）という現象があります。その展開を回避する意味もあります。詳細は「[バッチファイルでの試行錯誤を回避するためのメモ#サーカムフレックスの不思議](https://qiita.com/yz2cm/items/8058d503a1b84688af09#%E3%82%B5%E3%83%BC%E3%82%AB%E3%83%A0%E3%83%95%E3%83%AC%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AE%E4%B8%8D%E6%80%9D%E8%AD%B0)」を参照下さい。

##環境変数の遅延展開で気をつけたいこと

`setlocal enabledelayedexpansion`で遅延展開を有効にした場合、`%変数名%`を事前展開した結果に`!文字列!`が含まれるとき、さらに`!文字列!`が遅延展開され、予期しない結果となる場合があります。これは単一文字変数（例：`%%A`）についても例外ではありません。

回避方法は「[SetLocal EnableDelayedExpansionの罠とその回避方法](https://qiita.com/yz2cm/items/4983be006116c369d08b)」を参照下さい。

#forやdirでのワイルドカードのマッチング

`dir *.xml`や`for %%A in (*.xml)`のようにしてワイルドカード指定で検索するとき、内部的にはファイル名の**8.3形式とロングネーム形式でのOR条件でのマッチング**がおこなわれるようです。例えば`dir *.xml`を実行してXMLファイルのみをリストアップしたつもりでも、拡張子`.xml_`のファイルもリストに含まれてしまいます。

`forfiles`コマンドの`/M`オプションはロングネーム形式でのマッチングとなりますので、検索用途では`forfiles`がおすすめです。