破壊的変更を許さない設計:FSOによる「世代管理」の極致
業務自動化の現場において、最も「劇薬」となるコードは何か? それは、ファイル削除を伴うスクリプトだ。
「古いバックアップを消して、最新の5世代だけ残す」
一見、単純なロジックに見えるだろう。しかし、現場で「昨日まで動いていたスクリプトが、なぜか全ファイルを消し去った」という悲劇が起きるのは、設計思想が「正常系」のみに偏っているからだ。
本稿では、単なるコードの書き方を教えるのではない。ファイルシステムオブジェクト(FSO)を操り、不測の事態(ファイルロック、アクセス権限、予期せぬファイル名の混入)を回避しながら、堅牢に世代管理を実現するためのアーキテクトの思考プロセスを伝授する。
1. 初級者が陥る「3つの致命的な設計ミス」
まず、なぜ君たちの書いたスクリプトが信頼に値しないのか、その理由を明確にしよう。
① 文字列ソートへの依存
多くの人間は、ファイル名に含まれる日付(例:`backup_20231001.zip`)を文字列としてソートしようとする。これは極めて危険だ。ファイル命名規則がわずかに変更された瞬間、ソート順は崩壊し、最新のファイルが「古いもの」として判定され、削除の対象となる。
正解は、ファイルシステムの「更新日時(DateLastModified)」を真実のソース(Single Source of Truth)とすることだ。
② 例外処理の欠如と「全消去」のリスク
ループの中で `On Error Resume Next` を乱用し、エラーを握り潰してそのまま削除プロセスを進める行為は、プロフェッショナルとして失格だ。ファイルが使用中(ロック状態)であった場合、エラーを無視して次の処理へ進む設計は、ログの不整合や、不完全なバックアップ状態を招く。
③ 「ドライラン」概念の欠落
本番環境へ投入するコードに、「実際には削除せず、何が削除されるかを表示するだけ」のモードが実装されていない。これは、開発者の怠慢である。
2. アーキテクチャ設計:メタデータ抽出型アプローチ
堅牢な世代管理スクリプトを構築するには、以下の3ステップに分離して設計すべきだ。
1. フェッチ(Fetch): 対象フォルダ内のファイルを走査し、「パス」と「更新日時」のペアをメモリ上の構造体に格納する。
2. ソート(Sort): 格納した構造体を「更新日時」の降順(新しい順)で並び替える。
3. 判定・実行(Execute): 指定した世代数 $N$ を超えるインデックスの要素に対し、安全確認を経て削除を実行する。
この設計の利点は、「ファイル操作」と「ロジック(判定)」を完全に分離できる点にある。これにより、テストが容易になり、バグの混入を防ぐことができる。
3. プロダクション・グレードのコード実装
以下に、私が実務で推奨する、保守性と堅牢性を極限まで高めたVBScriptのテンプレートを示す。これをそのままコピペして使うのではない。なぜこのように書いているのか、その意図を読み解け。
'===============================================================================
' Script Name: RobustGenerationManager.vbs
' Description: 指定フォルダ内のファイルを更新日時順に管理し、最新N世代を残して削除する。
' Author: Chief Architect
'===============================================================================
Option Explicit
' --- 設定セクション ---
Const TARGET_DIR = "C:\Backup\Data\" ' 対象フォルダパス
Const KEEP_GENERATIONS = 5 ' 残す世代数
Const FILE_PATTERN = "backup_*.bak" ' 対象ファイル名のパターン(簡易判定用)
Const IS_DRY_RUN = True ' True: 削除シミュレーション, False: 実際に削除
Const LOG_FILE = "C:\Backup\log.txt" ' 実行ログ出力先
' --- メインプロセス ---
Main
Sub Main()
Dim fso, targetFolder, fileList, i
Set fso = CreateObject("Scripting.FileSystemObject")
' 1. フォルダ存在チェック
If Not fso.FolderExists(TARGET_DIR) Then
LogError "Target directory does not exist: " & TARGET_DIR
WScript.Quit 1
End If
LogInfo "Starting Generation Management. [DryRun=" & IS_DRY_RUN & "]"
' 2. ファイル情報の収集 (メタデータ抽出)
Set targetFolder = fso.GetFolder(TARGET_DIR)
Set fileList = GetSortedFileList(targetFolder, FILE_PATTERN)
If fileList.Count = 0 Then
LogInfo "No matching files found."
WScript.Quit 0
End If
' 3. 世代管理ロジック
' fileListは更新日時の降順(新しい順)で格納されている。
' インデックス 0 から (KEEP_GENERATIONS - 1) までは保持。
' それ以降のインデックスを削除対象とする。
Dim deleteCount : deleteCount = 0
For i = KEEP_GENERATIONS To fileList.Count - 1
Dim targetFile : targetFile = fileList.Item(i)
If IS_DRY_RUN Then
LogInfo "[DRY-RUN] Would delete: " & targetFile
deleteCount = deleteCount + 1
Else
If SafeDelete(fso, targetFile) Then
LogInfo "Deleted: " & targetFile
deleteCount = deleteCount + 1
Else
LogError "Failed to delete: " & targetFile & " (File might be locked)"
End If
End If
Next
LogInfo "Process completed. Files processed: " & fileList.Count & ", Deleted: " & deleteCount
LogInfo "-------------------------------------------------------------------------------"
End Sub
'-------------------------------------------------------------------------------
' 関数: ファイルリストを更新日時順(降順)に取得する
'-------------------------------------------------------------------------------
Function GetSortedFileList(folder, pattern)
Dim fso, file, dict, arrFiles, i, j, temp
Set fso = CreateObject("Scripting.FileSystemObject")
Set dict = CreateObject("Scripting.Dictionary") ' Key: Path, Value: DateLastModified
' 1. 収集
For Each file In folder.Files
' 簡易的なパターンマッチング (Like演算子の代用)
If InStr(file.Name, Replace(pattern, "*", "")) > 0 Then
' Dictionaryに「パス | 更新日時」の形式で格納
' ※VBScriptには構造体がないため、結合文字列で管理するか、
' 今回はソート用にカスタムクラスを使うのが理想だが、軽量化のため配列で実装
dict.Add file.Path, file.DateLastModified
End If
Next
' 2. ソート用の配列作成 (Path, Date)
' dictの内容を2次元配列に展開
Dim keys : keys = dict.Keys
Dim vals : vals = dict.Items
Dim sortedArr : Set sortedArr = CreateObject("System.Collections.ArrayList")
' ArrayListに「Date|Path」の形式で追加(文字列ソートを利用するため)
' ただし、日付は ISO 8601 形式(YYYY-MM-DD HH:MM:SS)にする必要がある
For i = 0 To dict.Count - 1
' 日付をソート可能な文字列に変換して結合
' [Date] + [Separator] + [Path]
sortedArr.Add FormatDateTimeISO(vals(i)) & "|" & keys(i)
Next
' 3. ソート (ArrayListのSortメソッドを利用)
sortedArr.Sort ' 昇順になる
' 4. 降順(新しい順)にするために逆転させる、または抽出ロジックを調整
' ここでは「新しい順」として扱いたいので、後ろから取り出すか、
' 今回は簡単のため、ArrayListをそのまま使い、インデックスを計算で逆転させる
' 結果を返すためのDictionary(実際にはソート済みのPathリストを返す)
Dim resultDict : Set resultDict = CreateObject("Scripting.Dictionary")
' 降順にするため、後ろから順にDictionaryに格納
For i = sortedArr.Count - 1 To 0 Step -1
Dim entry : entry = Split(sortedArr(i), "|")
' entry(0) は日付文字列、entry(1) はパス
resultDict.Add resultDict.Count, entry(1)
Next
Set GetSortedFileList = resultDict
End Function
'-------------------------------------------------------------------------------
' 関数: ISO形式の日付文字列変換 (ソート精度を担保するため)
'-------------------------------------------------------------------------------
Function FormatDateTimeISO(dt)
' YYYYMMDDHHMMSS 形式で返す(文字列ソートで日付順になるようにする)
FormatDateTimeISO = Year(dt) & _
Right("0" & Month(dt), 2) & _
Right("0" & Day(dt), 2) & _
Right("0" & Hour(dt), 2) & _
Right("0" & Minute(dt), 2) & _
Right("0" & Second(dt), 2)
End Function
'-------------------------------------------------------------------------------
' 関数: 安全な削除処理
'-------------------------------------------------------------------------------
Function SafeDelete(fso, filePath)
On Error Resume Next
Dim res : res = False
If fso.FileExists(filePath) Then
fso.DeleteFile filePath, True
If Err.Number = 0 Then
res = True
Else
' エラー内容はログに記録される
Err.Clear
End If
End If
On Error GoTo 0
SafeDelete = res
End Function
'-------------------------------------------------------------------------------
' ユーティリティ: ログ出力
'-------------------------------------------------------------------------------
Sub LogInfo(msg)
WriteLog "[INFO] " & msg
End Sub
Sub LogError(msg)
WriteLog "[ERROR] " & msg
End Sub
Sub WriteLog(msg)
Dim fso, logFile
Dim timestamp : timestamp = Now()
Dim fullMsg : fullMsg = "[" & timestamp & "] " & msg
' コンソール出力
WScript.Echo fullMsg
' ファイル出力
On Error Resume Next
Set fso = CreateObject("Scripting.FileSystemObject")
Set logFile = fso.OpenTextFile(LOG_FILE, 8, True) ' 8 = ForAppending
logFile.WriteLine fullMsg
logFile.Close
On Error GoTo 0
End Sub
4. エンジニアとしての「極限の知見」
このコードを理解した君に、最後に3つのアドバイスを送る。
1. `System.Collections.ArrayList` の活用
VBScript単体では配列のソートは非常に面倒だ。しかし、Windows環境であれば `System.Collections.ArrayList`(.NET Frameworkのコンポーネント)を呼び出すことで、極めて強力なソート機能を、わずか数行で手に入れることができる。これは「車輪の再発明」を避ける賢明な選択だ。
2. 日付の文字列化における罠
ソートのために日付を文字列にする場合、必ず `YYYYMMDDHHMMSS` のような、「桁数が固定され、かつ大きな単位から並ぶ形式」にせよ。`M/D/YYYY` のような形式でソートを行うと、10月と2月が正しく並ばない。これは初学者が必ず踏む地雷だ。
3. ログは「証拠」である
自動化スクリプトが動いた後、君の仕事は終わらない。「何が、いつ、なぜ消されたのか(あるいは消せなかったのか)」が、ログとして残っていなければならない。エラーが発生した際に、単に無視するのではなく、`SafeDelete` 関数の中でエラーを検知し、ログに刻む。これこそが、運用フェーズで君の身を守る唯一の手段となる。
「コードは書くものではない。設計し、検証し、管理するものだ。」
この意識を持って、次なる自動化の壁に挑んでほしい。
