Код ниже - мой взгляд на это. В манипуляциях с path и backsla sh есть некоторые особенности, поэтому я попытался объяснить все в комментариях.
Есть один ключевой момент, который заключается в том, что поиск неограниченной строки, такой как -replace
, -like
, .Contains()
, et c. и может привести к нежелательным результатам , когда значение пути одной переменной является подстрокой пути другой переменной или пути каталога. Например, с учетом %ProgramFiles%
(C:\Program Files
) и %ProgramFiles(x86)%
(C:\Program Files (x86)
) путь C:\Program Files (x86)\Test
может быть преобразован в %ProgramFiles% (x86)\Test
вместо %ProgramFiles(x86)%\Test
, если %ProgramFiles%
будет проверено до %ProgramFiles(x86)%
.
Решение состоит в том, чтобы сравнивать только путь переменной с полным сегментом пути . То есть в случае пути C:\Program Files (x86)\Test
сравнение будет go следующим образом ...
- Проверка на равенство с исходным путем
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\
Обратите внимание, что пути-предки-кандидаты всегда ищутся сначала с конечным sla sh, а затем без него. Это означает, что в маловероятном случае есть две переменные пути, которые отличаются только наличием или отсутствием конечного sla sh, переменная с конечным sla sh будет сопоставлена. Таким образом, как видно выше, C:\My Directory\Test
станет %MyDirectoryWithSlash%Test
, что выглядит немного странно. Путем изменения порядка первой foreach
l oop в функции от ...
foreach ($candidatePath in $filePathAncestorOrSelf, $filePathAncestorOrSelf.TrimEnd('\'))
... до ...
foreach ($candidatePath in $filePathAncestorOrSelf.TrimEnd('\'), $filePathAncestorOrSelf)
... the соответствующие выходные изменения в этом ...
InputPath OutputPath
--------- ----------
... ...
C:\My Directory\ %MyDirectoryWithoutSlash%\
... ...
C:\My Directory\Test %MyDirectoryWithoutSlash%\Test
C:\My Directory\Test\ %MyDirectoryWithoutSlash%\Test\
... ...