How to normalize a path in PowerShell?

I have two paths:

fred\frog

and

..\frag

I can join them together in PowerShell like this:

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

That gives me this:

fred\frog\..\frag

But I don't want that. I want a normalized path without the double dots, like this:

fred\frag

How can I get that?

95775 次浏览

有一个办法:

Join-Path 'fred\frog' '..\frag'.Replace('..', '')

等等,也许我误解了这个问题。在你的例子中,碎片是青蛙的子文件夹吗?

If you need to get rid of the .. portion, you can use a System.IO.DirectoryInfo object. Use 'fred\frog..\frag' in the constructor. The FullName property will give you the normalized directory name.

唯一的缺点是它会提供整个路径(例如,c: test fred frag)。

You could also use Path.GetFullPath, although (as with Dan R's answer) this will give you the entire path. Usage would be as follows:

[IO.Path]::GetFullPath( "fred\frog\..\frag" )

更有趣的是

[IO.Path]::GetFullPath( (join-path "fred\frog" "..\frag") )

两者都会产生以下结果(假设你的工作目录是 D:) :

D:\fred\frag

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

这就给出了完整的路径:

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

这给出了相对于工作目录的路径:

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

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

您可以使用 decision-path 将. . frag 扩展为其完整路径:

PS > resolve-path ..\frag

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

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

这个库很好: 帮助程序文件目录路径

编辑: 这是我想出来的:

[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
}

Call it like this:

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

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

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

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

以下非常简单的 cmdlet 应该适用于不存在的路径。它会让青蛙改变信仰。.即使找不到“ fred”或“ frag”文件或文件夹(当前的 PowerShell 驱动器是“’d:”) ,也可以将 frg’转换为“’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($_)
}
}
}

创建一个函数。此函数将规范化系统上不存在的路径以及不添加驱动器字母。

function RemoveDotsInPath {
[cmdletbinding()]
Param( [Parameter(Position=0,  Mandatory=$true)] [string] $PathString = '' )


$newPath = $PathString -creplace '(?<grp>[^\n\\]+\\)+(?<-grp>\.\.\\)+(?(grp)(?!))', ''
return $newPath
}

例如:

$a = 'fooA\obj\BusinessLayer\..\..\bin\BusinessLayer\foo.txt'
RemoveDotsInPath $a
'fooA\bin\BusinessLayer\foo.txt'

感谢 Oliver Schadlich 对正则表达的帮助。

可以使用 $pwdJoin-Path[System.IO.Path]::GetFullPath的组合来获得完全限定的扩展路径。

由于 cd(Set-Location)不会改变进程当前的工作目录,所以只需将一个相对文件名传递给。NET API 不理解 PowerShell 上下文,可能会产生意想不到的副作用,比如根据初始工作目录(不是你当前的位置)解析路径。

你要做的是首先对你的道路进行限定:

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:\的边缘情况。

接受的答案是一个很大的帮助,但它并没有适当的“正常化”一个绝对的路径太多。找到下面我的导数工作,规范化两个绝对和相对路径。

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;
}

这里评论的权宜部分结合在一起,统一了相对路径和绝对路径:

[System.IO.Directory]::SetCurrentDirectory($pwd)
[IO.Path]::GetFullPath($dapath)

一些例子:

$fps = '.', 'file.txt', '.\file.txt', '..\file.txt', 'c:\somewhere\file.txt'
$fps | % { [IO.Path]::GetFullPath($_) }

产出:

C:\Users\thelonius\tests
C:\Users\thelonius\tests\file.txt
C:\Users\thelonius\tests\file.txt
C:\Users\thelonius\file.txt
c:\somewhere\file.txt

如果路径包含一个限定符(驱动器号) ,那么 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

如果路径存在,并且您不介意返回一个绝对路径,那么您可以使用带有 -Resolve参数的 Join-Path:

Join-Path 'fred\frog' '..\frag' -Resolve

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

  • 它必须支持 Powershell 提供程序。
  • It must work for paths that don't exist in drives that don't exist.
  • 它必须处理“ . .”和“ . .”,这就是规范化路径。
  • 没有外部库,也没有正则表达式。
  • 它不能重新根路径,这意味着相对路径保持相对。

出于以下原因,我列出了这里列出的每种方法的预期结果,如下所示:


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://stackoverflow.com/a/52157943/1964796

$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的代码,而且由于递归的原因,您不能使用正则表达式来完成这项工作。

Sources: Https://gist.github.com/luiz-monad/d5aea290087a89c070da6eec84b33742#file-normalize-path-ps-md