このページでは、batファイルで長めの処理を作る際、それがスパゲッティコードにならないようにするためにはどうすべきかを、call :ラベル名
という制御文に目を向け解説します。
#もくじ
注意
1.スパゲッティコードとは
2.諸悪の根源
3.call :ラベル名
の出番
4.活用法
5.まとめ
#注意
この記事はbatファイルを使ってゲームを作るなど、かなり大規模な開発をする場合において重要なことを紹介します。
また、batファイルはコードの書き方で動作速度に大きな差が出ます。そのため長いコードを書く場合でも、動作速度を第一に考える場合などは、スパゲッティコードもやむなしとする場合があります。
#1.スパゲッティコードとは
「スパゲッティコード」がどんなコードを指すのか、既に知っている方はこの節を飛ばしてもかまいません。
「スパゲッティコード」とは、プログラムのソースコードがとても複雑に記述されていて可読性が低く、製作者以外には理解が難しい(場合によっては制作者にも理解が難しい)ようなプログラムを指します。様々なコードが複雑に絡み合っている様子をスパゲッティの麺に例えた名称です。
#2.諸悪の根源
batファイルがスパゲッティになる理由はこの1つをおいて他にありません。
goto文の濫用
これが最も大きい理由です。batでは、goto ラベル名
で指定されたラベルにジャンプするような仕様になっています。そのため、どうしてもgoto文が多用されがちです。またbatの場合、ファイル中のどこからどこへでも、ラベルさえ作ればgoto文でジャンプできる仕様であるため、処理順がファイル中を行ったり来たりするようなプログラムが簡単に作れてしまいます。
さらに、if %VAR1% == %VAR2% goto ラベル名
というように、batではif文で条件判定した後にgoto文で別のラベルに移動するという処理をするのが普通です。しかし、if文を多用する規模の大きなプログラムとなると、このような書式のコードが多数存在するというのもスパゲッティ化の原因となります。
加えて、本来for文を用いるべきループ構造にgotoを用いている例を多く見かけます。下のようなものです。
@echo off
rem 1から10までの総和を求めて表示
echo 1から入力された数までの総和を求めます。
set /p a="数値を入力してEnter >"
goto StartLoop
:EndLoop
echo %sum%
pause >nul
:StartLoop
set sum=0
set i=1
:Calc
set /a sum+=i
set /a i+=1
if %i% leq %a% goto Calc
goto EndLoop
以上のように、batファイルではジャンプにgoto文を多用する場面が多いため、スパゲッティ化の原因となります。
#3.call :ラベル名
の出番
では、2.で紹介した原因をbatファイルという厳しい環境下でどう乗り越えるかについて解説します。
まず、タイトルにもあるcall :ラベル名
という構文ですが、これはgoto文と同じく、指定したラベルにジャンプする構文です。ただ、goto文と違うところは、goto文が、指定のラベルに飛んだらそれでgoto文の役割は終わりなのに対し、call文はジャンプ前の位置を記憶しているため、もう一度制御文を使って戻る必要がないということです。また、call文はgoto文と違い、引数を取ることができ、終了時に戻り値を返させることができます。そのため、call文を用いることで疑似的なユーザ定義関数を作ることが出来るのです。以下は、上で例示したgoto文を使用したループをfor文とcall文で書き換えたものです。
@echo off
rem 1から入力された数までの総和を求めて表示
echo 1から入力された数までの総和を求めます。
set /p a="数値を入力してEnter >"
call :Calc
echo %errorlevel%
pause >nul
exit
:Calc
set sum=0
for /l %%i in (1,1,%a%) do set /a sum+=%%i
exit /b %sum%
このコードを解説すると、call文で:Calcへ飛び、for文でsumに1からaまで順番に足し合わせ、終わったらexit /b %sum%で変数sumの中身を戻り値として持って元のcall文の直後へ戻ります。batでは、exit文に数値や%変数%を指定すると、その値がerrorlevel
という名前の変数に格納されます。よってcall文を終えて戻ってきた後、echo %errorlevel%
とすることで、結果を表示させます。
こうすることで、:Calcラベルでまとめられた処理を1つの関数と見ることができます。Cで言えば、Calc(a)
で自作のCalc関数を呼んで、関数の終わりにreturn(sum);
として合計値を持ってくるという感じに例えることが出来ると思います。
また、このように疑似関数化し、ラベル内の処理を独立したものにすることで、このラベルはバッチファイル内のどの位置からも任意のタイミングで呼び出して1からaまでの総和を求めさせることができます。これをgoto文で実現するには、総和の計算に飛ぶ前にどの位置から飛ぶかを変数に記憶させ、総和の計算が終了した後、if文を使って戻る場所を探さなければなりません。
注意: batでは、setコマンドで定義された変数はグローバル変数です。どのラベルへcallされてもそのまま残りますし、callされたラベル内で新たにsetしたものは、exit /bで抜けた後も残ります。ラベル内でローカル変数を使用する場合は、setlocal
コマンドを宣言してからsetコマンドで定義し、ラベルを抜ける前にendlocal
と記述します。ただし、環境変数の遅延展開を使用するために、setlocal enabledelayedexpansion
を宣言している場合、endlocal
で遅延展開が無効になるため、そのラベルを出た後に再度setlocal enabledelayedexpansion
を宣言する必要があります。
余談ですが、上記のプログラムは等差数列の和の公式を用いることで、ループ自体を使わずに求めることもできます。公式を用いる方が早い動作となります。ただ、後述のような初項と公差から末項に辿り着かない(行き過ぎる)可能性があるプログラムの場合は、ソース短縮のためにfor文を用いてもよいでしょう。ですが、項数が膨大になることが予測できる場合には、(初項+末項)を公差で割った剰余分だけ末項を適切な方向へずらすなどしてから公式を用いるべきです。
#4.活用法
3で紹介した、call文を疑似関数として用いる方法を使用すると、汎用プログラミング言語でやっているのと似た記述が出来ます。
@echo off
:Main
rem 入力が適切な数値であるか検証し、初項A、末項L、公差Dとして等差数列の総和を計算し表示。
echo 初項がA、末項がL、公差がDの等差数列の総和を求めます。
echo AとDで定まる、Lに最も近い適切な項を末項として扱います。
echo 整数値以外を入力すると正しく動作しません。
call :GetALD
call :Verify
if %errorlevel% == 1 (
echo 入力された数値が正常ではありません。何かキーを押すと終了します。
pause >nul
exit
)
call :GetSum
echo 初項%A%、末項%L%、公差%D%の等差数列の総和:%errorlevel%
echo 何かキーを押すと終了します。
pause >nul
exit
:GetALD
rem A,L,Dの取得
set /p A="1以上の整数値で初項を入力してEnter >"
set /p L="1以上の整数値で末項を入力してEnter >"
set /p D="0以外の整数値で公差を入力してEnter >"
exit /b 0
:Verify
rem 入力値が正しいかどうか判別。
rem 入力が整数値以外の場合については今回の趣旨から逸脱するため想定外とした。
rem A,Lが1以上かどうかを検証。
if not %A% geq 1 (
exit /b 1
) else if not %L% geq 1 (
exit /b 1
) else if not %D% neq 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
rem 総和を計算
set sum=0
for /l %%i in (%A%,%D%,%L%) do set /a sum+=%%i
exit /b %sum%
このように、Mainラベルを用意し、そこに処理の流れをcall文で記述、実際の処理の大半はcall文で呼び出した疑似関数内で済ませるという方式をとることで、処理の流れやそれぞれの処理の内容が大幅に理解しやすくなります。また、どこかでバグが発生して処理が正しく実行できない場合も、そのエラー発生源と思しきラベルを呼ぶcall文の先頭にrem
と置いてコメントアウトすることができるし、call文の前後にpause
を置き、どこで落ちているのかを探すという作業が簡単になります。
さらにexit /b
の戻り値に0や1などを設定し、Mainラベルに戻った後errorlevel
で参照することにより、プロンプトが落ちないエラーでのエラー処理における無駄な変数の削減や、場合によってはif文の削減など、コードのスリム化を図ることもできます。
#5.まとめ
batファイルはその仕様上、長い処理を記述するとどうしてもスパゲッティになりやすいという特徴があります。しかし、call文に目を付け処理を整理していくことで、大抵のコードは大幅に見やすくなります。また、構文の再利用も大幅にしやすくなり、前時代的な要素を多く残すbatファイルにおいても今どきのプログラミングに近いコーディングを実現できるのではないかと思います。
そして今回「諸悪の根源」として紹介したgoto文ですが、batファイルにおいてgoto文は必要な存在です。その最も大きな理由が、「batにwhile文が存在しないこと」です。例えば上記プログラムにおいて、1度計算し終えたら再び先頭へ戻りもう一度A,L,D入力を受け付け、和を計算するという動作を、既定の終了動作がおこなわれるまで繰り返したいというとき、while文が存在すれば、全体をwhile(1)で囲んで、終了条件の後にbreakとでも書いて終わらせることができます。しかしbatの場合whileが存在しないため、このような動作を求められる時は、Mainラベルの最後にgoto Main
と書かなければいけません。
batファイルの構造は極力Mainラベルからcall文でそれぞれのラベルに移動し、各ラベルの処理が終了するごとにMainラベルへ戻ってくるようなものにし、goto文はごく限られた、batではgoto文でのみ実現できる動作を行う時のみの使用とするよう心がければ、いくら長いbatファイルを作ろうともスパゲッティコードにはならないはずです。
またこのようなソースコードの「構造」に対する関心を持つことはbatに限らず、どの言語においても重要です。最近は特にソースコードの「構造」が重要な言語が増えています。中にはコードの構造・組み方の仕様等でつまづいて、比較的書式の自由なbatファイルを使用しているという方もいるかもしれません。この記事が構造への理解関心を深めるきっかけになれば嬉しいです。
何かご指摘・ご意見などございましたらコメントでお願いいたします。