VBScriptをなめるな。ファイルシステム操作の「死角」を断つ
業務自動化の現場において、VBScriptはいまだにインフラの隙間を埋める隠れた主力だ。特に、ログファイルの定期的なパージ(削除)といった保守バッチにおいて、FSO(FileSystemObject)を用いたスクリプトの需要は尽きない。
しかし、ネット上に転がっている「動くだけの適当なコード」をそのまま本番環境に投入し、痛い目を見たエンジニアは数知れない。
「30日以上前のファイルを消す」という一見シンプルな要件であっても、タイムゾーンの罠、ファイルロックによる例外、そしてFSO特有のメモリリークの危険性を理解していなければ、システムはいずれ静かに、しかし確実に破綻する。
今回は、プロダクション環境(商用環境)の厳格な基準に耐えうる、極限まで堅牢な「30日以上前ログファイルの自動クリーンアップ・エンジン」の設計思想と実装を伝授しよう。
—
現場でありがちな「アンチパターン」と、その敗因
多くのプログラマは、FSOを使ったファイル削除スクリプトを書く際、以下のような安易な実装に走る。
1. エラーハンドリングの放棄 (`On Error Resume Next` の乱用と放置)
別のプロセス(ログ出力中のアプリなど)が掴んでいるファイルを削除しようとしてスクリプトがクラッシュするのを恐れ、エラーをすべて握りつぶす。結果、どのファイルが消えて、どれが消えなかったのかが闇に葬られる。
2. 日付計算の根拠の曖昧さ
`Date – 30` という記述。これは果たして意図通りに「正確な720時間前」を指しているだろうか? 時刻(時・分・秒)の要素が計算にどう影響するかを意識していないコードは、境界値(30日目のファイルの生死)で必ずバグを生む。
3. オブジェクトの解放漏れ
VBScriptのCOMコンポーネントは、スコープを抜けても即座にメモリから解放されるとは限らない。特にバッチ処理内で何度もFSOやFolderオブジェクトを生成・破棄する場合、メモリリークは致命傷になる。
プロのアーキテクトであれば、これらを完全にコントロール下に置くコードを書かなければならない。
—
プロダクション品質:堅牢クリーンアップ・スクリプト
以下に、実務でそのまま利用できる、ロギング機能付きの堅牢なVBScriptコードを提示する。
‘ ==============================================================================
‘ Script Name : LogCleaner.vbs
‘ Description : 指定フォルダ内の30日以上前に更新されたログファイルを安全に削除する
‘ Author : チーフアーキテクト
‘ ==============================================================================
Option Explicit
‘ — 定数定義 —
Const TARGET_FOLDER = “C:\AppLogs” ‘ 対象のログフォルダ(末尾の\は除外)
Const RETENTION_DAYS = 30 ‘ 保持期間(日)
Const LOG_FILE_PATH = “C:\AppLogs\Cleaner_Execution.log”
‘ — メイン処理の実行 —
Call Main()
Sub Main()
Dim fso, targetFolder, logger
Set fso = CreateObject(“Scripting.FileSystemObject”)
Set logger = New SimpleLogger
logger.Write “INFO: ログクリーンアップ処理を開始します。”
‘ フォルダの存在確認
If Not fso.FolderExists(TARGET_FOLDER) Then
logger.Write “ERROR: 対象フォルダが存在しません: ” & TARGET_FOLDER
Set logger = Nothing
Set fso = Nothing
Exit Sub
End If
Set targetFolder = fso.GetFolder(TARGET_FOLDER)
‘ 再帰的、または直下ファイルの走査(今回は直下を想定。サブフォルダも含める場合は拡張設計が必要)
Call CleanFiles(fso, targetFolder, RETENTION_DAYS, logger)
logger.Write “INFO: ログクリーンアップ処理が完了しました。”
‘ 明示的なオブジェクト解放
Set targetFolder = Nothing
Set logger = Nothing
Set fso = Nothing
End Sub
‘ — ファイル削除メインロジック —
Sub CleanFiles(fso, folder, thresholdDays, logger)
Dim file, fileList, criteriaDate, deletedCount, errorCount
Set fileList = folder.Files
‘ 削除基準日を算出(現在時刻から指定日数をマイナス)
‘ DateDiffの基準として、時刻も含めた厳密な比較を行うため Date を使用
criteriaDate = DateAdd(“d”, -thresholdDays, Now)
deletedCount = 0
errorCount = 0
For Each file in fileList
‘ 拡張子やファイル名で対象を絞り込む場合はここでガード節を入れる
‘ 例: If LCase(fso.GetExtensionName(file.Name)) = “log” Then
‘ DateLastModifiedプロパティで最終更新日時を取得
If file.DateLastModified < criteriaDate Then
' 削除トライ(ファイルロック等の例外に備える)
On Error Resume Next
fso.DeleteFile file.Path, True ' Force = True で読み取り専用も強制削除
If Err.Number = 0 Then
deletedCount = deletedCount + 1
logger.Write "DELETED: " & file.Path & " (最終更新: " & file.DateLastModified & ")"
Else
errorCount = errorCount + 1
logger.Write "WARNING: 削除失敗(ファイルが使用中の可能性があります): " & file.Path & " [Error " & Hex(Err.Number) & ": " & Err.Description & "]"
Err.Clear
End If
On Error GoTo 0 ' エラー捕捉を通常に戻す
End If
Next
logger.Write "SUMMARY: 削除成功件数 = " & deletedCount & "件, 削除失敗件数 = " & errorCount & "件"
Set fileList = Nothing
End Sub
' ==============================================================================
' 簡易ロガー(クラス定義)
' ==============================================================================
Class SimpleLogger
Private m_fso, m_stream
Private Sub Class_Initialize()
Set m_fso = CreateObject("Scripting.FileSystemObject")
' 追記モードでログファイルを開く(ファイルがなければ作成)
' 8 = ForAppending, True = Create
On Error Resume Next
Set m_stream = m_fso.OpenTextFile(LOG_FILE_PATH, 8, True)
If Err.Number <> 0 Then
‘ ログファイルが開けない場合はイベントログ等に逃げるべきだが、ここではサイレントスルー
Err.Clear
End If
On Error GoTo 0
End Sub
Public Sub Write(ByVal message)
Dim logMessage
logMessage = “[” & Now & “] ” & message
‘ 標準出力へのエコー(cscript実行時用)
WScript.Echo logMessage
‘ ファイル出力
If Not m_stream Is Nothing Then
On Error Resume Next
m_stream.WriteLine logMessage
On Error GoTo 0
End If
End Sub
Private Sub Class_Terminate()
If Not m_stream Is Nothing Then
m_stream.Close
Set m_stream = Nothing
End If
Set m_fso = Nothing
End Sub
End Class
—
コードの急所:アーキテクトが仕込んだ3つの技術的ポイント
1. `DateAdd`関数による堅牢な期限計算
単に `Date – 30` と書くこともできるが、`DateAdd(“d”, -thresholdDays, Now)` を使うことで、日付だけでなく「時刻」の粒度まで含めた正確な比較が可能になる。これにより、「30日前ピッタリの時間」を境目として、意図した通りのクリーンアップが保証される。
2. 局所的なエラーハンドリングと `Err.Clear` の作法
運用保守において最大の敵は「予期せぬファイルロック」だ。ログ出力中のプロセスがファイルを掴んでいる場合、`DeleteFile` は実行時エラーを吐く。
このコードでは、ファイル削除のループ内でのみ `On Error Resume Next` を使用し、エラーが発生した場合は処理を中断せずにカウンター (`errorCount`) をインクリメントしてログに記録する。さらに、`Err.Clear` を必ず実行してエラーバッファをクリアしている点がプロの仕事だ。ここを怠ると、直前のエラーが次のループに悪影響を及ぼす。
3. VBScriptにおけるクラス活用とライフサイクル管理
VBScriptはプリミティブな言語に見えて、実は `Class` 構文によるカプセル化をサポートしている。今回は `SimpleLogger` クラスを定義し、クラスの初期化(`Class_Initialize`)でログファイルをオープンし、終了時(`Class_Terminate`)にクローズする設計とした。
これにより、ファイルハンドルの閉じ忘れ(リソースリーク)を構造的に防いでいる。また、スクリプトの最後には主要なオブジェクト(`fso`, `targetFolder`, `logger`)を明示的に `Nothing` 代引きしてメモリ上に残さない配慮を行っている。
—
運用時の注意点とさらなる自動化へのステップ
- 実行権限とタスクスケジューラ
このスクリプトをWindowsの「タスクスケジューラ」に登録して定期実行する場合、「最特権の権限で実行する」にチェックを入れること。ログフォルダへのアクセス権がない場合、サイレントに失敗するかアクセス拒否エラーになる。必ず `cscript.exe` を使ってバッチ(`.bat`)経由で呼び出す構成にすること(`wscript.exe` だとポップアップダイアログが出て自動化がストップする)。
- サブフォルダの再帰的走査が必要な場合
もしログが `\log\2023\10\` のように階層化されている場合は、`folder.SubFolders` を再帰的に舐めるロジック(Recursive関数)に拡張する必要がある。その際も、今回解説したエラーハンドリングと日付比較のコアロジックはそのまま流用できる。
VBScriptは古い技術と言われるが、OSの標準機能だけで完結し、環境構築のコストがゼロであるという圧倒的なアドバンテージを持つ。
適当に書けばただの「爆弾」になるが、このように設計の美しさと堅牢性を持たせれば、企業のバックオフィスを長期間にわたって静かに支える「最高の下働き」となってくれるはずだ。
