楼主:
falcon (falken)
2026-06-23 23:15:27PowerShell 提供者是能让使用者能将各种资料存放区
(如登录档、凭证、系统环境变量)对应成类似磁盘机的结构
使用同一套命令进行新增、移除、迁移... 等管理操作
例如: 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