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
$ProfilePathand$SubPathare meant to form a single continuous path stem - then why not just accept a single[string]$Pathwith the full path? What problem are you trying to solve by accepting it in two parts?Resolving-Pathfor both strings. Then I combine$ProfilePathwith a* wildcardand SubPath usingJoin-Pathinside aGCI-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?Resolve-Pathis the right way, for additional validation you may test theProviderattached to its output to ensure it's a file system path, eg.$resolved = Resolve-Path $path |Where-Object { $_.Provider.Name -eq 'FileSystem'}Provideris very helpful. Thank you!