0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

秒単位でタイムスタンプを比較してビルドするバッチファイルをささっと作る(makeが欲しい)

Last updated at Posted at 2025-01-26

はじめに

あるとき出張先でちょっとしたプログラムを作ることになった。出張用のノートPCのため,開発用ツールはインストールされていないし,IT部門に申請する暇もないので,Windows に標準的に組み込まれているメモ帳と C# コンパイラで開発せざるを得なかった。当然のことながら便利なビルド自動化ツールもないので,十数個あるソースファイルのうち一つだけ弄っては全ビルドを実行するという無駄な作業を行っていた。

$\color{red}{\Huge\textsf{嗚呼 make が欲しい。}}$

ちなみに筆者は msbuild の使い方が分からないというか,正しくはプロジェクトファイルの書き方が分からない。たぶん人が手で書くようなものではないと思っている。

お品書き(仕様案)

ということで make のようにビルド自動化を行ってくれるバッチファイルを作ることにした。自動化といっても下記のような単純作業である。

  • ターゲットファイル(実行ファイル,EXE ファイルのこと)が存在しないとき,ビルドコマンドを実行する
  • ターゲットファイルが存在するとき,ターゲットファイルとソースファイル(CS ファイルのこと,複数)のタイムスタンプを比較し,ソースファイルのうち一つでもターゲットファイルより新しければビルドコマンドを実行する
  • ターゲットファイルが全てのソースファイルより新しければ何もしない

ターゲットファイルは複数あるので,これらを実行するサブルーチン AutoBuild としてまとめる。サブルーチン AutoBuild は以下の3つの引数を取るものとする。

ターゲットファイル名
ビルド後に生成される実行ファイル名とする。拡張子(この場合は .EXE)を含む。
ソースファイル名
拡張子(この場合は .CS)を含む。複数ファイルを指定するときはスペースで区切り,二重引用符で囲むこと。スペースを含むパス名には対応しない。
ビルドコマンド
スペースを含むときは二重引用符で囲むこと。

呼び出し時のイメージを以下に示す。TEST0.CSTEST9.CS まで10個のテストコードがあり,それらから実行ファイル TEST0.EXETEST9.EXE を作る。COMMON.CS は共用モジュールである。

make.cmd(こんな感じで呼び出したい)
@echo off
setlocal enabledelayedexpansion
for %%I in ( TEST0 TEST001 TEST2 TEST3 TEST4 TEST5 TEST6 TEST7 TEST8 TEST9 ) do (
  set  Target=%%I.EXE
  set  Source=%%I.CS COMMON.CS
  set  Option=-r:Microsoft.VisualBasic.DLL
  set Command=csc -nologo -w:4 -o -out:!Target! !Source! !Option!
  call :AutoBuild !Target! "!Source!" "!Command!"
)

課題)秒の単位を得ることができない

バッチファイルにおいて,ファイルの更新日時は簡単に得られる。CALL コマンドによって呼び出されるバッチパラメータ %1 あるいは FOR コマンドのループ変数 %%I とおくと,%~t1 もしくは %%~tI のように ~t という修飾子を付加すればファイルの更新日時を得ることができる。具体的には以下のように年月日に加えて24時間制の時分を得ることができるのだが,残念ながらの単位を取得することができない。

c:\qiita>@for %I in ( test0.* ) do @echo %~tI %I
2024/02/01 17:30 test0.exe
2024/02/01 17:27 test0.cs	

もちろんこれでもたいていの場合は問題ないが,コード修正~ビルド~テスト実行を1分以内に繰り返した場合,ソースファイルとビルドした実行ファイルの新旧判別ができないのだ。

※他に dir コマンドを用いてもファイルの更新日時を取得することができるが,秒の単位を得ることはできない。

案その1)forfiles コマンドを使う

Windows に標準搭載されているコマンドの中で解決しようとすれば,forfiles コマンドを使う方法が知られている。ただし,forfiles コマンドの動作は何故かもっさりとしていて受け入れ難い。もう少し,きびきびと動かないものだろうか。

c:\qiita>forfiles /M test0.* /c "cmd /c echo @fdate @ftime @file"

2024/02/01 17:30:39 "test0.exe"
2024/02/01 17:27:12 "test0.cs"

案その2)PowerShell を使う

PowerShell を使えば何でもできる。下記の場合は秒単位での表示に留めたが,ミリ秒単位で得ることも可能である。※参考文献参照のこと。

c:\qiita>powershell "Get-ChildItem test0.* | Select Name, LastWriteTime"

Name      LastWriteTime
----      -------------
test0.exe 2024/02/01 17:30:39
test0.cs  2024/02/01 17:27:12

加えて,タイムスタンプでソートすることも楽勝である。

c:\qiia>powershell "Get-ChildItem test0.* | Sort-Object LastWriteTime | Select Name"

Name
----
test0.cs
test0.exe

ただ,バッチファイルから PowerShell スクリプトを呼び出すと動作がもっさりしてしまうのが難点だ。ファイルのタイムスタンプを得るためだけに PowerShell を使うのは効率が悪い。PowerShell を使うのであればビルド自動化まで全部 PowerShell スクリプトで書くべきだろう。

案その3)dir コマンド を使う

PowerShell を使っていて閃いた。コマンドプロンプト上で使えるビルトインコマンドではファイルの更新日時を単位で得ることはできないが,dir コマンドを用いればファイルを単位で比較・ソートすることはできる。

c:\qiita>dir /B /OD /TW test0.*
test0.cs
test0.exe

最新のファイル名を取得したければ,for コマンドを用いて最終行のみ得ればよい。
※スキップする行数(=ソースファイル数)を事前に把握しておく必要がある。

c:\qiita>@for /F "skip=1" %I in ( 'dir /B /OD /TW test0.*' ) do @echo %I
test0.exe

コード実装

ということで,案その3を採用することにした。

一次チェック

最初から dir コマンドの出力を用いるともっさりとした動作になってしまうので,一次チェックとしてバッチパラメータの修飾子 %~t1 を用いてファイルの更新日時を比較する。ソースファイルのうちターゲットファイルよりも新しいものが一つでもあればビルド実行 ExecBuild=1 である。更新日時が同時分で新旧の判別がつかないものは Ambiguous=1 として二次チェックに進む。なお,一次チェックの中でソースファイル数をカウントしておく。

二次チェック

二次チェックでは dir コマンドの出力を用いる。ターゲットファイルとソースファイルの中で最新のファイル名を得て,これがターゲットファイル名と一致すればビルドしない。

実装コード

以上より,実装コードを以下に示す。

自動ビルド部
rem ----------------------------------------------------------------------------
rem 自動ビルド
rem
rem 引数(1) ターゲットファイル
rem 引数(2) ソースファイル(複数指定の場合は二重引用符で囲むこと)
rem 引数(3) 実行コマンド(スペースを含む場合は二重引用符で囲むこと)
rem ----------------------------------------------------------------------------
:AutoBuild
setlocal
rem ----------------------------------------------------------------------------
rem ターゲットファイルが存在しなければビルドを実行する
rem ----------------------------------------------------------------------------
if not exist %1 goto AutoBuild_Exec
rem ----------------------------------------------------------------------------
rem 一次チェック
rem
rem ・全ソースファイルのうちターゲットファイルよりも新しいファイルが一つでも存在
rem  すればビルドを実行する
rem ・全ソースファイルのうちターゲットファイルと同時分のファイルが存在すれば
rem  新旧不明として二次チェックに進む
rem ・ソースファイル数をカウントする ※二次チェックで使用する
rem ----------------------------------------------------------------------------
set Count=0
set ExecBuild=0
set Ambiguous=0
for %%I in ( %~2 ) do (
  if "%~t1" LSS "%%~tI" set ExecBuild=1
  if "%~t1" EQU "%%~tI" set Ambiguous=1
  set /a Count+=1
)
if %ExecBuild%==1 goto AutoBuild_Exec
if %Ambiguous%==0 goto AutoBuild_Return
rem ----------------------------------------------------------------------------
rem 二次チェック
rem
rem 全ソースファイルとターゲットファイルを更新日時でソートして一番新しいファイル
rem がターゲットファイルの場合はビルドを実行しない
rem ----------------------------------------------------------------------------
set ExecBuild=1
for /F "skip=%Count%" %%I in ( 'dir /B /OD /TW %~1 %~2' ) do (
  if /I %%I==%~1 set ExecBuild=0
)
if %ExecBuild%==0 goto AutoBuild_Return
rem ----------------------------------------------------------------------------
rem ビルド実行
rem ----------------------------------------------------------------------------
:AutoBuild_Exec
echo %~3
     %~3
:AutoBuild_Return
endlocal
exit /b

備考

自分で言うのもなんだが,今回作ったバッチファイルは二度と使わないかもしれない。

参考文献

0
0
0

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?