WindowsのようなGUIのOSではコマンドを知らなくても普通に画面で操作ができるように作られていますが、繰返しの操作を自動化したいとか、もっと仕事を単純化したい時に、バッチというものを使います。昔のMS-DOSのようなコマンド操作を羅列したプログラムのことで、Linuxのシェルのようにほぼなんでもできる訳ではありませんが、覚えておけば、きっと役に立つ日がくると思います。

ここでは文法やコマンドの詳しい説明よりも、私が作成したサンプルプログラムを使い、バッチの基本的な構造と流れを紹介していきたいと思います。

Quiz

バッチでQuizプログラムを作ってみました。少し長いですが、この1本にできるだけコマンドを凝縮してみました。ですので、コードを分割しながら説明していきます。

@echo off

rem 遅延環境変数の開始
setlocal enabledelayedexpansion

rem ログファイルの指定
set log=quiz.log

rem 集計変数の初期化
set index=0
set correct=0

まずは先頭の@echo offですが、これはコマンドを実行した時に、コマンドプロンプトで実行コマンドが表示されることを無効にします。

setlocalは遅延環境変数の設定に使います。簡単に言いますと、ループ処理の中で変数への値の設定を可能にしてくれます。このプログラムはファイルを読み込んで、そのファイルの内容をループ処理で見せますので、必要になります。

setは変数の設定に使います。色々なオプションがありますが、オプションなしでは普通に=の次にくる文字列を変数に設定します。

rem 問題と回答の取込
for /f "tokens=1,2 delims=," %%a in (input.txt) do (
  set question[!index!]=%%a
  set answer[!index!]=%%b
  set /a index=index+1
)

ここからが重要ですが、他の言語でもお馴染みのforを使い、ファイルを1行ずつ読み込みます。/fオプションはファイルを読み込むということです。続いてtokensですが、これはdelimsで設定した区切り文字で分割されたトークンを選択します。りんご,Appleという文字列の場合は、りんごAppleに分割されます。その分割された文字は順番に変数%%a%%bに設定されます。

続いて、inの次の括弧の中では、読み込み対象のファイルを指定しています。doの次からループ処理を定義します。

ループの中では、questionanswerという配列変数に、それぞれ問題と答えを設定しています。配列のINDEXを表す変数indexは、ループ処理の中で値を設定しているので、遅延環境変数を使うという意味で、変数を!で囲んで値を読み取ります。一般的には%を囲んで値を読み取ることができます。

また、変数indexはループ処理ごとに1ずつ増えるので、変数の設定時に、四則演算を行うオプションである/aを付けます。

rem 問題数の入力
:begin
set /p stage="Total stage count : "
cls

rem エラーハンドラー
if %stage% gtr !index! goto begin

rem 開始時間の設定
set start=%date% %time:~0,8%

先頭の:はラベルの定義を意味します。gotocallを使って、このラベルに飛ぶことができます。後に詳しく説明します。

またsetのオプションですが、/pを付けると、コマンドプロンプトで入力した値を変数に設定することができます。ここでは、問題数を任意に設定します。

clsはコマンドプロンプトの画面をクリアするコマンドです。

ifは条件分岐処理を行うコマンドで、ここでは入力した問題数が、実際の問題数(読み込んだファイルの行数)より多い場合、ラベルbeginに戻る内容です。

変数startに設定しているdateは現在の日付を、timeは時刻を表す変数です。dateをそのまま使うと、2017/11/06のように表示されます。しかし、time22:26:55.50のように出力されるので、変数に:~0,8を付けて22:26:55まで変数に設定します。

※詳しい日付の編集や文字列操作などは検索してみてください。

rem 問題数分のプログラムの呼出
for /l %%i in (0,1,%stage%) do (
  if not %%i == %stage% (
    set /a now = %%i+1
    echo [Stage !now!]
    call :quiz
  )
)

またforですが、オプション/lで普通に数を数えながらループを使えるようにします。括弧の中は、初期値、増分、最後の数になります。ここでは、コマンドプロンプトで入力した問題数分ループします。ループ処理内のifですが、問題数が3の場合、配列のINDEXは0、1、2になりますので、変数%%iの値が3の時は処理を行いません。

変数nowに現在のステージを設定し、echoよりコマンドプロンプトに表示します。そして、callでQuizのメイン処理を呼び出します。処理の内容は後述します。

rem 集計結果の表示
echo Total Stage : %stage%
echo Total Correct : %correct%
echo Thanks for Playing!

rem 集計結果の書出
echo [%start% Start] >> %log%
echo [%date% %time:~0,8% End] >> %log%
echo Total Stage : %stage% >> %log%
echo Total Correct : %correct% >> %log%
echo. >> %log%

rem プログラムの終了
pause>nul
exit /b

Quizのメイン処理が終了したら、結果を表示し、それをログファイルに出力します。リダイレクト>>を使ってechoより出力される内容をコマンドプロンプトではなく、ログファイル(ここでは変数log)に書き込むことができます。ちなみに、echo.は空白行を出力する時に使います。

コマンドプロンプトでの表示と、ログファイルへの書き込み後はexitよりプログラムを終了します。普通は、コマンドプロンプトが自動的に終了されますが、pauseを使うと、任意をキーを押すまで待てくれます。>nulを付けると、基本的に表示されるメッセージを無効にできます。

rem メイン処理の定義
:quiz
set /a rand=(%random%%%index)
if !rand[%rand%]! == pass goto quiz
set rand[%rand%]=pass
echo !question[%rand%]!
set /p input=""
if !input! == !answer[%rand%]! (
  set /a correct=correct+1
)
if not !input! == !answer[%rand%]! (
  echo !answer[%rand%]!
  pause > nul
)
cls

rem 遅延環境変数の終了
endlocal

最後はQuizのメイン処理となります。%random%%%indexは、ランダムに生成された整数に問題数indexを割った余りを求めるという意味です。つまり、変数randに、0から問題数-1の間の整数をランダムに設定します。

そして、一度生成された整数(=INDEX)の問題は二度と表示させないように、新しく配列変数randを用意します。そこに、passという任意の文字列を設定し、同じランダムの整数が生成されれば、gotoより再度ランダムの生成から処理をやり直します。永遠に…

次の処理はファイルの読み込み処理で設定した問題の配列から、問題を出力し、変数inputにユーザより入力された内容を設定します。そして、回答とユーザの内容が一致すれば、変数correctの数値を+1にします。違った場合は、回答を出力して、処理を一時中断にします。これは、ユーザに答えを確認させるためです。

ここまで処理が終われば、このメイン処理が呼ばれた行に戻ります。重要なことですが、これがgotocallの違いになります。gotoは行の最後に来たら、そこで終わりですが、callは呼ばれた行に戻ります。

Rename

少しは本格的なファイル操作のサンプルも必要かと思い、ファイル名にファイルの更新日を付けるバッチを作ってみました。Quizで説明した内容は割愛し、新しいコマンドや使い方を中心に説明して行きます。

@echo off

rem 遅延環境変数の開始
setlocal enabledelayedexpansion

rem プログラムの分岐選択
choice /c 123 /n /m "処理を選択してください(1:続行 2:復元 3:終了)"
set command=%errorlevel%
if %command% == 3 goto escape

choiceは予め用意した複数の選択肢を選ばせるコマンドです。オプション/cの次に選択肢を定義します。オプション/mは任意のメッセージを設定することができます。オプション/nはデフォルトのメッセージを無効化します。選択肢を123と定義しているので、他のキーでは処理は進まれません。

入力された選択肢は変数errorlevelに設定されますので、これで処理を分岐させることができます。ここで変数commandに再度設定している理由は、後述のループ処理にて使うためです。選択肢3が選択された場合は、ラベルescapeに飛んで処理を行わずにプログラムを終了します。

rem ファイルの一覧の取込
for /f "usebackq delims=:" %%a in (`dir /b /a-d-h`) do (
  rem 自分自身の処理はパス
  if not %%a == %~nx0 (

長いのでループ処理は分けて説明します。for /fはQuizでも説明しましたが、今回はテキストファイルではなく、dir /b /a-d-hというコマンドの結果をInputとしています。つまり、このバッチがおいてあるフォルダのファイル一覧となります。因みに、/a-d-hはディレクトリと隠しファイルは含まれないことを意味します。

このようにファイルではなくコマンドをInputにしたい場合は、usebackqを付けます。また、区切り文字は基本スペースタブになっているので、ファイル名で使われない文字列:を設定しています。もし、ファイル名にスペースがある場合は、処理が正常に行われないためです。

続いて、バッチ自身は対象とさせないために、ファイル名変数%%aがバッチ名%~nx0の場合は、処理を行いません。%~nx0について説明しますと、まず%0は実行されるバッチのパスを表します。~を付けることで"を取り除き、nxを付けることでプログラム名と拡張子だけを表示します。つまり、バッチファイル名になります。因みに、%~dp0は、カレントディレクトリを意味します。

補足:if notなしで、for文にdir /b /a-d-h | find /v バッチファイル名も行けるかもです。

    rem ファイルの更新日付を取得
    for %%i in ("%%a") do set fdate=%%~ti

    rem 更新日付の整形(YYMMDD)
    set fdate=!fdate:~2,8!
    set fdate=!fdate:/=!
    
    rem 元ファイル名の設定
    set oname=%%a

forの中でまたforを使う理由は、ファイルの更新日時を取得するためです。inの次の括弧の中にファイル名を指定します。"を付けているのは、ファイル名の中にスペースがあった時のためです。そして、変数fdate%%~tiを設定していますが、これは%%iの拡張構文です。~tを付けることで、ファイルの更新日時を取得できます。

取得した更新日は、ここではYYMMDDの形に整形するためにfdate:~2,8で再度変数を設定します。続いて、日付の中に含まれている/を取り除くためにfdate:/=で更に変数を設定します。

    rem ファイル名に更新日付を追加
    if %command% == 1 (
      ren "!oname!" "[!fdate!] !oname!"
    )
    
    rem ファイル名の更新日付を削除
    if %command% == 2 (
      ren "!oname!" "!oname:~9!"
    )
  )
)

プログラムの最初に入力した選択肢に合わせて処理が分岐されます。1が選択された場合は、ファイル名の前に[171113]を付けて、ファイル名変更コマンドであるrenよりファイル名が変更されます。2が選択された場合は、逆に[171113]を削除します。ファイル名変数oname~9を付けることで、9番目の文字列以降から文字列を設定します。

rem プログラムの終了
:escape
pause>nul
exit /b

rem 遅延環境変数の終了
endlocal

これでプログラムは終了されます。シンプルなプログラムですので、色々とエラーが起こる可能性もありますが、普通の使い方では問題ありませんでした。もちろん、ファイルを管理するプログラムは慎重に扱うべきです。

Clip

パソコンを日常的に使うと、いつも使う文章やコマンドなどは打つのが面倒くさくなりがちです。プログラミングにおいてもスニペットやマクロ化など、ルーチンワークを効率化するための方法やツールはたくさん出ています。ですが、たまには自分の手でこういったものを作るのも良いかと思い、バッチで簡単なコピペプログラムを作ってみました。

@echo off
set /p name=""
set /p temp=%name%< nul > temp.txt
copy /b temp.txt + mail.txt temp.txt > nul
clip < temp.txt
del temp.txt
exit /b

日常的に使うメールのテンプレート(例です)の先頭に入力した名前を付けて、クリップボードに保存してくれる簡単なプログラムです。入力した名前をテキストファイルに出力し、予め用意しておいたテンプレートテキストファイルと合体させる仕組みになります。しかし、注目すべきは3行目の方になります。echoで出力した名前をリダイレクトすると、後ろに改行コードが入るので、set /pを使って改行コードなしの名前をリダイレクトしています。

UTF-8 Encoder

一般的なファイルやデータの操作はバッチだけでもある程度はできますが、少しでも次元を超えてしまったら壁にぶつかってしまいます。Windowsにはバッチの他にもPowerShellというものがあって、LinuxのBashのような性能を持っています。今までは触れる機会がなく、存在自体も知らずにいましたが、最近ちょっとした出会いがありましたので、メモを残したいと思います。次のサンプルはPowerShellのコードですが、バッチで実行できる便利な方法がありましたので、普通に.batとして使って問題ありません。

# バッチファイルでPowerShellを使える魔法の言葉
@powershell -NoProfile -ExecutionPolicy Unrestricted "$s=[scriptblock]::create((gc \"%~f0\"|?{$_.readcount -gt 1})-join\"`n\");&$s" %*&goto:eof

# 相対パスを絶対パスに変換
$location = (Resolve-Path .\).Path
# CSV出力フォルダのパス
$output = "$($location)\Encoded"
# カレントディレクトリのcsvファイル一覧取得
$files = Get-Childitem -Path "$($location)\*.*" -include *.csv

# csvファイルがない場合、処理を中止
if ($files -eq $null) {
  Write-Host "Failed"
  Start-Sleep -s 1
  exit
}

# outputディレクトリがない場合、新規作成
if (-Not(Test-Path $output)) {
  New-Item $output -itemType Directory > $null
  Write-Host "Make Directory"
}

# csvファイルの一覧をループ
$files | foreach-object {
  # 拡張子を除いたファイル名
  $fileName = (Split-Path -Path $_.fullname -Leaf).Split(".")[0]
  # ファイルの拡張子
  $extension = (Split-Path -Path $_.fullname -Leaf).Split(".")[1]
  # エンコーディングするファイルの絶対パス
  $destFile = "$($location)\Encoded\$($fileName).$($extension)"
  # 既にファイルが存在する場合
  if (Test-Path $destFile) {
    "$($fileName).csv is existed"
  # ファイルが存在しない場合
  } else {
    Get-Content $_.fullname | Set-Content -Encoding UTF8 $destFile
    "$($fileName).csv is encoded"
  }
}

Write-Host "Done"
Start-Sleep -s 5

このサンプルはカレント・ディレクトリのcsvファイルの文字エンコーディングをUTF-8に変換するプログラムです。新規のディレクトリを作成し、そこにエンコーディングされたcsvファイルを格納します。既に同じ名前ディレクトリとファイルがある場合は、処理を行いません。

参考ページ

とても参考になったページを紹介します。

DOS コマンド一覧
Windowsバッチまとめ
バッチファイルから PowerShell を呼び出す方法