-1

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...
  #>
}
5
  • 1
    That depends on what you want to use them for. Accepting strings from the caller (as you do here) and then resolving + validating the paths inside the function is usually the correct approach. But if $ProfilePath and $SubPath are meant to form a single continuous path stem - then why not just accept a single [string]$Path with the full path? What problem are you trying to solve by accepting it in two parts? Commented Dec 15, 2023 at 13:11
  • @MathiasR.Jessen e.g. one of my functions does calculate the foldersize of subpaths from multiple userprofiles. I use Resolving-Path for both strings. Then I combine $ProfilePathwith a * wildcard and SubPath using Join-Path inside a GCI-Command. This does work fine. I'm just wondering if that is the correct way. Does Microsoft implement paths in their own cmdlets in a similar way? Commented Dec 15, 2023 at 13:39
  • @MathiasR.Jessen And what is the best way for validating? Check it with a regex-pattern? Commented Dec 15, 2023 at 13:45
  • Resolve-Path is the right way, for additional validation you may test the Provider attached to its output to ensure it's a file system path, eg. $resolved = Resolve-Path $path |Where-Object { $_.Provider.Name -eq 'FileSystem'} Commented Dec 15, 2023 at 13:47
  • @MathiasR.Jessen The hint with Provider is very helpful. Thank you! Commented Dec 15, 2023 at 13:54

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

1 Comment

This is the answer I was looking for. Very detailed. Thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.