Как найти потенциальную исходную переменную среды для частичного пути в PowerShell? - PullRequest
4 голосов
/ 06 февраля 2020

Я хочу написать функцию, которая преобразует обычный путь в путь, который включает переменные среды:

Например:

C:\Windows\SomePath

преобразовать в:

%Windir%\SomePath

Как мне это сделать, и возможно ли это?

Вот что я пытаюсь сделать, но проблема в том, что мне нужно проверить строку для всех возможных переменных, есть ли еще какой-нибудь автоматизированный c способ? ? такой, что оператор -relace не понадобится

function Format-Path
{
    param (
        [parameter(Mandatory = $true)]
        [string] $FilePath
    )

    if (![System.String]::IsNullOrEmpty($FilePath))
    {
        # Strip away quotations and ending backslash
        $FilePath = $FilePath.Trim('"')
        $FilePath = $FilePath.TrimEnd('\\')
    }

$FilePath = $FilePath -replace "C:\\Windows", "%Windir%"
$FilePath = $FilePath -replace "C:\\ProgramFiles", "%ProgramFiles%"
$FilePath = $FilePath -replace "C:\\ProgramFiles (x86)", "%ProgramFiles (x86)%"
# ETC.. the list goes on..

return $FilePath
}

# test case
Format-Path '"C:\Windows\SomePath\"'

Вывод:

%Windir%\SomePath

РЕДАКТИРОВАТЬ: Неверный ввод или неверный код на самом деле не проблема, потому что в конец $Path можно легко проверить с помощью:

Test-Path -Path ([System.Environment]::ExpandEnvironmentVariables($FilePath))

Ответы [ 3 ]

2 голосов
/ 06 февраля 2020

Код ниже - мой взгляд на это. В манипуляциях с 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\
...                                   ...
1 голос
/ 06 февраля 2020

Спасибо всем за предложения, я вижу, Owain Esau уже опубликовал вопрос, когда я также работал над решением.

Вероятно, было бы ошибкой, если бы я не опубликовал то, что я выслал :

function Format-Path
{
    param (
        [parameter(Mandatory = $true)]
        [string] $FilePath
    )

    $SearchString = Split-Path -Path $FilePath -Parent
    $Variables = @(Get-ChildItem Env:) | Sort-Object -Descending { $_.Value.length }

    foreach ($Value in $Variables.Value)
    {
        if ($Value -like "*$SearchString")
        {
            $Replacement = "%" + @(($Variables | Where-Object { $_.Value -eq $Value} ).Name)[0] + "%"

            return $FilePath.Replace($SearchString, $Replacement)
        }
    }
}

Format-Path "C:\Windows\SomePath"
Format-Path "C:\Program Files (x86)\SomePath"
Format-Path "C:\Program Files\SomePath"
Format-Path "C:\ProgramData\SomePath"

# does not work for %SystemDrive%
Format-Path "C:\SomePath"

Вывод:

%SystemRoot%\SomePath
%ProgramFiles(x86)%\SomePath
%ProgramFiles%\SomePath
%ProgramData%\SomePath

Это работает, за исключением %SystemDrive%, который требует дополнительных улучшений. Я оставлю этот вопрос открытым для улучшений.

1 голос
/ 06 февраля 2020

Вот что я в итоге попытался немного автоматизировать:

function Format-Path {
    param (
        [string]$path
    )

    ## Environmental variable list
    $EV_Variables = ( gci env:* | Where-Object {$_.Name -notin "SystemDrive", "HomeDrive" -and $_.Value -notmatch ";"} | sort-object name  )

    ## Compare string
    while ($EV_Variables.Value -like $path) {
        foreach ($EC in $EV_Variables) {
            if ( $path -like "*$($EC.value)*") {
                $path = $path.Replace($EC.Value, "%$($EC.Name)%")
            }
        }
    }

    $path
}

Format-Path "C:\Program Files (x86)\Nmap\nmap.exe"
Format-Path "C:\Windows"
Format-Path "C:\Program Files"
Format-Path "C:\Program Files (x86)"

Что возвращает:

%nmap%
%SystemRoot%
%ProgramFiles%
%ProgramFiles% (x%NUMBER_OF_PROCESSORS%%PROCESSOR_LEVEL%)

Я проверил это на нескольких переменных среды, и это сработало для большинства, с некоторыми проблемами, связанными с определенными переменными среды, такими как %NUMBER_OF_PROCESSORS%, показанными выше, но я уверен, что вы можете просто исключить их из списка переменных.

...