PowerShell 的 Get-ChildItem 需注意 -Include, -Path 與 Reparse Points 地雷

原文转载自 「The Will Will Web」 ( https://blog.miniasp.com/post/2023/01/21/PowerShell-Get-ChildItem-Include-Path-Recurse-Reparse-Points-Issues ) By Will Huang

预计阅读时间 0 分钟(共 0 个字, 0 张图片, 0 个链接)

我一直覺得 Windows PowerShell 是一個讓人又愛又恨的命令列執行環境,其強型別的優點確實是好的讓人無法拒絕,但其執行環境的複雜度、版本相容性與各種 Cmdlet 的奇葩設計,每次遇到也都是讓人心幹神疑心曠神怡,不免嘖嘖稱奇。今天我就來分享一個昨天寫文章時遇到的神奇狀況,也就是我們常用的 Get-ChildItem cmdlet 需注意 -Path-Include-Recurse 的各種用法組合,以及一個 Reparse Points 的問題。

PowerShell

Get-ChildItem 的 Alias 與 -Recurse 用法

以下我就列出幾種常見的用法,與其注意事項:

  1. 最常見的用法,就是用 dirls 來列出當前資料夾中的檔案資料夾

    以下這三個命令都是相同的:

    dir
    
    ls
    
    Get-ChildItem
    
  2. 列出當前資料夾與其子資料夾中的檔案資料夾

    Get-ChildItem -Recurse
    
  3. 篩選當前資料夾中的特定檔案類型,例如找出所有 *.png 檔案

    dir *.png
    
  4. 篩選當前資料夾與其子資料夾中的特定檔案類型,例如找出所有 *.png 檔案

    dir *.png -Recurse
    

總之,你只要加上 -Recurse 參數,就可以列出當前目錄與其子目錄下所有檔案內容!

Get-ChildItem 的 -Path-File-Directory 用法

你可以透過 -Path 來指定取得指定目錄下的檔案與資料夾:

  1. Get-ChildItem 列出指定資料夾中的檔案(不要顯示資料夾)

    Get-ChildItem -Path "G:\" -File
    
  2. Get-ChildItem 列出指定資料夾中的資料夾(不要顯示檔案)

    Get-ChildItem -Path "G:\" -Directory
    
  3. Get-ChildItem 列出指定資料夾中的檔案且僅顯示 *.png 檔案內容

    Get-ChildItem -Path "G:\*.png" -File
    
  4. Get-ChildItem 列出指定資料夾與其子資料夾中的檔案且僅顯示 *.png 檔案內容

    Get-ChildItem -Path "G:\*.png" -File -Recurse
    

上述幾種寫法,都是平時經常使用的用法與參數,其實沒太多地雷,但我在做 CI/CD 或批次作業時,就可能會用到一些進階技巧。

Get-ChildItem 的 -Include 用法

這個 -Include 用法就真的有雷了,請讓我娓娓道來。

  1. 列出當前資料夾與其子資料夾中所有 *.png*.jpg檔案

    這語法沒問題:

    Get-ChildItem -Include '*.png','*.jpg' -Recurse
    
  2. 列出指定資料夾與其子資料夾中所有 *.png*.jpg檔案

    這語法沒問題:

    Get-ChildItem -Path 'G:\' -Include '*.png','*.jpg' -Recurse
    
  3. 列出當前資料夾中所有 *.png*.jpg檔案

    你如果用以下命令執行,很抱歉,什麼結果都不會有!💥

    Get-ChildItem -Include '*.png','*.jpg'
    

    你如果用以下命令執行,很抱歉,什麼結果都不會有!💥

    Get-ChildItem -Path . -Include '*.png','*.jpg'
    

    你如果用以下命令執行,很抱歉,什麼結果都不會有!💥

    Get-ChildItem -Path $PWD -Include '*.png','*.jpg'
    

    這裡的 $PWD 是一個 PowerShell 內建的預設變數,代表目前資料夾路徑。

    如果加上 -Recurse 參數的話,就會有結果!🔥

    但我只想要在當前資料夾取得 *.png*.jpg 檔案要怎麼辦呢?你要這樣寫:

    Get-ChildItem -Path "*" -Include '*.png','*.jpg'
    
    Get-ChildItem -Path ".\*" -Include '*.png','*.jpg'
    
    Get-ChildItem -Path "$PWD\*" -Include '*.png','*.jpg'
    
    Get-ChildItem -Path "G:\*" -Include '*.png','*.jpg'
    

    這也太怪了吧,為啥 -Path 一定要加上 \* 才給我用 -Include 篩選檔案類型時才有結果呢?正常人應該只會覺得 -Path 應該放「資料夾路徑」吧?你說雷不雷!💥

    不過,其實官網文件範例寫的蠻清楚的,只是沒看到這段的人,肯定會跟我一樣,鬼打牆一段時間!😒

    範例 4:使用 Include 參數取得子專案

避免 Get-ChildItem 在遇到 NTFS Reparse Points 引發的無窮迴圈問題

NTFS reparse point 是一個相當特殊的設計,從 Windows 2000 (NTFS v3.0) 年代就有了,主要用來擴充 NTFS 檔案系統,你可以把他視為 Linux 的 Hard Link 來看待,但其實 Reparse Point 的功能更多些,而且檔案與資料夾都可以是 Reparse Point,可以讓你重新解析(連結)到另一個資料夾或檔案。

總之,如果你的檔案系統中有個 Reparse Point 目錄,很有可能在透過 Get-ChildItem 取得檔案清單時,遇到無窮迴圈的狀況,這個狀況不太好處理,要透過遞迴(Recursive)去避開 Reparse Point 資料夾才有辦法解決。

我從 How to make Get-ChildItem not to follow links - Stack Overflow 找到了一個不錯的解答:

  1. 先建立一個 Get-ChildItemNoFollowReparse Function

    Function Get-ChildItemNoFollowReparse
    {
        [Cmdletbinding(DefaultParameterSetName = 'Path')]
        Param(
            [Parameter(Mandatory=$true, ParameterSetName  = 'Path', Position = 0,
                      ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
            [ValidateNotNullOrEmpty()]
            [String[]] $Path
        ,
            [Parameter(Mandatory=$true, ParameterSetName  = 'LiteralPath', Position = 0,
                      ValueFromPipelineByPropertyName=$true)]
            [ValidateNotNullOrEmpty()]
            [Alias('PSPath')]
            [String[]] $LiteralPath
        ,
            [Parameter(ParameterSetName  = 'Path')]
            [Parameter(ParameterSetName  = 'LiteralPath')]
            [Switch] $Recurse
        ,
            [Parameter(ParameterSetName  = 'Path')]
            [Parameter(ParameterSetName  = 'LiteralPath')]
            [Switch] $Force
        )
        Begin {
            [IO.FileAttributes] $private:lattr = 'ReparsePoint'
            [IO.FileAttributes] $private:dattr = 'Directory'
        }
        Process {
            $private:rpaths = switch ($PSCmdlet.ParameterSetName) {
                                  'Path'        { Resolve-Path -Path $Path }
                                  'LiteralPath' { Resolve-Path -LiteralPath $LiteralPath }
                              }
            $rpaths | Select-Object -ExpandProperty Path `
                    | ForEach-Object {
                          Get-ChildItem -LiteralPath $_ -Force:$Force
                          if ($Recurse -and (($_.Attributes -band $lattr) -ne $lattr)) {
                              Get-ChildItem -LiteralPath $_ -Force:$Force `
                                | Where-Object { (($_.Attributes -band $dattr) -eq $dattr) -and `
                                                (($_.Attributes -band $lattr) -ne $lattr) } `
                                | Get-ChildItemNoFollowReparse -Recurse
                          }
                      }
        }
    }
    
  2. 接著你可以用以下命令取得指定資料夾中所有檔案資料夾,但排除 Reparse Point 的檔案資料夾

     Get-ChildItemNoFollowReparse G:\ -Recurse
    
  3. 如果想過濾出只有 *.png*.jpg 檔案的話,就要自己用 Where-Object 來過濾了

    Get-ChildItemNoFollowReparse G:\ -Recurse |
      Where-Object { ($_.Extension -eq '.png') -or ($_.Extension -eq '.jpg') }
    

相關連結