我一直覺得 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
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
}
}
}
}
-
接著你可以用以下命令取得指定資料夾中所有檔案與資料夾,但排除 Reparse Point 的檔案與資料夾
Get-ChildItemNoFollowReparse G:\ -Recurse
-
如果想過濾出只有 *.png
與 *.jpg
檔案的話,就要自己用 Where-Object
來過濾了
Get-ChildItemNoFollowReparse G:\ -Recurse |
Where-Object { ($_.Extension -eq '.png') -or ($_.Extension -eq '.jpg') }
相關連結