Parameter binding by name through pipeline when some values are empty - powershell

In my module, I have two functions a 'Get-Data' and 'Add-Data' I'm having trouble passing information between them through the pipeline.
The following simplified code shows my issue.
function Get-Data{
param ($path)
$out = Import-Csv $path
Write-Output $out
}
The data is the following, notice there are 'gaps' in the data and not every object has all three properties, in fact they cant. You can only have Name and Color or Name and Fruit. (In the real code there are many more properties)
Name,Color,Fruit
Jim,red,
Kate,,Apple
Bob,green,
Abby,,Banana
In the Add-Data function I want to use parameter sets as there are lots of parameters, but only 4 parameter sets possible (two in the simplified code, 'A' and 'B'). This function is exported from the module and I don't want the user to be able to input invalid parameter combinations.
Here is the add-data function:
function Add-Data {
[CmdletBinding()]
Param (
[Parameter(
Position = 1,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Name,
[Parameter(
ParameterSetName = 'A',
Position = 2,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Color,
[Parameter(
ParameterSetName = 'B',
Position = 3,
ValuefromPipelineByPropertyName = $true,
Mandatory = $true
)]
[System.String]$Fruit
)
BEGIN{}
PROCESS{
Write-Output "$Name $Color $Fruit"
}
END{}
}
When I pipe one to the other like below:
Get-Data -Path c:\some.csv | Add-Data
I get the error:
Cannot bind argument to parameter 'Fruit' because it is an empty string
I know that I am passing an empty string to the fruit parameter and it's what is causing me the issue, but it's obviously not the behaviour I want.
I'd like the fact that the property is empty to help resolve the parameter set so it only needs Name and Color.
I can't use [AllowNull()] or [AllowEmptyString()] and remove the parameter sets as I expect users to use 'add-data' from the command line and I want
help add-data
to show them the correct params to enter.
Thanks in advance for any help.

Related

export a string from poweshell 1 to powershell 2

I have a shell script file.
./1.ps1
./2.ps1
I generate a string such as env-0 in 1.ps1 and need to export it to 2.ps1.
UPDATE:
1.ps1 generate many string and I couldn't pass my env-0 as argument to 2.ps1.
Do you have another solution?
PowerShell's stream-like I/O model is a little unusual, but any value not captured is basically output - so to output the string env-0 from 1.ps1, this is literally all you need:
"env-0"
Now that 1.ps1 outputs a string value, we need some way to feed it to 2.ps1.
You can either rely on unbound positional arguments, which will automatically be available in the target script/function via the $args variable:
# 2.ps1
$envString = $args[0]
# work with your `$envString` value here
To invoke:
$envString = ./1.ps1
./2.ps1 $envString
# or
./2.ps1 $(./1.ps1)
You can also accept pipeline input by consuming the $input automatic input enumerator variable:
# 2.ps1
$input |ForEach-Object {
$envString = $_
# work with your `$envString` value here
}
To invoke:
./1.ps1 |./2.ps1
This might be useful if you intend to provide multiple inputs to 2.ps1 in succession:
# This will execute `./1.ps1` 100 times, but `./2.ps1` will only be executed once, and the output continuously fed to it as pipeline input
1..100 |ForEach-Object { ./1.ps1 } | ./2.ps1
Finally, if you want to write scripts for more advanced scenarios (multiple input parameters, input validation, argument completion etc.), you'll want to explicitly declare named parameter(s) with a param() block:
# ./2.ps1
param(
[Parameter(Mandatory)]
[string]$Environment
)
# work with $Environment here
To invoke:
./2.ps1 -Environment $(./1.ps1)
It's worth noting that you can use the [Parameter()] attribute to modify the binding behavior so you still get pipeline support:
# ./2.ps1
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Environment
)
# work with $Environment here
Now that we've added both the Position and a ValueFromPipeline flags, we can pass the string via the pipeline, by name, or positionally all at once:
# These all work now
./2.ps1 -Environment $(./1.ps1)
./2.ps1 $(./1.ps1)
./1.ps1 |./2.ps1
The only caveat is that you can't pipe multiple input values anymore - for that, you'll need to move the script body into a special process block:
# ./2.ps1
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Environment
)
process {
# work with $Environment here
}
The process block will execute exactly once for each pipeline input, so now piping multiple values works again ()along with the previous 3 examples:
1..100 |ForEach-Object { ./1.ps1 } | ./2.ps1
So that's my solution:
# 1.ps1
"env-0"
# 2.ps1
# ./2.ps1
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[string]$Environment
)
process {
# work with $Environment here
}
For more information about advanced parameters and their binding semantics and how to utilise them, see the about_Functions_Advanced_Parameters help topic

Powershell splatting operator only for accepted parameters?

Is it possible, using PowerShell, to use splatting from hashtable when the hashtable contains more entries that the function accepts ?
My use case is to have config objects I pass from one function to another. However, all functions does not require same parameters.
Ex:
function Process-Something{
param(
[Parameter()]
[string]$Owner
)
}
function Process-SomethingElse{
param(
[Parameter()]
[string]$Owner,
[Parameter()]
[int]$x,
[Parameter()]
[int]$y
)
}
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
Process-Something #config
Process-SomethingElse #config
It fails with these error:
Process-Something : Cannot find a matching parameter « Y ».
The idea is to avoid specifying individual properties for each functions.
As #Ansgar is stating in the comments, the whole idea of having defined your parameters, is to get validation. When you are splatting parameters to your function, you are forcing them to the function. So if a given property of your hashtable doesn't exist as a parameter, you will get an error - just like it is intended.
What you can do, is going into a PSCustomObject and utilize the pipe. If you set all you parameters to accept value from the pipeline, using property name (ValueFromPipelineByPropertyName = $true), then you can actually get the desired behavior.
First I'm redefining your different functions, to have the ValueFromPipelineByPropertyName = $true parameter attribute configured.
function Process-Something{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner
)
$PSBoundParameters
}
function Process-SomethingElse{
param(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$Owner,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$x,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[int]$y
)
$PSBoundParameters
}
With that in place, I'm able to create a hashtable like your example, convert it to a PSCustomObject, and now I can pipe that new object to the different methods and have them pick up only the properties that they need.
I included the PSBoundParameters to showcase that they get what they expect.
Testing is done like this:
$config = #{
"Owner" = "Bart Simpson"
"X" = 10
"Y" = 20
}
$psConfig = [PSCustomObject]$config
$psConfig | Process-Something
$psConfig | Process-SomethingElse

Powershell cmdlet parameter definition contain parameter name starts with $ creating change in default be haviour

function Test1 {
[CmdletBinding()]
Param (
[Parameter( Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
[String]
${$count},
[Parameter( Position = 1,ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $false)]
[String]
${$inlinecount}
)
Process {
Write-Host "TEST1 ${$count} : ${$inlinecount}"
}
}
Calling the function as below
PS> Test1 10 20
TEST1 10:20
PS> Test1 -$count 10
TEST1 : 10
PS> Test1 -$inlinecount 100
TEST1 : 100
PS> Test1 -$count 10 -$inlinecount 100
Error Test1 : A positional parameter cannot be found that accepts argument '-'.
I am not able to specify only one value, i.e only $count, it is taking 'Test1 $count 10' with $count as the value for $count, and 10 as the value for $inlinecount. I don't wanted to remove the $ prefixed with each parameter name. Also not able to specify both parameter values by name
How can i change the parameter definition, so that following will results
PS> Test1 -$count 10
TEST1 10 :
PS> Test1 -$inlinecount 100
TEST1 :100
PS> Test1 -$count 10 -$inlinecount 100
TEST1 10:100
I think this might clarify some things for you:
Function Start-Test1 {
[CmdletBinding()]
Param (
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[String[]]$Count,
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName )]
[String]$InlineCount
)
Process {
[PSCustomObject]#{
Count = $Count
InlineCount = $InlineCount
}
Write-Verbose "TEST1 ${$count} : ${$inlinecount}"
}
}
# No parameters, empty result
Start-Test1
# Only one parameter, output Count = 1
Start-Test1 -Count 1
# Only one parameter, output InlineCount = A
Start-Test1 -InlineCount A
# Two parameters, output Count = 1 InlineCount = A
Start-Test1 -Count 1 -InlineCount A
# Multiple input for Count
Start-Test1 -Count 1, 2, 3
The dollar sign is only used once in the Param clause for each parameter. It makes it also easier to read.
Defining positions, with Position = 1 and so on.. is only needed when the parameters are out of order. In this case, they can be omitted. The same goes for Mandatory = $false, when it is omitted, PowerShell assumes by default that the parameter is not mandatory. But when you write Mandatory it assumes you mean that the parameter is mandatory. So no need to use the boolean $true/$false.
In case you want to accept multiple arguments for one parameter, you can use []. As an example [String]$Name accepts only one name, but [String[]]$Name will accept multiple names.
Some tips:
Some remarks, always use a correct function name. You can find the verbs with Get-Verb. These are the allowed verbs you can use.
Try to output an object (array, PSCustomObject, HashTable, ...) whenever you can, not just Write-Host because that's only useful for visualizing output in the console. For this, it's better to use Write-Verbose. You can then run your function with Start-Test -Verbose only in case you want extra information to be displayed on the console.

How do you call a PowerShell function with an Object of Arguments

In PowerShell, one can generally call a function with arguments as follows:
DoRoutineStuff -Action 'HouseKeeping' -Owner 'Adamma George' -Multiples 4 -SkipEmail
To trap these 4 supplied arguments at runtime, one might place this inside the function definition
""
"ARGUMENTS:"
$PSBoundParameters
And the resulting object displayed might look like so:
ARGUMENTS:
Key Value
--- -----
Action HouseKeeping
Owner Adamma George
Multiples 4
SkipEmail True
Now, my question is: If I were to manually build the $MyObject identical to $PSBoundParameters displayed above, is there a way to say:
RunFunction 'DoRoutineStuff' -oArgument $MyObject
Again, if it were to be a script file rather than the function DoRoutineStuff, does that make any difference?
Why might one need to do this?
Picture a situation where you need to catch the arguments supplied to first script or function, using $PSBoundParameters, like so:
DoRoutineStuff{
param(
[string]$Action,
[string]$Owner,
[Int]$Multiples,
[switch]$SkipEmail
)
$Data = $PSBoundParameters
#Update one object property
$Data.Multiples = 1
#Then, recursively call `DoRoutineStuff` using `$Data`
#Other tasks
exit;
}
It sounds like the language feature you're looking for is splatting.
You simply pack you're named parameter arguments into a hashtable, store that in a variable and then pass the variable using # in front of its name:
$myArguments = #{
Action = 'HouseKeeping'
Owner = 'Adamma George'
Multiples = 4
SkipEmail = $true
}
Do-Stuff #myArguments
You can also use this technique to only pass a partial set of parameter arguments (or none at all), great for passing along conditional arguments:
$myArguments = #{}
if($someCondition){
$myArguments['Multiples'] = 1
$myArguments['SkipEmail'] = $true
}
if($somethingElse){
$myArguments['Multiple'] = 4
}
Do-Stuff -Action 'HouseKeeping' -Owner 'Adamma George' #myArguments
You can also reuse $PSBoundParameters for splatting further - very useful for proxy functions:
function Measure-Files
{
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $false)]
[string]$Filter,
[Parameter(Mandatory = $false)]
[switch]$Recurse
)
return (Get-ChildItem #PSBoundParameters |Measure-Object -Property Length).Sum
}

PowerShell Param ValidateSet values with Spaces and Tab completion

First, I do apologize for posting another question concerning PowerShell and tab completion. The StackOverflow system identified several excellent questions with answers concerning this very topic, but they all seemed too cumbersome to implement into this simple New-ADComputer script.
The params are going into a Splat to keep the script readable. The following code correctly tab completes in the ISE, but must be wrapped in double quotes.
Is there any native method in PowerShell to allow for tab completion of Parameter Sets that include spaces?
Param(
[Parameter(Mandatory=$true)]
[string]$Server,
[Parameter(Mandatory=$true)]
[ValidateSet('Env1','Env 2','Env 3')]
[string]$Environment,
[Parameter(Mandatory=$true)]
[ValidateSet('Application','Database','File and Print','Web Server')]
[string]$Type
)
$NewADitems = #{
Name = $server
Path = "OU=$Type,OU=$Environment,OU=Smaller DN string"
Location ='MySite'
Description = "Test Description"
ManagedBy = "Huge Distingushed Name string"
WhatIf = $true
}
Write-Host #NewADitems
Command used and error received:
PS C:\Scripts> .\ADComputer-ParamTest.ps1 -Server ThisTest -Environment Env 3 -Type File and Print
C:\Scripts\ADComputer-ParamTest.ps1 : Cannot validate argument on parameter
'Environment'. The argument "Env" does not belong to the set "Env1,Env 2,Env3"
specified by the ValidateSet attribute. Supply an argument that is in the
set and then try the command again.At line:1 char:58
+ .\ADComputer-ParamTest.ps1 -Server ThisTest -Environment Env 3 -Type File and Pr ...
+ ~~~
Edit: More information. If you leave off the single/double quotes in my example script for the parameter Environment, tab completion will not work for the final parameter Type. Enclosing the 2nd set in quotes will correct this but it's a way to keep watch for this behavior.
No, at least up to Powershell 5.0 April 2015 preview. Tab completion works as you describe. It will still need the quotes around the set to actually work without throwing the error. For what it's worth, it does add the closing quote of the matching type when you start the tab completion with a quote. For example, pressing "f then Tab will complete to "File and Print"(not sure when that was added as a feature).
I tried finding ways to auto-include the quotes as part of the ValidateSet including additional double quotes around the parameter sets and other attempts at escaping quotes. All attempts resulted in tab completion not working in various ways.
Some of the attempts, in case anyone might try that avenue:
[ValidateSet('Env1','"Env 2"','"Env 3"')]
[ValidateSet('Env1',"'Env 2'","'Env 3'")]
[ValidateSet('Env1','`"Env 2`"',"`'Env 3`'")]
[ValidateSet('Env1','\"Env 2\"',"\'Env 3\'")]
This has been entered as a bug since 2013. According to the workarounds listed in Auto-completed parameter values, with spaces, do not have quotes around them, you can update the TabExpansion2 function that Powershell uses for autocompletion. To do so, just run the following code:
function TabExpansion2
{
[CmdletBinding(DefaultParameterSetName = 'ScriptInputSet')]
Param(
[Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 0)]
[string] $inputScript,
[Parameter(ParameterSetName = 'ScriptInputSet', Mandatory = $true, Position = 1)]
[int] $cursorColumn,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 0)]
[System.Management.Automation.Language.Ast] $ast,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 1)]
[System.Management.Automation.Language.Token[]] $tokens,
[Parameter(ParameterSetName = 'AstInputSet', Mandatory = $true, Position = 2)]
[System.Management.Automation.Language.IScriptPosition] $positionOfCursor,
[Parameter(ParameterSetName = 'ScriptInputSet', Position = 2)]
[Parameter(ParameterSetName = 'AstInputSet', Position = 3)]
[Hashtable] $options = $null
)
End
{
if ($psCmdlet.ParameterSetName -eq 'ScriptInputSet')
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
$inputScript,
$cursorColumn,
$options)
}
else
{
$completion = [System.Management.Automation.CommandCompletion]::CompleteInput(
$ast,
$tokens,
$positionOfCursor,
$options)
}
$count = $completion.CompletionMatches.Count
for ($i = 0; $i -lt $count; $i++)
{
$result = $completion.CompletionMatches[$i]
if ($result.CompletionText -match '\s')
{
$completion.CompletionMatches[$i] = New-Object System.Management.Automation.CompletionResult(
"'$($result.CompletionText)'",
$result.ListItemText,
$result.ResultType,
$result.ToolTip
)
}
}
return $completion
}
}
It's worth noting that string insertion works properly for native cmdlets like Get-EventLog -LogName which will properly encase 'Internet Explorer'. Although if you look at the source for Get-EventLog, you'll see that $LogName doesn't actually use ValidateSet so it's intellisense must be provided through another mechanism.
Other Instances:
ValidateSet and tab completion does not work on strings with spaces