PowerShell 通配符 Get-Item 無法帶有方括號名稱的檔案
這算是 PowerShell 5.1 預設給的方便的,不過在不知情的情況下就造成bug了
假設有一個 [1]File.txt
的檔案,於是就這樣獲取
Get-Item [1]File.txt
居然什麼都沒有,於是乎聰明如你,用引號總能解吧?
Get-Item "[1]File.txt"
Get-Item '[1]File.txt'
想不到吧很遺憾還是沒有辦法
這東西叫做通配符,內建在 Get-Item 裡的所才沒法通過引號處理的
about Wildcards - PowerShell | Microsoft Learn
情況1
如果你只是單純想解決括號問題,沒有要搞通配符號組合,參考這個就能解了。
Get-Item -LiteralPath 'Test[1]'.txt
Get-Item '.\Test`[1`].txt'
Get-Item ".\Test```[1```].txt"
情況2
另一個坑是當括號與星號一起出現時,又會導致另一個解析問題,括號被當作通佩服一起解釋了,就是你想匹配 Test[*].txt 多個檔案的時候。
這時候情況更複雜了一些,你得連反引號都一起傳進去才能,免得反引號在傳入的當下被解析了少了一次解析。
Get-Item '.\Test``[*``].txt'
Get-Item ".\Test`````[*`````].txt"
測試結果
情況3
2024-09-28 發現一個新問題 'Test``[*``].txt' 實際上匹配的並不是 Test[*].txt 而是 Test*.txt,說起來繞口,原因是那個方括號被當作萬用字解讀了。
如何更明白的理解問題可以看下面這個例子
Test[1].txt 的檔案
Get-Item '.\Test``[[0-1]``].txt'
這樣寫出來應該就很好理解了,因為萬用字的匹配是在 cmdlet 裡面做的,所以才導致這個反人類思維的解...
第一層反引號會在傳入的時候被解掉,此時算法實際吃到的是一個反引號的 `[ 所以她成功識別是一個引號,第二個由於就是引號沒什麼好說的吃到引號當作萬用字解讀。
也就是可以理解成 [0-1] 這東西會被當作一個特殊字串對待,剩下的保持原本模樣。
那為什麼 '.\Test`[1`].txt' 能夠被正確識別呢? 因為 [1] 沒有構成合法的萬用字元所以被當作字串解讀了 (這鬼邏輯...雖然是正確的但別這樣設計搞人啊)
情況4
以為這樣就結束了嗎? 不還有一個更鬼畜,記得先溫得好情況3的神奇邏輯,這是同一套邏輯的變態版,現在考慮到檔名含有雙引號的情況
Test`[1].txt 的檔案
Get-Item '.\Test``````[[0-1]``].txt'
居然是要添加4個引號? 是的這個邏輯的是正確,因為傳入的時候會被吃掉一次只剩2個,真正在運算的時候又被吃掉一次只剩1個,所以正確的識別了
這個鬼邏輯來自於,不知道為啥路徑相關的cmdlet會對路徑做一次多餘的雙引號解析導致的,啥原因設計成這樣就不知道了。
下面是測試的方法,看了應該就秒懂上面的 情況4 發生什麼事情了
(注意雙引號與單引號)
'.\Test`[1].txt' -like ".\Test``````[[0-1]``].txt"
對他們是相等的...也就是說對於所有路徑相關的 Get-Item, Test-Path 等等必須自己有自覺的認識到,即使我用了單引號避免被解釋,但實務上進去還會被解析一次。
這個真該打屁股了誰寫的反人類設計...
不過嘴砲歸嘴砲,八成是身處歷史當下的那群人,遇到某個無法解決的問題,提出的無可奈何的解吧...。現在好拉已經過這麼久了,想修估計也改不了了,成為萬世毒瘤了
對於這個局面的處置方法
對於這個局面要人性化的處置可以參考這下面這個解法
Test-Path ('Test```[[0-1]`].txt' -replace('`','``'))
Test-Path ('Test```[*`].txt' -replace('`','``'))
這少這樣理解度就高一點了,為什麼會有三個是因為需要反轉意一個反引號跟方括號導致的,是可以讀懂的代碼。
深挖bug的本質
2024-10-09
這就完事了嗎? 並沒有,我又再次深入地去挖掘這個問題,發現老早就一堆人在講了
- 反引號轉義不一致 ·問題 #7999 ·PowerShell/PowerShell (github.com)
- 工作目錄路徑與相對路徑中的通配符 ·問題 #24260 ·PowerShell/PowerShell (github.com)
但是呢六年過去還是沒修,八成是因為歷史包袱改不了了。最大的絆腳石應該是因為
永遠都無法區分路徑字串上的 D:\test\Test`[0-2] 反引號到底是跳脫字元還是普通字符。
回到剛剛的話題為什麼這事情還沒完是因為,我們已經知道雙重跳脫可以協助我們區分到底誰是字元誰是跳脫
D:\test\Test```[0-2`]
我們上個色就知道誰是跳脫了,看上去很美好對吧,但是並沒有 PowerShell 在遇到反引號加上一個任意字符,再加上一個右方括號的時候,bug就由此而生
我找了很久還是沒找到這到底對應到哪個路徑,最後才發現
D:\test\Test```[0-2`] 可以找到 D:\test\Test`[0-2] 這個帶反引號的資料夾,但是但是
D:\test\Test```[0-2`]\File.txt 不能找到 D:\test\Test`[0-2]\File.txt 的檔案
很神奇吧,所謂的雙重引號只有最右邊的斜線有效而已,如果是中間層級的資料夾,雙重反引號是無效的。
好在社群有人發現另一個有趣的現象是,-include 的邏輯跟 -like 一樣是沒有雙重反引號的,我們可以鑽這個空子
function fix_get_item($Path){
$dir = (Split-Path $Path) -replace('`','``')
$file = (Split-Path $Path -Leaf)
$type = (Get-Item -Path $dir).PSProvider.Name
(Get-ChildItem $dir -Recurse -Depth 1 -Include $file).FullName
}
利用 Get-ChildItem 去帶的好處是
- for迴圈交由 Get-ChildItem 處理絕對比自己正則快很多很多
- 不需要去理會邏輯部分,交由現成的函式去處理
- 現在父資料跑到最右邊,父資料夾也支持奇耙路徑了 (反引號-任意字符-右方括號)
不過問題淺顯易見,爺資料夾以上還是只能使用正常路徑+通配符,但這問題不大已經足夠覆蓋絕大多數的情況了。
參考
- Wildcard matching when filename has square brackets · PowerShell/PowerShell · Discussion #18146 (github.com)