检查 PowerShell 中是否存在路径的更好方法

在 PowerShell 中是否有一种更简洁、更不容易出错的方法来检查路径是否不存在?

对于这样一个常见的用例来说,这实在是太冗长了:

if (-not (Test-Path $path)) { ... }
if (!(Test-Path $path)) { ... }

它需要太多的括号,并且在检查“不存在”时不是很易读。它也容易出错,因为这样的语句:

if (-not $non_existent_path | Test-Path) { $true } else { $false }

将实际返回 False,当用户可能期望 True

有什么更好的方法吗?

更新1: 我目前的解决方案是为 existnot-exist使用别名,正如解释的 给你

更新2: 一个提议的语法也将修复这个问题,它允许以下语法:

if !(expr) { statements* }
if -not (expr) { statements* }

下面是 PowerShell 存储库中的相关问题(请投票) : https://github.com/PowerShell/PowerShell/issues/1970

416808 次浏览

添加以下别名。我认为默认情况下应该在 PowerShell 中提供这些别名:

function not-exist { -not (Test-Path $args) }
Set-Alias !exist not-exist -Option "Constant, AllScope"
Set-Alias exist Test-Path -Option "Constant, AllScope"

这样,条件语句就会变成:

if (exist $path) { ... }

还有

if (not-exist $path) { ... }
if (!exist $path) { ... }

您发布的别名解决方案很聪明,但是我反对在脚本中使用它,原因与我不喜欢在脚本中使用任何别名的原因相同; 它往往会损害可读性。

如果这是您想要添加到您的配置文件,以便您可以键入快速命令或使用它作为一个 shell,那么我可以看到这是有意义的。

你可以考虑改用管道:

if ($path | Test-Path) { ... }
if (-not ($path | Test-Path)) { ... }
if (!($path | Test-Path)) { ... }

或者,对于否定的方法,如果适合你的代码,你可以使它成为一个肯定的检查,然后使用 else的否定:

if (Test-Path $path) {
throw "File already exists."
} else {
# The thing you really wanted to do.
}

如果只想使用 cmdlet 语法的替代方法,特别是针对文件,请使用 File.Exists().NET 方法:

if(![System.IO.File]::Exists($path)){
# file with path $path doesn't exist
}

另一方面,如果您希望为 Test-Path提供一个通用的否定别名,那么您应该这样做:

# Gather command meta data from the original Cmdlet (in this case, Test-Path)
$TestPathCmd = Get-Command Test-Path
$TestPathCmdMetaData = New-Object System.Management.Automation.CommandMetadata $TestPathCmd


# Use the static ProxyCommand.GetParamBlock method to copy
# Test-Path's param block and CmdletBinding attribute
$Binding = [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($TestPathCmdMetaData)
$Params  = [System.Management.Automation.ProxyCommand]::GetParamBlock($TestPathCmdMetaData)


# Create wrapper for the command that proxies the parameters to Test-Path
# using @PSBoundParameters, and negates any output with -not
$WrappedCommand = {
try { -not (Test-Path @PSBoundParameters) } catch { throw $_ }
}


# define your new function using the details above
$Function:notexists = '{0}param({1}) {2}' -f $Binding,$Params,$WrappedCommand

notexists现在的行为和 Test-Path一样,但总是返回相反的结果:

PS C:\> Test-Path -Path "C:\Windows"
True
PS C:\> notexists -Path "C:\Windows"
False
PS C:\> notexists "C:\Windows" # positional parameter binding exactly like Test-Path
False

正如你已经展示过的那样,相反的情况很简单,就是别名 existsTest-Path:

PS C:\> New-Alias exists Test-Path
PS C:\> exists -Path "C:\Windows"
True

另一个选择是使用 IO.FileInfo,它提供了如此多的文件信息,使得使用这种类型更容易:

PS > mkdir C:\Temp
PS > dir C:\Temp\
PS > [IO.FileInfo] $foo = 'C:\Temp\foo.txt'
PS > $foo.Exists
False
PS > New-TemporaryFile | Move-Item -Destination C:\Temp\foo.txt
PS > $foo.Refresh()
PS > $foo.Exists
True
PS > $foo | Select-Object *




Mode              : -a----
VersionInfo       : File:             C:\Temp\foo.txt
InternalName:
OriginalFilename:
FileVersion:
FileDescription:
Product:
ProductVersion:
Debug:            False
Patched:          False
PreRelease:       False
PrivateBuild:     False
SpecialBuild:     False
Language:


BaseName          : foo
Target            : {}
LinkType          :
Length            : 0
DirectoryName     : C:\Temp
Directory         : C:\Temp
IsReadOnly        : False
FullName          : C:\Temp\foo.txt
Extension         : .txt
Name              : foo.txt
Exists            : True
CreationTime      : 2/27/2019 8:57:33 AM
CreationTimeUtc   : 2/27/2019 1:57:33 PM
LastAccessTime    : 2/27/2019 8:57:33 AM
LastAccessTimeUtc : 2/27/2019 1:57:33 PM
LastWriteTime     : 2/27/2019 8:57:33 AM
LastWriteTimeUtc  : 2/27/2019 1:57:33 PM
Attributes        : Archive

更多细节请看我的博客。

若要检查某个目录是否存在 Path,请使用以下命令:

$pathToDirectory = "c:\program files\blahblah\"
if (![System.IO.Directory]::Exists($pathToDirectory))
{
mkdir $path1
}

要检查文件路径是否存在,请使用 @ Mathias的建议:

[System.IO.File]::Exists($pathToAFile)

这是我的强力新手做这件事的方式

if ((Test-Path ".\Desktop\checkfile.txt") -eq $true) {
Write-Host "Yay"
}
else {
Write-Host "Damn it"
}
if (Test-Path C:\DockerVol\SelfCertSSL) {
write-host "Folder already exists."
} else {
New-Item -Path "C:\DockerVol\" -Name "SelfCertSSL" -ItemType "directory"
}

在看了@Mathias R. Jessen 出色的回答之后,我突然想到,您不需要创建两个新函数。相反,您可以为本机 Test-Path函数创建一个包装器,其名称与添加 -Not开关的名称相同:

$TestPathCmd = Get-Command Test-Path
$TestPathCmdMetaData = New-Object System.Management.Automation.CommandMetadata $TestPathCmd
$Binding = [System.Management.Automation.ProxyCommand]::GetCmdletBindingAttribute($TestPathCmdMetaData)
$Params  = [System.Management.Automation.ProxyCommand]::GetParamBlock($TestPathCmdMetaData)


$Params += ', [Switch]${Not}'
$WrappedCommand = {
$PSBoundParameters.Remove('Not') | Out-Null
[Bool]($Not.ToBool() -bxor (Microsoft.PowerShell.Management\Test-Path @PSBoundParameters))
}


${Function:Test-Path} = '{0} Param({1}) {2}' -f $Binding,$Params,$WrappedCommand

例如:

Test-Path -Path 'C:\Temp'      # True
Test-Path -Path 'C:\Temp' -Not # False
Test-Path -Path 'C:\Txmp'      # False
Test-Path -Path 'C:\Txmp' -Not # True

这有两个好处:

  1. 熟悉的语法: 当您不使用自定义开关时,语法与本机命令相同,当您使用时,发生的事情非常直观,这意味着对用户的认知负担更少,在共享时更兼容。
  2. 因为包装器在底层调用本机函数,所以它将在本机函数所做的任何地方工作,例如:
    Test-Path -Path 'HKLM:\SOFTWARE'      # True
    Test-Path -Path 'HKLM:\SOFTWARE' -Not # False
    Test-Path -Path 'HKLM:\SXFTWARE'      # False
    Test-Path -Path 'HKLM:\SXFTWARE' -Not # True