「共有フォルダに何がどれだけあるか出して」と頼まれることがあります。容量の逼迫、古いファイルの整理、サーバ移行前の棚卸し。どれもファイル一覧のCSVから始まります。
PowerShell なら1行で書けます。書けてしまうのが問題です。
この1行はエラーを出さずに、件数が欠けたCSVを作ります。しかも欠けたことが分からない形で。

以下は Windows PowerShell 5.1 と PowerShell 7.4 の両方で実際に動かして比べた結果です。
1. まず1行で書いてみる
素直に書くとこうなります。
Get-ChildItem -Path "D:\share" -Recurse -File |
Select-Object FullName, Length, LastWriteTime |
Export-Csv -Path .\filelist.csv -NoTypeInformation
これで動きます。動いてしまいます。
問題は、このコマンドが都合の悪いことを何も言わずに済ませる点です。読めなかったフォルダがあっても、長すぎるパスがあっても、CSVは普通に出来上がります。
2. 罠①:読めなかったフォルダが無かったことになる
共有フォルダを -Recurse で舐めると、必ずどこかでアクセス拒否が出ます。赤いエラーが流れるので、こう書きたくなります。
# エラーを黙らせる。ただし「黙らせただけ」
Get-ChildItem -Path "D:\share" -Recurse -File -ErrorAction SilentlyContinue
画面はきれいになりますが、読めなかったフォルダの中身は一覧に入っていません。そしてどこが読めなかったのかも分からなくなります。
-ErrorVariable を足すと、握りつぶしたエラーが変数に残ります。
$errs = $null
$files = Get-ChildItem -Path "D:\share" -Recurse -File `
-ErrorAction SilentlyContinue -ErrorVariable errs
"取得: $($files.Count) 件 / 読めなかった場所: $($errs.Count) 件"
権限のないフォルダで試した結果です。
$errs[0].CategoryInfo.Category # PermissionDenied
$errs[0].Exception.GetType().Name # UnauthorizedAccessException
$errs[0].TargetObject # 読めなかったパスそのもの
TargetObject にパスが入っているのが実用上いちばん重要です。読めなかった場所の一覧を、そのまま別のCSVに出せます。
棚卸しの結果を人に渡すとき、「この範囲は読めていません」と一緒に渡せるかどうかで信頼度が変わります。
3. 罠②:260文字を超えるファイルが黙って消える
これが一番怖い罠です。
Windows には長らく MAX_PATH(260文字) の制限がありました。共有フォルダは階層が深くなりがちで、部署名・年度・案件名を重ねると簡単に超えます。
フルパス293文字のファイルを含む階層を作り、両方のバージョンで数えました。
$e = $null
$c = (Get-ChildItem -Path $target -Recurse -File `
-ErrorAction SilentlyContinue -ErrorVariable e | Measure-Object).Count
"files=$c errors=$($e.Count)"
| 実行環境 | 取得できたファイル | エラー |
|---|---|---|
| PowerShell 7.4 | 2件 | 0 |
| Windows PowerShell 5.1 | 1件 | 1 |
5.1 では、293文字のファイルが一覧から消えました。 -ErrorAction SilentlyContinue を付けていれば、この1件はエラーごと消えます。
しかも 5.1 が出したメッセージがこれです。
Could not find a part of the path 'C:\...\階層のながいフォルダ名11'.
「パスの一部が見つかりません」。 実際には存在します。長すぎて開けないだけです。このメッセージからパス長の問題だと気づくのは、知らなければ難しいはずです。
3.1 どう避けるか
いちばん簡単なのは PowerShell 7 で実行することです。 実測でも 7 は2件とも取得できました。
7 が無い端末しかない場合は、パスの先頭に \\?\ を付けます。
# 5.1 でも \\?\ を付ければ最後まで舐められる
Get-ChildItem -LiteralPath "\\?\D:\share" -Recurse -File
これも実測しました。5.1 でも2件とも取れて、エラーは0になりました。 ただし癖があります。
-Pathではなく-LiteralPathを使う(-Pathはワイルドカード解釈で崩れる)- UNCパスは書き方が変わる。
\\server\shareは\\?\UNC\server\shareになる - 取得した
FullNameにも\\?\が付いたまま入るので、CSVに出す前に外す
なお Windows 10 以降には LongPathsEnabled というレジストリ設定がありますが、有効にしても 5.1 側は .NET Framework の設定も必要で素直に効きません。端末のレジストリを触るより、7 で実行するほうが確実です。
4. 罠③:日本語が全部「?」になる
Windows PowerShell 5.1 の Export-Csv は、文字コードを指定しないと ASCII で書きます。
日本語を含むデータを 5.1 の既定設定で書き出し、バイト列を見ました。
[pscustomobject]@{ 名前='日本語テスト'; 値=1 } |
Export-Csv -Path .\enc51.csv -NoTypeInformation
22 3F 3F 22 2C 22 3F 22 0D 0A 22 3F 3F 3F 3F 3F "??","?"..."?????
3F は ? です。開くとこうなっています。
"??","?"
"??????","1"
列名も値も、日本語が全部 ? に置き換わりました。 エラーは一切出ません。ファイルサーバの一覧は日本語のファイル名だらけなので、これに気づかず渡すと、全部が ? のCSVを配ることになります。
指定すれば直ります。utf8BOM は PowerShell 7 以降の値で、5.1 では UTF8 と書くと BOM 付きになります。
Export-Csv -Path .\filelist.csv -NoTypeInformation -Encoding utf8BOM # 7
Export-Csv -Path .\filelist.csv -NoTypeInformation -Encoding UTF8 # 5.1
BOM を外すと Excel が Shift_JIS と解釈して化けます。この話はPowerShellで複数サーバーにSSH接続して結果をCSVにまとめるにも書いています。
5. 罠④:出ないもの、混ざるもの
細かいですが、棚卸しの数字がずれる原因になります。
Get-ChildItem は既定で隠しファイルとシステムファイルを返しません。 手元では -Force の有無で2件と3件に分かれました。Thumbs.db や desktop.ini が該当します。容量の内訳を出すなら付けないと合計が合いません。逆に「利用者が見えているファイルだけ数えたい」なら付けないほうが実態に合う、目的で使い分ける項目です。
もうひとつ、-File を付け忘れるとフォルダの行が混ざります。
"Name","Length"
"階層のながいフォルダ名1",
"enc51.csv","24"
"通常.txt","5"
フォルダには長さが無いので Length は空欄です。合計には影響しませんが、「行数=ファイル数」と思って数えると狂います。
なお、自分自身を指すジャンクションを作って -Recurse を掛けてみましたが、5.1 も 7 も辿らず件数は変わりませんでした。 「シンボリックリンクで無限ループする」は、既定の Get-ChildItem では起きません。
6. 完成形
4つを踏まえるとこうなります。PowerShell 7 で実行する前提です。
# ファイルサーバの棚卸し用。読めなかった場所も別ファイルに残す
$Target = "D:\share"
$OutDir = "C:\temp"
$errs = $null
$files = Get-ChildItem -LiteralPath $Target -Recurse -File -Force `
-ErrorAction SilentlyContinue -ErrorVariable errs
# 先に数える。ここを見ずに渡さない
"取得: {0} 件 / 読めなかった場所: {1} 件" -f $files.Count, $errs.Count
$files |
Select-Object FullName,
Name,
@{n='拡張子'; e={ $_.Extension }},
@{n='サイズMB'; e={ [math]::Round($_.Length / 1MB, 3) }},
@{n='更新日時'; e={ $_.LastWriteTime.ToString('yyyy/MM/dd HH:mm') }},
@{n='経過日数'; e={ [int]((Get-Date) - $_.LastWriteTime).TotalDays }} |
Export-Csv -Path (Join-Path $OutDir 'filelist.csv') -NoTypeInformation -Encoding utf8BOM
# 読めなかった場所。権限を直して取り直すための材料
if ($errs.Count -gt 0) {
$errs | Select-Object @{n='Path'; e={ $_.TargetObject }},
@{n='Reason'; e={ $_.CategoryInfo.Category }} |
Export-Csv -Path (Join-Path $OutDir 'skipped.csv') -NoTypeInformation -Encoding utf8BOM
}
経過日数 を入れておくと、Excel でそのまま「2年以上触られていないファイル」を絞れます。 棚卸しの目的はたいていそこなので、最初から列にしておくほうが早いです。
数十万ファイルになると $files がメモリを食います。そのときは変数に入れず直接パイプへ流しますが、代わりに件数の確認ができなくなるので、私はよほど大きくなるまでは変数に入れています。
7. まとめ
PowerShell でファイル一覧をCSVに出すときの要点です。
- 1行で書ける。書けるが、静かに欠ける
-ErrorAction SilentlyContinueは黙らせるだけ。-ErrorVariableで件数と場所を残す- エラーの
TargetObjectに読めなかったパスが入る。別CSVにして一緒に渡す - Windows PowerShell 5.1 は260文字を超えるファイルを落とす(実測 2件→1件)
- そのメッセージは「パスの一部が見つかりません」。存在しないと言われるが、実際は存在する
- 対策はPowerShell 7 で実行するか、
-LiteralPath "\\?\D:\share"を使う - 5.1 の
Export-Csvは既定が ASCII。日本語が全部?になる。エラーは出ない - 隠しファイルは
-Forceを付けないと出ない。-Fileを忘れるとフォルダが混ざる
一番伝えたいのは、出す前に件数を数えるという一手間です。「取得 12,438 件 / 読めなかった場所 3 件」と言えるかどうかで、そのCSVが使える資料になるかどうかが決まります。
エラーが出ないことは、正しく取れたことを意味しません。 ここだけは、コマンドが教えてくれないので自分で確かめるしかありません。
PowerShellの学習におすすめの書籍
PowerShell を体系的に学ぶなら、PowerShell Core 以降のクロスプラットフォーム対応をふまえた解説書が分かりやすいです。Windows の自動化にとどまらず、macOS や Linux でも動く現在の PowerShell を、基礎から実務での使い方まで通して押さえられます。
PowerShell実践ガイドブック クロスプラットフォーム対応の次世代シェルを徹底解説


コメント