看板 Windows 關於我們 聯絡資訊
PowerShell 提供者是能讓使用者能將各種資料存放區 (如登錄檔、憑證、系統環境變數)對應成類似磁碟機的結構 使用同一套命令進行新增、移除、遷移... 等管理操作 例如: New-Item、Remove-Item、Move-Item... 該虛擬磁碟機就是 PSDrive,使用 Get-PSDrive 檢視卦載狀態 由於 PowerShell 解析萬用字元路徑不可靠,必須自己實作了解析方法 在萬用字元路徑中,無法在檔名本身含有 ` 時,同時使用萬用字元進行多重符合 例如:使用 '``test?.txt' 去符合 '`test1.txt'、'`test2.txt'、'`test3.txt'... 理論上是成立的,但實際上只會得到 null 為了解決此問題就要將萬用字元模式轉為正規表示法去符合檔名 Get-ChildItem -Name | Where-Object { $_ -match '^`test.\.txt$' } # 萬用字元模式轉為正規表示法 [regex]::Replace($Pattern, '`?.', { param($m) $v = $m.Value if ($v -match '^[\[\]]') { return $v } if ($v -eq '*') { return '.*' } if ($v -eq '?') { return '.' } if ($v -eq '`]') { return '\]' } return [regex]::Escape(($v -replace '`(.)', '$1')) }) 如果是一段路徑,例如: 'series\season*\episode*.mp4' 就利用遞迴法一層層往下比對 以下說說一些主要技巧 若要先將萬元字元路徑展開為帶根目錄的萬用字元路徑 必須先將目前位置的路徑做跳脫處理再與子路進組合為完整路徑 $path = 'series\season*\episode*.mp4' $parent = $(Get-Location).Path -replace '[`\?\*\[\]]', '`$0' Join-Path $parent $path 但這邊要特別注意,PowerShell 在解讀萬院字元路徑時並不包含磁碟機代號 所以在跳脫處理時也要避開磁碟機代號 由檔案系統接入而來的磁碟由於命名方式指允許 A-Z,不會有問題 但 PSDrive 允許使用包含特殊字元的字串來命名磁碟機 所以允許像 'Temp[1]:' 這樣的磁碟機代號 預到這種情況需要保留磁碟機代號為原樣只對其子路徑做跳脫處理 $path = 'series\season*\episode*.mp4' $(Get-Location).Path -match '(^[^:]+:\?)(.*)$' $rootPath = $Matches[1] $parentWithoutRoot = $Matches[2] -replace '[`\?\*\[\]]', '`$0' Join-Path ($rootPath$parentWithoutRoot) $path 另外需要注意一些前綴特殊幾解析,例如 C:[Path] -> C:\CurrentLocation[\Path] ~[\Path] -> HomeDirectory[\Path] 如果是萬用字元路徑,例如 '~\Path\*' 則要將特殊前綴分割出來展開為完整的目錄路徑 還要將此目錄路徑做跳脫處理再組合回去 與處理萬用字元相對路徑同樣邏輯 # 取得磁碟機目前位置 (Get-PSDrive $PSDriveName).CurrentLocation # 取得提供者 Home 目錄路徑 (Get-PSProvider (Get-Location).Drive.Provider.Name).Home 還有相對路徑符號的處理 目前位置: . 父目錄位置: .. 1. 處理單點 (目前位置): - 位於頭層: 不處理 (例如 '.\Path') - 位於非頭層: 直接刪除 (例如 'C:\Path1\.\Path2' -> C:\Path1\Path2') 2. 處理雙點 (父目錄位置): - 位於頭層或前一層也是雙點: 不處理 (例如 '..\..\Path') - 前一層是根目錄:直接刪除 (例如 'C:\..\Path' -> 'C:\Path') - 前一層是一般目錄: 連同前一層一起刪除 (例如 'C:\Path1\..\Path2' -> C:\Path2') 實作方式可以採用正規表示法取代 或使用純邏輯判斷處理,例如拆分成陣列,由上向下檢查每層內容,使用對應處理 接下來,PowerShell 的 PSDrive 有一個很大的問題 它可接入任何提供者,並以 Windows 檔案路徑的形式導覽 例如 'HKLM:\SOFTWARE\Microsoft' 也就是對應登陸擋機碼 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft' 但如同一般檔案路徑,PSDrive 路徑也使用 . 與 .. 代表目前位置或父目錄位置 任何 . 與 .. 都會被當作相對路徑符號處理掉 而在提供者的原始路徑中是允與使用 . 與 .. 作為一般名稱使用 這導致 PSDrive 路徑,無法表示此類準確位置 如果要使用 Get-Item 取得下方機碼物件,結果根本不對 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\CSPs\. PS > Get-Item HKLM:\SOFTWARE\Microsoft\Provisioning\CSPs\. | % { $_.Name } HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\CSPs PS > Get-Item HKLM:\SOFTWARE\Microsoft\Provisioning\CSPs\.. | % { $_.Name } HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning 這時候就需要使用 PSPath ,它的形式 "[模組名稱\]提供者名稱::提供者原始路徑" Microsoft.PowerShell.Core\FileSystem::C:\Users\UserName Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\Software\Microsoft FileSystem::C:\Users\UserName Registry::HKEY_CURRENT_USER\Software\Microsoft 如果 PSPath 中的提供者原始路徑是帶根目錄的完整路徑 單雙點將會被當作正常一般名稱解讀,而不是相對路徑符號 PS > Get-Item Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\CSPs\. | % { $_.Name } HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\CSPs\. 現在我們需要一套在 PSPath 上解析萬用字元的方法了 由於每種提供者原始路徑的形式不一樣, 對於外部模組提供者的原始路徑組成方式是未知的 所以,除了提供者前綴 "[模組名稱\]提供者名稱::", 不能用字串處裡的方式寫死邏輯 只能交給 Join-Path、Split-Path 這些 cmdlet 來幫我們按照提供者方式方割、組合路徑 首先需要把路徑拆解成陣列,如下所示 'FileSystem::C:\Users\UserName' -> 'FileSystem::C:\', 'Users', 'UserName' 'FileSystem::C:Users\UserName' -> 'FileSystem::C:', 'Users', 'UserName' 'FileSystem::/mnt/c/Users/UserName' -> 'FileSystem::/', 'mnt', 'C', 'Users', 'UserName' 'Registry::HKEY_CURRENT_USER\Software' -> 'Registry::', 'HKEY_CURRENT_USER', 'Software' # 用 Split-Path 由後往前分割出最後一項存入陣列 $splitPath = @($leaf) while (-not [string]::IsNullOrWhiteSpace($path)) { Split-Path $path -Parent $leaf = Split-Path $path -Leaf $path = Split-Path $path -Parent $splitPath += @($leaf) } $newSplitPath = $splitPath[($splitPath.Count - 1)..0] 這裡會與到許多問題 問題1: Split-Path -Leaf 分割到最後一項,得到錯誤提供者路徑頭項 PS > Split-Path -Leaf 'FileSystem::/' PS > Split-Path -Leaf 'FileSystem::C:' C:\ PS > Split-Path -Leaf 'FileSystem::~' UserName 解決方法是在最後方割前跳出迴圈(如下所示),頭項另行處理 'FileSystem::C:\Users\UserName' -> 'FileSystem::C:\', 'Users', 'UserName' 'FileSystem::C:Users\UserName' -> 'FileSystem::C:Users', 'UserName' 'FileSystem::/mnt/c/Users/UserName' -> 'FileSystem::/mnt', 'C', 'Users', 'UserName' 'Registry::HKEY_CURRENT_USER\Software' -> 'Registry::HKEY_CURRENT_USER', 'Software' $splitPath = @($leaf) while (-not [string]::IsNullOrWhiteSpace($path)) { $parent = Split-Path $path -Parent if ([string]::IsNullOrWhiteSpace($parent)) { $splitPath += @($path); break } $leaf = Split-Path $path -Leaf $path = $parent $splitPath += @($leaf) } $splitPath = $splitPath[($splitPath.Count - 1)..0] # 由於提供者前綴是已知形式,所以對 $splitPath[0] 的分割 $splitHeadPath = $splitPath[0] -split '(?<=::)',2 # 然後再用 Test-Path 驗證是否可用,以免過度分割 if (Test-Path $splitHeadPath[0] -IsValid) { # 把 $splitPath 的頭項移除,在前方插入新的分割結果 } 問題2:Split-Path -Leaf 擅解析相對路徑符號,導致循環分割結果錯亂 PS > Split-Path 'FileSystem::~\Desktop\.' -Parent FileSystem::~\Desktop PS > Split-Path 'FileSystem::~\Desktop\.' -Leaf Desktop PS > Split-Path 'FileSystem::~\Desktop\..' -Parent FileSystem::~\Desktop PS > Split-Path 'FileSystem::~\Desktop\..' -Leaf UserName # 解決方法是先用 Join-Path 驗證最後一樣是不是 . 或點 .. $parent = Split-Path $path -Parent if ($path -eq (Join-Path $parent '.') -or # 帶尾分隔符號,以 FileSystem 為例: 'FileSystem::path\.\' $path -eq (Join-Path $parent '.' | Join-Path -ChildPath $null)) { $leaf = '.' } 則以同樣方式測試尾項是不是 .. 如果都是 false 則 $leaf = Split-Path $path -Leaf 把這麼方法封裝新函式 (例如 Split-PathFixed) 代替 Split-Path 即可 問題3:Join-Path 也有奇怪 Bug PS > Join-Path 'FileSystem::~' '.\file' C:\FileSystem::~\.\file 解決方法是 如果輸入是簡短提供者名稱,則先將其擴充為完整 例如:'Microsoft.PowerShell.Core\FileSystem::~' # 取得提供者所屬模組名稱 (Get-PSProvider $providerName).ModuleName PS > Join-Path 'Microsoft.PowerShell.Core\FileSystem::~' '.\file' Microsoft.PowerShell.Core\FileSystem::~\.\file 組合路徑後,如果原本是短名稱,再將前方的模組名稱移除就好 把這麼方法封裝新函式 (例如 Join-PathFixed) 代替 Join-Path 即可 到現在,可以將路徑按照期望確實最小分割 進入到處理相對路徑符號 . 與 .. 的部分了 首先要先釐清原始提供者路徑中是否保留 . 與 .. 作為相對路徑符號 測試前要先弄出一個含有不存在中間層與 . 還有 .. 的路徑用來測試 把此路徑餵給 Get-Item 看看會有什麼結果 因為 Get-Item 會將根目錄開頭的 PSPath 中的 . 與 .. 都視為一般名稱 $provider = Get-PSProvider $Name $psDrive = Get-PSdrive | Where-Object { $_.Provider.Name -eq $provider.Name } | Select-Object -First 1 $providerRootPSPath = "$($provider.Name)::$($psDrive.Root)" if (-not [string]::IsNullOrWhiteSpace($provider.ModuleName)) { $providerRootPSPath = "$($provider.ModuleName)\$providerRootPSPath" } # 以 FileSystem 為例就是 'Microsoft.PowerShell.Core\FileSystem::C:\foo\.\..' $testPSPath = Join-PathFixed $providerRootPSPath 'foo' | Join-PathFixed -ChildPath '.' | Join-PathFixed -ChildPath '..' $item = Get-Item -LiteralPath $testPSPath -ErrorAction SilentlyContinue if ($? -and $item.PSPath -ne $testPSPath) { # 項目存在,但返回了不同的路徑(例如退回根目錄) # 代表此提供者保留了純點作為相對路徑指標 return $true } else { # 執行失敗(找不到該特殊名稱)或返回了相同路徑 # 代表提供者允許純點作為子項目名稱 return $false } 為了節省資源,可以把已知的提供供者支援情形列為標單供查詢 只對未知的提供者進行測試 # 微軟官方核心提供者對於相對路徑符號 (".", "..") 的支援狀態 $script:ProviderSupportsRelativePathTokens = @{ 'Microsoft.PowerShell.Core\FileSystem' = $true # 檔案系統 'Microsoft.PowerShell.Core\Registry' = $false # 登錄檔 'Microsoft.PowerShell.Core\Alias' = $false # 別名 'Microsoft.PowerShell.Core\Environment' = $false # 環境變數 'Microsoft.PowerShell.Core\Function' = $false # 函式 'Microsoft.PowerShell.Core\Variable' = $false # 變數 'Microsoft.PowerShell.Core\Certificate' = $true # 憑證 'Microsoft.WSMan.Management\WSMan' = $false # WSMan } 處理完路徑中的相對路徑符號 接著就能把拆分、清理後的萬用字元提供者路徑丟去遞迴比對路徑每層名稱了 將其流程寫成cmdlet並命名為 Resolve-WildcardPath 以下是示範結果 PS > Resolve-WildcardPath 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\*\.' Registry::\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Provisioning\CSPs\. 註冊表的 . 與 .. 會被正確當為一般名稱,但 FileSystem 會被解析掉 PS > Resolve-WildcardPath 'FileSystem::C:\Users\UserName\Desktop\*\*\..\.\..\test\.\a```[*`]\a```[[3-5]`].txt' FileSystem::C:\Users\UserName\Desktop\test\a`[1-3]\a`[3].txt FileSystem::C:\Users\UserName\Desktop\test\a`[4-6]\a`[4].txt FileSystem::C:\Users\UserName\Desktop\test\a`[4-6]\a`[5].txt 在 PS 路徑模式工作目錄中,使用提供者相對路徑 PS > Set-Location -LiteralPath 'FileSystem::C:\Users\UserName\Desktop\test' PS > Resolve-WildcardPath 'a```[*`]\a```[[3-5]`].txt' Microsoft.PowerShell.Core\FileSystem::C:\Users\UserName\Desktop\test\a`[1-3]\a`[3].txt Microsoft.PowerShell.Core\FileSystem::C:\Users\UserName\Desktop\test\a`[4-6]\a`[4].txt Microsoft.PowerShell.Core\FileSystem::C:\Users\UserName\Desktop\test\a`[4-6]\a`[5].txt 不過由於這套流程預設輸入的 PSPath 帶的提供者內部路徑是從根目錄開始的 所以無法展開目錄前綴符號 Resolve-WildcardPath 'FileSystem::~\Desktop\*\*\..\.\..\test\.\a```[*`]\a```[[3-5]`].txt' FileSystem::~\Desktop\test\a`[1-3]\a`[3].txt FileSystem::~\Desktop\test\a`[4-6]\a`[4].txt FileSystem::~\Desktop\test\a`[4-6]\a`[5].txt 不過做到這地步,cmdlet 的 -Path 也能正常找到目標了 也就沒什麼動力解決這問題了 PS > Get-Item -LiteralPath 'FileSystem::~\Desktop\test\a`[1-3]\a`[3].txt' Directory: C:\Users\UserName\Desktop\test\a`[1-3] Mode LastWriteTime Length Name ---- ------------- ------ ---- -a--- 2026/6/16 20:39 22 a`[3].txt 但 PSPath 也是有大問題 'FileSystem::C:Desktop' -> 被 cmdlet 視為 C 曹目前位置下的 Desktop,這符合定義 'FileSystem::C:' -> 被 cmdlet 視為 "C:\",這理反而與定義不符了 'FileSystem::C:*' -> 理論上是符合 C 曹目前位置下的所以項目,但 cmdlet 完元不認得 完全不知道怎麼解決 因為在處理 PSPath 是以不知道提供者原始路徑形式下 不寫死的字串處理,只能使用 PowerShell 的路徑處理功能來實現 畢竟 PSPath 是用來應對非 FileSystem 的提供者無法完美卦載到 PSDrive 所以只能到此為止了 PowerShell 的坑實在是太多 Windows 兩個內建命令殼層 CMD 老舊又難用 PowerShell 強大,但到處都是暗坑等你踩 寫好的完整模組我就不獻醜了 如果有人需要在貼出來 -- ※ 發信站: 批踢踢實業坊(ptt.cc), 來自: 39.15.49.188 (臺灣) ※ 文章網址: https://www.ptt.cc/bbs/Windows/M.1782227729.A.3E2.html ※ 編輯: falcon (39.15.49.188 臺灣), 06/23/2026 23:33:51
kyrc: 想請問這是使用 5.1 還是 PowerShell 7.x 版的踩坑紀錄? 06/23 23:27
falcon: 最新社群版也只是解決工作目錄含有`時,解析相對路徑錯誤 06/23 23:37
falcon: 還是無法處理稍微複雜的萬用字元路徑 06/23 23:38