149

Is there a more concise and less error-prone way in PowerShell to check if a path DOES NOT exist?

This is objectively too verbose for such a common use case:

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

It needs too many parenthesis and is not very readable when checking for "not exist". It's also error-prone because a statement like:

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

will actually return False, when the user may expect True.

What is a better way to do this?

Update 1: My current solution is to use aliases for exist and not-exist as explained here.

Update 2: A proposed syntax that will also fix this is to allow the following grammar:

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

Here's the related issue in PowerShell repository (please vote up ): https://github.com/PowerShell/PowerShell/issues/1970

orad
  • 15,272
  • 23
  • 77
  • 113
  • 2
    You could use `try{ Test-Path -EA Stop $path; #stuff to do if found } catch { # stuff to do if not found }` – Eris Aug 08 '15 at 17:20
  • 1
    Related issue: https://github.com/PowerShell/PowerShell/issues/1970 – orad Nov 15 '16 at 22:50

8 Answers8

159

If you just want an alternative to the cmdlet syntax, specifically for files, use the File.Exists() .NET method:

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

If, on the other hand, you want a general purpose negated alias for Test-Path, here is how you should do it:

# 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 will now behave exactly like Test-Path, but always return the opposite result:

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

As you've already shown yourself, the opposite is quite easy, just alias exists to Test-Path:

PS C:\> New-Alias exists Test-Path
PS C:\> exists -Path "C:\Windows"
True
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • 1
    If `$path` is "special", like on a Powershell Provider (think HKLM:\SOFTWARE\...) then this will fail miserably. – Eris Aug 08 '15 at 17:15
  • 5
    @Eris question specifically asks to check if a *file* exists or not – Mathias R. Jessen Aug 08 '15 at 18:32
  • 2
    Definitely, and creating a new cmdlet on the fly is neat. Nearly as unmaintainable as an alias, but still really neat :) – Eris Aug 08 '15 at 20:25
  • Nice! I think PS should add native support for this. – orad Aug 08 '15 at 20:59
  • 4
    @orad I seriously doubt you'll get them to do that. "Too many parentheses" is a *very* subjective reasoning and doesn't really merit deviating from the language design/specification. FWIW, I also agree with the if/else construct proposed by @briantist as a better alternative if you really hate parentheses that much: `if(Test-Path $path){}else{ # do your thing }` – Mathias R. Jessen Aug 08 '15 at 21:04
  • @mathias-r-jessen Well, I'm trying anyways. It's a minor but very common thing in PS that needs a little more love IMHO. – orad Aug 08 '15 at 22:07
53

The alias solution you posted is clever, but I would argue against its use in scripts, for the same reason I don't like using any aliases in scripts; it tends to harm readability.

If this is something you want to add to your profile so you can type out quick commands or use it as a shell, then I could see that making sense.

You might consider piping instead:

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

Alternatively, for the negative approach, if appropriate for your code, you can make it a positive check then use else for the negative:

if (Test-Path $path) {
    throw "File already exists."
} else {
   # The thing you really wanted to do.
}
briantist
  • 45,546
  • 6
  • 82
  • 127
  • 1
    I like the piping here, but your proposed checks for negatives are incorrect without parenthesis, or it will always evaluate to `False`. You need to do it like `if (-not ($path | Test-Path)) { ... }`. – orad Aug 08 '15 at 02:34
  • 1
    @orad you're correct! Actually that's a negative of piping in that case. I was lulled into a false sense of security by it not throwing an exception, when it fact it was failing. Calling it the original way throws an exception, making it easier to catch the problem. – briantist Aug 08 '15 at 03:21
15

Add the following aliases. I think these should be made available in PowerShell by default:

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

With that, the conditional statements will change to:

if (exist $path) { ... }

and

if (not-exist $path) { ... }
if (!exist $path) { ... }
dalle
  • 18,057
  • 5
  • 57
  • 81
orad
  • 15,272
  • 23
  • 77
  • 113
  • 4
    If you want the PowerShell team to add an "exist" alias, you should submit a feature request through Microsoft Connect – Mathias R. Jessen Aug 08 '15 at 19:30
  • 2
    Even though I answered it myself, I accept @mathias-r-jessen's [answer](http://stackoverflow.com/a/31896279/450913) because it handles parameters better. – orad Aug 13 '15 at 00:36
6

This is my PowerShell newbie way of doing this

if (Test-Path ".\Desktop\checkfile.txt") {
    Write-Host "Yay"
} 
else {
    Write-Host "Damn it"
}
Community
  • 1
  • 1
David Bohbot
  • 61
  • 2
  • 3
3

Another option is to use IO.FileInfo which gives you so much file info it make life easier just using this type:

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

More details on my blog.

VertigoRay
  • 5,935
  • 6
  • 39
  • 48
3

To check if a Path exists to a directory, use this one:

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

To check if a Path to a file exists use what @Mathias suggested:

[System.IO.File]::Exists($pathToAFile)
shaheen g
  • 691
  • 6
  • 6
2

After looking at @Mathias R. Jessen's excellent answer, it occurred to me that you don't need to create two new functions. Instead, you can create a wrapper around the native Test-Path function with the same name that adds a -Not switch:

$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

E.g.:

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

This has a couple of advantages:

  1. Familiar syntax: when you're not using the custom switch, syntax is identical to the native command, and when you are it's pretty intuitive what's happening, which means less cognitive burden for the user, and more compatibility when sharing.
  2. Because the wrapper is calling the native function under the hood, it will work anywhere the native function does, e.g.:
    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
    
nmbell
  • 451
  • 3
  • 7
0
if (Test-Path C:\DockerVol\SelfCertSSL) {
    write-host "Folder already exists."
} else {
   New-Item -Path "C:\DockerVol\" -Name "SelfCertSSL" -ItemType "directory"
}
fcdt
  • 2,371
  • 5
  • 14
  • 26
Vivek Raj
  • 353
  • 3
  • 3
  • Please add more explanation. – Paul Floyd Nov 07 '20 at 11:01
  • Please don't post only code as answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes. – Pouria Hemi Nov 07 '20 at 13:01