バッチファイルでよく使う書き方まとめ

# はじめに
バッチファイルでちょっとしたバッチ処理やラッパーを書くときに「よく使ってるなーこれ」という書き方を、備忘録がてらまとめてみました。

# テンプレート(1) 引数を受け取らないバッチファイル
そらで書けるくらいよく使います。

## 内容

```bat
@echo off
setlocal

pushd "%~dp0"
rem ここに処理を書く
rem ここに処理を書く
rem ここに処理を書く
popd

pause
```

## 解説
目的1: 「バッチファイル自身のコンテキスト」と「呼び出し元のコンテキスト」を混同しない、汚染しない

- `setlocal` で set 文による環境変数のローカル化（呼び出し元を汚染しない）
- `pushd "%~dp0"` でこのバッチファイルのディレクトリに移動し、最後に `popd` で撤収。こうすればこのバッチファイルの存在するディレクトリを基点として、周辺のファイルに相対パスでアクセスできる

目的2: 表示をすっきりさせる

- `@echo off` で「実行されるコマンド」が標準出力に出るのを抑止する（これしないと画面が見辛い）

目的3: 実行結果を読む

- `pause` を実行させてキーを押すまで画面を停止させる（これしないとダブクリで実行した時にすぐにウィンドウが消えて何が起きたのかわからない）

# テンプレート(2) 引数を受け取るバッチファイル
バッチファイルでコマンドを作る時に使うテンプレートです。（というより「引数判定やサブルーチン部分はこんな感じで書けば動くよ」というメモに近いですが :sweat:）

## 内容
addcommand.bat

```bat
@echo off
setlocal
set selfname=%~n0
set ret_success=0
set ret_fail=1

if "%1"=="" (
	call :usage
	exit /b %ret_success%
)

if "%1"=="help" (
	call :usage
	exit /b %ret_success%
)

if "%2"=="" (
	call :abort Parameter `op2` required.
	exit /b %ret_fail%
)

set /a ret=%1+%2
echo %ret%

exit /b %ret_success%

rem ----------
rem subrootins
rem ----------

:usage
echo [Usage]
echo %selfname%          : Show the usage.
echo %selfname% help     : Show the usage.
echo %selfname% op1 op2  : Print the result of `op1+op2`
exit /b

:abort
echo (%selfname%) Error! %*
exit /b
```

実行結果

```
$ addcommand
[Usage]
addcommand          : Show the usage.
addcommand help     : Show the usage.
addcommand op1 op2  : Print the result of `op1+op2`

$ addcommand help
[Usage]
addcommand          : Show the usage.
addcommand help     : Show the usage.
addcommand op1 op2  : Print the result of `op1+op2`

$ addcommand 1
(addcommand) Error! Parameter `op2` required.

$ echo %errorlevel%
1 ★失敗を表す戻り値

$ addcommand 1 2
3

$ echo %errorlevel%
0 ★成功を表す戻り値

```

## 解説
- 引数判定
  - `if "%1"==""` こんな感じ
  - ダブルクォートは必要（無いと値によっては構文エラーでハマる）
- コマンドの戻り値
  - `exit /b 戻り値` で返す
  - 呼び出し元から見たいなら `%errorlevel%` を調べる
- サブルーチンは積極的に使って、少しでも見やすく

# 標準入出力

## メッセージ付きで標準入力を受け付ける
`set /p` を使う。

yourname.bat

```
@echo off
setlocal
set /p yourname="お名前を入力してください>>>"
echo あなたのお名前は %yourname% です
```

実行結果

```
$ yourname
お名前を入力してください>>>まいける
あなたのお名前は まいける です
```

## 改行のみ標準出力する
`echo.` を使う。

```
$ echo 改行も標準出力されます
改行も標準出力されます

$ echo.


$
```

## 改行せずに標準出力する
`set /p` と `<nul` を使う。

```bat
echo この行は改行されて表示される
echo この行は改行されて表示される
set /p dummyvar=この行は改行されずに表示される<nul
set /p dummyvar=この行は改行されずに表示される<nul
```

結果

```terminal
この行は改行されて表示される
この行は改行されて表示される
この行は改行されずに表示されるこの行は改行されずに表示される
```

※余談。なぜ `<nul` があるとこのような動作になるかはまだ理解できてません。完全なるおまじない状態です :sweat:

## 標準出力をクリップボードにコピー
パイプで `clip` に渡す。

```bat
$ echo %date% %time% | clip
```

# 数値計算

## 乱数（精度は期待しないこと）
`%random%` 変数で乱数アクセスしつつ、`set /a` 表記で剰余計算( `%%` 演算子)して範囲を絞る。

print_random_0to49.bat

```bat
@echo off
setlocal

set /a rnd=%random%%%50
echo %rnd%
```

実行結果

```

$ print_random_0to49.bat
31

$ print_random_0to49.bat
11

$ print_random_0to49.bat
41

$ print_random_0to49.bat
27
```

## 数値計算
`set /a VARNAME=(数式)` を使う。

```bat
set /a a=1+2*3
echo %a%             # 7
set /a a="2<<5"
echo %a%             # 64
set /a a="10&3"
echo %a%             # 2
set /a a=1+2*3, b=a
echo a=%a% b=%b%     # a=7, 7
```

細かい文法はヘルプ `set /?` を見る。

# ファイルパス操作

## 自身に渡されたパスを一つずつ処理する
`for` 文を使う。

each_file_of_arguments.bat

```bat
@echo off

for %%f in (%*) do (
	echo [%%f]
)
pause
```

実行結果（コマンドラインから呼び出した場合）

```
$ each_file_of_arguments.bat datetime.bat string_replace.bat
[datetime.bat]
[string_replace.bat]
```

実行結果（each_file_of_arguments.bat に D&D した場合）

```
[C:\work\bat\datetime.bat]
[C:\work\bat\string_replace.bat]
続行するには何かキーを押してください . . .
```

## 自身に渡されたパスから一部（ファイル名のみとか）を取り出す
例として 0 番目の引数（自身のパス名）をサンプルに。

C:\work\bat\selfname.bat

```bat
@echo off
echo %%0   : %0
echo %%~0  : %~0
echo %%~f0 : %~f0
echo %%~d0 : %~d0
echo %%~p0 : %~p0
echo %%~n0 : %~n0
echo %%~x0 : %~x0
echo %%~dp0 : %~dp0
echo %%~nx0 : %~nx0
```

実行結果

```
%0   : selfname
%~0  : selfname
%~f0 : C:\work\bat\selfname.bat
%~d0 : C:
%~p0 : \work\bat\
%~n0 : selfname
%~x0 : .bat
%~dp0 : C:\work\bat\
%~nx0 : selfname.bat
```

1番目の引数について取り出したいなら `%~dp1` のように指定する。

# 外部ファイル呼び出し

## ノンブロッキングにファイルを開く
`start "" (FILEPATH) ARGUMENTS` を使う。

nonblocking.bat

```bat
@echo off
setlocal

c:\windows\notepad.exe
"c:\windows\notepad.exe" "%cd%\newfile.txt"

start "" c:\windows\notepad.exe
start "" "c:\windows\notepad.exe" "%cd%\newfile.txt"
```

実行結果

- メモ帳が起動する
- 開かれたメモ帳を閉じると、再びメモ帳が起動する（ `(カレントディレクトリ)\newfile.txt` を開こうとする）
  - ここまではブロッキングの動作
- 開かれたメモ帳を閉じると、上記二つが **一気に** 開かれ、バッチファイルも終了する
  - これはノンブロッキングの動作

紛らわしいが `start ""` この第一引数のダブルクォートまでが決まり文句。この次からパスと引数を書く。

## エイリアス
`doskey` コマンドを使う。

alias.bat

```
@echo off
doskey pwd=cd
doskey np="c:\windows\notepad.exe" $*
```

実行結果（エイリアス登録前後を比較）

```
$ pwd
'pwd' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

$ np "%cd%\newfile.txt"
'np' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

$ alias

$ pwd
C:\work\bat

$ np "%cd%\newfile.txt"
メモ帳が立ち上がって
「ファイル C:\work\bat\newfile.txt が見つかりません。新しく作成しますか?」
のようなダイアログが出るはず。
```

## ワンライナー（複数のコマンドを一行で実行）
`&` で繋げる。

```
$ echo %date% & echo %time% & hostname & echo 3秒タイムアウトします & timeout 3
2018/01/29
16:10:50.03
COMPUTER-XXXXX
3秒タイムアウトします

0 秒待っています。続行するには何かキーを押してください ...
```

# コマンドプロンプト表示制御

## タイトル文字列を変える
`title (文字列)` を使う。

## プロンプトを `$ ` にする
`prompt $$ ` を使う。

```
C:\Users\USERNAME1>prompt $$ 

$ 
```

## ウィンドウサイズを変える
`mode con: cols=72 lines=16` コマンドを使う。

cols と lines の単位はよくわからない。トライアンドエラーすれば何となく慣れる。

（追記） [cmd_resizer](https://github.com/stakiran/cmd_resizer) ←ウィンドウサイズを対話的に変えるバッチファイルを作ってみました。

# 文字列操作

## 現在日時文字列を得る
datetime.bat

```bat
@echo off
setlocal

set datebase=%date%
set timebase=%time%

set shortdate=%datebase:/=%
set shortdate=%shortdate:~2,6%
set shorttime=%timebase::=%
set shorttime=%shorttime:~0,6%
set shortdatetime=%shortdate%_%shorttime%
echo Short Format(YYMMDD_HHMMSS)     : %shortdatetime%

set longdate=%datebase%
set longtime=%timebase:~0,8%
set longdatetime=%longdate% %longtime%
echo Long Format(YYYY/MM/DD HH:MM:SS): %longdatetime%
```

実行結果

```
Short Format(YYMMDD_HHMMSS)     : 180129_131222
Long Format(YYYY/MM/DD HH:MM:SS): 2018/01/29 13:12:22
```

## Slice やら Replace やら
string_operation.bat

```bat
@echo off
setlocal
set str=I love you.

echo 普通に表示
echo [%str%]

echo Replace。スペースをスラッシュに
echo [%str: =/%]

echo 指定範囲を取り出す
echo [%str:~2,4%]

echo 末尾からn文字取り出す
echo →負の値は (文字数) + (負の値) として計算される
echo [%str:~-3,2%]
```

実行結果

```terminal
普通に表示
[I love you.]
Replace 置換 を適用
[I/love/you.]
指定範囲を取り出す
[love]
末尾からn文字取り出す
→負の値は (文字数) + (負の値) として計算される
[ou]

```

# スリープ

## 秒単位スリープ(精度は期待しないこと)
ping を使う。

```
rem 約3秒待つ(待ちたい秒数 +1 を指定する)
ping localhost -n 4 > nul
```

## ~~ミリ秒単位スリープ~~
バッチファイル単体では実現不可。

# ループ

## 指定回数だけループを回す
`for /L %%i in (開始値,増分値,終了値)` を使う。

loop_with_interval.bat(ping waitを使って約1秒インターバルで60回回すサンプル)

```bat
@echo off
setlocal

set vstart=1
set vincrement=1
set vend=60

for /L %%i in (%vstart%,%vincrement%,%vend%) do (
	ping localhost -n 2 > NUL
	echo %%i/%vend%秒...
)
```

実行結果

```
$ loop_with_interval.bat
1/60秒...
2/60秒...
3/60秒...
4/60秒...
...
```

## 遅延展開（ループ中の変数更新を意図通りに動作させる）
`setlocal enabledelayedexpansion` を定義し、当該変数を `!VARNAME!` 表記で扱う。

no_deployed_expansion.bat（遅延展開が無い場合）

```bat
@echo off
setlocal
set msg=こんにちは
set times=10
set ret=

rem 「こんにちは」を10回連結したい.
for /L %%i in (1,1,%times%) do (
	set ret=%ret%%msg%
)
echo %ret%
```

実行結果（意図した動作になってない）

```
$ no_deployed_expansion.bat
こんにちは
```

続いて deployed_expansion.bat（遅延展開を使った場合）

```bat
@echo off
setlocal enabledelayedexpansion
set msg=こんにちは
set times=10
set ret=

rem 「こんにちは」を10回連結したい.
for /L %%i in (1,1,%times%) do (
	set ret=!ret!%msg%
)
echo !ret!
```

実行結果

```
$ deployed_expansion.bat
こんにちはこんにちはこんにちはこんにちはこんにちはこんにちはこんにちはこんにちはこん
にちはこんにちは
```

# 構文

## 何もしない処理（Python でいう pass みたいなやつ）
コメント構文 `rem` を使う。「何も書いてないコメント」を何もしない処理とみなす感じ。

```bat
rem
```


## 複数行コメントアウト
`goto` で飛ばすか `if` で飛ばす。

commentout.bat

```bat
@echo off

echo コメントアウトその1、gotoジャンプ。
echo ここは表示されます。
goto :commentout_end
echo ここはスキップされるので表示されません。
echo ここはスキップされるので表示されません。
echo ここはスキップされるので表示されません。
:commentout_end

echo コメントアウトその2、条件分岐スルー。
echo ここは表示されます。
if 1==2 (
echo ここはスキップされるので表示されません。
echo ここはスキップされるので表示されません。
echo ここはスキップされるので表示されません。
)
```

実行結果

```
$ commentout.bat
コメントアウトその1、gotoジャンプ。
ここは表示されます。
コメントアウトその2、条件分岐スルー。
ここは表示されます。
```

## 関数/サブルーチン
関数構文は無い。ラベルと Goto ジャンプで頑張る。頑張れ。

subrootin.bat

```
@echo off
setlocal 

call :add 1 1
echo 1+2       = %ret%

call :add 100 -99
echo 100+(-99) = %ret%

exit /b

rem ----------
rem Subrootins
rem ----------

rem add(op1, op2)
rem op1+op2 の結果を ret に返す.
:add
set op1=%1
set op2=%2
set /a ret=op1+op2
exit /b
```

実行結果

```
$ subrootin.bat
1+2       = 2
100+(-99) = 1
```

書き方まとめ

- サブルーチンの定義
  - `:サブルーチン名` と `exit /b` で囲む
  - 引数は `%1`, `%2`, ... でアクセス
  - 戻り値は適当な変数を使う (Return 文は無い)
- サブルーチンの利用
  - `call :(サブルーチン名) (引数1) (引数2)...` で呼ぶ
  - 戻り値は call 後、サブルーチン側でセットした値にアクセスして入手

# 心がけ
スクリプト例ではないですが、よく使う「（バッチファイルとの付き合い方に関する）心がけ」についてもまとめてみました。

## とりあえずググる
やりたいことや困ったことがあったら、とりあえずググる。大体答えが見つかる（ただし「バッチファイルでは無理です」も含む）。

日本語だけでなく英語でも調べると、Stackoverflow あたりにヒットしやすい。

## バッチファイルで実現することにこだわらない
ついつい意固地になって、やりたいことを「絶対にバッチファイルで実現してやる！」と固執してしまうが、手段と目的を取り違えてはいけない。

バッチファイルは言語能力も微妙だし、文法もカオスなので、 **込み入ったことを実現するのは本質的に難しい**。別のスクリプトや言語で書くことを積極的に検討する。

参考(バッチファイルと戦いたい方向け):

- [猫科研究所 - コマンドプロンプトのエスケープ仕様](http://up-cat.net/%25A5%25B3%25A5%25DE%25A5%25F3%25A5%25C9%25A5%25D7%25A5%25ED%25A5%25F3%25A5%25D7%25A5%25C8%25A4%25CE%25A5%25A8%25A5%25B9%25A5%25B1%25A1%25BC%25A5%25D7%25BB%25C5%25CD%25CD.html)
- [cmd.exe のコマンドラインの仕様を解析してみた - 永遠に未完成](http://thinca.hatenablog.com/entry/20100210/1265813598)
- [バッチファイルでの試行錯誤を回避するためのメモ - Qiita](https://qiita.com/yz2cm/items/8058d503a1b84688af09)

## 文字化けにご用心
バッチファイルはコマンドプロンプト(cmd)上で動作するが、このコマンドプロンプトの文字コードは大体 cp932 なので、よく文字化けが発生する。

原因としてありがちなのは以下。

- バッチファイル自体が UTF-8 等で書かれている
- バッチファイルから呼び出したツールやコマンドが、cp932 以外の文字コードで標準出力している（特に UNIX ベースのツールと相性が悪い）

## ヘルプに目を通す
以下コマンドあたりのヘルプを読んでおくと、文法の理解が深まる。

- `if /?`
- `set /?`
- `for /?`

コマンドプロンプト上で読み辛い場合は `if /? | clip` などでコピーして、テキストエディタに貼り付けて読む。

## おわりに
以上、かなり雑多＆偏っているとは思いますが、ざっとまとめてみました。コメントなどありましたらお気軽にいただけると幸いです :smile: