Powershell pipe and command line arguments - powershell

I have a Powershell function that takes 3 arguments (two of them optional), parameters for function are defined like this:
Param(
[parameter(Mandatory=$true, ValueFromPipeline=$true)]
[String]
$text,
[parameter(Mandatory=$false)]
[String]
$from = "auto",
[parameter(Mandatory=$false)]
[String]
$to = "lt"
)
If I run the script with 1, 2 or 3 command line parameters - everything works fine. If I run with no parameters, but pass value from pipeline - everything works fine. Working examples:
Foo "hello"
Foo "hola" es
Foo "hola" es lt
"hello" | Foo
I want to be able to pass value from pipeline to the first argument and provide additional command line parameters, but I recieve error: "The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input".
There is one working example, if i provide parameters by name:
"hello" | Foo -from en -to ru
But I don't really like typing the name of parameter all the time. Can this be ommited, is there any shortcut? Not working (wishlist) examples (any will do):
"hello" | Foo en es
"hello" | Foo - en es
"hello" | Foo -- en es

You could make -Text the last positional parameter:
Param(
[Parameter(Position=2, Mandatory=$true, ValueFromPipeline=$true)]
[String]$text,
[Parameter(Position=0, Mandatory=$false)]
[String]$from = "auto",
[Parameter(Position=1, Mandatory=$false)]
[String]$to = "lt"
)
but then you'll have to call the function with named parameters when not reading the manadatory parameter from a pipeline or calling the function with 3 arguments:
Foo -Text 'something'
otherwise you'll be prompted for input:
PS C:\> Foo 'something'
cmdlet Foo at command pipeline position 1
Supply values for the following parameters: _
So, basically you can choose in which scenario you want to provide parameter names:
for the optional parameters when reading from the pipeline, or
for the mandatory parameter when not reading from the pipeline.

Related

Dynamic invoke command with different parameters

In a PowerShell script, I want to read a CSV file that contains something like this:
Type Title Param1 Param2
---- ----- ------ ------
Type1 Foo type 1 ValueForType1
Type2 Foo type 2 ValueForType2
When type is Type1, I have to call a function named New-FooType1, when type is Type2, the funcation is named New-FooType2, and so on:
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
I'm trying to route the call to either of the functions, using a dynamic invocation:
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
However, I always get an error:
Parameter set cannot be resolved using the specified named parameters
As you can see, different type mean different parameters set.
How can I solve this? I would like to avoid manipulating properties manually, because in my real life script, I have a dozen of different types, with up to 6 parameters.
Here is a complete repro sample of the issue:
$csvData = "Type;Title;Param1;Param2`nType1;Foo type 1;ValueForType1;;`nType2;Foo type 2;;ValueForType2"
$csv = ConvertFrom-csv $csvData -Delimiter ';'
$csv | ft -AutoSize
function New-FooType1{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param1
)
Write-Host "New-FooType1 $Title with $Param1"
}
function New-FooType2{
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Title,
[Parameter(Mandatory=$true, ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[string]$Param2
)
Write-Host "New-FooType2 $Title with $Param2"
}
$csv | % {
$cmdName = "New-Foo$($_.Type)"
Invoke-Command (gcm $cmdName) -InputObject $_
}
The expected output of this script is:
New-FooType1 Foo type 1 with ValueForType1
New-FooType2 Foo type 2 with ValueForType2
Use the call operator &:
$CmdName = "New-FooType1"
$Arguments = "type1"
& $CmdName $Arguments
the call operator also supports splatting if you want the arguments bound to specific named parameters:
$Arguments = #{
"title" = "type1"
}
& $CmdName #Arguments
To invoke command by name you should use invoke operator &. Invoke-Command cmdlet support only ScriptBlock and file invocation, and file invocation only supported for remote calls.
For dynamic parameter binding you can use spatting, but in that case you have to convert PSCustomObjects, returned by ConvertFrom-Csv cmdlet, to Hashtable. You also have to strip any extra parameters from Hashtable because splatting will fail if you try to bind non-existing parameter.
Another approach for dynamic parameter binding would be to use binding from pipeline object. It looks like it is what you want to do, since you mark all your parameters with ValueFromPipelineByPropertyName option. And this approach will just ignore any extra property it can not bind to parameter. I recommend you to remove ValueFromPipeline option, because with this option in case of absence of property with parameter name PowerShell will just convert PSCustomObject to string (or to whatever type you use for parameter) and pass it as value for parameter.
So, all you need is to pass object by pipeline and use invoke operator for invocation of command with dynamic name:
$_ | & "New-Foo$($_.Type)"
dont know exactly what your trying to do, but
Invoke-Command (gcm $cmdName) ?
Try invoke-expression $cmdname

Passing function arguments by position: Do explicit values get used before pipeline input?

I'm experimenting with creating a function that can take multiple arguments by position, including pipeline input.
Here's a simple test function:
function TestMultiplePositionalParameters
{
param
(
[Parameter(
Position=0,
Mandatory=$true)
]
[String]$FirstParam,
[Parameter(
Position=1,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$SecondParam
)
begin
{
Write-Host '================================='
}
process
{
Write-Host '$FirstParam:' $FirstParam
Write-Host '$SecondParam:' $SecondParam
Write-Host ''
}
}
When I call it, it works fine:
"Input1","Input2" | TestMultiplePositionalParameters 'ExplicitArgument'
Which results in:
=================================
$FirstParam: ExplicitArgument
$SecondParam: Input1
$FirstParam: ExplicitArgument
$SecondParam: Input2
However, if I change the parameter which takes the value from the pipeline:
function TestMultiplePositionalParameters
{
param
(
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$FirstParam,
[Parameter(
Position=1,
Mandatory=$true)
]
[String]$SecondParam
)
# etc...
}
And again call it:
"Input1","Input2" | TestMultiplePositionalParameters 'ExplicitArgument'
This time I get an error:
TestMultiplePositionalParameters : The input object cannot be bound to any parameters
for the command either because the command does not take pipeline input or the input
and its properties do not match any of the parameters that take pipeline input.
At C:\...\DEMO_Function_Pipeline_MultiPositionalParams.ps1:77 char:21
+ "Input1","Input2" | TestMultiplePositionalParameters 'ExplicitArgument'
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (Input1:String)
[TestMultiplePositionalParameters ], ParameterBindingException
+ FullyQualifiedErrorId : InputObjectNotBound,TestMultiplePositionalParameters
My question is: Why does the second version of the function pass the explicit argument 'ExplicitArgument' to the first parameter? I would have thought that given only the first parameter can accept pipeline input the function should have passed the pipeline input to the first parameter and the explicit argument to the second parameter.
Why does the second version of the function pass the explicit argument
'ExplicitArgument' to the first parameter?
Because $FirstParam is marked as being at position '0' and the first parameter passed to the cmdlet is 'ExplicitArgument'. Essentially, the binding of 'ExplicitArgument' to $FirstParam has already happened by the time PowerShell is looking for a parameter that will accept pipeline input.
To get your expected behaviour, you would have to execute the following:
"Input1","Input2" | TestMultiplePositionalParameters -SecondParam 'ExplicitArgument'
The reason for this is that you can have more than one parameter that accepts its value from the pipeline. If you had the following, what would you expect to happen in your second example?
param
(
[Parameter(
Position=0,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$FirstParam,
[Parameter(
Position=1,
Mandatory=$true,
ValueFromPipeline=$true)
]
[String]$SecondParam
)
There has to be a precedence and it is that named and positional arguments have higher priority than the pipeline arguments.

Powershell parameters string or file

I have a script that i'd like the user to be able to enter a string or use a file(array) that my script can cycle through.
Can this be done with a parameter?
I'd like to be able to do something like this
script.ps1 -file c:\users\joerod\desktop\listofusers.txt
or
script.ps1 -name "john doe"
Sure, but you will need to pick the default parameterset to use when a positional parameter is used since the type of both parameters is a string e.g.:
[CmdletBinding(DefaultParameterSetName="File")]
param(
[Parameter(Position=0, ParameterSetName="File")]
[string]
$File,
[Parameter(Position=0, ParameterSetName="Name")]
[string]
$Name
)
if ($psCmdlet.ParameterSetName -eq "File") {
... handle file case ...
}
else {
... must be name case ...
}
Where the DefaultParameterSetName is essential is when someone specifies this:
myscript.ps1 foo.txt
If the default parametersetname specified, PowerShell can't tell which parameterset should be used since both position 0 parameters are the same type [string]. There is no way to disambiguate which parameter to place the argument in.

Accepting an optional parameter only as named, not positional

I'm writing a PowerShell script that's a wrapper to an .exe. I want to have some optional script params, and pass the rest directly to the exe. Here's a test script:
param (
[Parameter(Mandatory=$False)] [string] $a = "DefaultA"
,[parameter(ValueFromRemainingArguments=$true)][string[]]$ExeParams # must be string[] - otherwise .exe invocation will quote
)
Write-Output ("a=" + ($a) + " ExeParams:") $ExeParams
If I run with the a named param, everything is great:
C:\ > powershell /command \temp\a.ps1 -a A This-should-go-to-exeparams This-also
a=A ExeParams:
This-should-go-to-exeparams
This-also
However, if I try to omit my param, the first unnamed param is assigned to it:
C:\ > powershell /command \temp\a.ps1 This-should-go-to-exeparams This-also
a=This-should-go-to-exeparams ExeParams:
This-also
I would expect:
a=DefaultA ExeParams:
This-should-go-to-exeparams
This-also
I tried adding Position=0 to the param, but that produces the same result.
Is there a way to achieve this?
Maybe a different parameter scheme?
By default, all function parameters are positional. Windows PowerShell assigns position numbers to parameters in the order in which the parameters are declared in the function. To disable this feature, set the value of the PositionalBinding argument of the CmdletBinding attribute to $False.
have a look at How to disable positional parameter binding in PowerShell
function Test-PositionalBinding
{
[CmdletBinding(PositionalBinding=$false)]
param(
$param1,$param2
)
Write-Host param1 is: $param1
Write-Host param2 is: $param2
}
The main answer still works in version 5 (according to comments, it may have been broken for a while in version 2).
There is another option: add Position to the ValueFromRemainingArgs parameter.
Sample CommandWrapper.ps1:
param(
$namedOptional = "default",
[Parameter(ValueFromRemainingArguments = $true, Position=1)]
$cmdArgs
)
write-host "namedOptional: $namedOptional"
& cmd /c echo cmdArgs: #cmdArgs
Sample output:
>commandwrapper hello world
namedOptional: default
cmdArgs: hello world
This appears to follow from PowerShell assigning parameter positions from the first parameter with a Position designated.

How do I provide param attributes for a script in PowerShell?

I have this script that can be called in two ways:
MyScript -foo path\to\folder
or
MyScript -bar path\to\folder
(That is, I can either pass a switch plus a folder or a string argument plus a folder.)
I have tried to put parameter declarations into my script as to reflect that syntax:
param(
[parameter(Mandatory=$false)] [switch]$foo,
[parameter(Mandatory=$false)] [String]$bar,
[parameter(Mandatory=$true)] [System.IO.FileInfo]$path
)
But then I have to pass path explicitly to invoke the script:
MyScript -l -path path\to\folder
So (how) can I do that making both bar and path positional parameters?
Note: If I have picked an extraordinarily stupid syntax for invoking the script, I can still change it.
A couple of things: You need to use parameter sets to tell PowerShell that there are mutually exclusive ways to invoke your script; that is to say, you cannot use the switch and the string at the same time. The sets also serve to allow you to set the position of both $bar and $filepath to be at index 0. Switches don't need to be positionally placed as they are not ambiguous to the binder and be placed anywhere. Also, at least one parameter in each set should be mandatory.
function test-set {
[CmdletBinding(DefaultParameterSetName = "BarSet")]
param(
[parameter(
mandatory=$true,
parametersetname="FooSet"
)]
[switch]$Foo,
[parameter(
mandatory=$true,
position=0,
parametersetname="BarSet"
)]
[string]$Bar,
[parameter(
mandatory=$true,
position=1
)]
[io.fileinfo]$FilePath
)
#"
Parameterset is: {0}
Bar is: '{1}'
-Foo present: {2}
FilePath: {3}
"# -f $PSCmdlet.ParameterSetName, $bar, $foo.IsPresent, $FilePath
}
The CmdletBinding attribute is needed to specify which parameter set should be the default if the function is invoked without parameters.
Here's the syntax help for the above configuration:
PS> test-set -?
NAME
test-set
SYNTAX
test-set [-Bar] <string> [-FilePath] <FileInfo> [<CommonParameters>]
test-set [-FilePath] <FileInfo> -Foo [<CommonParameters>]
And here's the output for various invocations:
PS> test-set barval C:\temp\foo.zip
Parameterset is: BarSet
Bar is: 'barval'
-Foo present: False
FilePath: C:\temp\foo.zip
PS> test-set -foo c:\temp\foo.zip
Parameterset is: FooSet
Bar is: ''
-Foo present: True
FilePath: c:\temp\foo.zip
Hope this helps.
As explained here you can specify the parameter position of your parameter.
[parameter(Position=0)]