A better way to check if a path exists or not in PowerShell [closed]
Asked Answered
B

8

154

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

Beefcake answered 8/8, 2015 at 1:1 Comment(2)
You could use try{ Test-Path -EA Stop $path; #stuff to do if found } catch { # stuff to do if not found } – Burgundy
Related issue: github.com/PowerShell/PowerShell/issues/1970 – Beefcake
A
161

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
Anthroposophy answered 8/8, 2015 at 16:46 Comment(6)
If $path is "special", like on a Powershell Provider (think HKLM:\SOFTWARE\...) then this will fail miserably. – Burgundy
@Burgundy question specifically asks to check if a file exists or not – Anthroposophy
Definitely, and creating a new cmdlet on the fly is neat. Nearly as unmaintainable as an alias, but still really neat :) – Burgundy
Nice! I think PS should add native support for this. – Beefcake
@Beefcake 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 } – Anthroposophy
@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. – Beefcake
G
55

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.
}
Gastelum answered 8/8, 2015 at 1:24 Comment(2)
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)) { ... }. – Beefcake
@Beefcake 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. – Gastelum
B
16

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) { ... }
Beefcake answered 8/8, 2015 at 1:1 Comment(2)
If you want the PowerShell team to add an "exist" alias, you should submit a feature request through Microsoft Connect – Anthroposophy
Even though I answered it myself, I accept @mathias-r-jessen's answer because it handles parameters better. – Beefcake
A
8

This is my PowerShell newbie way of doing this

if (Test-Path ".\Desktop\checkfile.txt") {
    Write-Host "Yay"
} 
else {
    Write-Host "Damn it"
}
Amargo answered 1/8, 2020 at 17:51 Comment(0)
P
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.

Peterus answered 27/2, 2019 at 14:25 Comment(0)
Z
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)
Zion answered 30/6, 2020 at 20:7 Comment(0)
L
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
    
Larrikin answered 24/4, 2021 at 2:45 Comment(0)
S
0
if (Test-Path C:\DockerVol\SelfCertSSL) {
    write-host "Folder already exists."
} else {
   New-Item -Path "C:\DockerVol\" -Name "SelfCertSSL" -ItemType "directory"
}
Surveying answered 6/11, 2020 at 14:57 Comment(2)
Please add more explanation. – Kerry
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. – Markos

© 2022 - 2024 β€” McMap. All rights reserved.