バッチファイルでSVNを叩いてみた

Windowsマシンで作業を行っていると、ツールを作成する事があると思います。

アプリとして作る必要がある場合は、C++やC#等で、Windowsプログラムを作成するでしょう。
そこまでしっかりしたものでなく、ちょっとしたスクリプトを、WindowsOSで用意したい場合には、バッチファイルを作成する機会も多いと思います。

久々に、batファイルを作成した際に、曖昧な点も多く、調べながら作業した為、振り返りのつもりで記事にします。


# やりたかった事

今回やりたかった事は、特定の期間に更新されたファイルの一覧を取得するという事になります。
ツールを作成する上での仕様は、以下の要件が満たせるものとしました。

 * 特定の期間は、定数または入力によって、変更可能
 * ファイルの一覧は、ディレクトリ込みのファイルのみのリストを作成し、重複は除外
 * batファイル単体で機能するようにして、SVNの管理化のディレクトリであれば、そこを作業のルートディレクトリとして、どこでも実行可能なものとします

# ファイルが残っていたら削除

```bash
set TMP_HOGE=確認したいファイル
if exist %TMP_HOGE% del %TMP_HOGE%
```

既に出力ファイルが作成されていないかチェックし、存在していた場合は削除します。


# SVNのリストをログを取ってくる

SVNのログを取得するには、以下のコマンドで行います。

```bash
svn log --incremental -r {開始日時}:{終了日時} -v リポジトリのURL
```

『svn log』を実行し、オプションで『SVNのリポジトリ』『取得する期間』『 --incremental -r -v』を付けています。

:::note info

取得する期間は、時間だけでなく、**リビジョン番号**にもできます。
また、『開始日時』から『終了日時』に向かってログを取得してくる為、左右の引数を入れ替える事で**昇順・降順**も制御出来ます。

:::



# ローカルスコープを使う

```bash
setlocal enabledelayedexpansion

ローカルスコープでやる処理

endlocal
```

今回、詰まったポイントでした。
ローカルスコープを使用する必要があった為、使用しています。
下のループ内では、**グローバル変数か遅延環境変数でないと、変数が使用できません**でした。

:::note warn

注意点として、ローカルスコープ内では、『%value%』のように、『%』では変数にアクセス出来ない為、『!value!』のように、『!』で囲みます。
『!』で囲れたものは、**遅延環境変数**と呼びます。

:::



# ファイルから1行読み出し

```bash
for /f "delims=" %%a in (%TMP_SVN_LOG%) do (
    echo %%a
)
```

for文でファイルの中身を読み取っています。
これは、SVNのログを一旦、ファイルに書き出し、そのファイルの中身を解析するという事をやっています。
1行での読み出したかった為、**『"delims="』のように、『delims』に何も設定していません**。

:::note info

『delims』の設定の仕方によって、**スペースを区切り文字**として引数を取得する事も可能です。
[**こちら**](https://jj-blues.com/cms/wantto-readstringfromfile/)の記事では、具体例も載っている為、必要な実装でお試しください。

:::

# 文字列にキーワードが入っているか判定する

```bash
echo "%%a" | find "%キーワード%" > nul
if ERRORLEVEL 1 (
  print キーワードが含まれていない
) else (
  print キーワードが含まれている
)

```

特定の文字列に目的のキーワードが含まれているかチェックします。
今回の場合は、最終的に更新ファイルのリストが欲しかった為、なるべくキーワード余計な行は除外しました。

# 数字の比較

```bash
if !value1! equ 1 (
   print 真
) else (
   print 偽
)
```

ここも少し、迷いました。
文字と数字の比較は、書き方が異なります。

:::note warn

文字場合は、『**==**』で比較してください。
数字の場合は、『**equ**』や『**equ**』を使用する必要があります。

:::


# サブルーチンの呼び出し

```bash
rem サブルーチン呼び出し
call :STRIP !global_string!

:STRIP
SET global_string=%*
exit /b
```

実は、サブルーチンもほとんど使った事が無かった為、何回か試しました。
**Forループ内で、GOTO文を使用した際に、上手く動作しませんでした**が、サブルーチンは使用可能でした。
[こちら](https://jj-blues.com/cms/wantto-usingsubroutineinbatfile/)を参考にさせていただきました。

:::note info

サブルーチンは引数を渡す事ができます。

:::

##### 以上の事を踏まえて、実際に作成してみたbatが次の項目です。


# 実際のコード(コードを見て確認したい人向け)

ファイル名やSVNのディレクトリは、任意に変更してください。
おまじないも追加しています。
取得期間とかは、ユーザの入力にしてしまった方が、ユーザビリティは高いかもしれません。

```bash
@echo off
cd /d %~dp0


rem 定数(変更禁止)
set TMP_SVN_LOG=中間の出力ファイル名①
set TMP_LOOP=中間の出力ファイル名②
set OUTPUT_FILE=出力ファイル名
set SVN_REPOSITORY=任意のSVNのリポジトリ
rem キーワード文字は、任意に変更してください
set ExclusionKeyword1=Changed
set ExclusionKeyword2=------------------------------------------------------------------------

rem 任意の期間入力してください
set DATE_START=2023-01-01
set DATE_END=2023-12-31


rem 
rem SVNのログ取得
rem 
if exist %TMP_SVN_LOG% del %TMP_SVN_LOG%
svn log --incremental -r {%DATE_START%}:{%DATE_END%} -v %SVN_REPOSITORY% >> %TMP_SVN_LOG%


rem 
rem キーワードで不要なログを除外
rem 
if exist %TMP_LOOP% del %TMP_LOOP%

setlocal enabledelayedexpansion

for /f "delims=" %%a in (%TMP_SVN_LOG%) do (

   set error1=1
   echo "%%a" | find "%ExclusionKeyword1%" > nul
   if not ERRORLEVEL 1 (
      set error1=0
   )
   set error2=1
   echo "%%a" | find "%ExclusionKeyword2%" > nul
   if not ERRORLEVEL 1 (
      set error2=0
   )

   if !error1! equ 1 (
       if !error2! equ 1 (
           echo %%a >> %TMP_LOOP%
       )
   )
)

endlocal


rem 
rem 重複を削除する
rem 
if exist %OUTPUT_FILE% del %OUTPUT_FILE%

set global_string=""

setlocal enabledelayedexpansion
set n=0
for /f "delims=" %%a in (%TMP_LOOP%) do (

    set /a n=!n!+1
    set m=0
    set write=1

    for /f "delims=" %%b in (%TMP_LOOP%) do (
        set /a m=!m!+1

        if !m! lss !n! (
             if %%a == %%b ( set write=0 )
        )
    )

    set global_string=%%a

    rem サブルーチン呼び出し
    call :STRIP !global_string!

    rem SVNのアクションのログのみ書き出し
    set result=!global_string:~0,1!
    set true_flag=0
    if !result!==A set true_flag=1
    if !result!==M set true_flag=1
    if !result!==D set true_flag=1
    if !true_flag! equ 0 (
        set write=0
    )

    rem 書き込み判定
    if !write! equ 1 (
       echo !global_string! >> %OUTPUT_FILE%
    )
)


rem 
rem サブルーチンで先頭と文末の開業を削除
rem ※サブルーチンの下に処理を書く際は注意
rem 
:STRIP
SET global_string=%*
exit /b

endlocal
```



# 参考

 * [windowsバッチ コマンド別解説](https://jj-blues.com/cms/quick_search/top-command/)
 * [Subversion　ファイルの最初のコミットの日時をまとめて調べる](https://qiita.com/yotsak/items/dc13f45b0784a95aac0b)
 * [ファイルから文字列を読み込む](https://jj-blues.com/cms/wantto-readstringfromfile/)
 * [バッチファイルで条件分岐を使う(IF)](https://www.javadrive.jp/command/bat/index8.html)
 * [バッチファイルでサブルーチンを使用する](https://jj-blues.com/cms/wantto-usingsubroutineinbatfile/)
 * [ある文字列の中に指定した文字が含まれるか調べる方法](https://www.projectgroup.info/tips/Windows/cmd_0023.html)
 * [SetLocal EnableDelayedExpansionの罠とその回避方法](https://qiita.com/yz2cm/items/4983be006116c369d08b)
 * [テキストファイル内の重複行を取り除いて別ファイルへ出力する](https://papermelt.hatenadiary.org/entry/20111014/1318579400)
 * [変数を使用する](https://shellscript.sunone.me/variable.html)
 * [バッチファイルで変数を使う方法](https://somachob.com/bat-set/
)