バッチでバックアップ

 13日02:15 コメントで指摘のあった部分を訂正&追記。
 16日21:20 ブロック外のため、FORにする必要の無かったファイルサイズ取得部分を修正。

はじめに

 FCE Ultra(以下FCEU)というファミコンエミュレータがある。

 これにはムービー記録の機能が付いていて、それで記録したファイルや、それを後で再生しながらAVI等に録画したファイルを相手に渡すことで、ゲームのプレイ動画を相手に見せることが出来る。

 また、記録中にスナップショット(どこでもセーブ)を取っておけば、失敗したときにそれをロードしてそこから何度でもやり直すことが出来る。もちろん記録ファイルには失敗した部分は残らない。

 こういう機能を使ってTAS Videoは作られている。もちろんこの機能だけではそれなりの動画しか作れないが…。

 まあ詳しいことはGamesTechなどを見てもらった方がいい。

本題

 ここでは、そのムービー記録を行っているときに作成されるファイルを、自動的にバックアップするバッチファイルを紹介する。

 ちなみに、拡張機能を使っているため、2000以降のOSでないと動かないと思う。

 まずはソース(バッチでもそう呼ぶのかな?)。

@echo off

set P=bku.log
set TMPF=temp.$$$

set D=%DATE:~-10%
set T=%TIME:~0,8%

set DT=%D:/=%%T::=%
set DT=%DT: =0%

echo 日時:%DT%>%P%
echo ============>>%P%
echo セーブデータ>>%P%
echo ============>>%P%

for %%f in (.\fcs\*.fc?) do (
 echo savefile: %%f>>%P%
 call :bku "%%f" fcs
)

echo ------------------------------->>%P%
echo.>>%P%
echo ==============>>%P%
echo ムービーデータ>>%P%
echo ==============>>%P%

for %%f in (.\movie\*.fcm) do (
 echo moviefile: %%f>>%P%
 call :bku "%%f" movie
)

goto end

:bku
setlocal ENABLEDELAYEDEXPANSION
set FILE=%1

echo %FILE%

if "%2"=="fcs" (
 set FN=!FILE:~7,-1!
) else (
 set FN=!FILE:~9,-1!
)

set FNAME=%FN:~0,-4%
set EXT=%FN:~-3%

set FSIZE1=%~z1

for %%f in (".\%2\bku\%FNAME%_??????????????.%EXT%") do (
 for %%S in ("%%f") do set FSIZE2=%%~zS
 if !FSIZE1! EQU !FSIZE2! (
  echo fc /b "%%f" %FILE%>>%P%
  fc /b "%%f" %FILE%>%TMPF%
  for %%S in (%TMPF%) do set FSIZE=%%~zS
  if !FSIZE! LSS 1000 (
   FOR /F "eol=0 skip=1 tokens=2" %%D IN (%TMPF%) DO IF "%%D"=="相違点は検出されませんでした" goto no_diff
  ) else (
   echo skipped[chk]: %%f>>%P%
  )
 ) else (
  echo skipped[fc]: %%f>>%P%
 )
)

echo copy %FILE% ".\%2\bku\%FNAME%_%DT%.%EXT%">>%P%
copy %FILE% ".\%2\bku\%FNAME%_%DT%.%EXT%"

goto bku_end

:no_diff
echo 既にバックアップ済み>>%P%

:bku_end
echo.>>%P%
endlocal
exit /b

:end
del %TMPF%
start fceu.exe

 これを適当なファイル名.bat(たとえばbku.batとか)でFCEUをインストールしたフォルダに保存すればいい。

 このまま実行しても特に問題はないが、好みに応じて二つほど設定できる項目がある。

 最初に出てくる

set P=bku.log
set TMPF=temp.$$$

 のP(rint)は、このようにファイル名にすればそのファイルにログが記録されるし、CON とすれば画面に出て、NUL とすれば闇に葬られる。

 そのすぐ下は

echo 日時:%DT%>%P%

 となっているから、ファイル名を指定した場合は毎回上書きされてしまう。追記したい場合は > を >> にすればよい。

 次の TMPF はテンポラリファイルの指定。上書きをするので、存在しない・消えても構わないファイル名にしよう。

動作

 現在のスナップショット、ムービーデータが既にバックアップされているかどうかをチェックしてからコピーしているから、重複してコピーされることはない。

 内容チェックは、全ファイルに対して行うとかなり時間が掛かってしまうため、必要のない比較は行わないようにしている。

 まず、ファイルサイズのチェック。サイズが違うのに内容が同じと言うことはあり得ない。これで一番時間の掛かる fc が飛ばせる。

 次に、fc が返してきた内容をリダイレクトしたファイルのサイズをチェック。1000バイト以下の時のみチェックする。内容が一致している場合は、恐らく200〜300バイト以内には収まっているはずだが、多少余裕を持って1000バイトということにした。ファイルパスの長さもあることだし。

 また、FOR を回すときにも、0から始まる行はスキップすることで無駄な IF が実行されるのを防いでいる。

ファイル .\MOVIE\BKU\A_20071011212801.fcm と .\MOVIE\A.FCM を比較しています
0000000C: AD 8E
0000000D: 77 76
00000010: 9B 15

 こういった、下の三行みたいなのを飛ばすというわけだ。不一致部分が多くなると、先頭に1が出てくることもあるが、1000バイト制限を掛けているので問題はない。

 一致しているときには

FC: 相違点は検出されませんでした

 と出るわけで、0で始まる行が一致する訳がない。

 こうやって内容チェックを行い、途中で一致するファイルが出てきたらバックアップ済み、出てこなければまだバックアップしてないということでそのファイルをコピーする。

 ちなみに、fc が返した ERRORLEVEL を利用せずに FOR を使ってチェックしている理由は、fc の返す値が2000とXPとで違うため。

 2000では、たとえ内容が違っていてもファイルサイズが同じなら0を返すのに対し、XPでは、内容が違えば1を返す。

 普通に考えればXPでの挙動が当然のはずなんだけど、なんで2000はこんな仕様になっているのやら…。

バッチファイルについて

 それにしても、久々にバッチファイルに触れてみてびっくりした。

 これまでもバッチファイルはたまに作っていたけど、MS-DOS時代の機能しか使っていなかった。

 知らない間にこんなに進化していたとは…。

 まず、環境変数の値の、途中だけを取り出したり簡単な置換が出来るようになっている。

 文字列の長さを知ることが出来ればもっと便利なんだけどな。

 そして、不完全ながら IF や FOR でブロック文が使えるようになっている。

 不完全というのは、ブロック内で goto を使うと、そこがブロックであったことを忘れてしまうから。

たとえば、

for %%i in (1 2 3) do (
 if "%%i"=="2" goto next
 echo %%i
:next
echo next
)

 こんなのを実行したとする。期待しているのは、

1
next
next
3
next

 なのに、実際には

1
next
next

 となってしまう。

 まあバッチファイルにあまり凄いことを求めすぎてもいけないのかな。

 その IF や FOR 自体もいろいろ機能アップしていて、IF では、比較演算子というものが導入されている。このバッチファイルでも使っているが、EQU や LSS 等がそれに当たる。

 FOR の方はかなり色々と追加されていて…HELP FOR などで見た方がいいかもしれない。

 本来、一つのファイルのサイズを求めるためだけなら FOR を使わずに %~z1 なんかを使った方がいいのかもしれないが、うまくいかなかったから仕方がない。

 %1と%2は既に使っていたから実際には%3で %~z3 となるのだけど、サイズを取得したいテンポラリファイルを削除した状態で始めると空欄になってしまったし、存在する状態だと同じサイズばかりを報告してきた。fc を実行した直後に取得しているのに。

 ループ内でやっていたため、おそらく後述する即時変数展開が原因だろう。

 まあそんなわけで、%~z1 を使わずに FOR を利用して取得するようにしたのだ。


 さて、その即時変数展開の話。

 ブロック内に書かれた変数は、そのブロックが読み取られた時点で展開されてしまう。

 HELP SET に書いてあるサンプルを少しだけいじったものだが、これを見ればわかりやすいだろう。

set VAR=before
if "%VAR%" == "before" (
 set VAR=after
 echo %VAR%
)

 これ、どう見たって画面には after と表示されるとしか思えないだろう。なんせ VAR に after をセットした直後にその内容を echo なんだから。

 でも、実際に出力されるのは before である。何故?

 IF文の行まで来たとき、( があるからブロックであることが分かる。そして ) まで読み込み、そこで変数の内容が展開される。そうすると、上の内容が

if "before" == "before" (
 set VAR=after
 echo before
)

 に置き換えられてから実行される。

 つまり、いくら VAR に値をセットしても、次の行は echo before となってしまっているため、before が出力されるのは当然ということになる。

 これを回避するには、cmd /V:ON としてコマンドプロンプトを起動するかバッチファイル内に

setlocal ENABLEDELAYEDEXPANSION

 と書くかして遅延環境変数の展開を有効にした上で、環境変数を % ではなく ! で括ればいい。

 要するに

setlocal ENABLEDELAYEDEXPANSION
set VAR=before
if "%VAR%" == "before" (
 set VAR=after
 echo !VAR!
)
endlocal

 とすれば、after と表示されるようになる。

参照:http://f32.aaa.livedoor.jp/~kobun/index.php?CMD.EXE%A4%CE%C3%D9%B1%E4%B4%C4%B6%AD%CA%D1%BF%F4%A4%CE%C5%B8%B3%AB

 というわけで先に書いた「ファイルサイズが正常に取得されない」というのは、ループ内で fc→%~z1 とやったために、ループに入る前、つまり fc がテンポラリファイルを作成する前にファイルサイズを取得しに行ったため、ファイルが無い状態で実行した時は空欄、存在した状態で実行した時は前に実行したファイルのサイズを返してきたのだろう。

 出来るかと思って、一応 !~z1 なんてことをしてみたのだけど、そのまま !~z1 と表示されてしまった。うーん。


 あともう一つ。

 サブルーチンのような物が使えるようになっている。

 実際には、バッチファイルの中から別のバッチファイルを呼び出す call 命令を拡張したような物みたいだけど。

 call の引数として、ファイル名ではなくラベルを指定すればいい。

 return の代わりに goto :eof もしくは exit /b と書けば戻ってくる。サブルーチン側で単に exit としてしまうと、大元のバッチファイル自体が終了してしまうので注意。


 というわけで何が何だか分からなくなってしまったが、まあ「バッチファイルでも色々出来るんだね!」という感じでよろしく。