Windowsバッチの逆引き的な情報

# はじめに
今時色々な便利な言語が多いですが、動作環境の都合上、Windowsでバッチを作る機会が度々あります。
しかし他のプログラムなら簡単なのにWindowsバッチでの書き方が分からない、ということが何度もあります。
なので情報整理も兼ねて、Windowsバッチでの実現するちょっと複雑な操作を逆引き的にまとめます。
Windowsでやりたいことが増えたら更新する予定です。

# したいことの逆引き

## バッチ上で実行したコマンドを非表示にしたい。
バッチファイルをコマンドプロンプト上で実行してみると、次のようにバッチに記述されたコマンドが表示されます。
これはバッチファイルの実行＝書かれているコマンドを実行しているためです。

```batch:sample.bat
echo %date% %time%
```

```:コマンドプロンプト
C:\sample>sample.bat

C:\sample>echo 2017/12/27 18:33:59.89
2017/12/27 18:33:59.89
```

※「C:\sample>echo 2017/12/27 18:33:59.89」が表示されている。

これを解消するには2つの対処法があります。

### 行毎にコマンドを非表示にする
行毎にコマンドを非表示にする場合、その行の先頭に@(アットマーク)を付けます。

```batch:sample.bat
echo この行はコマンドが表示される。
@echo この行はコマンドが表示されない。
```

```:コマンドプロンプト
C:\sample>sample.bat

C:\sample>echo この行はコマンドが表示される。
この行はコマンドが表示される。
この行はコマンドが表示されない。
```

上記の通り、@の付いていない「echo この行はコマンドが表示される。」だけコマンドが表示されます。
一方、@の付いた行はコマンドの実行結果だけが表示されています。

### バッチ全体でコマンドを非表示にする
バッチの最初に「echo off」と記述することで、それ以降はバッチのコマンドが非表示になります。
ただしそままだと「echo off」コマンド自体は表示されるため、先頭にアットマークを付けます。

```batch:sample.bat
@echo off

echo このバッチ内では、
echo 全てのコマンドが、
echo 非表示になる。
```

```:コマンドプロンプト
C:\sample>sample.bat
このバッチ内では、
全てのコマンドが、
非表示になる。
```

#### バッチ全体でコマンド非表示の状態でも、時に一部のメッセージだけは表示させたい
「echo on/off」は後から切り替えられるため、一部のコマンドだけ表示することも可能です。

```batch:sample.bat
@echo off

echo このバッチ内では、

echo on 
echo 一部を除き
@echo off

echo 全てのコマンドが、
echo 非表示になる。
```

```:コマンドプロンプト
C:\sample>sample.bat
このバッチ内では、

C:\sample>echo 一部を除き
一部を除き
全てのコマンドが、
非表示になる。
```

## 変数を使いたい
Windowsバッチで使用可能な変数は環境変数だけです。
使う時は既存の環境変数と被らないように注意しましょう。

宣言は「SET 変数名=値」、参照時は「%変数%」です。
文字列などを記号("とか'とか)で囲むと、その記号も変数に保存される点に注意して下さい。

```batch:sample.bat
@echo off

set VAR1=Hello
set VAR2="World!"

echo %VAR1% %VAR2%
```

```:コマンドプロンプト
C:\sample>sample.bat
Hello "World!"
```


バッチ実行後はバッチ内で宣言した環境変数はそのまま残ることに注意してください。。
(SETによる一時的な変更のため、コマンドプロンプト終了時に消えますが)

```:コマンドプロンプト
C:\sample>sample.bat
Hello "World!"

C:\sample>
C:\sample>
C:\sample>echo %VAR1%
Hello
```

##コメントアウトしたい
コメントアウトする場合、「rem コメント」と記述して下さい。
それだけだとパットみ分かりにくいので、個人的には#とかを付けて「rem # コメント」と記述したりします。
ちなみに複数行コメントアウトは残念ながら分かりません。恐らくないと思います。

```batch:sample.bat
@echo off

rem # コメントです。

rem # 変数VARの設定
set VAR=Hello World

rem # 変数VARの値表示
echo %VAR%
```

```:コマンドプロンプト
C:\sample>sample.bat
Hello World
```

## 文字列の一部分だけ切り出す
環境変数に格納された文字列は一部分だけ切り出すことが可能です。

|記述|説明|
|---|---|
|%環境変数:文字1=文字2%|文字1を文字2へ置換。<br>文字2を省略した場合、文字1を削除。|
|%環境変数:~m,n%|m文字目(0スタート)からn文字分を表示。<br>「m」を省略した場合、先頭からn文字分を表示。<br>「,n」を省略した場合最後まで表示。|
|%環境変数:~-m,n%|後ろm文字目(1スタート)からn文字分を表示。<br>「,n」を省略した場合最後まで表示|
|%環境変数:~m,-n%|m文字目(0スタート)から後ろn文字分削除した結果を表示。<br>「m」を省略した場合、先頭から後ろn文字分削除した結果を表示。|
|%環境変数:~-m,-n%|後ろm文字目(1スタート)から後ろn文字分削除した結果を表示。|

```:コマンドプロンプト
C:\sample>set VAR=ABCDEFG

C:\sample>echo %VAR:A=X%
XBCDEFG

C:\sample>echo %VAR:~2,3%
CDE

C:\sample>echo %VAR:~-4,2%
DE

C:\sample>echo %VAR:~2,-2%
CDE

C:\sample>echo %VAR:~-5,-2%
CDE
```

## 整ったタイムスタンプを取得したい
現在時刻を取得して、そこからタイムスタンプを記述する手順をまとめます。

現在の年月日は環境変数date、時分秒は環境変数timeにより取得できます。
ですがこのままだと使いにくいため、例えば「YYYYMMDDHHMISS」形式のタイムスタンプに変えたいと思います。

### 時刻の0埋めした値を取得する
環境変数により日時と時刻の取得は可能ですが、次のように時刻が1桁時の場合、桁数が変わってしまいます。

```:コマンドプロンプト
C:\sample>echo %date% %time%
2017/02/02  9:25:25.96
```
※月日や分秒は1桁でもデフォルトで0埋めされて桁数に変化なし。

これだと整形されたタイムスタンプとして使いにくいため、timeを「%time: =0%」と参照することで0埋めをします。
これは文字列の置換を利用しており、「%環境変数:置換前=置換後%」という風に使います。
今回はスペースを0に変換しています。

```:コマンドプロンプト
C:\sample>echo %date% %time: =0%
2017/02/02 09:25:25.96
```

### 日時を整形して取得する。
環境変数date、timeは上記の通り/(スラッシュ)や:(コロン)で区切られています。
そのため整形しないとファイル名のタイムスタンプなどには使いにくいです。
よって次のように一部分だけを切り出すことで数値だけを抜け出せます。

```batch:sample.bat
@echo off

rem # 年月日を数値のみにする
SET TS_DATE=%date:/=%

rem # 時分秒を数値のみにする
SET TS_TIME=%time: =0%
SET TS_TIME=%TS_TIME::=%
SET TS_TIME=%TS_TIME:~,-3%

rem # 年月日と時分秒を1行に繋げる
SET TIME_STMP=%TS_DATE%%TS_TIME%

rem # 結果表示
echo %TIME_STMP%
```

```:コマンドプロンプト
C:\sample>sample.bat
20171227195644
```

年月日は通常、「年/月/日」と表示されるため、置換によりスラッシュを削除しています。
「%date:/=%」のように置換対象を/にし、置換後の値を空白にすることで削除可能です。

時分秒は通常、「時:分:秒.マイクロ秒」と表示されます。
「%time: =0%」は前述した通り、スペースを0に置換することで1桁時の0埋めをします。
「%TS_TIME::=%」は0埋めした時刻から区切り文字の:を削除しています。
「%TS_TIME:~,-3%」は文字の切り出しです。
後ろ3文字を削除しているためマイクロ秒を消しています。
タイムスタンプを分までにしたい場合は、「%TS_TIME:~,-5%」とすれば可能です。


## 実行結果をファイルに出力したい
コマンドの結果はそのままでは標準出力され、コマンドプロンプト上に表示されます。
結果をログファイルに吐き出したい場合、次のように不等号記号(>)でリダイレクトすることで可能です。
書き込み先がない場合、自動的にファイルが作成されます。
逆に書き込み先が存在する場合、新しく上書きしてしまう点に注意してください。
また既存の書き込みに追記する場合不等号記号を2重(>>)にします。

```batch:sample.bat
@echo off

rem # メッセージをログ出力
echo # dirコマンドの実行結果をログへ出力する。> sample.log

rem # dirを実行
dir /b>>sample.log
```

```:コマンドプロンプト
C:\sample>sample.bat

```

```:sample.log
# dirコマンドの実行結果をログへ出力する。
sample.bat
sample.log
```

### エラーを含めた結果をログファイルに出力したい
コマンドの実行結果でエラーが発生した場合はリダイレクト先ではなく、コマンドプロンプト上に表示されます。
下記の場合、エラーメッセージである「ファイルが見つかりません」がコマンドプロンプト上に表示されています。

```batch:sample.bat
@echo off

rem # メッセージをログ出力
echo # dirコマンドの実行結果をログへ出力する。> sample.log

rem # エラーが発生するdirを実行
dir /b hoge>>sample.log
```

```:コマンドプロンプト
C:\sample>sample.bat
ファイルが見つかりません
```

```:sample.log
# dirコマンドの実行結果をログへ出力する。
```

これを解消するには次のようにリダイレクトの最後に「2>&1」を付与します。

```batch:sample.bat
@echo off

rem # メッセージをログ出力
echo # dirコマンドの実行結果をログへ出力する。> sample.log

rem # エラーが発生するdirを実行(2>&1を付与)
dir /b hoge>>sample.log 2>&1
```

```:コマンドプロンプト
C:\sample>sample.bat

```

```:sample.log
# dirコマンドの実行結果をログへ出力する。
ファイルが見つかりません
```

今度はちゃんとエラーメッセージがログに出力されました。
「2>&1」は簡単に説明すると、「2=標準エラー」の出力先を「1=標準出力」と一緒の場所にする設定です。
今回標準出力先は「sample.log」なため、エラー文も「sample.log」に出力されます。

## 戻り値を設定したい
Windows上で実行された処理の戻り値は環境変数ERRORLEVELにより取得可能です。
コマンドが成功した場合0、失敗した場合は1です。
これと後述するif文を使うことでスクリプト内で例外処理が対応可能です。

```:コマンドプロンプト
C:\sample>dir /b
sample.bat
sample.log

C:\sample>echo %ERRORLEVEL%
0
```

```:コマンドプロンプト
C:\sample>dir hoge /b
ファイルが見つかりません

C:\sample>echo %ERRORLEVEL%
1
```

自作スクリプトで戻り値を設定する場合、「exit /b <戻り値の値>」と記述してください。
「/b」オプションがないと、コマンドプロンプト上でスクリプト実行した場合、コマンドプロンプトが閉じてしまいます。

```batch:sample.bat
@echo off

rem # 戻り値11を返すスクリプト
exit /b 11
```

```:コマンドプロンプト
C:\sample>sample.bat

C:\sample>echo %ERRORLEVEL%
11
```

ただし次の点に注意してください。

* 戻り値が設定されていないコマンドを実行した場合、ERRORLEVELは更新されない。
* ERRORLEVELに直接値を設定した場合、以降そのセッションにおいて戻り値の値はERRORLEVELへ代入されない。
* 戻り値を取得したいコマンドの直後に、別の戻り値を取得するコマンドを実行すると当然ERRORLEVELは上書きされる。

## if文を使いたい
if文は他の言語と似たように次のように使うことが可能です。
またifの後にnotを付けることで、not条件にすることができます。

```batch:if文の形式
if 条件1 (
  処理内容1
) else if 条件2 (
  処理内容2
) else (
  処理内容3
)
```

注意点として、カッコの位置がずれると、高確率で上手く動きません。
具体的には次のような記述です。

```batch:失敗するif文の形式
if 条件 ( 処理内容 ) 
```

```batch:失敗するif文の形式
if 条件
(
  処理内容1
)
else 
(
  処理内容2
)
```

バッチファイルはミスがあっても気付きにくいため、if文を記述する際は、初めのうちはelse処理も記述することを推奨します。
else処理が実施されていない場合、そもそものif文が間違っていることが分かるからです。

if文を使うことで次のように午前/午後で実行結果が異なるスクリプトを作成可能です。

```batch:sample.bat
@echo off

rem # 時分秒から時のみを取得する
set HOUR=%time:~0,2%

rem # 時間から午前/午後を表示する
if %HOUR% lss 12 (
  echo 午前です。
) else (
  echo 午後です。
)
```

```:コマンドプロンプト(午後に実行)
C:\sample>echo %time%
19:22:58.92

C:\sample>sample.bat
午後です。
```

## ソート・絞り込みをしたファイルを処理したい
ソート・絞り込みしたファイルに何かしらの処理をしたい場合はFOR文を使用します。
FOR文は非常にややこしいですが、Windowsで複雑なバッチを作る際は避けては通れないコマンドです。
簡単な使い方は以下の通りです。
FOR文については専用の説明が必要なほどややこしいため、本文書では詳細な解説はしません。

```batch:FOR文の使い方
FOR /f "usebackq" %%i in (`コマンド`) do (
  コマンド出力結果を1行ずつ処理(%%iで出力結果を指定)
)
```

では試しにあるフォルダ内の拡張子logと付くファイルを名前順で逆順ソートし、先頭3件以外を表示します。
今回は次のフォルダを対象にテストしてみます。

```:コマンドプロンプト
C:\sample>dir C:\sample_log
 ドライブ C のボリューム ラベルは Windows です

 C:\sample_log のディレクトリ

2018/01/29  16:57    <DIR>          .
2018/01/29  16:57    <DIR>          ..
2018/01/23  18:21                 0 abc_20170217.log
2018/01/23  18:21                 0 abc_20171203.log
2018/01/23  18:21                 0 abc_20171222.log
2018/01/23  18:21                 0 abc_20180108.log
2018/01/23  18:21                 0 abc_20180111.log
2018/01/23  18:21                 0 abc_20180201.log
2018/01/23  18:02    <DIR>          abc_20180208.log
2018/01/23  18:21                 0 abc_20180217.log
2018/01/23  18:21                 0 abc_20200101.txt
2018/01/23  18:02    <DIR>          backup
2018/01/23  18:02    <DIR>          base_dir.log
2018/01/23  18:02    <DIR>          sample
2018/01/23  18:21                 0 sample.txt
```

見ての通り、logと名の付くファイルは、名前にタイムスタンプが含んでいます。
またlogと名の付くフォルダや、logファイルと同様の名前規則のあるtxtファイルは除外する必要があります。
よって今回のスクリプト例は実質的に、logファイルの最新3件以外を表示するスクリプトです。
(名前の逆順=タイムスタンプの新しい順。先頭3件以外=タイムスタンプの新しい順の先頭3件以外なため)

```batch:sample.bat
@echo off

rem # 処理実行対象のフォルダ
set DIR=C:\sample_log

rem # フォルダ内の最新logファイル3件以外を表示
FOR /f "usebackq skip=3" %%i in (`dir C:\sample_log\*.log /b /a-d /o-n`) do (
  echo on
  echo %DIR%\%%i
  @echo off
)
```

```:コマンドプロンプト
C:\sample>sample.bat
C:\sample_log\abc_20180108.log
C:\sample_log\abc_20171222.log
C:\sample_log\abc_20171203.log
C:\sample_log\abc_20170217.log
```

このスクリプトのFOR文は前述した通りコマンドの実行結果に対して1行ずつ処理をしています。
今回、実行結果を出力するコマンドは「dir C:\sample_log /b /a-d /o-n」です。
これに付いているオプションは次の通りです。

* /b:ファイル名のみ表示
* /a-d:ディレクトリ以外を表示
* /o-n:アルファベットの逆順で表示

よって「dir C:\sample_log\*.log /b /a-d /o-n」により、次の通り拡張子がlogであるファイルのみが表示されます。
(log拡張子が付くフォルダやtxtファイルは無視されます)

```:コマンドプロンプト
C:\sample>dir C:\sample_log\*.log /b /a-d /o-n
abc_20180217.log
abc_20180201.log
abc_20180111.log
abc_20180108.log
abc_20171222.log
abc_20171203.log
abc_20170217.log
```

FOR文は上記の実行結果に対して1行ずつ(abc_20180217.log～abc_20170217.logまで)処理を実行します。
今回実行する処理は「echo %DIR%\%%i」=「echo <処理対象ディレクトリ>\<出力結果(1行)>」なため、
logファイルの絶対パスを、1行ずつ表示します。
ただしFOR文に「skip=3」を付けたため、先頭3行(abc_20180217.log～abc_20180111.logまで)は処理されません。
よってこのスクリプトでは残ったタイムスタンプが古い4件(abc_20180108.log～C:\sample_log\abc_20170217.logまで)が表示されます。

このスクリプト内の「echo %DIR%\%%i」を「dell %DIR%\%%i」と書き換えれば、
最新のlogファイル3件以外を削除する処理に変わります。
(というか元々その目的で作っています)
dir文を変更すれば、ファイル or フォルダのみを更新順に並べられるため、
様々な絞り込みの処理が可能です。
このようにFOR文を使うことで柔軟なバッチを作成することが可能です。