2025年、新社会人だった私は、生成AIにやりたいことを伝えれば、業務で使える自動化コードが簡単に完成すると思っていました。
実際、AIは数秒でPowerShellでもExcel VBAでもコードを書いてくれます。
簡単な処理なら、そのまま動くこともあります。
しかし、私が最初に作ったPowerShellのコードは、
コピー先フォルダーが存在しないだけでエラーになりました。
原因はAIの性能ではありませんでした。私が「コピー先が存在しない場合にどうするか」を決めておらず、AIにも伝えていなかったことでした。
この記事では、その失敗をきっかけにAIとの向き合い方が変わった過程と、現在使っている具体的な手順・プロンプトを紹介します。
この記事で伝えたいこと
先に結論だけ書いておきます。
- AIへ指示する前に、自分が正常時と異常時の動作を決める
- 一度動いても完成とは判断せず、異なる条件でテストする
- すべてを自動化せず、重要な判断には人の確認を残す
AIにコードを書いてもらうことは、自動化のゴールではありませんでした。私にとっては、そこがスタートでした。
私について
- 社会人2年目のエンジニア(24歳)
- 大学ではC系とPythonが中心。PowerShellとExcel VBAは業務効率化のため勉強中
- 設計は、まさに現在進行形で学んでいる段階
なぜ自動化を始めたのか
業務をしていると、同じような操作を繰り返す場面があります。
- 決められたフォルダーへファイルをコピーする
- ファイル名を所定の形式へ変更する
- Excelの決められたセルへ情報を入力する
- 複数のファイルから必要な情報を探す
一つひとつは難しい作業ではありません。しかし何度も繰り返すと時間がかかり、手作業によるミスも起こり得ます。
同じ操作を繰り返しているなら、PowerShellやExcel VBAで自動化できるのではないか。
そう考えたものの、当時はやりたい処理を最初からコードにできるほど詳しくありませんでした。
そこで使い始めたのが生成AIです。
日本語で伝えるだけで、自分ではすぐに書けないコードが数秒で出てくる。初めてコードが動いたときは、素直に感動しました。
同時に、こんな思い込みも生まれました。
AIに作業内容を伝えれば、あとは生成されたコードを実行するだけでよい。
今振り返ると、コードを「作ること」だけに注目し、その後の確認を軽く考えていました。
AIに最初に出した指示
例として、指定したフォルダー内のExcelファイルを、バックアップ用フォルダーへコピーする処理を考えます。
当時の私は、こう指示していました。
フォルダー内のExcelファイルを
別のフォルダーへコピーするPowerShellを作成してください。
かなり曖昧な指示です。それでもAIは、次のコードを生成しました。
$sourceFolder = "C:\Work\Source"
$destinationFolder = "C:\Work\Backup"
Get-ChildItem -Path $sourceFolder -Filter "*.xlsx" |
Copy-Item -Destination $destinationFolder
短く、見た目も分かりやすいコードです。私はコピー元のフォルダーにExcelファイルを置き、そのまま実行しました。
実際に起きた失敗
実行すると、ファイルはコピーされませんでした。コピー先が存在しない、というエラーが表示されます。
確認すると、コピー元の C:\Work\Source は作成していましたが、コピー先の C:\Work\Backup を作っていませんでした。
私はAIに「Excelファイルを別のフォルダーへコピーしてほしい」としか伝えていません。そのため、次の条件は何も決まっていませんでした。
- コピー先が存在しない場合は自動作成するのか
- エラーを表示して終了するのか
- 親フォルダーも存在しない場合はどうするのか
補足として、Copy-Item は存在しないパスを渡されたとき、必ずエラーになるとは限りません。対象ファイルが1件だけの場合、コピー先を「フォルダー」ではなく「コピー先のファイル名」と解釈し、C:\Work\Backup という拡張子のないファイルが作られることがあります。エラーで止まってくれた方が、まだ気づけるだけ親切だったことになります。
コピー先フォルダーを手動で作成して再実行すると、今度はコピーできました。しかし、そこで新たな疑問が出てきます。
今回はフォルダーを作れば動いたが、ほかにも決めていない条件があるのではないか。
確認すると、次の条件も抜けていました。
- コピー先に同名ファイルがある場合
- 対象ファイルが1件もない場合
- Excelの一時ファイルが含まれる場合
- ファイルへのアクセス権限がない場合
- 途中でコピーに失敗した場合
- 何件コピーできたか確認したい場合
最初のコードは、必要なフォルダーが存在し、対象ファイルにも問題がない状態でのみ期待どおりに動くコードでした。
原因は「AIの間違い」だけではなかった
当初は、AIが不完全なコードを生成したと考えました。
しかし、そもそも私は「コピー先が存在しない場合にどうするか」をAIへ伝えていません。それどころか、自分自身でも決めていませんでした。
AIへの指示が曖昧だったというより、自動化する処理の仕様そのものが曖昧だったのです。
そして、コピーが成功した時点でも完成ではありませんでした。私は次の質問に答えられなかったからです。
-
Get-ChildItemは具体的に何を取得しているのか - サブフォルダーのファイルも対象になるのか
- 同じスクリプトを2回実行するとどうなるのか
コードが一度動いたことと、安全に繰り返し使えることは別です。確認できたのは、特定の条件でコピーできたことだけでした。
AIへの指示を具体化した
不足していた条件を整理し、指示を次のように変更しました。
PowerShellで、指定したフォルダー内の.xlsxファイルを
バックアップフォルダーへコピーするスクリプトを作成してください。
条件は以下のとおりです。
・サブフォルダー内のファイルは対象外
・ファイル名が「~$」から始まる一時ファイルは対象外
・コピー元フォルダーが存在しない場合はエラー終了
・コピー先フォルダーが存在しない場合は作成
・同名ファイルが存在する場合は上書きせずスキップ
・実行前に対象ファイルを一覧表示
・成功、失敗、スキップの件数を最後に表示
・コード内に日本語のコメントを付ける
・ファイルを削除する処理は含めない
生成されたコードのうち、要点は次の3箇所です。
コピー先が存在しない場合は作成します。New-Item -ItemType Directory は、途中の親フォルダーが存在しない場合もまとめて作成してくれます。
if (-not (Test-Path -LiteralPath $DestinationFolder -PathType Container)) {
New-Item -ItemType Directory -Path $DestinationFolder -Force | Out-Null
}
対象ファイルの取得時に、Excelの一時ファイルを除外します。
$files = @(
Get-ChildItem -LiteralPath $SourceFolder -File -Force |
Where-Object { $_.Extension -eq '.xlsx' -and $_.Name -notlike '~$*' } |
Sort-Object Name
)
コピー先に同名ファイルがある場合は、上書きせずスキップします。
if (Test-Path -LiteralPath $destinationPath) {
Write-Host "スキップ : $($file.Name)(コピー先に同名ファイルあり)"
$skipCount++
continue
}
最初のコードより長くなりましたが、何を正常とし、どの条件では処理しないかが明確になりました。極端に言えば、自分が望んでいない処理をさせないためのコードです。
完成版スクリプト
条件をすべて反映したものが以下です。Backup-ExcelFiles.ps1 として保存すれば動きます。
Windows PowerShell 5.1 は、BOMなしのUTF-8で保存された .ps1 をANSIとして読み込みます。日本語のコメントや Write-Host の文字列が化けるため、「UTF-8 (BOM付き)」で保存してください。VS Codeなら右下のエンコード表示から Save with Encoding → UTF-8 with BOM です。
#Requires -Version 5.1
<#
.SYNOPSIS
指定したフォルダー直下の .xlsx ファイルを、バックアップフォルダーへコピーします。
.DESCRIPTION
- サブフォルダーは対象外です
- Excel の一時ファイル(~$ で始まるもの)は対象外です
- コピー先に同名ファイルがある場合は、上書きせずスキップします
- ファイルの削除・移動は一切行いません
.EXAMPLE
# まず何が起きるかだけ確認する(実際にはコピーされません)
.\Backup-ExcelFiles.ps1 -SourceFolder 'C:\Work\Source' -DestinationFolder 'C:\Work\Backup' -WhatIf
.EXAMPLE
# 実行する
.\Backup-ExcelFiles.ps1 -SourceFolder 'C:\Work\Source' -DestinationFolder 'C:\Work\Backup'
#>
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory = $true)]
[string]$SourceFolder,
[Parameter(Mandatory = $true)]
[string]$DestinationFolder
)
# ---------------------------------------------------------------
# 1. コピー元フォルダーの存在確認
# -PathType Container を付けることで、同名の「ファイル」を
# フォルダーと誤認することを防ぐ
# ---------------------------------------------------------------
if (-not (Test-Path -LiteralPath $SourceFolder -PathType Container)) {
throw "コピー元フォルダーが見つかりません: $SourceFolder"
}
# ---------------------------------------------------------------
# 2. 対象ファイルの取得
# -File : ファイルのみ取得(フォルダーを除外)
# -Force : 隠し属性が付いたファイルも「いったん」取得する
# ※Excel の ~$ 一時ファイルは隠しファイルなので、
# -Force を付けないとそもそも一覧に出てこない
# Extension : -Filter は 8.3 形式の短い名前にもマッチして
# 意図しないファイルを拾うことがあるため、
# 拡張子は Extension プロパティで厳密に比較する
# Name : Excel でファイルを開いている間だけ作られる
# ~$xxxx.xlsx を除外する
# @( ) で囲む : 対象が 0 件・1 件でも配列として扱えるようにする
# ---------------------------------------------------------------
$files = @(
Get-ChildItem -LiteralPath $SourceFolder -File -Force |
Where-Object { $_.Extension -eq '.xlsx' -and $_.Name -notlike '~$*' } |
Sort-Object Name
)
if ($files.Count -eq 0) {
Write-Host '対象ファイルが 1 件もありません。処理を終了します。'
return
}
# ---------------------------------------------------------------
# 3. 実行前に対象ファイルを一覧表示する
# ---------------------------------------------------------------
Write-Host ''
Write-Host "コピー元 : $SourceFolder"
Write-Host "コピー先 : $DestinationFolder"
Write-Host "対象ファイル: $($files.Count) 件"
foreach ($file in $files) {
Write-Host " - $($file.Name)"
}
Write-Host ''
# ---------------------------------------------------------------
# 4. コピー先フォルダーの作成
# New-Item -ItemType Directory は、途中の親フォルダーが
# 存在しない場合もまとめて作成してくれる
# -Force は「すでに存在していてもエラーにしない」ためのもので、
# 既存フォルダーの中身を消すものではない
# ---------------------------------------------------------------
if (-not (Test-Path -LiteralPath $DestinationFolder -PathType Container)) {
if ($PSCmdlet.ShouldProcess($DestinationFolder, 'コピー先フォルダーを作成')) {
New-Item -ItemType Directory -Path $DestinationFolder -Force | Out-Null
Write-Host "コピー先フォルダーを作成しました: $DestinationFolder"
}
}
# ---------------------------------------------------------------
# 5. コピー処理
# ---------------------------------------------------------------
$successCount = 0
$skipCount = 0
$failureCount = 0
foreach ($file in $files) {
$destinationPath = Join-Path -Path $DestinationFolder -ChildPath $file.Name
# 同名ファイルがある場合は上書きせずスキップする
# ※名前だけで判定しており、中身や更新日時までは比較していない
if (Test-Path -LiteralPath $destinationPath) {
Write-Host "スキップ : $($file.Name)(コピー先に同名ファイルあり)"
$skipCount++
continue
}
try {
# Copy-Item のエラーは既定では「非終了エラー」で catch できないため、
# -ErrorAction Stop を明示して try/catch で拾えるようにする
Copy-Item -LiteralPath $file.FullName -Destination $destinationPath -ErrorAction Stop
# -WhatIf のときは実際にはコピーされていないので、成功件数に数えない
if (-not $WhatIfPreference) {
Write-Host "コピー成功: $($file.Name)"
$successCount++
}
}
catch {
# 1 件失敗しても、残りのファイルの処理は続ける
Write-Warning "コピー失敗: $($file.Name) / $($_.Exception.Message)"
$failureCount++
}
}
# ---------------------------------------------------------------
# 6. 処理結果の表示
# ---------------------------------------------------------------
Write-Host ''
Write-Host '---------------- 実行結果 ----------------'
Write-Host "成功 : $successCount 件"
Write-Host "スキップ: $skipCount 件"
Write-Host "失敗 : $failureCount 件"
Write-Host '------------------------------------------'
# 失敗があったことを呼び出し元(タスクスケジューラーなど)へ伝える
if ($failureCount -gt 0) {
exit 1
}
スクリプトの実行がブロックされる場合は、実行ポリシーの設定が必要です。ただしAIは Set-ExecutionPolicy Bypass を安易に勧めてくることがあります。恒久的に緩めるのではなく、Set-ExecutionPolicy -Scope Process のように影響範囲を限定するか、社内の運用ルールに従ってください。
実行する前に「何が起きるか」だけ確認する
このスクリプトには -WhatIf を付けられるようにしてあります。実際にはコピーせず、何が実行されるかだけを表示するPowerShellの標準機能です。
PS C:\Work> .\Backup-ExcelFiles.ps1 -SourceFolder 'C:\Work\Source' -DestinationFolder 'C:\Work\Backup' -WhatIf
コピー元 : C:\Work\Source
コピー先 : C:\Work\Backup
対象ファイル: 2 件
- test01.xlsx
- test02.xlsx
WhatIf: 対象 "C:\Work\Backup" に対して操作 "コピー先フォルダーを作成" を実行しています。
WhatIf: 対象 "アイテム: C:\Work\Source\test01.xlsx 到着先: C:\Work\Backup\test01.xlsx" に対して操作 "ファイルのコピー" を実行しています。
WhatIf: 対象 "アイテム: C:\Work\Source\test02.xlsx 到着先: C:\Work\Backup\test02.xlsx" に対して操作 "ファイルのコピー" を実行しています。
---------------- 実行結果 ----------------
成功 : 0 件
スキップ: 0 件
失敗 : 0 件
------------------------------------------
「実行する前に確認を残す」ことは、自分でロジックを書かなくても標準機能で実現できます。Remove-Item や Move-Item のような取り返しのつかない処理を扱うときほど、まずこれを付けて動かすようにしています。
動作確認
次の構成のフォルダーで確認しました。
C:\Work\Source
├─ test01.xlsx
├─ test02.xlsx
├─ ~$test03.xlsx ← Excel で test03.xlsx を開いている間だけ生成される
└─ test_memo.txt
1回目の実行
コピー元 : C:\Work\Source
コピー先 : C:\Work\Backup
対象ファイル: 2 件
- test01.xlsx
- test02.xlsx
コピー先フォルダーを作成しました: C:\Work\Backup
コピー成功: test01.xlsx
コピー成功: test02.xlsx
---------------- 実行結果 ----------------
成功 : 2 件
スキップ: 0 件
失敗 : 0 件
------------------------------------------
2回目の実行(同名ファイルが存在する状態)
コピー元 : C:\Work\Source
コピー先 : C:\Work\Backup
対象ファイル: 2 件
- test01.xlsx
- test02.xlsx
スキップ : test01.xlsx(コピー先に同名ファイルあり)
スキップ : test02.xlsx(コピー先に同名ファイルあり)
---------------- 実行結果 ----------------
成功 : 0 件
スキップ: 2 件
失敗 : 0 件
------------------------------------------
一時ファイル ~$test03.xlsx とテキストファイル test_memo.txt は、どちらも対象外として扱われています。初期コードでは、そもそもコピー先フォルダーが存在しない時点で止まっていました。
検証中に気づいたこと
~$ 一時ファイルは、そもそも一覧に出てこない
除外条件を書いたあと、念のため Get-ChildItem の結果だけを表示してみたところ、~$test03.xlsx は最初から一覧に含まれていませんでした。
Excelが作る ~$xxxx.xlsx には隠しファイル属性が付いており、Get-ChildItem は -Force を付けない限り隠しファイルを返さないためです。
つまり -Force なしのコードでは、「除外条件が正しく効いた」のではなく「そもそも取得されていなかった」だけでした。
さらに厄介なのが、テスト用に自分で作った ~$test03.xlsx には隠し属性が付かないという点です。手作業で用意したテストデータは、本番の条件と一致しているとは限りません。
このスクリプトでは、あえて -Force で隠しファイルも取得したうえで ~$ を明示的に除外しています。そうすることで、何を除外しているのかがコードから読み取れる状態になります。
プロンプトを長くするだけでは解決しなかった
条件を詳しく指定すると、目的に近いコードが生成されやすくなります。一方で、条件を増やすほどコードは長くなります。
私は一時期、思いついた条件をすべてプロンプトへ追加し、最初から完璧なコードを作らせようとしていました。その結果が次の状態です。
- 簡単な指示では、条件不足のコードが生成される
- 条件を大量に追加する
- 長く複雑なコードが生成される
- 自分ではコード全体を確認できなくなる
自分の理解を超えるコードを生成できることはAIの長所ですが、理解できないまま実行するとリスクにもなり得ます。そこで現在は、最初から完成版を作らせないようにしています。
現在のAIの使い方
1. コードを書く前に仕様を整理する
最初に、コードではなく必要な条件の洗い出しを依頼します。
指定したフォルダー内のExcelファイルを、
バックアップフォルダーへコピーする作業を
PowerShellで自動化したいです。
まだコードは作成せず、先に以下を整理してください。
・必要な入力情報
・処理の流れ
・事前に決めるべき条件
・想定されるエラー
・実行前に確認すべきこと
・人が判断すべき部分
AIが挙げた条件を自分で確認し、採用するものを決めます。
2. 処理を小さく分けて生成する
今回の例なら、次の単位に分割します。
- フォルダーの存在を確認する
- 対象ファイルを取得する
- 対象ファイルを表示する
- ファイルをコピーする
- 処理結果を表示する
小さく作ることで、各処理の意味と動作を確認しやすくなりました。
3. 分からない行を確認する
理解できない行は、その部分だけをAIに質問します。
次のPowerShellコードを初心者向けに説明してください。
Get-ChildItem -LiteralPath $sourceFolder -File -Force
以下を分けて説明してください。
・各パラメーターの役割
・取得されるファイル
・取得されないファイル
・サブフォルダーが対象になるか
コード全体ではなく、分からない部分を限定した方が確認しやすくなります。今回の隠しファイルの件も、この質問から気づきました。
4. 問題がある前提でレビューする
「問題ありませんか」だけでは確認観点が曖昧になるため、具体的な条件を指定します。
このコードには問題がある前提で、厳しくレビューしてください。
特に以下を確認してください。
・既存ファイルを上書きする可能性
・指定フォルダー外を処理する可能性
・対象ファイルが0件の場合の動作
・フォルダーが存在しない場合の動作
・途中でコピーに失敗した場合の動作
・2回連続で実行した場合の動作
・削除処理や破壊的な処理の有無
問題がある場合は、
該当箇所、発生条件、影響を分けて説明してください。
AIのレビューも必ず正しいとは限らないため、確認材料の一つとして扱います。
5. テスト用データで実行する
コードを確認した後は、実際の業務ファイルではなくテスト用のフォルダーで試します。特に確認するのは次の条件です。
- 対象ファイルがない
- コピー先が存在しない
- 同名ファイルが存在する
- 一時ファイルが含まれる
- 同じ処理を2回実行する
正常に動くことだけでなく、想定外の条件で危険な動作をしないことも確認します。
すべてを自動化しない
以前の私は、手作業が残ると自動化に失敗したように感じていました。
現在は、定型処理だけをスクリプトへ任せ、重要な判断は人が行う半自動化も有効だと考えています。
- 既存ファイルを上書きする前の確認
- ファイルを削除する前の確認
- 大量のファイルを処理する前の確認
- 業務上の判断が必要な処理
すべてを自動化するより、確認を一つ残した方が、安全性と効率を両立できる場合があります。
残っている制約
今回のスクリプトは、以下については何もしていません。
- 同名ファイルの中身を比較していない — 名前が同じならスキップするため、コピー元だけ更新されたファイルは反映されません
- 長いパスに対応していない — 合計260文字を超えるパスは失敗します
-
Excelで開いている最中のファイル — ロック状態によっては失敗します(
失敗として記録されます) - 世代管理をしていない — 日付フォルダーへの退避などは含みません
必要になった時点で足す、という方針にしています。最初からすべてを盛り込もうとして、自分で読めないコードになったのが前述の失敗でした。
まとめ
今回の失敗は、コピー先フォルダーが存在しなかったことでした。表面上は単純なエラーですが、原因をたどると、私が異常時の動作を決めていなかったことに行き着きました。
以前の私は、AIにコードを書かせて、そのまま実行していました。
現在は、AIと一緒に仕様を整理し、小さくコードを作り、テストしてから使っています。
特に学んだことは、次の3点です。
- AIへ指示する前に、自分が正常時と異常時の動作を決める
- 一度動いても完成とは判断せず、異なる条件でテストする
- すべてを自動化せず、重要な判断には人の確認を残す
AIにコードを書いてもらうことは、自動化のゴールではありません。
私にとっては、そこが自動化のスタートでした。