Como normalizar um caminho no PowerShell?

94

Eu tenho dois caminhos:

fred\frog

e

..\frag

Posso juntá-los no PowerShell assim:

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

Isso me dá isso:

fred\frog\..\frag

Mas eu não quero isso. Quero um caminho normalizado sem os pontos duplos, como este:

fred\frag

Como posso conseguir isso?

dan-gph
fonte
1
Frag é uma subpasta de sapo? Do contrário, a combinação do caminho resultaria em fred \ frog \ frag. Se for, então essa é uma questão muito diferente.
EBGreen

Respostas:

81

Você pode usar uma combinação de pwd, Join-Pathe [System.IO.Path]::GetFullPathpara obter um caminho expandido totalmente qualificado.

Como cd( Set-Location) não altera o diretório de trabalho atual do processo, simplesmente passar um nome de arquivo relativo a uma API .NET que não entende o contexto do PowerShell pode ter efeitos colaterais indesejados, como resolver para um caminho baseado no trabalho inicial diretório (não sua localização atual).

O que você faz é primeiro qualificar seu caminho:

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

Isso produz (dada minha localização atual):

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

Com uma base absoluta, é seguro chamar a API .NET GetFullPath:

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

O que fornece o caminho totalmente qualificado e com o ..removido:

C:\WINDOWS\system32\fred\frag

Também não é complicado, pessoalmente, desprezo as soluções que para isso dependem de scripts externos, é um problema simples resolvido de forma bastante adequada por Join-Pathe pwd( GetFullPathserve apenas para torná-lo bonito). Se você quiser manter apenas a parte relativa , basta adicionar .Substring((pwd).Path.Trim('\').Length + 1)e pronto!

fred\frag

ATUALIZAR

Obrigado a @Dangph por apontar o C:\caso extremo.

John Leidegren
fonte
A última etapa não funciona se pwd for "C: \". Nesse caso, recebo "red \ frag".
dan-gph
@Dangph - Não tenho certeza se entendi o que você quis dizer, o acima parece estar funcionando bem? Qual versão do PowerShell você está usando? Estou usando a versão 3.0.
John Leidegren,
1
Quero dizer a última etapa: cd c:\; "C:\fred\frag".Substring((pwd).Path.Length + 1). Não é grande coisa; apenas algo para estar ciente.
dan-gph
Ah, boa pegada, poderíamos corrigir isso adicionando uma chamada de corte. Experimente cd c:\; "C:\fred\frag".Substring((pwd).Path.Trim('\').Length + 1). Mas está ficando meio longo.
John Leidegren
2
Ou simplesmente use: $ ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath (". \ Nonexist \ foo.txt") Funciona com caminhos inexistentes também. "x0n" merece o crédito por isso btw. Como ele observa, ele resolve para PSPaths, não caminhos do sistema de fliles, mas se você estiver usando os caminhos no PowerShell, quem se importa? stackoverflow.com/questions/3038337/…
Joe the Coder,
105

Você pode expandir .. \ frag para seu caminho completo com resolve-path:

PS > resolve-path ..\frag 

Tente normalizar o caminho usando o método combine ():

[io.path]::Combine("fred\frog",(resolve-path ..\frag).path)
Shay Levy
fonte
E se o seu caminho for C:\Windowscontra C:\Windows\ o mesmo caminho, mas com dois resultados diferentes
Joe Phillips
2
Os parâmetros para [io.path]::Combineestão invertidos. Melhor ainda, use o Join-Pathcomando PowerShell nativo : Join-Path (Resolve-Path ..\frag).Path 'fred\frog'Observe também que, pelo menos a partir do PowerShell v3, Resolve-Pathagora oferece suporte à -Relativeopção para resolver para um caminho relativo à pasta atual. Conforme mencionado, Resolve-Pathsó funciona com caminhos existentes, ao contrário [IO.Path]::GetFullPath().
mklement0
25

Você também pode usar Path.GetFullPath , embora (como na resposta de Dan R) isso forneça o caminho completo. O uso seria o seguinte:

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

ou mais interessante

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

ambos produzem o seguinte (assumindo que seu diretório atual seja D: \):

D:\fred\frag

Observe que este método não tenta determinar se fred ou frag realmente existem.

Charlie
fonte
Isso está chegando perto, mas quando tento, recebo "H: \ fred \ frag", embora meu diretório atual seja "C: \ scratch", o que está errado. (Não deveria fazer isso de acordo com o MSDN.) No entanto, isso me deu uma ideia. Vou adicioná-lo como uma resposta.
dan-gph
8
Seu problema é que você precisa definir o diretório atual no .NET. [System.IO.Directory]::SetCurrentDirectory(((Get-Location -PSProvider FileSystem).ProviderPath))
JasonMArcher
2
Apenas para declarar explicitamente [IO.Path]::GetFullPath():, ao contrário do nativo do PowerShell Resolve-Path, funciona com caminhos inexistentes também. Sua desvantagem é a necessidade de sincronizar a pasta de trabalho do .NET com o PS 'primeiro, como @JasonMArcher aponta.
mklement0
Join-Pathcausa uma exceção se estiver se referindo a uma unidade que não existe.
Tahir Hassan
20

A resposta aceita foi de grande ajuda, mas também não "normaliza" adequadamente um caminho absoluto. Encontre abaixo meu trabalho derivado que normaliza os caminhos absolutos e relativos.

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;
}
Sean Hanna
fonte
Dadas as diferentes soluções, esta funciona para todos os diferentes tipos de caminhos. Como um exemplo [IO.Path]::GetFullPath(), não determina corretamente o diretório para um nome de arquivo simples.
Jari Turkia
10

Quaisquer funções de manipulação de caminho não PowerShell (como aquelas em System.IO.Path) não serão confiáveis ​​do PowerShell porque o modelo de provedor do PowerShell permite que o caminho atual do PowerShell seja diferente do que o Windows pensa que é o diretório de trabalho do processo.

Além disso, como você já deve ter descoberto, os cmdlets Resolve-Path e Convert-Path do PowerShell são úteis para converter caminhos relativos (aqueles contendo '..' s) em caminhos absolutos qualificados de unidade, mas eles falham se o caminho referenciado não existir.

O seguinte cmdlet muito simples deve funcionar para caminhos não existentes. Ele converterá 'fred \ frog \ .. \ frag' em 'd: \ fred \ frag' mesmo se um arquivo ou pasta 'fred' ou 'frag' não puder ser encontrado (e a unidade PowerShell atual for 'd:') .

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

    process {
        $Path | ForEach-Object {
            $PSCmdlet.SessionState.Path.GetUnresolvedProviderPathFromPSPath($_)
        }
    }
}
Jason Stangroome
fonte
2
Isso não funciona para caminhos inexistentes onde a letra da unidade não existe, por exemplo, não tenho unidade Q :. Get-AbsolutePath q:\foo\bar\..\bazfalha, embora seja um caminho válido. Bem, dependendo da sua definição de um caminho válido. :-) FWIW, mesmo o embutido Test-Path <path> -IsValidfalha em caminhos enraizados em drives que não existem.
Keith Hill
2
@KeithHill Em outras palavras, o PowerShell considera um caminho em uma raiz inexistente inválido. Acho que é bastante razoável, já que o PowerShell usa a raiz para decidir que tipo de provedor usar ao trabalhar com ele. Por exemplo, HKLM:\SOFTWAREé um caminho válido no PowerShell, referindo-se à SOFTWAREchave na seção de registro da Máquina Local. Mas para descobrir se ele é válido, ele precisa descobrir quais são as regras para caminhos de registro.
jpmc26
3

Esta biblioteca é boa: NDepend.Helpers.FileDirectoryPath .

EDIT: Isto é o que eu inventei:

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

Chame assim:

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

Observe que este trecho requer o caminho para a DLL. Existe um truque que você pode usar para encontrar a pasta que contém o script em execução no momento, mas no meu caso, eu tinha uma variável de ambiente que poderia usar, então acabei de usá-la.

dan-gph
fonte
Não sei por que isso teve um voto negativo. Essa biblioteca é realmente boa para fazer manipulações de caminho. É o que acabei usando no meu projeto.
dan-gph
Menos 2. Ainda confuso. Espero que as pessoas percebam que é fácil usar assemblies .Net do PowerShell.
dan-gph
Esta não parece ser a melhor solução, mas é perfeitamente válida.
JasonMArcher
@Jason, não me lembro dos detalhes, mas na época foi a melhor solução porque foi a única que resolveu meu problema particular. É possível, entretanto, que desde então outra solução melhor tenha surgido.
dan-gph
2
ter uma DLL de terceiros é uma grande desvantagem para esta solução
Louis Kottmann
1

Isso fornece o caminho completo:

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

Isso fornece o caminho relativo ao diretório atual:

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

Por algum motivo, eles só funcionam se fragfor um arquivo, não um directory.

dan-gph
fonte
1
gci é um alias de get-childitem. Os filhos de um diretório são seu conteúdo. Substitua gci por gi e deve funcionar para ambos.
zdan
2
Get-Item funcionou bem. Mas, novamente, essa abordagem requer que as pastas existam.
Peter Lillevold
1

Crie uma função. Esta função normalizará um caminho que não existe em seu sistema e também não adicionará letras de unidade.

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

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

Ex:

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

Agradeço a Oliver Schadlich pela ajuda no RegEx.

M.Hubers
fonte
Observe que isso não funciona para caminhos como somepaththing\.\filename.txt, pois mantém aquele único ponto
Mark Schultheiss
1

Se o caminho incluir um qualificador (letra da unidade), a resposta de x0n para o Powershell: resolver caminho que pode não existir? irá normalizar o caminho. Se o caminho não incluir o qualificador, ele ainda será normalizado, mas retornará o caminho totalmente qualificado relativo ao diretório atual, que pode não ser o que você deseja.

$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
WileCau
fonte
0

Se você precisar se livrar da parte .., você pode usar um objeto System.IO.DirectoryInfo. Use 'fred \ frog .. \ frag' no construtor. A propriedade FullName fornecerá o nome do diretório normalizado.

A única desvantagem é que ele fornecerá o caminho completo (por exemplo, c: \ test \ fred \ frag).

Dan R
fonte
0

As partes convenientes dos comentários aqui combinadas de modo que unificam os caminhos relativos e absolutos:

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

Algumas amostras:

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

resultado:

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
TNT
fonte
-1

Bem, uma maneira seria:

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

Espere, talvez eu tenha entendido mal a pergunta. No seu exemplo, frag é uma subpasta de sapo?

EBGreen
fonte
"frag é uma subpasta de sapo?" Não. O .. significa subir um nível. frag é uma subpasta (ou um arquivo) em fred.
dan-gph