Powershell

PowerShellでファイル一覧をCSVに出す【静かに欠ける4つの罠】

スポンサーラベル
PowerShellでファイル一覧をCSVに出すときの4つの罠。アクセス拒否、260文字のパス長、文字コード、隠しファイルとフォルダの混入 Powershell

当サイトはアフィリエイト広告を利用しています。

「共有フォルダに何がどれだけあるか出して」と頼まれることがあります。容量の逼迫、古いファイルの整理、サーバ移行前の棚卸し。どれもファイル一覧のCSVから始まります。

PowerShell なら1行で書けます。書けてしまうのが問題です。

この1行はエラーを出さずに、件数が欠けたCSVを作ります。しかも欠けたことが分からない形で。

PowerShellでファイル一覧をCSVに出すときの4つの罠。アクセス拒否、260文字のパス長、文字コード、隠しファイルとフォルダの混入

以下は 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.42件0
Windows PowerShell 5.11件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.dbdesktop.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実践ガイドブック クロスプラットフォーム対応の次世代シェルを徹底解説

あわせて読みたい

コメント

タイトルとURLをコピーしました