読みくくなりがちなバッチファイルのコードを可能な限り読みやすくしましょう。というネタ提言です。
####1. べた書き
例として以下のコードを使用します。
@echo off
set i=0
set /p m="自然数を入力 >"
set sum=0
:loop
set /a n=%i%*%i%
set /a sum+=%n%
set /a i+=1
if %i% leq %m% goto loop
echo %sum%
pause
exit
これは1つの自然数mをコンソールから読み込んで、0からmまでの自然数の2乗の総和を求めるプログラムです。
最近のプログラミング言語を普段使っている人にとってはこの程度のものでも読みにくいと感じるでしょう。私もそう思います。
####2. 処理ごとに分けてみた
まず、処理のまとまりがよく分かりません。このプログラムにおいて主な処理は、
- 初期化、入力の受付と出力
- 2乗の計算
- 総和の計算
に分けられるはずです。
分けてみたのが下のものです。
@echo off
set /p m="自然数を入力 >"
call :calc %m%
set sum=%errorlevel%
echo %sum%
pause
exit
:calc
set i=0
:loop
call :square %i%
set /a sum+=%errorlevel%
set /a i+=1
if %i% leq %1 goto loop
exit /b %sum%
:square
set /a n=%1*%1
exit /b %n%
少し構造が見えてきたのではないでしょうか。
バッチファイルではcall :ラベル名 引数1 引数2 ...
というようにラベルからexit /bまでの処理をサブルーチンのように呼び出すことができます。ルーチン内で引数にアクセスするときは、指定した引数の順番に%1 %2 ... %9
を使用します。(%9
までの9つしか見ることができませんが、shift
コマンドを使うことによって10個以上の引数でもアクセス可能です。)
callで呼ばれた先でexit /b 戻り値
とすることで、errorlevel
環境変数に戻り値がセットされます。(戻り値は数値しか取れません。)
基本上から下にしか進まないバッチファイルでもこのように構造化していくことで読みやすくなり、またコードの再利用も容易になります。
####3. インデント
しかし、まだまだ読みにくいでしょう。なにしろインデントなしですからね。
なので、インデントを付けます。
@echo off
set /p m="自然数を入力 >"
call :calc %m%
set sum=%errorlevel%
echo %sum%
pause
exit
:calc
set i=0
:loop
call :square %i%
set /a sum+=%errorlevel%
set /a i+=1
if %i% leq %1 (
goto loop
)
exit /b %sum%
:square
set /a n=%1*%1
exit /b %n%
バッチファイルでのインデントの目安は、ラベルや制御文(if, for)で1段階上げ、exitで戻すのがよいでしょう。
####4. gotoループの書き方を改善
結構読みやすくなってきましたが、まだ分かりにくい所はどこだろうと探しますと、やはりgoto文でループしているところがわかりにくいということになりそうです。
バッチファイルにもfor文はあるので使える場合は**for文を使うことをお勧めします。**しかし、バッチファイルは様々な部分で制約がありますので、goto文によるループを必要とする場面も出ると思います。そこで、goto文を使ってもそこまで読みにくくならないように改善してみます。
@echo off
set /p m="自然数を入力 >"
call :calc %m%
set sum=%errorlevel%
echo %sum%
pause
exit
:calc
set i=0
:loop
if %i% gtr %1 (
exit /b %sum%
)
call :square %i%
set /a sum+=%errorlevel%
set /a i+=1
goto loop
echo エラー
exit
:square
set /a n=%1*%1
exit /b %n%
if文を継続条件の記述ではなく終了条件の記述に変更し、位置をラベルの直後に移動しました。これで、:loopラベルのすぐ下を見ればどういうループなのか分かるようになりました。また、:loopラベルの最後に付けるgoto loop
のインデントレベルを1つ戻すことで、あたかもコードブロックを作っているかのような自然な記述にすることができました。
####5. サブルーチンに引数・戻り値アノテーションを追加
まだまだ終わりではありません。このプログラムではラベル名をサブルーチンの目印に使っています。しかし一般的なプログラミング言語のように、引数をいくつ取ってどんな値を返すのかを指定することができません。これはバッチファイルの仕様上仕方のないことですが、せめて後から見返した時にすぐ理解できるようにすることはできないでしょうか。
そこで::
の使用を提案します。バッチファイルでは::
の後は行の終わりまでコメントとして扱われます。これは当然ラベルの後ろにも付けられるので、これをアノテーションとして使います。
@echo off
set /p m="自然数を入力 >"
call :calc %m%
set sum=%errorlevel%
echo %sum%
pause
exit
:calc::int m -> int
set i=0
:loop
if %i% gtr %1 (
exit /b %sum%
)
call :square %i%
set /a sum+=%errorlevel%
set /a i+=1
goto loop
echo エラー
exit
:square::int n -> int
set /a n=%1*%1
exit /b %n%
これはあくまでコメントですので、**従わなくても警告も何も出ません。**しかしこうしておけばサブルーチンがどんな引数をいくつ必要とし、どんな値を返すのか一目でわかります。しかも、::
以降はコメントですから、呼び出しは:ラベル名
のみでOKです。
####6. :loopラベルにもアノテーション
そして、これができるということは、:loop
ラベルにも同じことができるということです。
@echo off
set /p m="自然数を入力 >"
call :calc %m%
set sum=%errorlevel%
echo %sum%
pause
exit
:calc::int m -> int
set i=0
:loop::i in 0:1:m
if %i% gtr %1 (
exit /b %sum%
)
call :square %i%
set /a sum+=%errorlevel%
set /a i+=1
goto loop
echo エラー
exit
:square::int n -> int
set /a n=%1*%1
exit /b %n%
書き方はわかれば何でもいいです。もっとわかりやすい書き方があれば教えてください。
こんな感じで、例えバッチファイルであろうともそれなりに現代的な書き方ができることをつい最近思いついたので勢いのまま記事にしました。
####7. 自身の過去記事のプログラムを改善してみた
私が過去この記事で書いたexample.bat
を上記の記法に倣いつつ、新たにいくつかの新たな記法を導入して書き直したものが以下になります。
@echo off
:Main
rem 入力が適切な数値であるか検証し、初項A、末項L、公差Dとして等差数列の総和を計算し表示。
setlocal
echo 初項がA、末項がL、公差がDの等差数列の総和を求めます。
echo AとDで定まる、L以下で最大の項を末項として扱います。
call :GetALD A L D
call :Verify %A% %L% %D%
if %errorlevel% == 1 (
echo 入力された数値が正常ではありません。何かキーを押すと終了します。
pause >nul
exit
)
call :GetSum %A% %L% %D%
echo 初項%A%、末項%L%、公差%D%の等差数列の総和:%errorlevel%
echo 何かキーを押すと終了します。
pause >nul
exit
:GetALD::ref, ref, ref -> void
rem A,L,Dの取得
set /p %1="1以上の整数値で初項を入力してEnter >"
set /p %2="1以上の整数値で末項を入力してEnter >"
set /p %3="0以外の整数値で公差を入力してEnter >"
exit /b
:Verify::int A, int L, int D -> bool
rem 入力値が正しいかどうか判別。
rem A,Lが1以上かどうかを検証。
setlocal
set A=%1
set L=%2
set D=%3
if %A% lss 1 (
exit /b 1
) else if %L% lss 1 (
exit /b 1
) else if %D% lss 0 (
exit /b 1
)
rem 初項末項の大小関係と公差の値が正しいか検証。
if %A% lss %L% if %D% leq 0 (
exit /b 1
)
if %A% gtr %L% if %D% geq 0 (
exit /b 1
)
exit /b 0
:GetSum::int A, int L, int D -> int
rem 総和を計算
setlocal
set A=%1
set L=%2
set D=%3
set sum=0
for /l %%i in (%A%,%D%,%L%) do (
set /a sum+=%%i
)
exit /b %sum%
Main
ラベルについては特にエントリポイントなどの機能はありませんが、主処理がとこからどこまでかを示すには使えそうです。(バッチファイルは何も指示がない限り完全に上から下に実行します。ラベルで区切ったサブルーチンの中にも勝手に入ってしまうので、主処理群は基本的に一番上に記述してください。)
Main
、Verify
、GetSum
の各ラベルの下にあるsetlocal
は環境変数のスコープを作るコマンドです。スコープの有効範囲はendlocal
かexit
に到達するまでです。つまりこれらのサブルーチンでは、setlocal
以下でset
された環境変数についてはそのサブルーチン内で有効なローカル変数になります。
GetALD
では引数アノテーションにref, ref, ref
と書きましたが、これは環境変数名を引数に取ってくださいという意味です。(使い方がreferenceつまり参照に近いと思ったのでrefを使いましたがもっと的確な表現がありましたらコメントください。)
環境変数名を引数に取り、その環境変数に対してそれぞれ入力を受け付けて代入します。上のプログラムでは、
set /p %1="1以上の整数値で初項を入力してEnter >"
は%1
(引数の1番目)にA
が入っているので、
set /p A="1以上の整数値で初項を入力してEnter >"
として動作します。また、このサブルーチンに対してsetlocal
を指定してしまうと、このサブルーチンで代入した環境変数が呼び出し元に引き継がれないため、何も指定せずスコープは呼び出し元と結合しています。
####8. まとめ ~これどこで使うの?~
どうでしょう、それなりによく使われるプログラミング言語の記述にかなり近づいたように見えないでしょうか。まぁ、バッチファイルでこのような記述をすると正直コードが長くなるので、進んでやる人は少ないと思います~~(というより、わざわざバッチファイルなんかで書かないよね)~~
バッチファイルでゲーム制作とか苦行がしたい(特に複数人で)という方はこのような書き方で可読性を大幅に向上させて圧倒的成長が期待できるでしょう。(本当か?)