Passing $args to a script works differently than passing created array - powershell

I call a script: "TestArgs1 xxx -T". From within TestArgs1, I call TestArgs2, trying to pass it the same arguments. If I use: "TestArgs2 #args", switch -T is correctly passed as true. Also if I copy $args to another array and pass it, it works. But if I create my own array first, (in order to modify some arguments), switch -T is passed as false. Why is this? How can I correctly pass the switch argument? See sample code below:
###### TestArgs1
Write-Host "#### pass incoming args ###"
TestArgs2 #args
Write-Host "#### copy incoming args ###"
$a = $args
TestArgs2 #a
Write-Host "#### pass created array ###"
$b = "xxx", "-T"
TestArgs2 #b
###### TestArgs2
function Main {
param ($n, [switch] $t, [switch] $d)
"n = $n"
"t = $t"
}
Main #args
The output of this is the follows:
#### pass incoming args ###
n = xxx
t = True
#### copy incoming args ###
n = xxx
t = True
#### pass created array ###
n = xxx
t = False
When I create my own array and pass the same arguments, t shows up as false.

PowerShell does this because the following two commands behave differently:
Some-Command -Param
Some-Command "-Param"
In the first case, Some-Command is called with a parameter named Param, in the second case Some-Command is called with a positional argument that has the value "-Param".
With a little digging, we can figure out how PowerShell knows the difference.
function foo { $args[0] }
foo -SomeParam | Get-Member -MemberType NoteProperty -Force
After running the above, we see the following output:
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
<CommandParameterName> NoteProperty System.String <CommandParameterName>=SomeParam
We see that PowerShell added a NoteProperty to the value in $args. We can conclude from this that PowerShell is using that NoteProperty when splatting to decide if the value in the array is passed as a value or as a parameter.
So - one solution that I don't recommend - you could add a NoteProperty to your strings that are really parameters. I don't recommend this because it would rely on an undocumented implementation detail.
An alternative solution is to use a function like my foo function to turn a syntactic switch into a value that splats as a parameter. That might look like:
function Get-AsParameter { $args[0] }
$b = "xxx", (Get-AsParameter -T)
TestArgs #b

I ran your script and got the same for all three:
PS C:\> .\TestArgs1.ps1 xxx -T
#### pass incoming args ###
n = xxx
t = False
#### copy incoming args ###
n = xxx
t = False
#### pass created array ###
n = xxx
t = False
Code:
###### TestArgs2
function TestArgs2 {
param ($n, [switch] $t, [switch] $d)
"n = $n"
"t = $t"
}
###### TestArgs1
Write-Host "#### pass incoming args ###"
TestArgs2 #args
Write-Host "#### copy incoming args ###"
$a = $args
TestArgs2 #a
Write-Host "#### pass created array ###"
$b = "xxx", "-T"
TestArgs2 #b

Related

How does $_ get set in a ScriptBlock?

I'm looking to have a function in script where I can use a ScriptBlock passed in as either a predicate or with Where-Object.
I can write
cat .\.gitignore | Where-Object { $_.contains('pp') }
and this works; as does:
$f = { $_.contains('pp') }; cat .gitignore | Where-Object $f
however trying
$f.Invoke( 'apple' )
results in
MethodInvocationException: Exception calling "Invoke" with "1" argument(s): "You cannot call a method on a null-valued expression.
Whereas I expected True. So clearly $_ wasn't set.
Likewise
$ff = { echo "args: $args`nauto: $_" }; $ff.Invoke( 'apple' )
outputs
args: apple
auto:
So $_ is clearly not getting set.
'apple' | %{ $_.contains('pp') }
Works, but I want the scriptblock to be a variable and
$f = { $_.contains('pp') }; 'apple' | %$f
Is a compile error.
tl;dr: So how do I set/pass the value of $_ inside a scriptblock I am invoking?
Note, this answer only covers how does $_ gets populated in the context of a process block of a script block. Other use cases can be found in the about_PSItem documentation.
In the context of a process block of a Script Block, the $_ ($PSItem) variable is automatically populated and represents each element coming from the pipeline, i.e.:
$f = { process { $_.contains('pp') }}
'apple' | & $f # True
You can however achieve the same using InvokeWithContext method from the ScriptBlock Class:
$f = { $_.contains('pp') }
$f.InvokeWithContext($null, [psvariable]::new('_', 'apple')) # True
Do note, this method always returns Collection`1. Output is not enumerated.
Worth noting as zett42 points out, the scoping rules of script blocks invoked via it's methods or via the call operator & still apply.
Script Blocks are able to see parent scope variables (does not include Remoting):
$foo = 'hello'
{ $foo }.Invoke() # hello
But are not able to update them:
$foo = 'hello'
{ $foo = 'world' }.Invoke()
$foo # hello
Unless using a scope a modifier (applies only to Value Types):
$foo = 'hello'
{ $script:foo = 'world' }.Invoke()
$foo # world
Or via the dot sourcing operator .:
$foo = 'hello'
. { $foo = 'world' }
$foo # world
# still applies with pipelines!
$foo = 'hello'
'world' | . { process { $foo = $_ }}
$foo # world
See about Scopes for more details.
Using the .Invoke() method (and its variants, .InvokeReturnAsIs() and .InvokeWithContext()) to execute a script block in PowerShell code is best avoided, because it changes the semantics of the call in several respects - see this answer for more information.
While the PowerShell-idiomatic equivalent is &, the call operator, it is not enough here, given that you want want the automatic $_ variable to be defined in your script block.
The easiest way to define $_ based on input is indeed ForEach-Object (one of whose built-in aliases is %):
$f = { $_.contains('pp') }
ForEach-Object -Process $f -InputObject 'apple' # -> $true
Note, however, that -InputObject only works meaningfully for a single input object (though you may pass an array / collection in which case $_ then refers to it as a whole); to provide multiple ones, use the pipeline:
'apple', 'pear' | ForEach-Object $f # $true, $false
# Equivalent, with alias
'apple', 'pear' | % $f
If, by contrast, your intent is simply for your script block to accept arguments, you don't need $_ at all and can simply make your script either formally declare parameter(s) or use the automatic $args variable which contains all (unbound) positional arguments:
# With $args: $args[0] is the first positional argument.
$f = { $args[0].contains('pp') }
& $f 'apple'
# With declared parameter.
$f = { param([string] $fruit) $fruit.contains('pp') }
& $f 'apple'
For more information about the parameter-declaration syntax, see the conceptual about_Functions help topic (script blocks are basically unnamed functions, and only the param(...) declaration style can be used in script blocks).
I got it to work by wrapping $f in () like
$f = { $_.contains('pp') }; 'apple' | %($f)
...or (thanks to #zett42) by placing a space between the % and $ like
$f = { $_.contains('pp') }; 'apple' | % $f
Can even pass in the value from a variable
$f = { $_.contains('pp') }; $a = 'apple'; $a | %($f)
Or use it inside an If-statement
$f = { $_.contains('pp') }; $a = 'apple'; If ( $a | %($f) ){ echo 'yes' }
So it appears that $_ is only set by having things 'piped' (aka \) into it? But why this is and how it works, and if this can be done through .invoke() is unknown to me. If anyone can explain this please do.
From What does $_ mean in PowerShell? and the related documentation, it seems like $PSItem is indeed a better name since it isn't like Perl's $_

Chaining powershell scripts with varying named parameters

I have several Powershell scripts which I want to chain together. Each script has some named parameters, and some of these parameters can be shared across scripts.
a.ps1
param(
[String]$foo="",
[String]$bar=""
)
& "./b.ps1"
b.ps1
param(
[String]$baz=""
)
& "./c.ps1"
c.ps1
param(
[String]$thing="",
[String]$stuff="",
[String]$baz="",
[String]$foo=""
)
I want to call the first script in this stack with a single list of arguments
powershell ./a.ps1 -foo myfoo -bar mybar -baz mybaz -thing mything -stuff mystuff
and have each script take what it needs from the parameter list, and then pass the entire list on to the next script, all the way down the chain. The number and order of the parameters in each script can change over time, and this shouldn't have to require changing code in the other scripts. Is this possible? I've tried the usual splatting stuff like #args, and that doesn't seem to work (received parameters are all empty). I can combined bound and unbound args with
$allArgs = $PsBoundParameters.Values + $args
but this arranges all parameters in a fixed sequence that requires each script in the chain to follow the parameter list of its caller.
Edit : The purpose of this exercise to completely obscure the logic within the chain from the point where we call it. I simply provide a list of all parameters required by the entire stack, and each unit script can throw an error if a parameter is invalid.
Create a property bag and pass that.
function a ([PSCustomObject] $obj)
{
"foo: $($obj.foo)"
"bar: $($obj.bar)"
b #PSBoundParameters
}
function b ([PSCustomObject] $obj)
{
"baz: $($obj.baz)"
c #PSBoundParameters
}
function c ([PSCustomObject] $obj)
{
"thing: $($obj.thing)"
"stuff: $($obj.stuff)"
"baz: $($obj.baz)"
"foo: $($obj.foo)"
}
$obj = [PSCustomObject]#{
foo = $null
bar = $null
baz = $null
thing = $null
stuff = $null
}
$obj.foo = "1"
$obj.bar = "2"
$obj.baz = "3"
$obj.thing = "4"
$obj.stuff = "5"
a $obj
# output
foo: 1
bar: 2
baz: 3
thing: 4
stuff: 5
baz: 3
foo: 1
However, I don't recommend doing this because it is shortsighted
$obj = [PSCustomObject]#{
foo = $null
bar = $null
baz = $null
thing = $null
stuff = $null
foov2 = $null
barv2 = $null
bazv2 = $null
thingv2 = $null
stuffv2 = $null
foov3 = $null
barv3 = $null
bazv3 = $null
thingv3 = $null
stuffv3 = $null
stuffv3 = $null
}
and dangerous.
function executeCommand ([PSCustomObject] $obj) { "$($obj.myCommand)" }
$obj = [PSCustomObject]#{
myCommand = "harmless"
}
# lab
$obj.myCommand = "format drive"
executeCommand $obj
# production
executeCommand $obj
Instead, explicitly define and call functions. You might choose to save and reuse variables in scripts. You can dot source functions or create a script-based module.
# UtilityFunctions.ps1
function executeCommand ([string] $command, [string] $hostname) { "$command $hostname" }
# lab.ps1
. C:\scripts\UtilityFunctions.ps1
$hostname = "lab"
$command = "format drive"
executeCommand $command $hostname
# production.ps1
. C:\scripts\UtilityFunctions.ps1
$hostname = "production"
$command = "list free space"
executeCommand $command $hostname
Script scope and dot sourcing
How to Write a PowerShell Script Module - PowerShell | Microsoft Docs
Also, your parameters have a default value of an empty string which probably is not your intent.
param(
[String]$foo="",
[String]$bar=""
)
param(
[String]$foo,
[String]$bar
)
default values

Start-Job - Positional order of arguments

I'm trying to understand the correct order of arguments when using Start-Job. What is the correct way to supply parameters to a PowerShell job?
I would expect this to print hello world, but it prints world hello.
Is Param() or -ArgumentList the issue here?
$foo = "hello"
$bar = "world"
$job = Start-Job -ScriptBlock {
Param(
$foo,
$bar
)
Write-Host $foo
Write-Host $bar
} -ArgumentList $bar, $foo
Receive-Job $job
Output:
world
hello
The argument of the parameter -ArgumentList is an array, whose values are passed to parameters defined inside the scriptblock in positional order. You're confused about the result you're getting, because you apparently expected your global variables to be mapped to the parameter names you defined in your scriptblock. That is not how this works.
To maybe illustrate a bit better what is happening in your example let's use distinct variable names in the scriptblock and global scope:
$a = "hello"
$b = "world"
$job = Start-Job -ScriptBlock {
Param(
$c,
$d
)
Write-Host $c
Write-Host $d
} -ArgumentList $b, $a
Essentially, the names of the parameters have nothing to do with the names of the variables in the global scope.
You're switching the values when you're passing $b, $a to the scriptblock instead of $a, $b, hence the value of $b is passed to $c and the value of $a is passed to $d.
Normally one would use splatting for mapping values to specific named parameters. However, that won't work here, since -ArgumentList expects an array of values, not a hashtable. If the difference between positional and named parameters is not clear to you please have a look at the documentation.
What you can do if you want to use the same variable names inside and outside the scriptblock is use the using: scope qualifier instead of passing the variables as arguments:
$a = "hello"
$b = "world"
$job = Start-Job -ScriptBlock {
Write-Host $using:a
Write-Host $using:b
}

Pass an unspecified set of parameters into a function and thru to a cmdlet

Let's say I want to write a helper function that wraps Read-Host. This function will enhance Read-Host by changing the prompt color, calling Read-Host, then changing the color back (simple example for illustrative purposes - not actually trying to solve for this).
Since this is a wrapper around Read-Host, I don't want to repeat the all of the parameters of Read-Host (i.e. Prompt and AsSecureString) in the function header. Is there a way for a function to take an unspecified set of parameters and then pass those parameters directly into a cmdlet call within the function? I'm not sure if Powershell has such a facility.
for example...
function MyFunc( [string] $MyFuncParam1, [int] $MyFuncParam2 , Some Thing Here For Cmdlet Params that I want to pass to Cmdlet )
{
# ...Do some work...
Read-Host Passthru Parameters Here
# ...Do some work...
}
It sounds like you're interested in the 'ValueFromRemainingArguments' parameter attribute. To use it, you'll need to create an advanced function. See the about_Functions_Advanced and about_Functions_Advanced_Parameters help topics for more info.
When you use that attribute, any extra unbound parameters will be assigned to that parameter. I don't think they're usable as-is, though, so I made a little function that will parse them (see below). After parsing them, two variables are returned: one for any unnamed, positional parameters, and one for named parameters. Those two variables can then be splatted to the command you want to run. Here's the helper function that can parse the parameters:
function ParseExtraParameters {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$ParamHashTable = #{}
$UnnamedParams = #()
$CurrentParamName = $null
$ExtraParameters | ForEach-Object -Process {
if ($_ -match "^-") {
# Parameter names start with '-'
if ($CurrentParamName) {
# Have a param name w/o a value; assume it's a switch
# If a value had been found, $CurrentParamName would have
# been nulled out again
$ParamHashTable.$CurrentParamName = $true
}
$CurrentParamName = $_ -replace "^-|:$"
}
else {
# Parameter value
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName += $_
$CurrentParamName = $null
}
else {
$UnnamedParams += $_
}
}
} -End {
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName = $true
}
}
,$UnnamedParams
$ParamHashTable
}
You could use it like this:
PS C:\> ParseExtraParameters -NamedParam1 1,2,3 -switchparam -switchparam2:$false UnnamedParam1
UnnamedParam1
Name Value
---- -----
switchparam True
switchparam2 False
NamedParam1 {1, 2, 3}
Here are two functions that can use the helper function (one is your example):
function MyFunc {
[CmdletBinding()]
param(
[string] $MyFuncParam1,
[int] $MyFuncParam2,
[Parameter(Position=0, ValueFromRemainingArguments=$true)]
$ExtraParameters
)
# ...Do some work...
$UnnamedParams, $NamedParams = ParseExtraParameters #ExtraParameters
Read-Host #UnnamedParams #NamedParams
# ...Do some work...
}
function Invoke-Something {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string] $CommandName,
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$UnnamedParameters, $NamedParameters = ParseExtraParameters #ExtraParameters
&$CommandName #UnnamedParameters #NamedParameters
}
After importing all three functions, try these commands:
MyFunc -MyFuncParam1 Param1Here "PromptText" -assecure
Invoke-Something -CommandName Write-Host -Fore Green "Some text" -Back Red
One word: splatting.
Few more words: you can use combination of $PSBoundParameters and splatting to pass parameters from external command, to internal command (assuming names match). You would need to remove any parameter that you don't want to use though from $PSBoundParameters first:
$PSBoundParameters.Remove('MyFuncParam1')
$PSBoundParameters.Remove('MyFuncParam2')
Read-Host #PSBoundParameters
EDIT
Sample function body:
function Read-Data {
param (
[string]$First,
[string]$Second,
[string]$Prompt,
[switch]$AsSecureString
)
$PSBoundParameters.Remove('First') | Out-Null
$PSBoundParameters.Remove('Second') | Out-Null
$Result = Read-Host #PSBoundParameters
"First: $First Second: $Second Result: $Result"
}
Read-Data -First Test -Prompt This-is-my-prompt-for-read-host

Invoke method on ScriptBlock truncating array

I have the following ScriptBlock defined:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = #('String3','String4')
)
Write-Host "Passed in params:"
foreach($m in $Modules){
Write-Host $m
}
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
As the parameter states in the ScriptBlock, I want to be able to pass in an array of strings. When I call $strSb.Invoke(#('String11','String12')) I receive the following:
Passed in params:
String11
Final set:
String11
String3
String4
What I expect is:
Passed in params:
String11
String12
Final set:
String11
String12
String3
String4
Why is the invoke method truncating my array to the first item entered? And how would I go about fixing it so I can pass in an array of strings?
FWIW: I'm working in v2 and v3.
The problem is that the Invoke method takes an array of arguments (kind of like commands that have an -ArgumentList parameter), so each element in your array is parsed as a separate argument. The first argument, 'String11', is assigned to the first postitional parameter, $Modules, and any subsequent arguments are discarded, since there are no more positional parameters. It doesn't matter that $Modules is declared as a string array; since each element of the argument list is a separate argument, you're setting $Modules to an array of one element.
If you use the , operator to indicate that you're passing in a single array argument, it works as intended:
$strSb.Invoke((,#('String11','String12')))
BTW, you don't really need the #, because a comma-separated list of strings is interpreted as an array by default. Not just in this particular context, but in general. So just use this:
$strSb.Invoke((,('String11','String12')))
To prove out the explanation above, try this scriptblock, which is the same except that a second parameter (creatively named $SecondParameter) is declared, and then displayed after the loop that displays the value of the first parameter:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = #('String3','String4'),
[String]$SecondParameter
)
Write-Host "Passed in params:"
foreach($m in $Modules){
Write-Host $m
}
Write-Host "`nSecondParameter: $SecondParameter`n"
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
If you then pass in the arguments as you were, $strSb.Invoke(#('String11','String12')), you get these results:
11-26-13 19:02:12.55 D:\Scratch\soscratch» $strSb.Invoke(#('String11','String12'))
Passed in params:
String11
SecondParameter: String12
Final set:
String11
String3
String4
11-26-13 19:02:29.34 D:\Scratch\soscratch»
One last tip, not directly related to the question, is that you can compact the foreach loops by using a pipelines, which is are not only more succinct but generally more efficient. Here's a compacted version of your code:
[ScriptBlock]$strSb = {
param(
[Parameter(Mandatory=$false,Position=0)]
[String[]]$Modules = ('String3','String4')
)
Write-Host "Passed in params:"
$Modules | Write-Host
$defaultModules = 'String3','String4'
# Add Default Modules back if not present #
$defaultModules | ?{$Modules -notcontains $_} | %{$Modules += $_}
Write-Host "Final set:"
# Load Dependencies #
$Modules | Write-Host
}
If I understand what you're doing, you want to take 2 arrays, concatenate them, and ensure uniqueness...
First, Since you have a [Parameter...] on your parameter, you magically get [CmdletBinding()] on the method. This means that you are automatically going to get $Modules split into multiple calls.
Second, ScriptBlock.Invoke() takes a params style array and puts them into the method as separate arguments.
The first thing I would try is to add the attribute to gather all values:
[Parameter(ValueFromRemainingArguments=$true, Position=0, Mandatory=$true)]
[String[]]$Modules
However, for the Join, you can much more easily do something like:
($modules + $defaultModules) | Select -Unique
Not sure exactly why, but it doesn't seem to like that named parameter. Seems to like $args, tho :
[ScriptBlock]$strSb = {
$Modules = $args
Write-Host "Passed in params:"
foreach($m in $modules){
Write-Host $m
}
$defaultModules = #('String3','String4')
# Add Default Modules back if not present #
foreach($module in $defaultModules){
if($Modules -notcontains $module){
$Modules += $module
}
}
Write-Host "Final set:"
# Load Dependencies #
foreach($m in $Modules){
Write-Host $m
}
}
$strSb.Invoke('String11','String12')
Passed in params:
String11
String12
Final set:
String11
String12
String3
String4