我一直覺得 Windows PowerShell 是一個讓人又愛又恨的命令列執行環境,其強型別的優點確實是好的讓人無法拒絕,但其執行環境的複雜度、版本相容性與各種 Cmdlet 的奇葩設計,每次遇到也都是讓人心幹神疑心曠神怡,不免嘖嘖稱奇。今天我就來分享一個昨天寫文章時遇到的神奇狀況,也就是我們常用的 Get-ChildItem cmdlet 需注意 -Path
與 -Include
與 -Recurse
的各種用法組合,以及一個 Reparse Points 的問題。
Get-ChildItem 的 Alias 與 -Recurse
用法
以下我就列出幾種常見的用法,與其注意事項:
-
最常見的用法,就是用
dir
或ls
來列出當前資料夾中的檔案與資料夾以下這三個命令都是相同的:
dir
ls
Get-ChildItem
-
列出當前資料夾與其子資料夾中的檔案與資料夾
Get-ChildItem -Recurse
-
篩選當前資料夾中的特定檔案類型,例如找出所有
*.png
檔案dir *.png
-
篩選當前資料夾與其子資料夾中的特定檔案類型,例如找出所有
*.png
檔案dir *.png -Recurse
總之,你只要加上 -Recurse
參數,就可以列出當前目錄與其子目錄下所有檔案內容!
Get-ChildItem 的 -Path
、-File
或 -Directory
用法
你可以透過 -Path
來指定取得指定目錄下的檔案與資料夾:
-
用
Get-ChildItem
列出指定資料夾中的檔案(不要顯示資料夾)Get-ChildItem -Path "G:\" -File
-
用
Get-ChildItem
列出指定資料夾中的資料夾(不要顯示檔案)Get-ChildItem -Path "G:\" -Directory
-
用
Get-ChildItem
列出指定資料夾中的檔案且僅顯示*.png
檔案內容Get-ChildItem -Path "G:\*.png" -File
-
用
Get-ChildItem
列出指定資料夾與其子資料夾中的檔案且僅顯示*.png
檔案內容Get-ChildItem -Path "G:\*.png" -File -Recurse
上述幾種寫法,都是平時經常使用的用法與參數,其實沒太多地雷,但我在做 CI/CD 或批次作業時,就可能會用到一些進階技巧。
Get-ChildItem 的 -Include
用法
這個 -Include
用法就真的有雷了,請讓我娓娓道來。
-
列出當前資料夾與其子資料夾中所有
*.png
與*.jpg
的檔案這語法沒問題:
Get-ChildItem -Include '*.png','*.jpg' -Recurse
-
列出指定資料夾與其子資料夾中所有
*.png
與*.jpg
的檔案這語法沒問題:
Get-ChildItem -Path 'G:\' -Include '*.png','*.jpg' -Recurse
-
列出當前資料夾中所有
*.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
應該放「資料夾路徑」吧?你說雷不雷!💥不過,其實官網文件範例寫的蠻清楚的,只是沒看到這段的人,肯定會跟我一樣,鬼打牆一段時間!😒
避免 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 找到了一個不錯的解答:
-
先建立一個
Get-ChildItemNoFollowReparse
FunctionFunction 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 } } } }
-
接著你可以用以下命令取得指定資料夾中所有檔案與資料夾,但排除 Reparse Point 的檔案與資料夾
Get-ChildItemNoFollowReparse G:\ -Recurse
-
如果想過濾出只有
*.png
與*.jpg
檔案的話,就要自己用Where-Object
來過濾了Get-ChildItemNoFollowReparse G:\ -Recurse | Where-Object { ($_.Extension -eq '.png') -or ($_.Extension -eq '.jpg') }