Search code examples
powershellpathpowershell-cmdlet

How to parse filepaths in PowerShell-Functions the correct way?


I have written functions that have path parameters. I'm not sure if I implemented it correctly. Is there a standardized or better way of doing this in Powershell?

function Get-PathExample {
  param(
    [Parameter(Position=0,mandatory=$true,HelpMessage="Profilepath e.g. C:\Users or \\computername\c$\users\")]
    [string]$ProfilePath,

    [Parameter(Position=1,mandatory=$true,HelpMessage="SubPath e.g. AppData\Roaming\")]
    [string]$SubPath
  )

  <#
    code...
  #>
}

Solution

  • As mentioned in the comments, your basic approach is correct - accept path stems as string arguments, then resolve and validate inside the function.

    You can add the most basic level of input validation to the param block itself - like validating that the $ProfilePath only resolves to directories for example:

    param(
      [Parameter(...)]
      # Any path that doesn't exclusively resolve to 1 or more
      # directories will now cause a parameter validation error
      [ValidateScript({ Test-Path -Path $_ -PathType Container })]
      [string]$ProfilePath,
    
      ...
    )
    

    Then inside the function you can perform more domain-specific validation - like testing that the resolved paths are indeed filesystem paths:

    foreach ($resolvedPath in Resolve-Path -Path $ProfilePath) {
      if ($resolvedPath.Provider.Name -ne 'FileSystem') {
        Write-Warning "Resolved non-filesystem item at $($resolvedPath.Path), skipping entry"
        continue
      }
    
      # work with $resolvedPath.Path here (or store it for later)
    }
    

    In the most basic scenarios - where you don't need to care about the paths themselves, but just want to resolve 1 or more provider items from a caller-supplied path - a better option is to mimic the parameter surface of the corresponding provider cmdlet (like Get-Item) and then just offload all the heavy lifting to that command instead.

    To do that, use the following to generate the source code for a new parameter blocks:

    # locate the provider cmdlet we want to mimic
    $targetCommand = Get-Command Get-Item
    
    # create CommandMetadata object from command info
    $commandMetadata = [System.Management.Automation.CommandMetadata]::new($targetCommand)
    
    # generate new proxy param block from Get-Item
    $paramBlock = [System.Management.Automation.ProxyCommand]::GetParamBlock($commandMetadata)
    

    On Windows you can copy the resulting code to your clipboard with $paramBlock |Set-ClipBoard

    The result will look like this:

    
    [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
    [string[]]
    ${Path},
    
    [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string[]]
    ${LiteralPath},
    
    [string]
    ${Filter},
    
    [string[]]
    ${Include},
    
    [string[]]
    ${Exclude},
    
    [switch]
    ${Force},
    
    [Parameter(ValueFromPipelineByPropertyName=$true)]
    [pscredential]
    [System.Management.Automation.CredentialAttribute()]
    ${Credential}
    

    Now manually remove the parameter definitions related to features you don't need, and you might end up with something like:

    [Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
    [string[]]
    ${Path},
    
    [Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
    [Alias('PSPath')]
    [string[]]
    ${LiteralPath},
    
    [switch]
    ${Force}
    

    Replace the contents of your param block with the above and add a [CmdletBinding()] decorator to set the default parameter set to the $Path one:

    [CmdletBinding(DefaultParameterSetName = 'Path')]
    param(
      <# generated parameter definitions from above go here #>
    )
    

    ... at which point you can just pass the caller's parameter arguments off to Get-Item as-is:

    foreach ($item in Get-Item @PSBoundParameters) {
      # work with $item 
    }
    

    Now the caller can supply either wildcard paths or exact paths as they see fit, and Get-Item takes care of the rest