LoginSignup
13
18

More than 5 years have passed since last update.

構造化バッチファイル ~読みやすさ重視~

Last updated at Posted at 2017-07-24

読みくくなりがちなバッチファイルのコードを可能な限り読みやすくしましょう。というネタ提言です。

1. べた書き

例として以下のコードを使用します。

Sum.bat
@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乗の計算
  • 総和の計算

に分けられるはずです。
分けてみたのが下のものです。

Sum.bat
@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. インデント

 しかし、まだまだ読みにくいでしょう。なにしろインデントなしですからね。
なので、インデントを付けます。

Sum.bat
@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文を使ってもそこまで読みにくくならないように改善してみます。

Sum.bat
@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. サブルーチンに引数・戻り値アノテーションを追加

 まだまだ終わりではありません。このプログラムではラベル名をサブルーチンの目印に使っています。しかし一般的なプログラミング言語のように、引数をいくつ取ってどんな値を返すのかを指定することができません。これはバッチファイルの仕様上仕方のないことですが、せめて後から見返した時にすぐ理解できるようにすることはできないでしょうか。

 そこで::の使用を提案します。バッチファイルでは::の後は行の終わりまでコメントとして扱われます。これは当然ラベルの後ろにも付けられるので、これをアノテーションとして使います。

Sum.bat
@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ラベルにも同じことができるということです。

Sum.bat
@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を上記の記法に倣いつつ、新たにいくつかの新たな記法を導入して書き直したものが以下になります。

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ラベルについては特にエントリポイントなどの機能はありませんが、主処理がとこからどこまでかを示すには使えそうです。(バッチファイルは何も指示がない限り完全に上から下に実行します。ラベルで区切ったサブルーチンの中にも勝手に入ってしまうので、主処理群は基本的に一番上に記述してください。)

 MainVerifyGetSumの各ラベルの下にあるsetlocalは環境変数のスコープを作るコマンドです。スコープの有効範囲はendlocalexitに到達するまでです。つまりこれらのサブルーチンでは、setlocal以下でsetされた環境変数についてはそのサブルーチン内で有効なローカル変数になります。

 GetALDでは引数アノテーションにref, ref, refと書きましたが、これは環境変数名を引数に取ってくださいという意味です。(使い方がreferenceつまり参照に近いと思ったのでrefを使いましたがもっと的確な表現がありましたらコメントください。)
環境変数名を引数に取り、その環境変数に対してそれぞれ入力を受け付けて代入します。上のプログラムでは、
set /p %1="1以上の整数値で初項を入力してEnter >"
%1(引数の1番目)にAが入っているので、
set /p A="1以上の整数値で初項を入力してEnter >"
として動作します。また、このサブルーチンに対してsetlocalを指定してしまうと、このサブルーチンで代入した環境変数が呼び出し元に引き継がれないため、何も指定せずスコープは呼び出し元と結合しています。

8. まとめ ~これどこで使うの?~

 どうでしょう、それなりによく使われるプログラミング言語の記述にかなり近づいたように見えないでしょうか。まぁ、バッチファイルでこのような記述をすると正直コードが長くなるので、進んでやる人は少ないと思います(というより、わざわざバッチファイルなんかで書かないよね)
 バッチファイルでゲーム制作とか苦行がしたい(特に複数人で)という方はこのような書き方で可読性を大幅に向上させて圧倒的成長:muscle::muscle::muscle:が期待できるでしょう。(本当か?)

13
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
18