もっぱらLinuxなエンジニアがWindowsのバッチファイルというものを試してみた

普段Linuxで開発作業している私がWindowsのバッチファイルというものを試してみました。以下不正確なところがあるかもしれませんが、Linuxとの違いや感想を交えつつ、コードの書き方を説明します。

# バッチファイルの実行の仕方

「コマンドプロンプト」というのを開くとコマンドを実行できる。バッチファイルはファイル名が `.bat` で終わっていればファイル名を指定してエンターするだけで実行できる。Linuxのように実行権限の設定とかはなさそう。また、Linuxと違ってカレントディレクトリにあるバッチファイルはPATHが通ってなくても `./` のようなものを付けなくてもファイル名だけで実行できる。

「コマンドプロンプト」はWindowsスタートメニューの「Windows システム ツール」の中にある。

# 先頭に書くおまじない

バッチファイルは実行時に実行するコマンドがいちいち表示される。Bashでいう `set -x` の状態に近い。

`set -x` を解除するには `echo off` というコマンドを呼ぶ。またはコマンドの先頭に `@` を付けると、そのコマンドだけ `set -x` 解除状態で実行される。バッチファイル全体で `set -x` 解除状態にしたい場合は、バッチファイルの先頭で `echo off` を呼ぶことになるが、  `echo off` と書くと `echo off` と表示されてしまう。これを防ぐために `@echo off` と最初に書くのがバッチファイルの習わしになる。

後述するが `setlocal enabledelayedexpansion` という1行も最初のほうに書いておくのがよさそう。

Linuxのようなshebangはない。

```bat
@echo off
setlocal enabledelayedexpansion
```

# コメント

`REM` というコマンドがコメントを表すらしい。コメントがコマンド？！

```bat
REM ここはコメント
```

コマンドなので、上記 `@echo off` がない場合は、コメントが実行時に表示されてしまう。

コマンドなので、行の途中からコメントを入れることもできない。

`:` を行の先頭に書くと本来はラベルを表すらしいが、参照されなければなにも意味がないので、コメントの代わりに使うのもありらしい。

```bat
: ここはコメント
```

複数行のコメントは、ラベルでジャンプしてしまえばいいらしい。強引だ。

```bat
goto EOC
コメント1
コメント2
:EOC
```

# 変数

変数への代入は

```bat
set var1=Hello
```

ダブルクオーテーションで囲もうとしても、ダブルクオーテーション自体も文字列の一部として変数に含まれる。Bashとは違う。

```bat
set var1="Hello"
```

文字列の一部に空白があってもいいらしい。見た目がちょっと違和感あるが。

```bat
set var1=Hello World
```

`=` の前後にスペースを置けないのはBashと同じだ。

変数の参照は変数名の先頭と最後に `%` を付ける。

```bat
echo %var1%
```

変数が空だと変数展開されない様子。

```bat
set var1=
echo %var1%
```

`%var1%` とそのまま表示されてしまう

# ディレクトリ階層の区切り文字

ファイルやディレクトリを参照するときの区切り文字は `/` ではなく `\ ` 。バックスラッシュは円記号で表示されるかもしれない。 `..` はLinuxと同じ意味で使える。Linuxでいう `ls` は `dir` だ。

```bat
dir ..\
```

# if文

```bat
if %var1% == Hello (
  echo World
)
```

括弧の中はif文判定前に変数展開されてしまうので、以下は期待した動きにはならない。

```bat
set var1=Hello
if %var1% == Hello (
  set msg=World
  echo %msg%
)
```

msgへの代入前にechoの引数を展開しようとして、その段階ではまだ変数が代入されていないので、 `%msg%` とそのまま表示される

バッチファイルの中で `setlocal enabledelayedexpansion` と宣言して、 `!` で変数名を囲むと、if分岐の中も順番に変数展開してくれる。これは遅延変数と言うらしい。そんな特別な名前の付く機能でなくてもBashとかは普通に期待通りに動くのに。。。

```bat
setlocal enabledelayedexpansion

set var1=Hello
if %var1%==Hello (
  set msg=World
  echo !msg!
)
```

この場合は `World` と表示される。 `setlocal enabledelayedexpansion` はコマンドプロンプトで直接試そうとしてもできないようだ。バッチファイルに書くことで確認できた。

遅延変数はfor文でも必要になってくる。

面倒だから変数の参照は全部遅延変数にしてしまおうと思ったが、if文やfor文の外側では遅延変数は使えない様子。

# 日時

`date`, `time` という変数のようなものを参照すると、参照したときの日時が取得できる。以下は実行するごとにその時の時刻が表示される。

```bat
echo %time%
```

自分の環境ではこれで `17:47:10.90` のような表記で出力された。

以下のように書くと時刻の部分文字列を取得できる。

```bat
echo %time:~0,5%
```

これで `17:47` になる。夕方である。

次の例では分だけを得る。

```bat
echo %time:~3,2%
```

if文やfor文の中で時間かかる処理をする場合は時刻も `!time!` と遅延変数のようにすることが必要。

# ファイルへの書き込み

`>` や `>>` はBashと同じように使えるようだ。

```bat
echo Hello >> log.txt
```

最後の改行はWindowsらしく `\r\n` の2バイトである。あと、なぜか最後改行直前に空白(`\x20`)が入る。

後日追記：以下のように `>>` の前を詰めれば空白は出力されないらしい。 `>>` 直前の空白が出力されるなんて意外すぎる。

```bat
echo Hello>> log.txt
```

もしくは書く順番を変える。

```bat
>> log.txt echo Hello
```

か、カッコで囲む。

```bat
(echo Hello) >> log.txt
```


# for文

サンプル

```bat
for %%a in (111 222 333) do (
    echo %%a
)
```

これで以下の出力になる。

```plain
111
222
333
```

`%%a` というのがfor文の中だけで使える特別な変数。 `%%` のあとに続くアルファベットは、なんと1文字しか使えない！

`/f` というオプションでファイルの中身を行単位で実行できる。

`input.txt` が以下の内容だとして、

```plain
111,aaa,xxx
222,bbb,yyy
```

`/f` を使ってみる。

```bat
for /f %%a in (input.txt) do (
    echo Line: %%a
)
```

これで以下のような出力になる。

```plain
Line: 111,aaa,xxx
Line: 222,bbb,yyy
```

`/f "tokens=1,2,3 delims=,"` というオプションを書くと、行をカンマ区切りで3列分を取得してくれる。3列分の文字列がどこに保管されるかというと、 `%%a` と書いたら `%%a`, `%%b`, `%%c` というアルファベット順に3つの変数が勝手に作られる。 `%%b` と書いたら `%%b`, `%%c`, `%%d` になる。変態的だ。 `%%z` と書いたらどうなるかはわからない。

```bat
for /f "tokens=1,2,3 delims=," %%a in (input.txt) do (
    echo Col1: %%a
    echo Col2: %%b
    echo Col3: %%c
)
```

```plain
Col1: 111
Col2: aaa
Col3: xxx
Col1: 222
Col2: bbb
Col3: yyy
```

# コマンドの終了コード

`%ERRORLEVEL%` という変数で直前のコマンドの終了コードを得られる。0が正常で、それ以外は異常終了。Bashの `$?` みたいなものか。

if文やfor文の中では遅延変数を使って `!ERRORLEVEL!` としないと予期しないことになる。

# コマンドの呼び出し方：サンプルとしてfindコマンド

`find` というコマンドは、Linuxでいう `find` ではなく `grep` だ。指定のファイルから文字列を検索してマッチした行のみを出力できる。Linuxとは違って余計なお世話な出力があり機械処理はしづらい。

パラメータの指定の仕方がBashと違っていて、ちょっと予想外な動きをする。

```plain
echo Sample1
find xxx input.txt

echo Sample2
find "xxx" input.txt
```

出力はこうなる。パラメータのダブルクオートがあるかどうか区別している様子。Bashに慣れているとダブルクオートを外してからコマンドに渡すイメージだけど、コマンドプロンプトではコマンド側で解釈しているのだろうか。

```plain
Sample1
FIND: パラメーターの書式が違います
Sample2

---------- INPUT.TXT
111,aaa,xxx
```

後日追記：
ダブルクオートどころか、空白も含めてまるごとコマンドに渡されているっぽい。

`findstr` というコマンドもあるらしい。あと、 `where` というコマンドがLinuxでいう `find` に近いらしい。