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

80 Views Asked by At

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...
  #>
}
1

There are 1 best solutions below

1
Mathias R. Jessen On BEST ANSWER

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