Inconsistent binding when piping DirectoryInfo objects - powershell

I've often used a construct similar to this (using aliases for brevity):
gci -ad | %{$_ | gci}
which works fine. But when trying to help another user on this forum, I found that the following doesn't work:
gci -ad | %{$_.Parent | gci}
throws the following error for each iteration:
gci : Cannot find path 'C:\Users\keith\Documents\Documents' because it does not exist.
At line:1 char:25
+ gci -ad | %{$_.Parent | gci}
+ ~~~
+ CategoryInfo : ObjectNotFound: (C:\Users\keith\Documents\Documents:Stri
ng) [Get-ChildItem], ItemNotFoundException
+ FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemC
ommand
even though:
gci -ad | %{$_.GetType() -eq $_.Parent.GetType()}
produces a scrennfull of True.
I'm pretty sure it has something to do with parameter binding, but would like to understand the apparent inconsistancy....

Easiest way to see what's happening is to emulate the binding with a test function, but the issue is, as you might know in .NET Framework when a DirectoryInfo instance is coerced into a string the result will be it's .Name property value as opposed to .NET where the coercion results in the .FullName property.
Since the object returned by calling the .Parent property does not have the ETS Property .PSPath, the input object will be bound the the Path Parameter and coerced to a string.
Assuming you have both versions of PowerShell, you can try the following to see the difference:
function Test-Binding {
[CmdletBinding(DefaultParameterSetName='Items')]
param(
[Parameter(ParameterSetName='Items', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${Path},
[Parameter(ParameterSetName='LiteralItems', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${LiteralPath}
)
process {
$PSBoundParameters
$PSBoundParameters.Clear()
}
}
(Get-Item .), (Get-Item .).Parent | Test-Binding

Related

Reject parameters not in Param() statement

I have a powershell script that starts with
Param([switch]$NoDownload, [switch]$NoUnpack, [switch]$NoExtract, [switch]$NoImport, [switch]$NoBackup)
and I was very happy because I thought that it would provide automatic parameter validation. Until one day I made a mistake and wrote:
powershell -f myscript.ps1 -NoDowload
(notice the lack of n), and it happily downloaded something I didn't want it to.
How do I tell the powershell parameter handling machinery that the only valid parameters are the ones I explicitly state in the Param statement?
Add a CmdletBinding attribute to the param() block - this makes PowerShell treat your script/function like a cmdlet - rather than a "simple" function - and it will apply much more rigorous parameter binding validation, including throwing errors when you attempt to bind a non-existing parameter name:
[CmdletBinding()]
param(
[switch]$NoDownload,
[switch]$NoUnpack,
[switch]$NoExtract,
[switch]$NoImport,
[switch]$NoBackup
)
PS ~> .\myScript.ps1 -NoDowload
myScript.ps1 : A parameter cannot be found that matches parameter name 'NoDowload'.
At line:1 char:16
+ .\myScript.ps1 -NoDowload
+ ~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [myScript.ps1], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : NamedParameterNotFound,myScript.ps1
Adding an explicit [Parameter()] attribute to any defined parameter will also implicitly make PowerShell treat your script/function as "advanced", even in the absence of a [CmdletBinding()] attribute:
param(
[switch]$NoDownload,
[switch]$NoUnpack,
[switch]$NoExtract,
[switch]$NoImport,
[switch]$NoBackup,
[Parameter(Mandatory = $false, DontShow = $true)]
$DummyParameterThatTriggersCmdletBinding
)

ValidateScript unexpectedly returning false when condition is true

I have a PowerShell function I'm writing to build and execute a variety of logman.exe commands for me so I don't have to reference the provider GUIDs and type up the command each time I want to capture from a different source. One of the parameters is the file name and I am performing some validation on the parameter. Originally I used -match '.+?\.etl$' to check that the file name had the .etl extension and additionally did some validation on the path. I later decided to remove the path validation but neglected to change the validation attribute to ValidatePattern.
What I discovered was that while it worked perfectly on the machine I was using to author and validate it, on my Server 2016 Core machine it seemed to misbehave when calling the function but that if I just ran the same check at the prompt it worked as expected.
The PowerShell:
[Parameter(ParameterSetName="Server", Mandatory=$true)]
[Parameter(ParameterSetName="Client", Mandatory=$true)]
[ValidateScript({$FileName -match '.+?\.etl$'}]
[string] $FileName = $null
The Output:
PS C:\Users\Administrator> Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
PS C:\Users\Administrator> Start-TBLogging : Cannot validate argument on parameter 'FileName'. The "$FileName -match '.+?\.etl$'" validation script
for the argument with value ".\TestLog.etl" did not return a result of True. Determine why the validation script failed,
and then try the command again.
At line:1 char:50
+ Start-TBLogging -ServerLogName HTTPSYS -FileName ".\TestLog.etl"
+ ~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (:) [Start-TBLogging], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,Start-TBLogging
Trying it manually worked:
PS C:\Users\Administrator> $FileName = ".\TestLog.etl"
PS C:\Users\Administrator> $FileName -match '.+?\.etl$'
True
After changing the function to use ValidatePattern it works just fine everywhere but I was wondering if anyone could shed light on the discontinuity.
As Joshua Shearer points out in a comment on a question, you must use automatic variable $_ (or its alias form, $PSItem), not the parameter variable to refer to the argument to validate inside [ValidateScript({ ... })].
Therefore, instead of:
# !! WRONG: The argument at hand has NOT yet been assigned to parameter
# variable $FileName; by design, that assignment
# doesn't happen until AFTER (successful) validation.
[ValidateScript({ $FileName -match '.+?\.etl$' }]
[string] $FileName
use:
# OK: $_ (or $PSItem) represents the argument to validate inside { ... }
[ValidateScript({ $_ -match '.+?\.etl$' })]
[string] $FileName
As briantist points out in another comment on the question, inside the script block $FileName will have the value, if any, from the caller's scope (or its ancestral scopes).

Parameter validateset wildcard

Is it possible to make a Paramater validateset work with a wildcard?
I would want on the * places to accept 0-100.
param
(
[Parameter(Mandatory=$True)]
[validateset("6.1.*.*")]
[string]$variable
)
Error message:
Cannot validate argument on parameter 'variable'. The argument "6.1.1.0" does not belong to the set "6.1.." specified by the ValidateSet attribute. Supply an argument that is in the set and then try the command again.
+ CategoryInfo : InvalidData: (:) [], ParentContainsErrorRecordException
+ FullyQualifiedErrorId : ParameterArgumentValidationError
Since it looks like you're looking to validate a version, you may want to declare the parameter of the type [version] and use the ValidateScript attribute to validate the value rather than using string matching:
function Test-Version {
param(
[ValidateScript({
$_.Major -eq '6' -and
$_.Minor -eq '1' -and
$_.Build -in (0..100) -and
$_.Revision -in (0..100) -or
$(throw 'Wrong Version')
})]
[version]$Version
)
}
No, that's what [ValidatePattern()] is for:
param(
[Parameter(Mandatory=$True)]
[ValidatePattern('6\.1\.\d{1,3}\.\d{1,3}')]
[string]$variable
)
It takes a regular expression as the parameter.
[ValidateSet()] is meant to be used if there is a small, constant set of values. PowerShell also provides autocompletion for these. For example:
[ValidateSet('Windows', 'Mac', 'Linux')
$OperatingSystem
See this article for more parameter validation attributes.

Value for switch parameter from pipeline

I need to pass parameters to a script from pipeline input by importing the required values from CSV file. The original script has more than 15 parameters to be passed and input values are stored in a CSV file. I am posting a simple example to express the issue.
Below are the contents of CSV file (Input.csv)
ResourceGroupName,SetThrottling
TestRG1,1
TestRG2,0
TestRG3,1
TestRG4,0
TestRG5,0
Script file - Switch-FromPipelineTest.ps1
[CmdletBinding()]
Param(
[Parameter(Mandatory=$True, ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
[String]$ResourceGroupName,
[Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
[Switch]$SetThrottling
)
Begin {}
Process {
Function TestingValues {
Param(
$ResourceGroupName,
$SetThrottling
)
Write-Host "$ResourceGroupName is set to $SetThrottling"
}
TestingValues -ResourceGroupName $ResourceGroupName -SetThrottling $SetThrottling
}
End {}
If I run the command Import-Csv .\Input.csv | .\Switch-FromPipelineTest.ps1 it gives an error as given below:
C:\Scripts\Switch-FromPipelineTest.ps1 : Cannot process argument
transformation on parameter 'SetThrottling'. Cannot convert value
"System.String" to type "System.Management.Automation.SwitchParameter".
Boolean parameters accept only Boolean values and numbers, such as
$True, $False, 1 or 0.
At line:1 char:26
+ Import-Csv .\Input.csv | .\Switch-FromPipelineTest.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (#{ResourceGroup...etThrottling=1}:PSObject) [Swith-FromPipelineTest.ps1], ParameterBindingArgumentTransformationException
+ FullyQualifiedErrorId : ParameterArgumentTransformationError,Switch-FromPipelineTest.ps1
In order to make this work, I have to run below command:
Import-Csv -Path .\Input.csv -Verbose |
Select-Object -Property ResourceGroupName,#{n='SetThrottling';e={[bool][int]$_.SetThrottling}} |
.\Switch-FromPipelineTest.ps1
Is there a way we can omit the type casting done by using custom property expression in the second command? As in the original script, I have several [switch] parameters and I need to do the same thing for each [switch] parameter.
Convert the string values to boolean values upon importing the CSV:
Import-Csv 'C:\path\to\input.csv' |
Select-Object -Property *,#{n='SetThrottling';e={
[bool][int]$_.SetThrottling
}} -Exclude SetThrottling | ...
You need to do this for every switch parameter you want to import from a CSV.
If you want to avoid this, change your parameters from switches to set-validated parameters and adjust the parameter evaluation accordingly:
[Parameter(ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
[ValidateSet('0', '1')]
[string]$SetThrottling

In PowerShell, can Test-Path (or something else) be used to validate multiple files when declaring a string array parameter

I have a function that accepts a string array parameter of files and I would like to use Test-Path (or something else) to ensure that all the files in the string array parameter exists. I would like to do this in the parameter declaration if possible.
Is this possible?
You can use ValidateScript
param(
[parameter()]
[ValidateScript({Test-Path $_ })]
[string[]]$paths
)
For more documentation on parameter validation visit about_Functions_Advanced_Parameters
You can set the parameter to use a validation script like this:
Function DoStuff-WithFiles{
Param([Parameter(Mandatory=$true,ValueFromPipeline)]
[ValidateScript({
If(Test-Path $_){$true}else{Throw "Invalid path given: $_"}
})]
[String[]]$FilePath)
Process{
"Valid path: $FilePath"
}
}
It is recommended to not justpass back $true/$false as the function doesn't give good error messages, use a Throw instead, as I did above. Then you can call it as a function, or pipe strings to it, and it will process the ones that pass validation, and throw the error in the Throw statement for the ones that don't pass. For example I will pass a valid path (C:\Temp) and an invalid path (C:\Nope) to the function and you can see the results:
#("c:\temp","C:\Nope")|DoStuff-WithFiles
Valid path: c:\temp
DoStuff-WithFiles : Cannot validate argument on parameter 'FilePath'. Invalid path given: C:\Nope
At line:1 char:24
+ #("c:\temp","C:\Nope")|DoStuff-WithFiles
+ ~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidData: (C:\Nope:String) [DoStuff-WithFiles], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationError,DoStuff-WithFiles
Edit: I partially retract the Throw comment. Evidently it does give descriptive errors when the validation fails now (thank you Paul!). I could have sworn it (at least used to) just gave an error stating that it failed the validation and left off what it was validating and what it was validating against. For more complex validation scripts I would still use Throw though because the user of the script may not know what $_ -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$' means if the error throws that at them (validating IPv4 address).
As #Matt said. Test-Path already accepts pipeline input, so you really just have to send the array directly in:
#($path1, $path2) | Test-Path
Which then returns:
> #("C:\foo", "C:\Windows") | Test-Path
False
True
If you just want to know if ALL of them exist:
($pathArray | Test-Path) -notcontains $false
Which yields:
False