バッチにあるコメントが全体のどのくらいの割合か調べます。（GOTO %変数%も使ってみた。）

## バッチにコメント書いてますか？
バッチに限らず、自分が作ったプログラムを1年後に見ると、ほとんどわけわからなくなってませんか？
それを防止するためにもコメントを書いておくと、備忘録にもなるし、そのプログラムを他で活用する際の助けにもなります。

で、自分の作ったバッチにどのくらいコメントがあるか調べてみたくなってこのバッチを作りました。

## とはいえ、勉強用です。
このバッチは、以下の様々な要素を含んでます。
1.コマンド出力結果から、必要なデータを取り出す。
2.GOTO先のラベルを変数で指定する。
3.ファイルの更新日を取得する。
4.テキストを1行ずつ読みだして、先頭に特定文字があるかどうかを判定する。
5.ファイル名や、分析の文中に半角のスペースや＆があっても処理できるようにする。
6.変数の最初の方のスペースを全て取り除く。（置換だとすべて取り除いてしまうため）

色々なところで調べた結果の集約版ですので、サンプルとしてご利用ください。
バッチは"&"やスペースで結構思いもよらぬ動きをしますので、無理やり避けてます。

特に、目新しいところとしては、gotoのラベルに変数を使ってみました。
あまり例が無いようですが、ちょっと面白いです。
気をつけて使わないとプログラムの可読性が悪くなりがちなのですが、if文無しで分岐できるところは面白いです。

## バッチは以下の通りです。
これもすべてオリジナルなので、ご自由にお使いください。
```bat:rem_count.bat
@echo off
setlocal enabledelayedexpansion
chcp 932 >NUL

rem *Encode with shift-JIS(Japanese)
rem このファイルを拡張子.txtで入手した場合は、拡張子を.batに変更してから利用ください。
rem shift-JISで保存しないと文字化けして正常に動作しません。

rem 目的【バッチを分析するバッチ】
rem バッチに結構コメントを書いてるので、それを統計化できるように調査バッチを作った。
rem 最初は1行ずつ読んでrem、: 、::等を数えたけど、時間がかかるのでfindコマンドを使うことにした。
rem 1行ずつの解析でないので、:や、::で始める小技的コメントは拾えないし、最終空行を数えないなどあるが、概要なのでこれで良しとする。
rem なお、findコマンドを使うと、空白行もカウントするが、全行読み取りの場合は空白行が無視されることが分かった。
rem 最終的には、どちらの方法でカウントするかを指定できるような仕様にしました。

rem ■このバッチの勉強ポイント
rem 1.コマンド出力結果から、必要なデータを取り出す。
rem 2.goto先のラベルを変数で指定する。
rem 3.ファイルの更新日を取得する。
rem 4.テキストを1行ずつ読みだして、中に特定文字があるかどうかを判定する。
rem 5.ファイル名や、分析の文中に半角のスペースや＆があっても処理できるようにする。
rem 6.変数の最初の方のスペースを全て取り除く。（置換だとすべて取り除いてしまうため）

set target=%1

rem 以下はremをカウントする方法
rem 「find」モード(mode 0)にすると"rem "をコメントとしてカウントする。
rem そのため、コマンドの途中に出現してもカウントしてしまう。
rem また、空行も含めて全行としてカウントする。
rem 「all_read」モード(mode 1)にすると、1行ずつ読み込んで左側のインテンド用のスペースを削除し、
rem "rem ",": ",":: "のいずれかで始まっているものをコメントとしてカウントする。
rem また、空行はカウントしない。
rem all_readの方が正確だが遅い。

set count_method=:find_only
if "%2"=="1" (set count_method=:all_read)

rem 第1パラメータは、必須で、分析対象のバッチファイル名指定です。
rem 第1パラメータが"で括られていることがあります、すると""がダブってエラーになるので、区切りとして、'にしておきました。
if '%target%'=='' ( goto :usage )
if '%target%'=='/?' ( goto :usage )
if /i '%target%'=='/h' ( goto :usage )
if /i '%target%'=='/help' ( goto :usage )
if /i '%target%'=='-list' ( goto :list_scan )

rem 一つのファイルを指定して実行するパターン
call :scan_batch %target%
exit /b

:list_scan
rem 複数のバッチを指定して実行のパターン

set list_file=%~n0.list
if not exist %list_file% (
  echo 「%list_file%」ファイルがカレントディレクトリに見つかりません。
  echo -list指定の場合は、調べたいバッチ名を「%list_file%」に1行、1バッチファイル名で列挙してください。
  echo ちなみに、dir /b /s [調べたいディレクトリのパス\*.bat]^>%list_file%をしてから -list指定すれば、全バッチを調べることもできます。
  exit /b
)

rem 結果ファイルのタイトル行
rem 結果ファイルが既にある場合、確認せずに上書きします。
echo ファイル名,コマンド行数,コマンド率,コメント行数,コメント率,合計行数,最終更新日時>%~n0.csv

rem 指定ホストを1台ずつ調べて、結果出力します。
rem 1行も無い場合は、メッセージが不自然ですが、複雑になるだけなので、あえてコーディングしません。
rem for文に中で、変化する変数は!で囲みます。
rem %で囲むと、for文に入った時点の値に固定されるので、値を変更しても参照値は変化しません。

for /f "delims=" %%a in (%list_file%) do (
   call :scan_batch "%%a"
   echo !target!,!command_count!,!command_pct!%%,!rem_count!,!comment_pct!%%,!total_count!,!UPDATE!>>%~n0.csv
    )
echo.
echo 「%list_file%」で指定された上記の結果をファイル「%~dpn0.csv」に出力しました。
exit /b

rem ===============Sub Routines=================
:scan_batch
set target=%1

rem 以下で、.batが無い場合に付け足すが、&があるとそこ以降を新たなコマンドと解釈されて、
rem 処理が異常になるので、文字列として処理する際は、一旦ほかの文字に置き替えて処理する。
rem また、""で囲まれている場合と囲まれていない場合があるので、少々面倒なつくりになっている。
set and_dummy=_@@__@@_
set x=%target:&=!and_dummy!%
rem ""で囲まれている場合と、囲まれていない場合があるので、一旦取り除く。
rem &が残った状態で、""を取り除くと誤動作するので注意！
set x=%x:"=%
rem .batで終わってい無ければ、.batを補足する。
if /i not "%x:~-4%"==".bat" (set x=%x%.bat)
rem 再度""で囲い直す。
set x="%x%"
rem &のダミーを元に戻す。
set target=!x:%and_dummy%=^&!

if not exist %target% (
  echo 「%target%」は、存在しません。ディレクトリまたはファイル名を確認ください。
  exit /b
)

rem 更新日
for %%i in (%target%) do set "UPDATE=%%~ti"

rem gotoの先のラベルは変数指定もできる！
rem モードによって、:find_onlyまたは、:all_readにジャンプします。
goto %count_method%


:find_only
rem 以下はfindコマンドでのカウント。
rem まず、remの数を数えます。:とか、::でコメントする人もいるけど、それらはコメントとしてカウントしません。
rem findコマンドだけで処理し、行の初めのブランクをトリムをしないため、速いです。

FOR /F "tokens=* delims=" %%i in ('find /i /c "rem " %target%') DO SET count=%%i
rem &をエスケープするために、苦肉の策です。
set count=%count:&=_%
rem findコマンドの結果は、ファイル名に「ドライブ:」や「スペース」が含まれる場合があるため、何番目のtokenかは、不定です。
rem 最後のスペースの後ろに見つかった数が書かれてるので、最後のスペースを探します。
rem スマートじゃないけど、この方法しか思いつかない・・・。
rem ちなみに、スペースを含む文字列をパラメータにするので、""で囲みます。
call :get_last_space "%count%"
set last_space= %ERRORLEVEL%
set rem_count=!count:~%last_space%!

rem 上記同様の方法で全部の行数を数えます。
FOR /F "tokens=* delims=" %%i in ('find /v /c "" %target%') DO SET count=%%i
set count=%count:^&=_%
call :get_last_space "%count%"
set last_space= %ERRORLEVEL%
set total_count=!count:~%last_space%!

goto :count_complete


:all_read
rem 以下は全行を読み込んで分析
rem これだと、行の途中に登場する"rem "をremと数えない＋": "や":: "でのコメントも数えるので、より正確。
rem しかし、時間がかかる。また、空行はカウントしない。

set rem_count=0
set total_count=0

FOR /F "delims=" %%i in ( 'type %target%' ) DO (
  SET row="%%i"
  set /a total_count=!total_count!+1
  
  set rem_exist=0
  rem &をエスケープします。
  set row=!row:^&=_!

  rem スペースで始まるremを見つけるためのtrim
  call :trim
  
  rem %V:~m,n%	m文字目から、n文字分
  set row2=!row:~1,2!
  set row3=!row:~1,3!
  set row4=!row:~1,4!
  
  if "!row2!"==": " set rem_exist=1
  if "!row3!"==":: " set rem_exist=1
  if /i "!row4!"=="rem " set rem_exist=1
  
  if !rem_exist!==1 (
    set /a rem_count=!rem_count!+1)
  
  )


:count_complete
rem 上記から、関連数字(%など)を計算します。
set /a command_count=%total_count% - %rem_count%
set /a comment_pct=%rem_count%*100/%total_count%
set /a command_pct=100-%comment_pct%

echo %target% の分析結果
echo コマンド行数＝%command_count%行 (%command_pct%%%) 【コメント行数＝%rem_count%行 (%comment_pct%%%)】 
echo =====================================
echo 合計＝%total_count%行 最終更新日時＝%UPDATE%
exit /b

rem 以下はサブルーチン群

:usage
echo.
echo ■バッチの中にどの程度のコメント行があるか調べます。
echo.
echo 使用法: %0 [バッチファイル名、または -list] [mode]
echo ※ファイル名に^&など、特殊文字を含む場合は、""で括ってください。
echo.
echo オプション1:
echo ターゲットを-listとした場合は、カレントディレクトリの「%~n0.list」にバッチファイル名を「1行に1つ」書いてください。
echo -listとした場合は、画面表示だけでなく、カレントディレクトリの「%~n0.csv」ファイルに結果を出力します。
echo.
echo オプション2:
echo modeは[0または1]デフォルトmodeは、0
echo mode0・・・高速モード＝"rem "をremとして数えるので、コマンド中に"rem "があるとコメント行としてカウントしてしまう。
echo            また、高速モードでは空行もコマンドとしてカウントしてしまう。
echo            本プログラムは、コメント率を調べるものであり、正確性はあまり重要でないので、高速モードでも十分機能は果たします。
echo mode1・・・通常モード＝全ての行を読み込んで、remを検査する。
echo            正確にカウントしますが、若干時間がかかります。
echo.
echo ちなみに、dir /b /s [調べたいディレクトリのパス\*.bat]^>%~n0.listをしてから -list指定すれば、全部調べることもできます。

exit /b

:get_last_space
rem 最後のスペースの場所を返す
set str=%~1
set length=0

rem 文字の中に)があると、if文でエラーになる。
rem なぜか、ここではエスケープも効かないのですが、文字数を数えるだけなので、苦肉の策です。
set str=%str:)=_%

:get_last_LOOP
if not "%str%"=="" (
    set str=%str:~1%
    set space_check=%str:~0,1%
    rem 最後の長さ０の文字をいきなり比較するとエラーになっちゃんです。
    rem カッコ悪いけど、まず長さ0（つまり空っぽ）でない事を確認してから比較することにした。
    if not "%space_check%"=="" (
       if "%space_check%"==" " set last_space=%length%
    )
    set /a length+=1
    goto :get_last_LOOP
)
exit /b %last_space%


:trim
rem 途中のスペースを取ると、"rem ",": ",":: "がみつけられなくなるので、最初のスペースだけ削除する。

set trim_row=!row:~1,1!
if "!trim_row!"==" " (
  set row=!row:~0,1!!row:~2!
  goto :trim
)
exit /b
```

## 上記バッチのポイントだけ抜き出しました。

1.コマンド出力結果から、必要なデータを取り出す。

以下のような感じで、コマンドの結果を変数に代入してます。
下記は、説明も書いているので、コピペしてもエラーになります。

```bat
"rem "を探して、見つかった行数が入った行をcountという変数に入れます。
なお、その後、カウンター値だけを取り出してます。
FOR /F "tokens=* delims=" %%i in ('find /i /c "rem " %target%') DO SET count=%%i

全ての行数をcountという変数に入れます。
なお、その後、カウンター値だけを取り出してます。
FOR /F "tokens=* delims=" %%i in ('find /v /c "" %target%') DO SET count=%%i

rem テキストファイルを1行ずつ読みだして処理してます。
FOR /F "delims=" %%i in ( 'type %target%' ) DO (
・・・
)
```


2.GOTO先のラベルを変数で指定する。
ちょっと変態な変数の使い方です。
変数にあらかじめラベル名を代入しておいて、そこにジャンプします。
間接ジャンプっぽい感じ。
GOTO %label[!number!]%
みたいにして、仮想配列っぽい変数、label[n]を使うと、メニュー番号とか面白いことが出来そう。


```bat
set count_method=:find_only
if "%2"=="1" (set count_method=:all_read)

goto %count_method%
```


3.ファイルの更新日を取得する。

これ、いまいち仕組みが良くわかってませんが、決まり事として使ってます。
%target%にファイル名が入ってます。
変数UPDATEに、ファイルの更新日が入ってきます。

```bat
for %%i in (%target%) do set "UPDATE=%%~ti"
```


4.テキストを1行ずつ読みだして、先頭に特定文字があるかどうかを判定する。

"delims="と指定しないと、スペースで区切られてしまいます。
1行まるまる読み込みたいときは、これを指定します。区切りが無いという意味です。
本当は、'type %target%'は、%target%でもいいはずですが、変数の中身が""で囲まれていると
なぜか、うまく動かなかったので、こうしてます。
実際には、この後取り出した行を、トリミングして、冒頭の文字を比較してます。

```bat

FOR /F "delims=" %%i in ( 'type %target%' ) DO (
  SET row="%%i"
・・・
)
```

5.ファイル名や、分析の文中に半角のスペースや＆があっても処理できるようにする。

一番面倒なパターンは以下の通りです。
文字判定をする際にファイル名に&があると誤動作します。
そこで、いったん変な文字列に置き替えて、最後に元に戻します。
and_dummyという変数に「_@@__@@_」を入れてますが、もっとへんちくりんな値でも構いません。
ただし、ここにバッチ上の特殊文字（&,%,",()など）は使わないでください。


```bat
set and_dummy=_@@__@@_
set x=%target:&=!and_dummy!%
rem ""で囲まれている場合と、囲まれていない場合があるので、一旦取り除く。
rem &が残った状態で、""を取り除くと誤動作するので注意！
set x=%x:"=%
rem .batで終わってい無ければ、.batを補足する。
if /i not "%x:~-4%"==".bat" (set x=%x%.bat)
rem 再度""で囲い直す。
set x="%x%"
rem &のダミーを元に戻す。
set target=!x:%and_dummy%=^&!
```

&が入ったファイル名を文字として処理したいけど、そのあと元に戻す必要が無ければ、
以下で十分です。「_」はよく使われているので、不可逆（元に戻せない）です。

```bat
set count=%count:^&=_%
```


6.変数の最初の方のスペースを全て取り除く。（置換だとすべて取り除いてしまうため）

文字置換でスペースを削除すると、文字れる中のすべてのスペースが無くなってしまいます。
変数の頭にあるスペースだけを取りたい場合は、1文字ずつ削除するしかないみたいです。
これは、サブルーチンとして、callで利用できます。
本当は、対象文字列をグローバル変数でなく、パラメータで渡せればよいのですが、
リターンが数字しか返せないので、今のところこうしています。
後日、パラメータで渡して、削除文字数をリターンする、見たいにするかもしれません。

```bat
:trim
rem rowという変数から、最初のスペースだけ削除する。

set trim_row=!row:~1,1!
if "!trim_row!"==" " (
  set row=!row:~0,1!!row:~2!
  goto :trim
)
exit /b
```