如何在PowerShell中规范化路径?

120

我有两个路径:

fred\frog

..\frag

我可以在PowerShell中像这样将它们连接起来:

join-path 'fred\frog' '..\frag'

那给我这个:

fred\frog\..\frag

但我不想那样。我想要一个没有双点的规范化路径,就像这样:

fred\frag

我该如何获取那个?


1
frag是frog的子文件夹吗?如果不是,那么组合路径将会得到fred\frog\frag。如果是,那么这就是一个非常不同的问题了。 - EBGreen
14个回答

117

你可以使用resolve-path将..\frag扩展为其完整路径:

PS > resolve-path ..\frag 

尝试使用combine()方法规范化路径:

[io.path]::Combine("fred\frog",(resolve-path ..\frag).path)

如果您的路径是 C:\WindowsC:\Windows\,它们虽然相同但却会产生两个不同的结果。 - Joe Phillips
3
[io.path]::Combine 的参数顺序是反的。更好的做法是使用本机的 Join-Path PowerShell 命令:Join-Path (Resolve-Path ..\frag).Path 'fred\frog'。另外需要注意的是,至少在 PowerShell v3 中,Resolve-Path 现在支持 -Relative 开关来解析相对于当前文件夹的路径。正如提到的那样,与 [IO.Path]::GetFullPath() 不同,Resolve-Path 仅适用于已存在的路径。 - mklement0
1
@mklement0 根据此在线参考文档:http://adamringenberg.com/powershell2/resolve-path/,`Resolve-Path` 的 -Relative 开关似乎是在 PSv2 中添加的。 - Prid
@JoePhillips 这是我正在使用的函数,也许对你有帮助。链接 - Kevin Holtkamp
它也无法处理不存在的路径。正如您在这些测试中所看到的:https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-only_resolve_path - Luiz Felipe

92

您可以使用组合的 $pwd, Join-Path以及 [System.IO.Path]::GetFullPath 命令获取完全限定的扩展路径。

因为 cd (Set-Location) 命令不会更改进程的当前工作目录, 因此直接向不了解 PowerShell 上下文的 .NET API 传递相对文件名可能会产生意外的副作用,例如解析为基于初始工作目录而非当前位置的路径。

您需要先使您的路径合格化:

Join-Path (Join-Path $pwd fred\frog) '..\frag'

根据我的当前位置,这将产生以下结果:

C:\WINDOWS\system32\fred\frog\..\frag

有了绝对路径,现在可以安全地调用.NET API GetFullPath

[System.IO.Path]::GetFullPath((Join-Path (Join-Path $pwd fred\frog) '..\frag'))

这将为您提供完全限定路径,正确解析 ..

C:\WINDOWS\system32\fred\frag

其实这也不复杂,个人而言,我鄙视那些依赖外部脚本来解决此问题的解决方案,Join-Path$pwd 相当巧妙地解决了这个简单的问题(GetFullPath 只是为了让它看起来漂亮)。如果你只想保留相对部分,只需添加.Substring($pwd.Path.Trim('\').Length + 1),就大功告成了!

fred\frag

更新

感谢 @Dangph 指出了 C:\ 边缘情况。


如果pwd是"C:",则最后一步无法工作。在这种情况下,我会得到"red\frag"。 - dan-gph
@Dangph - 不确定我理解你的意思,上面的代码看起来运行良好。你使用的是哪个版本的PowerShell?我使用的是3.0版本。 - John Leidegren
1
我的意思是最后一步:cd c:\; "C:\fred\frag".Substring((pwd).Path.Length + 1)。这不是什么大问题,只是需要注意一下。 - dan-gph
啊,发现得好,我们可以通过添加一个 trim 调用来修复它。尝试 cd c:\; "C:\fred\frag".Substring((pwd).Path.Trim('\').Length + 1)。不过这个命令有点长了。 - John Leidegren
3
或者,只需使用: $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(".\nonexist\foo.txt")它也适用于不存在的路径。 顺便说一下,“x0n”得到了这个功劳。 正如他所指出的那样,它会解析为PSPaths而不是文件系统路径,但如果您在PowerShell中使用这些路径,谁在乎呢? https://dev59.com/ZHA75IYBdhLWcg3w6Nn8 - Joe the Coder
这里有很多边缘情况,最好使用GetUnresolveProviderPath,你可以在这里的测试中看到。https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-combine_pwd_then_getfullpath - Luiz Felipe

28

你也可以使用 Path.GetFullPath,不过这个方法(与 Dan R 的答案类似)会返回整个路径。用法如下:

[IO.Path]::GetFullPath( "fred\frog\..\frag" )
更有趣的是
[IO.Path]::GetFullPath( (join-path "fred\frog" "..\frag") )

假设您的当前目录为D:\,则两者都会产生以下结果:

D:\fred\frag

请注意,此方法不尝试确定fred或frag是否实际存在。


这已经很接近了,但是当我尝试时,即使我的当前目录是"C:\scratch",它也会给我返回"H:\fred\frag",这是错误的。(根据MSDN的说法,它不应该这样做。)然而,这给了我一个想法。我会将其作为答案添加。 - dan-gph
9
你的问题是需要在.NET中设置当前目录。[System.IO.Directory]::SetCurrentDirectory(((Get-Location -PSProvider FileSystem).ProviderPath))可以实现这一功能。 - JasonMArcher
2
明确声明一下:[IO.Path]::GetFullPath()与PowerShell本地的Resolve-Path不同,它也可以处理不存在的路径。但缺点是需要先将.NET的工作文件夹与PS同步,正如@JasonMArcher所指出的那样。 - mklement0
如果引用不存在的驱动器,则 Join-Path 会引发异常。 - Tahir Hassan

28

接受的答案非常有帮助,但它没有正确“规范化”绝对路径。请看我的派生作品,可以同时规范化绝对路径和相对路径。

function Get-AbsolutePath ($Path)
{
    # System.IO.Path.Combine has two properties making it necesarry here:
    #   1) correctly deals with situations where $Path (the second term) is an absolute path
    #   2) correctly deals with situations where $Path (the second term) is relative
    # (join-path) commandlet does not have this first property
    $Path = [System.IO.Path]::Combine( ((pwd).Path), ($Path) );

    # this piece strips out any relative path modifiers like '..' and '.'
    $Path = [System.IO.Path]::GetFullPath($Path);

    return $Path;
}

1
鉴于所有不同的解决方案,这个解决方案适用于所有不同类型的路径。例如,[IO.Path] :: GetFullPath()无法正确确定纯文件名的目录。 - Jari Turkia
规范化绝对路径的正确解决方案就是使用 https://dev59.com/ZHA75IYBdhLWcg3w6Nn8#3040982 - Luiz Felipe
这个解决方案的问题在于它可以处理不存在的路径,但是只对已存在的驱动器有效。正如您在这个测试结果中看到的 https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-only_unresolved_provider_path_from_pspath - Luiz Felipe

15

由于PowerShell的提供者模型允许PowerShell的当前路径与Windows进程工作目录不同,因此任何非PowerShell路径操作函数(例如System.IO.Path中的函数)在PowerShell中将不可靠。

此外,正如您可能已经发现的那样,PowerShell的Resolve-Path和Convert-Path cmdlet对于将相对路径(包含'..'的路径)转换为驱动器限定的绝对路径非常有用,但如果所引用的路径不存在,则会失败。

以下非常简单的cmdlet适用于不存在的路径。即使找不到'fred'或'frag'文件或文件夹(且当前的PowerShell驱动器是'd:'),它也会将'fred\frog\..\frag'转换为'd:\fred\frag'。

function Get-AbsolutePath {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]
        $Path
    )

    process {
        $Path | ForEach-Object {
            $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_)
        }
    }
}

3
对于不存在驱动器的路径,这种方法无法使用,例如我没有 Q: 驱动器。即使它是一个有效的路径,Get-AbsolutePath q:\foo\bar\..\baz 也会失败,具体取决于您对有效路径的定义。值得一提的是,即使是内置的 Test-Path <path> -IsValid 在根源于不存在驱动器的路径上也会失败。 - Keith Hill
3
换句话说,PowerShell认为在不存在的根路径上的路径是无效的。我认为这是相当合理的,因为PowerShell使用根路径来决定在处理它时要使用哪种提供程序。例如,HKLM:\SOFTWARE 是PowerShell中有效的路径,指的是本地计算机注册表中的 SOFTWARE 键。但为了确定它是否有效,它需要弄清楚注册表路径的规则。 - jpmc26
你需要使用split-path来剥离驱动器,可以参考我的回答。 - Luiz Felipe
请查看此处以获取正确的实现 https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-proper_drive_provider_path_from_pspath - Luiz Felipe

7
如果路径包含限定符(驱动器号),则 x0n对Powershell:解析可能不存在的路径? 将规范化路径。如果路径不包含限定符,仍将进行规范化,但会返回相对于当前目录的完全限定路径,这可能不是您想要的结果。
$p = 'X:\fred\frog\..\frag'
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
X:\fred\frag

$p = '\fred\frog\..\frag'
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
C:\fred\frag

$p = 'fred\frog\..\frag'
$ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($p)
C:\Users\WileCau\fred\frag

3
如果路径存在,并且您不介意返回绝对路径,可以使用带有-Resolve参数的Join-Path
Join-Path 'fred\frog' '..\frag' -Resolve

这总是产生绝对规范化路径,并且它不适用于不存在的路径。正如您在我进行的测试中所看到的那样:https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#test-only_join_path_resolve - Luiz Felipe

3

这个库很好用: NDepend.Helpers.FileDirectoryPath.

编辑: 这是我想到的内容:

[Reflection.Assembly]::LoadFrom("path\to\NDepend.Helpers.FileDirectoryPath.dll") | out-null

Function NormalizePath ($path)
{
    if (-not $path.StartsWith('.\'))  # FilePathRelative requires relative paths to begin with '.'
    {
        $path = ".\$path"
    }

    if ($path -eq '.\.')  # FilePathRelative can't deal with this case
    {
        $result = '.'
    }
    else
    {
        $relPath = New-Object NDepend.Helpers.FileDirectoryPath.FilePathRelative($path)
        $result = $relPath.Path
    }

    if ($result.StartsWith('.\')) # remove '.\'. 
    {
        $result = $result.SubString(2)
    }

    $result
}

按照以下方式调用:

> NormalizePath "fred\frog\..\frag"
fred\frag

请注意,此代码片段需要DLL文件的路径。有一个技巧可以用来找到包含当前执行脚本的文件夹,但在我的情况下,我有一个可以使用的环境变量,所以我只是使用了它。

我不知道为什么那个被踩了。那个库真的非常适合进行路径操作。这就是我在项目中最终选择使用的。 - dan-gph
减2。仍然感到困惑。我希望人们意识到从PowerShell使用.Net程序集很容易。 - dan-gph
@Jason,我不记得细节了,但当时它是最好的解决方案,因为它是唯一解决我的特定问题的方法。但有可能自那时以来出现了另一个更好的解决方案。 - dan-gph
它可能被踩是因为你有好的答案,但选择添加一个带链接的一行代码并接受它。 - Louis Kottmann
3
使用第三方 DLL 对于这个解决方案来说是一个很大的劣势。 - Louis Kottmann
显示剩余3条评论

1

出于以下原因,没有一个答案是完全可以接受的。

  • 它必须支持powershell提供程序。
  • 它必须适用于不存在于不存在驱动器中的路径。
  • 它必须处理“..”和“。”,这就是规范化路径的含义。
  • 无外部库和无正则表达式。
  • 它不能重新定位路径,这意味着相对路径保持相对。

基于以下原因,我列出了每个方法的预期结果,如下:


function tests {
    context "cwd" {
        it 'has no external libraries' {
            Load-NormalizedPath
        }
        it 'barely work for FileInfos on existing paths' {
            Get-NormalizedPath 'a\..\c' | should -be 'c'
        }
        it 'process .. and . (relative paths)' {
            Get-NormalizedPath 'a\b\..\..\c\.' | should -be 'c'
        }
        it 'must support powershell providers' {
            Get-NormalizedPath "FileSystem::\\$env:COMPUTERNAME\Shared\a\..\c" | should -be "FileSystem::\\$env:COMPUTERNAME\Shared\c"
        }
        it 'must support powershell drives' {
            Get-NormalizedPath 'HKLM:\Software\Classes\.exe\..\.dll' | should -be 'HKLM:\Software\Classes\.dll'
        }
        it 'works with non-existant paths' {
            Get-NormalizedPath 'fred\frog\..\frag\.' | should -be 'fred\frag'
        }
        it 'works with non-existant drives' {
            Get-NormalizedPath 'U:\fred\frog\..\frag\.' | should -be 'U:\fred\frag'
        }
        it 'barely work for direct UNCs' {
            Get-NormalizedPath "\\$env:COMPUTERNAME\Shared\a\..\c" | should -be "\\$env:COMPUTERNAME\Shared\c"
        }
    }
    context "reroot" {
        it 'doesn''t reroot subdir' {
            Get-NormalizedPath 'fred\frog\..\frag\.' | should -be 'fred\frag'
        }
        it 'doesn''t reroot local' {
            Get-NormalizedPath '.\fred\frog\..\frag\.' | should -be 'fred\frag'
        }
        it 'doesn''t reroot parent' {
            Get-NormalizedPath "..\$((Get-Item .).Name)\fred\frog\..\frag\." | should -be 'fred\frag'
        }
    }
    context "drive root" {
        beforeEach { Push-Location 'c:/' }
        it 'works on drive root' {
            Get-NormalizedPath 'fred\frog\..\..\fred\frag\' | should -be 'fred\frag\'
        }
        afterEach { Pop-Location }
    }
    context "temp drive" {
        beforeEach { New-PSDrive -Name temp -PSProvider FileSystem 'b:/tools' }
        it 'works on temp drive' {
            Get-NormalizedPath 'fred\frog\..\..\fred\frag\' | should -be 'fred\frag\'
        }
        it 'works on temp drive with absolute path' {
            Get-NormalizedPath 'temp:\fred\frog\..\..\fred\frag\' | should -be 'temp:\fred\frag\'
        }
        afterEach { Remove-PSDrive -Name temp }
    }
    context "unc drive" {
        beforeEach { Push-Location "FileSystem::\\$env:COMPUTERNAME\Shared\​" }
        it 'works on unc drive' {
            Get-NormalizedPath 'fred\frog\..\..\fred\frag\' | should -be 'fred\frag\'
        }
        afterEach { Pop-Location }
    }
}

正确的答案使用GetUnresolvedProviderPathFromPSPath,但它不能独自工作,如果您尝试直接使用它,您会得到这些结果。来自此答案https://dev59.com/7nRB5IYBdhLWcg3w1Kn0#52157943

$path = Join-Path '/' $path
$path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
$path = $path.Replace($pwd.Path, '').Replace($pwd.Drive.Root, '')

pros: simple
cons: needs boilerplate to make it correct, doesn't work with other providers or non-ex drives.

 Context cwd
   [+] has no external libraries 4ms (1ms|3ms)
   [+] barely work for FileInfos on existing paths 3ms (2ms|0ms)
   [+] process .. and . (relative paths) 3ms (2ms|0ms)
   [-] must support powershell providers 4ms (3ms|1ms)
    Expected: 'FileSystem::\\LUIZMONAD\Shared\c'
    But was:  '\\LUIZMONAD\Shared\a\..\c'
               ^
   [-] must support powershell drives 14ms (4ms|10ms)
    Expected: 'HKLM:\Software\Classes\.dll'
    But was:  'Cannot find drive. A drive with the name '\HKLM' does not exist.'
               ^
   [+] works with non-existant paths 3ms (2ms|1ms)
   [-] works with non-existant drives 4ms (3ms|1ms)
    Expected: 'U:\fred\frag'
    But was:  'Cannot find drive. A drive with the name '\U' does not exist.'
               ^
   [-] barely work for direct UNCs 3ms (3ms|1ms)
    Expected: '\\LUIZMONAD\Shared\c'
    But was:  '\\LUIZMONAD\Shared\a\..\c'
               -------------------^
 Context reroot
   [+] doesn't reroot subdir 3ms (2ms|1ms)
   [+] doesn't reroot local 33ms (33ms|1ms)
   [-] doesn't reroot parent 4ms (3ms|1ms)
    Expected: 'fred\frag'
    But was:  '\fred\frag'
               ^
 Context drive root
   [+] works on drive root 5ms (3ms|2ms)
 Context temp drive
   [+] works on temp drive 4ms (3ms|1ms)
   [-] works on temp drive with absolute path 6ms (5ms|1ms)
    Expected: 'temp:\fred\frag\'
    But was:  'Cannot find drive. A drive with the name '\temp' does not exist.'
               ^
 Context unc drive
   [+] works on unc drive 6ms (5ms|1ms)
Tests completed in 207ms
Tests Passed: 9, Failed: 6, Skipped: 0 NotRun: 0

所以,我们需要做的是去除驱动程序/提供程序/UNC,然后使用GetUnresolvedProviderPathFromPSPath,最后再把驱动程序/提供程序/UNC放回去。 不幸的是,GetUPPFP依赖于当前pwd状态,但至少我们没有改变它。
$path_drive = [ref] $null
$path_abs = $ExecutionContext.SessionState.Path.IsPSAbsolute($path, $path_drive)
$path_prov = $ExecutionContext.SessionState.Path.IsProviderQualified($path)
# we split the drive away, it makes UnresolvedPath fail on non-existing drives.
$norm_path  = Split-Path $path -NoQualifier
# strip out UNC
$path_direct = $norm_path.StartsWith('//') -or $norm_path.StartsWith('\\')
if ($path_direct) {
    $norm_path = $norm_path.Substring(2)
}
# then normalize
$norm_path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($norm_path)
# then we cut out the current location if same drive
if (($path_drive.Value -eq $pwd.Drive.Name) -or $path_direct) {
    $norm_path = $norm_path.Substring($pwd.Path.Trim('/', '\').Length + 1)
} elseif (-not $path_prov) {
    # or we cut out the current drive
    if ($pwd.Drive) {
        $norm_path = $norm_path.Substring($pwd.Drive.Root.Length)
    } else {
        # or we cut out the UNC special case
        $norm_path = $norm_path.Substring($pwd.ProviderPath.Length + 1)
    }
}
# then add back the UNC if any
if ($path_direct) {
    $norm_path = $pwd.Provider.ItemSeparator + $pwd.Provider.ItemSeparator + $norm_path
}
# then add back the provider if any
if ($path_prov) {
    $norm_path = $ExecutionContext.SessionState.Path.Combine($path_drive.Value + '::/', $norm_path)
}
# or add back the drive if any
elseif ($path_abs) {
    $norm_path = $ExecutionContext.SessionState.Path.Combine($path_drive.Value + ':', $norm_path)
}
$norm_path

pros: doesn't use the dotnet path function, uses proper powershell infrastructure.
cons: kind of complex, depends on `pwd`

 Context cwd
   [+] has no external libraries 8ms (2ms|6ms)
   [+] barely work for FileInfos on existing paths 4ms (3ms|1ms)
   [+] process .. and . (relative paths) 3ms (2ms|1ms)
   [+] must support powershell providers 13ms (13ms|0ms)
   [+] must support powershell drives 3ms (2ms|1ms)
   [+] works with non-existant paths 3ms (2ms|0ms)
   [+] works with non-existant drives 3ms (2ms|1ms)
   [+] barely work for direct UNCs 3ms (2ms|1ms)
 Context reroot
   [+] doesn't reroot subdir 3ms (2ms|1ms)
   [+] doesn't reroot local 3ms (2ms|1ms)
   [+] doesn't reroot parent 15ms (14ms|1ms)
 Context drive root
   [+] works on drive root 4ms (3ms|1ms)
 Context temp drive
   [+] works on temp drive 4ms (3ms|1ms)
   [+] works on temp drive with absolute path 3ms (3ms|1ms)
 Context unc drive
   [+] works on unc drive 9ms (8ms|1ms)
Tests completed in 171ms
Tests Passed: 15, Failed: 0, Skipped: 0 NotRun: 0

我进行了几次尝试,因为这是科学家的工作。如果你觉得这太复杂了,那你可以选择其他方法。
相信我,你需要使用一堆路径来正确地完成它,如果你不相信,请去查看GetUnresolvedProviderPathFromPSPath的代码,而且不,你不能使用正则表达式因为它会出现递归问题。
来源: https://gist.github.com/Luiz-Monad/d5aea290087a89c070da6eec84b33742#file-normalize-path-ps-md

1
我写这篇文章是因为我打算建议在“resolve-path”中添加一个“-notexists”参数来实现这一点。 - Luiz Felipe

1

这将给出完整路径:

(gci 'fred\frog\..\frag').FullName

这将返回相对于当前目录的路径:

(gci 'fred\frog\..\frag').FullName.Replace((gl).Path + '\', '')

由于某种原因,它们只能在 frag 是文件而不是 目录 的情况下起作用。


1
gci 是 get-childitem 的别名。一个目录的子项是它的内容。将 gci 替换为 gi,这样就可以同时使用了。 - zdan
2
Get-Item 的效果很好。但是,这种方法要求文件夹存在。 - Peter Lillevold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接