データの破壊を防げ:FSO `CreateTextFile` における「Err 58」の制御と、プロフェッショナルな堅牢設計
業務自動化の世界において、我々が扱うのは単なる「文字列」ではない。それは企業の意思決定を支える「資産」であり、一歩間違えれば数年分の実績データを一瞬で灰にする「凶器」にもなり得る。
特に、VBScriptを用いたファイル操作において、`FileSystemObject` (FSO) の `CreateTextFile` メソッドを扱う際、多くのジュニアエンジニアが「既存ファイルの上書き」という、極めて初歩的かつ致命的な設計ミスを犯す。
本稿では、`Overwrite` 引数を `False` に設定した際に発生するエラー `Err.Number = 58`(ファイルは既に存在します)を単なる「エラー」としてではなく、「データの整合性を守るための防波堤」としてどう扱うべきか。その極限の知見を伝授する。
1. なぜ「If FileExists」による事前チェックでは不十分なのか
初心者は、エラーを回避するために以下のようなコードを書きたがる。
' --- 避けるべき、脆弱な実装例 ---
If Not fso.FileExists(strPath) Then
Set ts = fso.CreateTextFile(strPath, False)
' ... 書き込み処理 ...
Else
MsgBox "ファイルが既にあります。"
End If
一見、論理的に正しいように見える。しかし、これは「TOCTOU (Time-of-Check to Time-of-Use)」と呼ばれる、マルチタスク・共有環境における典型的な脆弱性だ。
「ファイルが存在しないことを確認した瞬間」から「ファイルを作成する瞬間」までの、コンマ数秒の間に、別のプロセスやユーザーが同じ名前のファイルを作成する可能性がある。このわずかな隙が、システムを不安定にし、予期せぬ挙動を引き起こす。
プロフェッショナルは、「確認してから動く」のではなく、「動いてみて、結果を制御する」という、より堅牢なアプローチを取る。
2. エラーハンドリングの設計思想:Err 58 をどう飼い慣らすか
`CreateTextFile(filename, False)` を実行した際、ファイルが既に存在していれば、VBScriptは `Err.Number = 58` を吐いて停止する。
我々の任務は、このエラーを「無視」することではない。このエラーを「状態の検知」として利用し、ビジネスロジックに基づいた適切な分岐(リネームして保存するのか、処理を中断するのか、あるいはログに記録するのか)を行うことだ。
設計の3原則
1. 局所化 (Localization): `On Error Resume Next` をスクリプト全体に適用してはならない。エラーハンドリングは、操作が予測不能な「境界線」でのみ、最小限のスコープで行う。
2. 意図の明確化: エラーを握りつぶしてはいけない。エラーが発生したという事実は、必ずログまたは呼び出し元に伝播させる。
3. 冪等性 (Idempotency) の確保: 同じスクリプトを何度実行しても、データの破壊が起きず、かつ一貫した結果が得られるように設計する。
3. 【実践】プロダクション・レディな実装パターン
以下に、実務でそのまま利用可能な、堅牢なファイル作成関数を示す。このコードは、単にエラーを防ぐだけでなく、エラー発生時に「タイムスタンプを付与して別名で保存する」という、業務継続性を考慮した機能を備えている。
' ==========================================================================
' Module: FileSystemManager
' Description: 堅牢なファイル操作を提供するプロフェッショナル用関数
' ==========================================================================
Option Explicit
' メイン処理のデモ
Call Main()
Sub Main()
Dim targetPath
targetPath = "C:\Temp\Report.txt" ' テスト用のパス
' フォルダが存在しない場合は作成しておく(前提条件の整備)
Dim fso: Set fso = CreateObject("Scripting.FileSystemObject")
Dim folderPath: folderPath = fso.GetParentFolderName(targetPath)
If Not fso.FolderExists(folderPath) Then fso.CreateFolder(folderPath)
' 堅牢なファイル作成の呼び出し
Dim resultPath
resultPath = SafeCreateTextFile(targetPath, "業務報告データ: " & Now())
If resultPath <> "" Then
MsgBox "成功: " & resultPath, vbInformation, "完了"
Else
MsgBox "失敗: ファイルの作成に失敗しました。", vbCritical, "エラー"
End If
End Sub
''' <summary>
''' ファイルの重複を回避しつつ、安全にテキストファイルを作成する。
''' </summary>
''' <param name="filePath">作成するファイルのフルパス</param>
''' <param name="content">書き込む内容</param>
''' <returns>作成されたファイルのパス。失敗した場合は空文字を返す。</returns>
Function SafeCreateTextFile(filePath, content)
Dim fso, ts, errNum, finalPath
Set fso = CreateObject("Scripting.FileSystemObject")
finalPath = filePath
' --- エラーハンドリングの境界線 ---
On Error Resume Next
' Overwrite = False に設定し、既存ファイルの破壊を物理的に防ぐ
Set ts = fso.CreateTextFile(finalPath, False)
' エラー判定
errNum = Err.Number
If errNum = 0 Then
' 【正常系】ファイル作成成功
ts.WriteLine content
ts.Close
Set ts = Nothing
ElseIf errNum = 58 Then
' 【特定エラー系】ファイルが既に存在する場合
' 既存ファイルを破壊せず、タイムスタンプを付与して別名でリトライする設計
Err.Clear
Dim baseName, ext, timestamp
baseName = fso.GetBaseName(filePath)
ext = fso.GetExtensionName(filePath)
timestamp = FormatDateTime(Now, 2) ' YYYY/MM/DD形式(環境により調整)
' 記号を置換してファイル名として使用可能にする
timestamp = Replace(Replace(timestamp, "/", ""), "-", "")
finalPath = fso.GetParentFolderName(filePath) & "\" & baseName & "_" & timestamp & "." & ext
' 再試行(再帰呼び出しではなく、一度きりのリトライとして実装)
Set ts = fso.CreateTextFile(finalPath, False)
If Err.Number = 0 Then
ts.WriteLine content
ts.Close
Set ts = Nothing
Else
' リトライも失敗した場合(権限不足など)
finalPath = ""
End If
Else
' 【未知のエラー系】権限不足やディスクフルなど、別の致命的な問題
' ここでは詳細なエラー内容をログに投げるべき(今回は簡易的に)
' WScript.Echo "Critical Error: " & Err.Description
finalPath = ""
End If
On Error GoTo 0 ' エラーハンドリングを即座に解除
' ----------------------------------
SafeCreateTextFile = finalPath
End Function
4. アーキテクトの視点:ファイル操作からデータベース連携へ
この「存在チェックとエラーハンドリング」の考え方は、ファイルシステムに留まらない。SQLを用いたデータベース操作においても、全く同じ原理が適用される。
例えば、SQL Server等へのデータ投入において、`INSERT` 文を無条件に発行するのは、`CreateTextFile(path, True)` を実行するのと同じくらい危険だ。
- ファイル操作における `Overwrite = False`
$\rightarrow$ DB操作における `WHERE NOT EXISTS` または `UPSERT (MERGE)`
既存のレコードを破壊(上書き)してしまうのか、それとも新しいバージョンとして追加(履歴管理)するのか。この設計判断こそが、システムエンジニアと、単なるスクリプトライターを分かつ境界線である。
結論
`Err.Number = 58` は、あなたのコードが間違っていることを告げる宣告ではない。それは、「既存のデータに干渉しようとしているが、本当にそれで良いのか?」とシステムが問いかけている、守護的なシグナルである。
エラーを隠蔽するのではなく、エラーをロジックの一部として組み込む。この「防御的プログラミング」の姿勢こそが、業務の自動化を「便利なツール」から「信頼に足るインフラ」へと昇華させるのだ。
