PowerShell 通配符 Get-Item 無法帶有方括號名稱的檔案
2024-06-06
這算是 PowerShell 5.1 預設給的方便的,不過在不知情的情況下就造成困擾了
假設有一個 File[1].txt
的檔案,於是就這樣獲取
Get-Item File[1].txt
居然什麼都沒有,於是乎聰明如你,用引號總能解吧?
Get-Item "File[1].txt"
Get-Item 'File[1].txt'
想不到吧很遺憾還是沒有辦法
這東西叫做通配符,內建在 Get-Item 裡的所才沒法通過引號處理的
about Wildcards - PowerShell | Microsoft Learn
2025-02-20 這是一個多重buf疊加的問題
第①個原因是上面的方括號被當作萬用字符對待了,解法就是上跳脫字元
Get-Item "File``[1``].txt"
Get-Item 'File`[1`].txt'
引出我們第②個問題,雙引號會進行一次 "解釋"
也就是會解析跳脫字元所以如果只用雙引號記得要雙重引號
再來引出第③個問題,Get-Item 並不支援萬用字元 * 或 ?
導致如果你要合著用就用不了,你只能選擇使用 Get-ChildItem 函式了
但是這 Get-ChildItem 的預設參數 -Path 自帶一次解析,這和Get-Item有區別
也就是你得這樣 (這個問題在 Pwsh7 中已經被修復不需要雙重反引號)
Get-ChildItem -Path "File``````[1``````].txt"
Get-ChildItem -Path 'File```[1```].txt'
很礙眼的三重引號出現了
為什麼會這樣呢,因為你無法區分那方括號到底是檔名還是萬用字元
舉例子來說檢索條件是 File[0-1].txt ,那你期望搜倒的檔案到底是哪個呢?
再來是第④個問題,那反引號是合法檔名
舉例子來說檢索條件是 File`[1`].txt ,那你期望搜倒的檔案到底是哪個呢?
你把第一個 File`[1`].txt 複製起來,建立一個文字文件貼上會發現居然是合法檔名
那這樣就完拉,你怎麼分區,就只能是三重反引號去多解析一次了
這就是為什麼裡面會自帶一次解析雙引號的理由
- Get-Item 'File```[1```].txt' ->檢索到-> File`[1`].txt
- Get-Item 'File`[1`].txt' -> 檢索到 -> File[1].txt
(Get-ChildItem 自行雙倍反引號)
一個簡易解法是把問題③處理掉,自己多加一層反引號
Get-ChildItem -Path ('File`[1`].txt' -replace('`','``'))
這樣就能應付掉那個不合協感了
最後一個第⑤個問題算是比較隱形的,剛剛提到的 Get-ChildItem 的 -Path 其實只有最右邊才會自動解析兩次,比如說 C:\folder\sub1\sub2 只有 sub2 的位置才能用雙重反引號解這個問題。
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 去把檔名調到最右邊,這樣父資料夾就被調到最右邊了
(這個問題在 Pwsh7 中已經被修復不需要雙重反引號)
下面是一步一步的推導到底發生什麼了
情況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('`','``'))
這少這樣理解度就高一點了,為什麼會有三個是因為需要反轉意一個反引號跟方括號導致的,是可以讀懂的代碼。
2024-10-09 深挖bug的本質
這就完事了嗎? 並沒有,我又再次深入地去挖掘這個問題,發現老早就一堆人在講了
- 反引號轉義不一致 ·問題 #7999 ·PowerShell/PowerShell (github.com)
- 工作目錄路徑與相對路徑中的通配符 ·問題 #24260 ·PowerShell/PowerShell (github.com)
- [心得] PowerShell 那些惱人的路徑 BUG - 看板 Windows - 批踢踢實業坊
但是呢六年過去還是沒修,八成是因為歷史包袱改不了了。最大的絆腳石應該是因為
永遠都無法區分路徑字串上的 D:\test\Test`[0-2] 反引號到底是跳脫字元還是普通字符。
(但是D:\test\Te`st[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 的檔案
(這個問題在 Pwsh7 中已經被修復)
很神奇吧,所謂的雙重引號只有最右邊的斜線有效而已,如果是中間層級的資料夾,雙重反引號是無效的。
好在社群有人發現另一個有趣的現象是 【-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 處理絕對比自己正則快很多很多
- 不需要去理會邏輯部分,交由現成的函式去處理
- 現在父資料跑到最右邊,父資料夾也支持奇耙路徑了 (反引號-任意字符-右方括號)
不過問題淺顯易見,爺資料夾以上還是只能使用正常路徑+通配符,但這問題不大已經足夠覆蓋絕大多數的情況了。
另一個比較易懂得的例子是,下面的方式都可以搜到 `test`[0]`
Get-Item "Z:\Test\test``````[0``]"
Get-Item 'Z:\Test\test``````[0``]'
Get-Item "Z:\Test\*" -Include "test``````[0``]"
Get-Item "Z:\Test\*" -Include 'test```[0`]'
Get-Item "Z:\Test\*" -Include 'test```[0]' # 最後一個反引號可省略
成功避開雙重反引號的問題,剩下的再繼續消除會導致無法區別了
參考
- Wildcard matching when filename has square brackets · PowerShell/PowerShell · Discussion #18146 (github.com)