[心得] PowerShell 路徑問題攻略
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
→
06/23 23:27,
2小時前
, 1F
06/23 23:27, 1F
→
06/23 23:37,
2小時前
, 2F
06/23 23:37, 2F
→
06/23 23:38,
2小時前
, 3F
06/23 23:38, 3F
Windows 近期熱門文章
PTT數位生活區 即時熱門文章