I am unable to understand Parameter passing behavior in Powershell.
Say, I have a script callScript.ps1
param($a="DefaultA",$b="DefaultB")
echo $a, $b
Say, another script calls callScript.ps1.
.\callScript.ps1
# outputs DefaultA followed by DefaultB as expected
.\callScript.ps1 -a 2 -b 3
# outputs 2 followed by 3 as expected
$arguments='-a 2 -b 3'
callScript.ps1 $arguments
# I expected output as previous statement but it is as follows.
# -a 2 -b 3
# DefaultB
How can I run a powershell script by constructing command dynamically as above?
Can you please explain why the script the $arguments is interpreted as $a variable in callScript.ps1?
what's happening here is that you're passing a string:
'-a 2 -b 3' as the parameter for $a
you need to specify the values within the param, if you really needed to do it as you have above (there's definitely a better way though) you could do this using Invoke-Expression (short iex)
function doprint {
param( $a,$b )
$a ; $b
}
$arg = '-a "yes" -b "no"'
"doprint $arg" | iex
you could also change your function to take in an array of values like this:
function doprint {
param( [string[]]$a )
$a[0] ; $a[1]
}
$arg = #('yes','no')
doprint $arg
As has already been hinted at, you can't pass a single string as your script is expecting two params - the string is taken as input for param $a, whilst param $b takes the default value.
You can however build a simple hash table containing your arguments, and then use splatting to pass them to the script.
The changes to your code are minimal:
$arguments=#{a="2";b="3"}
callScript.ps1 #arguments
Related
Code:
$text=Get-Content -Path "E:\1.txt"
$text.GetType() | Format-Table -AutoSize
For($i=0; $i -le 5 ;$i++)
{
$var=Write-host '$'text[$i]
$var
}
Actual Result:
$ text[0]
$ text[1]
$ text[2]
$ text[3]
$ text[4]
$ text[5]
I need below Result:
$text[0]
$text[1]
$text[2]
$text[3]
$text[4]
$text[5]
If you must use the quotes, using -separator also works:
$text=Get-Content -Path "E:\1.txt"
$text.GetType() | Format-Table -AutoSize
For($i=0; $i -le 5 ;$i++)
{
$var=Write-host '$'text[$i] -Separator ''
$var
}
Your code fundamentally doesn't do what you intend it to do, due to the mistaken use of Write-Host:
# !! Doesn't capture anything in $var, prints directly to the screen.
$var=Write-host '$'text[$i]
$var # !! is effectively $null and produces no output.
See the bottom section for details.
Instead, what you want is an expandable string (aka interpolating string, "..."-enclosed), with selective `-escaping of the $ character you want to be treated verbatim:
$var= "`$text[$i]" # Expandable string; ` escapes the $ char., $i is expanded
$var
There are other ways to construct the desired string:
$var = '$text[{0}]' -f $i, using -f, the format operator.
$var = '$' + "text[$i]", using string concatenation with +
but the above approach is simplest in your case.
As for what you tried:
Write-Host is typically - and definitely in your case - the wrong tool to use, unless the intent is to write to the display only, bypassing the success output stream and with it the ability to send output to other commands, capture it in a variable, or redirect it to a file. To output a value, use it by itself; e.g., $value instead of Write-Host $value (or use Write-Output $value, though that is rarely needed); see this answer.
What you thought of as a single argument, '$'text[$i], was actually passed as two arguments, verbatim $ and expanded text[$i] (e.g., text[0]) and because Write-Host simply space-concatenates multiple arguments, a space was effectively inserted in the (for-display) output.
That '$'text[$i] becomes two arguments is a perhaps surprising PowerShell idiosyncrasy; unlike in POSIX-compatible shells such as bash, composing a single string argument from a mix of unquoted and (potentially differently) quoted parts only works if the argument starts with an unquoted substring or (mere) variable reference; for instance:
Write-Output foo'bar none' does pass a single argument (passes foobar none), whereas
Write-Output 'bar none'foo does not (passes bar none and foo)
See this answer for more information.
Has anyone written the "PowerShell Gotchas for VBA Coders" guide? I am attempting to teach myself Powershell, I have some experience in VBA. What is the "correct" method of defining and passing parameters to functions.
Here is my test code:
Function Pass-Parameters1
{param( $s1, $s2)
write-host Pass-Parameters1 s1: $s1
Write-Host Pass-Parameters1 s2: $s2
return $s1 + $s2
}
Function Pass-Parameters2($ss1, $ss2){
Write-Host Pass-Parameters2 ss1: $ss1
Write-Host Pass-Parameters2 ss2: $ss2
return $ss1 + $ss2
}
$x = "Hello "
$y = "There!!"
$z = Pass-Parameters1 -s1 $x -s2 $y
$zz = Pass-Parameters2 $x, $y
$zzz = Pass-Parameters2 $x $y
Write-Host 1..Z = $z
write-host 1.ZZ = $zz
Write-Host 1ZZZ = $zzz
Here are the results:
Pass-Parameters1 s1: Hello
Pass-Parameters1 s2: There!!
Pass-Parameters2 ss1: Hello There!!
Pass-Parameters2 ss2:
Pass-Parameters2 ss1: Hello
Pass-Parameters2 ss2: There!!
1..Z = Hello There!!
1.ZZ = Hello There!!
1ZZZ = Hello There!!
Which is the recommended method, example 1 or example 2? I have a lot to learn about Powershell as $zz = Pass-Parameters2 $x, $y did not do what I expected, which is the way I would call the function in VBA. I am assuming $z = Pass-Parameters1 -s1 $x -s2 $y is the recommended method of calling the function as there is no ambiguity.
Any comments or suggestions welcome!
For specifying the parameters to a function I would choose the 2 form because to me 1 is overly verbose/I'm well used to .net languages where it's functionname(argument1,argument2)/the majority of C-like programming languages don't have a separate line inside the function that describes the parameters, but this is personal preference
You can provide a name of an argument, prefixed by hyphen, and provide the arguments in any order:
$z = Pass-Parameters1 -s1 $x -s2 $y
$z = Pass-Parameters1 -s2 $y -s1 $x
You can separate the arguments with spaces and provide the arguments in order:
$zzz = Pass-Parameters2 $x $y
Either of these is correct, and most languages have positional and named arguments. The advantage to using a named argument approach is if you don't want to specify all parameters. There is also the need to consider that power shell developers can force some arguments to be positional and others as named so sometimes you'll need to specify names
For example Write-Host can take an array of things to output as it's first argument and has parameters for what to separate them with as a named argument (third, as written). If you wanted to pass the array but not specify the second arg (as written, which controls new line behavior) at all you need to Write-Host $someArray -separator ":" - it mixes positional and named And needs to be presented thus because of the way positional/named has been specified by (Microsoft), otherwise write will just end up being given more things to output
If you're specifying all arguments, or only need to specify eg the first 3 of a 5 argument function, use whichever is more terse but readable. If you have a (terrible) habit of naming your string variables s1, s2, s3 then calling Add-Person -name $s1 -address $s2 -ssn $s3 keeps things readable. Or use good variable names, and Add-Person $name $address $ssn because it's more terse/redundant to specify parameter names when the variable does a pretty good job of describing the data. Consider using names if you're passing string literals: Add-Person "Markus Crescent" "Lee Street" "12345" - which is the name and which is the address (ok, its a stretch, but consider if these strings are just paths like "file to keep" and "file to delete")
This one turned your X and Y into an array, passed it into the first parameter and passed nothing for the second.. which Write-Host then duly printed the array contents on one line:
$zz = Pass-Parameters2 $x, $y
Your PowerShell code looks very unusual to PowerShell users imho. Here is an example, how I would format your functions:
function Pass-Parameters1 {
Param(
$s1,
$s2
)
Write-Host "Pass-Parameters1 s1: $s1"
Write-Host "Pass-Parameters1 s2: $s2"
return $s1 + $s2
}
function Pass-Parameters2($ss1, $ss2) {
Write-Host "Pass-Parameters2 ss1: $ss1"
Write-Host "Pass-Parameters2 ss2: $ss2"
return $ss1 + $ss2
}
Both versions are valid. In my experience, the first method is more common, because it is more clear, if you have many parameters with type definitions, switches, mandatory tags, default values, constraints, positional info or other additional info. Check out this documentation, to see how complex the definition of even one parameter can be.
You may be used to the syntax like function Pass-Parameters2($ss1, $ss2) from other languages, but as you will call functions in PowerShell like Pass-Parameters2 -ss1 "String1" -ss2 "String2" you won't stick to the also used call syntax from other languages like Pass-Parameters2("String1", "String2") anyway.
Thank you both Caius Jard and Thomas for your input. The vital piece of information I have discovered is Pass-Parameters2 $x, $y passes $x and $y as an array to the first variable in the function, whereas Pass-Parameters2 $x $y passes the two variables.
I need to read a few more basic tutorials on Powershell to learn the finer points of syntax and not think in VBA mode.
The arguments that you pass into the function are added to an array called 'args'. This args[] array can be used as below:
Function TestFunction(){
# access your arguments in the args array
Write-Host args[0]
}
TestFunction $param1 $param2 $param3
Or you can explicitly name the arguments:
Function TestFunction($test1, $test2, $test3){
# access your arguments in the args array
Write-Host $test1
}
TestFunction $param1 $param2 $param3
I am porting a script from bash to PowerShell, and I would like to keep the same support for argument parsing in both. In the bash, one of the possible arguments is -- and I want to also detect that argument in PowerShell. However, nothing I've tried so far has worked. I cannot define it as an argument like param($-) as that causes a compile error. Also, if I decide to completely forego PowerShell argument processing, and just use $args everything appears good, but when I run the function, the -- argument is missing.
Function Test-Function {
Write-Host $args
}
Test-Function -- -args go -here # Prints "-args go -here"
I know about $PSBoundParameters as well, but the value isn't there, because I can't bind a parameter named $-. Are there any other mechanisms here that I can try, or any solution?
For a bit more context, note that me using PowerShell is a side effect. This isn't expected to be used as a normal PowerShell command, I have also written a batch wrapper around this, but the logic of the wrapper is more complex than I wanted to write in batch, so the batch wrapper just calls the PowerShell function, which then does the more complex processing.
I found a way to do so, but instead of double-hyphen you have to pass 3 of them.
This is a simple function, you can change the code as you want:
function Test-Hyphen {
param(
${-}
)
if (${-}) {
write-host "You used triple-hyphen"
} else {
write-host "You didn't use triple-hyphen"
}
}
Sample 1
Test-Hyphen
Output
You didn't use triple-hyphen
Sample 2
Test-Hyphen ---
Output
You used triple-hyphen
As an aside: PowerShell allows a surprising range of variable names, but you have to enclose them in {...} in order for them to be recognized; that is, ${-} technically works, but it doesn't solve your problem.
The challenge is that PowerShell quietly strips -- from the list of arguments - and the only way to preserve that token is you precede it with the PSv3+ stop-parsing symbol, --%, which, however, fundamentally changes how the arguments are passed and is obviously an extra requirement, which is what you're trying to avoid.
Your best bet is to try - suboptimal - workarounds:
Option A: In your batch-file wrapper, translate -- to a special argument that PowerShell does preserve and pass it instead; the PowerShell script will then have to re-translate that special argument to --.
Option B: Perform custom argument parsing in PowerShell:
You can analyze $MyInvocation.Line, which contains the raw command line that invoked your script, and look for the presence of -- there.
Getting this right and making it robust is nontrivial, however.
Here's a reasonably robust approach:
# Don't use `param()` or `$args` - instead, do your own argument parsing:
# Extract the argument list from the invocation command line.
$argList = ($MyInvocation.Line -replace ('^.*' + [regex]::Escape($MyInvocation.InvocationName)) -split '[;|]')[0].Trim()
# Use Invoke-Expression with a Write-Output call to parse the raw argument list,
# performing evaluation and splitting it into an array:
$customArgs = if ($argList) { #(Invoke-Expression "Write-Output -- $argList") } else { #() }
# Print the resulting arguments array for verification:
$i = 0
$customArgs | % { "Arg #$((++$i)): [$_]" }
Note:
There are undoubtedly edge cases where the argument list may not be correctly extracted or where the re-evaluation of the raw arguments causes side effect, but for the majority of cases - especially when called from outside PowerShell - this should do.
While useful here, Invoke-Expression should generally be avoided.
If your script is named foo.ps1 and you invoked it as ./foo.ps1 -- -args go -here, you'd see the following output:
Arg #1: [--]
Arg #2: [-args]
Arg #3: [go]
Arg #4: [-here]
I came up with the following solution, which works well also inside pipelines multi-line expressions. I am using the PowerShell Parser to parse the invocation expression string (while ignoring any incomplete tokens, which might be present at the end of $MyInfocation.Line value) and then Invoke-Expression with Write-Output to get the actual argument values:
# Parse the whole invocation line
$code = [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null)
# Find our invocation expression without redirections
$myline = $code.Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '
# Get the argument values
$command, $arguments = Invoke-Expression ('Write-Output -- ' + $myline)
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
Please be aware that the original values in the function call are reevaluated in Invoke-Expression, so any local variables might shadow values of the actual arguments. Because of that, you can also use this (almost) one-liner at the top of your function, which prevents the pollution of local variables:
# Parse arguments
$command, $arguments = Invoke-Expression ('Write-Output -- ' + ([System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Line.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null).Find({$args[0].CommandElements}, $true).CommandElements | % { $_.ToString() } | Join-String -Separator ' '))
# Fine-tune arguments to be always an array
if ( $arguments -is [string] ) { $arguments = #($arguments) }
if ( $arguments -eq $null ) { $arguments = #() }
I'm trying to figure out how I can get my PowerShell script to allow arbitrary parameters without me having to specify everything I could potentially expect using param.
Searching around I see I can use $args and $myinvocation.mycommand.unboundarguments.
What I would like to do is -
.\myscript.ps1 -prop1 "foo" -prop2 "bar"
The purpose of this is that I plan on using the property to value key pairs for updating my config files.
Is such thing possible?
$Args will capture all inputs which have not been explicitly defined as parameters.
See about_Automatic_Variables for more info.
Illustration of $Args from #Jon-Tirjan's answer,
showing how it skips the parameters which have been declared.
# myscript.ps1
param (
[string] $param1,
[string] $param2
)
Write-Host "param1 = $param1"
Write-Host "param2 = $param2"
$n = 0
foreach ($arg in $args) {
write-host "$n $arg"
$n += 1
}
.\myscript.ps1 -param2 'p2' -foo -bar bim -param1 'p1' -zip zap
param1 = p1
param2 = p2
0 -foo
1 -bar
2 bim
3 -zip
4 zap
This is a nasty issue I am facing. Wont be surprised if it has a simple solution, just that its eluding me.
I have 2 batch files which I have to convert to powershell scripts.
file1.bat
---------
echo %1
echo %2
echo %3
file2.bat %*
file2.bat
--------
echo %1
echo %2
echo %3
On command line, I invoke this as
C:> file1.bat one two three
The output I see is as expected
one
two
three
one
two
three
(This is a crude code sample)
When I convert to Powershell, I have
file1.ps1
---------
Write-Host "args[0] " $args[0]
Write-Host "args[1] " $args[1]
Write-Host "args[2] " $args[2]
. ./file2.ps1 $args
file2.ps1
---------
Write-Host "args[0] " $args[0]
Write-Host "args[1] " $args[1]
Write-Host "args[2] " $args[2]
When I invoke this on powershell command line, I get
$> & file1.ps1 one two three
args[0] one
args[1] two
args[2] three
args[0] one two three
args[1]
args[2]
I understand this is because $args used in file1.ps is a System.Object[] instead of 3 strings.
I need a way to pass the $args received by file1.ps1 to file2.ps1, much the same way that is achieved by %* in .bat files.
I am afraid, the existing manner will break even if its a cross-function call, just the way its a cross-file call in my example.
Have tried a few combinations, but nothing works.
Kindly help. Would much appreciate it.
In PowerShell V2, it's trivial with splatting. bar just becomes:
function bar { foo #args }
Splatting will treat the array members as individual arguments instead of passing it as a single array argument.
In PowerShell V1 it is complicated, there's a way to do it for positional arguments. Given a function foo:
function foo { write-host args0 $args[0] args1 $args[1] args2 $args[2] }
Now call it from bar using the Invoke() method on the scriptblock of the foo function
function bar { $OFS=','; "bar args: $args"; $function:foo.Invoke($args) }
Which looks like
PS (STA) (16) > bar 1 2 3
bar args: 1,2,3
args0 1 args1 2 args2 3
when you use it.
# use the pipe, Luke!
file1.ps1
---------
$args | write-host
$args | .\file2.ps1
file2.ps1
---------
process { write-host $_ }