下面的代码是我对此的看法。路径和反斜杠操作有一些特殊性,所以我试图在评论中解释所有内容。
有一个关键点,那就是无限的字符串搜索,例如由-replace
, -like
,.Contains()
等执行的搜索,当一个变量的路径的值是另一个变量的路径或目录路径的子字符串时,可能会产生不良结果。例如,给定%ProgramFiles%
( C:\Program Files
) 和%ProgramFiles(x86)%
( C:\Program Files (x86)
),路径C:\Program Files (x86)\Test
可以转换为%ProgramFiles% (x86)\Test
,而不是%ProgramFiles(x86)%\Test
if%ProgramFiles%
恰好在之前测试过%ProgramFiles(x86)%
。
解决方案是仅将变量的路径与完整的路径段进行比较。也就是说,在 path 的情况下C:\Program Files (x86)\Test
,比较会像这样......
- 测试是否与原始路径相等
C:\Program Files (x86)\Test
。没有变量匹配。
- 测试是否与父路径相等
C:\Program Files (x86)
。 %ProgramFiles(x86)%
火柴。没有进一步的祖先路径(即C:
)被测试。
%ProgramFiles%
永远不会匹配,因为C:\Program Files
未测试部分路径。
通过仅针对完整路径段进行测试,将变量与候选路径进行比较的顺序无关紧要。
New-Variable -Name 'VariablesToSubstitute' -Option Constant -Value @(
# Hard-code system variables that contain machine-wide paths
'CommonProgramFiles',
'CommonProgramFiles(x86)',
'ComSpec',
'ProgramData', # Alternatively: ALLUSERSPROFILE
'ProgramFiles',
'ProgramFiles(x86)',
'SystemDrive'
'SystemRoot' # Alternatively: WinDir
'MyDirectoryWithoutSlash' # Defined below
'MyDirectoryWithSlash' # Defined below
);
function Format-Path
{
param (
[parameter(Mandatory = $true)]
[string] $FilePath
)
if (![System.String]::IsNullOrEmpty($FilePath))
{
# Strip away quotations
$FilePath = $FilePath.Trim('"')
# Leave trailing slashes intact so variables with a trailing slash will match
#$FilePath = $FilePath.TrimEnd('\')
}
# Initialize this once, but only after the test code has started
if ($null -eq $script:pathVariables)
{
$script:pathVariables = $VariablesToSubstitute | ForEach-Object -Process {
$path = [Environment]::GetEnvironmentVariable($_)
if ($null -eq $path)
{
Write-Warning -Message "The environment variable ""$_"" is not defined."
}
else
{
return [PSCustomObject] @{
Name = $_
Path = $path
}
}
}
}
# Test against $FilePath and its ancestors until a match is found or the path is empty.
# Only comparing with complete path segments prevents performing partial substitutions
# (e.g. a path starting with %ProgramFiles(x86)% being substituted with %ProgramFiles%,
# or "C:\Windows.old" being transformed to "%SystemRoot%.old")
for ($filePathAncestorOrSelf = $FilePath;
-not [String]::IsNullOrEmpty($filePathAncestorOrSelf);
# Split-Path -Parent removes the trailing backslash on the result *unless* the result
# is a drive root. It'd be easier to normalize all paths without the backslash, but
# Split-Path throws an error if the input path is a drive letter with no slash, so
# normalize everything *with* the backslash and strip it off later.
$filePathAncestorOrSelf = EnsureTrailingBackslash (
# Protect against the case where $FilePath is a drive letter with no backslash
# We have to do this here because we want our initial path above to be
# exactly $FilePath, not (EnsureTrailingBackslash $FilePath).
Split-Path -Path (EnsureTrailingBackslash $filePathAncestorOrSelf) -Parent
)
)
{
# Test against $filePathAncestorOrSelf with and without a trailing backslash
foreach ($candidatePath in $filePathAncestorOrSelf, $filePathAncestorOrSelf.TrimEnd('\'))
{
foreach ($variable in $pathVariables)
{
if ($candidatePath -ieq $variable.Path)
{
$variableBasePath = "%$($variable.Name)%"
# The rest of the path after the variable's path
$pathRelativeToVariable = $FilePath.Substring($variable.Path.Length)
# Join-Path appends a trailing backslash if the child path is empty - we don't want that
if ([String]::IsNullOrEmpty($pathRelativeToVariable))
{
return $variableBasePath
}
# Join-Path will join the base and relative path with a slash,
# which we don't want if the variable path already ends with a slash
elseif ($variable.Path -like '*\')
{
return $variableBasePath + $pathRelativeToVariable
}
else
{
return Join-Path -Path $variableBasePath -ChildPath $pathRelativeToVariable
}
}
}
}
}
return $FilePath
}
function EnsureTrailingBackslash([String] $path)
{
return $(
# Keep an empty path unchanged so the for loop will terminate properly
if ([String]::IsNullOrEmpty($path) -or $path.EndsWith('\')) {
$path
} else {
"$path\"
}
)
}
使用此测试代码...
$Env:MyDirectoryWithoutSlash = 'C:\My Directory'
$Env:MyDirectoryWithSlash = 'C:\My Directory\'
@'
X:
X:\Windows
X:\Windows\system32
X:\Windows\system32\cmd.exe
X:\Windows.old
X:\Windows.old\system32
X:\Windows.old\system32\cmd.exe
X:\Program Files\Test
X:\Program Files (x86)\Test
X:\Program Files (it's a trap!)\Test
X:\My Directory
X:\My Directory\Test
'@ -split "`r`n?" `
| ForEach-Object -Process {
# Test the path with the system drive letter
$_ -replace 'X:', $Env:SystemDrive
# Test the path with the non-system drive letter
$_
} | ForEach-Object -Process {
$path = $_.TrimEnd('\')
# Test the path without a trailing slash
$path
# If the path is a directory (determined by the
# absence of an extension in the last segment)...
if ([String]::IsNullOrEmpty([System.IO.Path]::GetExtension($path)))
{
# Test the path with a trailing slash
"$path\"
}
} | ForEach-Object -Process {
[PSCustomObject] @{
InputPath = $_
OutputPath = Format-Path $_
}
}
...我得到这个结果...
InputPath OutputPath
--------- ----------
C: %SystemDrive%
C:\ %SystemDrive%\
X: X:
X:\ X:\
C:\Windows %SystemRoot%
C:\Windows\ %SystemRoot%\
X:\Windows X:\Windows
X:\Windows\ X:\Windows\
C:\Windows\system32 %SystemRoot%\system32
C:\Windows\system32\ %SystemRoot%\system32\
X:\Windows\system32 X:\Windows\system32
X:\Windows\system32\ X:\Windows\system32\
C:\Windows\system32\cmd.exe %ComSpec%
X:\Windows\system32\cmd.exe X:\Windows\system32\cmd.exe
C:\Windows.old %SystemDrive%\Windows.old
X:\Windows.old X:\Windows.old
C:\Windows.old\system32 %SystemDrive%\Windows.old\system32
C:\Windows.old\system32\ %SystemDrive%\Windows.old\system32\
X:\Windows.old\system32 X:\Windows.old\system32
X:\Windows.old\system32\ X:\Windows.old\system32\
C:\Windows.old\system32\cmd.exe %SystemDrive%\Windows.old\system32\cmd.exe
X:\Windows.old\system32\cmd.exe X:\Windows.old\system32\cmd.exe
C:\Program Files\Test %ProgramFiles%\Test
C:\Program Files\Test\ %ProgramFiles%\Test\
X:\Program Files\Test X:\Program Files\Test
X:\Program Files\Test\ X:\Program Files\Test\
C:\Program Files (x86)\Test %ProgramFiles(x86)%\Test
C:\Program Files (x86)\Test\ %ProgramFiles(x86)%\Test\
X:\Program Files (x86)\Test X:\Program Files (x86)\Test
X:\Program Files (x86)\Test\ X:\Program Files (x86)\Test\
C:\Program Files (it's a trap!)\Test %SystemDrive%\Program Files (it's a trap!)\Test
C:\Program Files (it's a trap!)\Test\ %SystemDrive%\Program Files (it's a trap!)\Test\
X:\Program Files (it's a trap!)\Test X:\Program Files (it's a trap!)\Test
X:\Program Files (it's a trap!)\Test\ X:\Program Files (it's a trap!)\Test\
C:\My Directory %MyDirectoryWithoutSlash%
C:\My Directory\ %MyDirectoryWithSlash%
X:\My Directory X:\My Directory
X:\My Directory\ X:\My Directory\
C:\My Directory\Test %MyDirectoryWithSlash%Test
C:\My Directory\Test\ %MyDirectoryWithSlash%Test\
X:\My Directory\Test X:\My Directory\Test
X:\My Directory\Test\ X:\My Directory\Test\
请注意,总是先搜索候选祖先路径,然后使用斜杠,然后再不搜索。这意味着在不太可能的情况下,有两个变量路径仅因是否存在尾部斜杠而有所不同,带有尾部斜杠的变量将被匹配。因此,如上所示,C:\My Directory\Test
将变为%MyDirectoryWithSlash%Test
,这看起来有点奇怪。foreach
通过将函数中第一个循环的顺序从...反转
foreach ($candidatePath in $filePathAncestorOrSelf, $filePathAncestorOrSelf.TrimEnd('\'))
...到...
foreach ($candidatePath in $filePathAncestorOrSelf.TrimEnd('\'), $filePathAncestorOrSelf)
...相关输出更改为...
InputPath OutputPath
--------- ----------
... ...
C:\My Directory\ %MyDirectoryWithoutSlash%\
... ...
C:\My Directory\Test %MyDirectoryWithoutSlash%\Test
C:\My Directory\Test\ %MyDirectoryWithoutSlash%\Test\
... ...