Two function parameters getting collapsed into one? - function

I ran into some strange behavior writing a .ps1 script. I wrote a function that takes two parameters, but for some reason the second parameter was always null.
Upon closer inspection, it seems like my two parameters are somehow getting collapsed into the first one.
Given the following script, I would have expected, one line of output showing ...
function Foo($first, $second) {
echo $first
}
$x = "..."
$y = "why?"
Foo($x, $y)
But when I run this script, I get
...
why?
Is there some PowerShell syntax I don't know about that I'm accidentally (mis-)using?

Do not use parens around your arguments and don't use commas to separate arguments. Invoke your functions just like you would any other PowerShell command - using space separated arguments e.g.:
foo $x $y
When you put parens around ($x, $y) PowerShell passes that as a single expression/argument, in this case an array containing two items to the first parameter ($x) of your function. You can use Strict-Mode -Version Latest to warn you when you do this e.g.:
114> function foo($x,$y){}
115> foo(3,4)
116> Set-StrictMode -Version latest
117> foo(3,4)
The function or command was called as if it were a method. Parameters should be separated by
spaces. For information about parameters, see the about_Parameters Help topic.
At line:1 char:1
+ foo(3,4)
+ ~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : StrictModeFunctionCallWithParens

Is there some PowerShell syntax...that I'm accidentally (mis-)using?
Yes, you are not calling your function properly. In PowerShell, function calls do not use parenthesis or have their arguments separated by commas.
Instead, you would call Foo like so:
Foo $x $y
See a demonstration below:
PS > function Foo($first, $second) {
>> echo $first
>> }
>>
PS > $x = "..."
PS > $y = "why?"
PS > Foo $x $y
...
PS >
PS > function Foo($first, $second) {
>> echo "$first and $second"
>> }
>>
PS > Foo $x $y
... and why?
PS >
In case you are wondering, your current code has PowerShell interpreting ($x, $y) as a single argument to Foo: a two-item array. Thus, it assigns this array to $first and $null to $second:
PS > function Foo($first, $second) {
>> echo "Type of `$first: $($first.Gettype())"
>> echo "`$second is `$null: $($second -eq $null)"
>> }
>>
PS > $x = "..."
PS > $y = "why?"
PS > Foo($x, $y)
Type of $first: System.Object[]
$second is $null: True
PS >

The "," is used differently in powershell than in other programming languages. Here it is the array constructor.
(1,2,3).GetType() # System.Object[]
(,'element').GetType() # System.Object[]
Because you havent specified a datatype on your parameters the powershell assumes it is dealing with plain old System.Objects (the superclass of all classes). It then takes the array and assigns it to the first input parameter because it is the only one in this line. It can do that because an array is, by extension, also a System.Object.
Also, the old synthax for defining functions is not recommended anymore:
function foo ( $first, $second ) {}
When writing this, the powershell interpreter would internally convert this into an advanced function:
function foo {
PARAM(
[Parameter(Position=1)]
[object]$first,
[Parameter(Position=2)]
[object]$second
)
BEGIN { <# Do stuff here #> }
PROCESS { <# Do stuff here #> }
END { <# Do stuff here #> }
}
foo -first 'first' -second 'second'
foo 'first' 'second'
Causing unneeded overhead.
I hope that clears it up a little :)

Related

passing a parameter in a function, the second parameter is always null [duplicate]

If I have a function which accepts more than one string parameter, the first parameter seems to get all the data assigned to it, and remaining parameters are passed in as empty.
A quick test script:
Function Test([string]$arg1, [string]$arg2)
{
Write-Host "`$arg1 value: $arg1"
Write-Host "`$arg2 value: $arg2"
}
Test("ABC", "DEF")
The output generated is
$arg1 value: ABC DEF
$arg2 value:
The correct output should be:
$arg1 value: ABC
$arg2 value: DEF
This seems to be consistent between v1 and v2 on multiple machines, so obviously, I'm doing something wrong. Can anyone point out exactly what?
Parameters in calls to functions in PowerShell (all versions) are space-separated, not comma separated. Also, the parentheses are entirely unneccessary and will cause a parse error in PowerShell 2.0 (or later) if Set-StrictMode -Version 2 or higher is active. Parenthesised arguments are used in .NET methods only.
function foo($a, $b, $c) {
"a: $a; b: $b; c: $c"
}
ps> foo 1 2 3
a: 1; b: 2; c: 3
The correct answer has already been provided, but this issue seems prevalent enough to warrant some additional details for those wanting to understand the subtleties.
I would have added this just as a comment, but I wanted to include an illustration--I tore this off my quick reference chart on PowerShell functions. This assumes function f's signature is f($a, $b, $c):
Thus, one can call a function with space-separated positional parameters or order-independent named parameters. The other pitfalls reveal that you need to be cognizant of commas, parentheses, and white space.
For further reading, see my article Down the Rabbit Hole: A Study in PowerShell Pipelines, Functions, and Parameters. The article contains a link to the quick reference/wall chart as well.
There are some good answers here, but I wanted to point out a couple of other things. Function parameters are actually a place where PowerShell shines. For example, you can have either named or positional parameters in advanced functions like so:
function Get-Something
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string] $Name,
[Parameter(Mandatory=$true, Position=1)]
[int] $Id
)
}
Then you could either call it by specifying the parameter name, or you could just use positional parameters, since you explicitly defined them. So either of these would work:
Get-Something -Id 34 -Name "Blah"
Get-Something "Blah" 34
The first example works even though Name is provided second, because we explicitly used the parameter name. The second example works based on position though, so Name would need to be first. When possible, I always try to define positions so both options are available.
PowerShell also has the ability to define parameter sets. It uses this in place of method overloading, and again is quite useful:
function Get-Something
{
[CmdletBinding(DefaultParameterSetName='Name')]
Param
(
[Parameter(Mandatory=$true, Position=0, ParameterSetName='Name')]
[string] $Name,
[Parameter(Mandatory=$true, Position=0, ParameterSetName='Id')]
[int] $Id
)
}
Now the function will either take a name, or an id, but not both. You can use them positionally, or by name. Since they are a different type, PowerShell will figure it out. So all of these would work:
Get-Something "some name"
Get-Something 23
Get-Something -Name "some name"
Get-Something -Id 23
You can also assign additional parameters to the various parameter sets. (That was a pretty basic example obviously.) Inside of the function, you can determine which parameter set was used with the $PsCmdlet.ParameterSetName property. For example:
if($PsCmdlet.ParameterSetName -eq "Name")
{
Write-Host "Doing something with name here"
}
Then, on a related side note, there is also parameter validation in PowerShell. This is one of my favorite PowerShell features, and it makes the code inside your functions very clean. There are numerous validations you can use. A couple of examples are:
function Get-Something
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[ValidatePattern('^Some.*')]
[string] $Name,
[Parameter(Mandatory=$true, Position=1)]
[ValidateRange(10,100)]
[int] $Id
)
}
In the first example, ValidatePattern accepts a regular expression that assures the supplied parameter matches what you're expecting. If it doesn't, an intuitive exception is thrown, telling you exactly what is wrong. So in that example, 'Something' would work fine, but 'Summer' wouldn't pass validation.
ValidateRange ensures that the parameter value is in between the range you expect for an integer. So 10 or 99 would work, but 101 would throw an exception.
Another useful one is ValidateSet, which allows you to explicitly define an array of acceptable values. If something else is entered, an exception will be thrown. There are others as well, but probably the most useful one is ValidateScript. This takes a script block that must evaluate to $true, so the sky is the limit. For example:
function Get-Something
{
Param
(
[Parameter(Mandatory=$true, Position=0)]
[ValidateScript({ Test-Path $_ -PathType 'Leaf' })]
[ValidateScript({ (Get-Item $_ | select -Expand Extension) -eq ".csv" })]
[string] $Path
)
}
In this example, we are assured not only that $Path exists, but that it is a file, (as opposed to a directory) and has a .csv extension. ($_ refers to the parameter, when inside your scriptblock.) You can also pass in much larger, multi-line script blocks if that level is required, or use multiple scriptblocks like I did here. It's extremely useful and makes for nice clean functions and intuitive exceptions.
You call PowerShell functions without the parentheses and without using the comma as a separator. Try using:
test "ABC" "DEF"
In PowerShell the comma (,) is an array operator, e.g.
$a = "one", "two", "three"
It sets $a to an array with three values.
Function Test([string]$arg1, [string]$arg2)
{
Write-Host "`$arg1 value: $arg1"
Write-Host "`$arg2 value: $arg2"
}
Test "ABC" "DEF"
If you're a C# / Java / C++ / Ruby / Python / Pick-A-Language-From-This-Century developer and you want to call your function with commas, because that's what you've always done, then you need something like this:
$myModule = New-Module -ascustomobject {
function test($arg1, $arg2) {
echo "arg1 = $arg1, and arg2 = $arg2"
}
}
Now call:
$myModule.test("ABC", "DEF")
and you'll see
arg1 = ABC, and arg2 = DEF
Because this is a frequent viewed question, I want to mention that a PowerShell function should use approved verbs (Verb-Noun as the function name).
The verb part of the name identifies the action that the cmdlet performs. The noun part of the name identifies the entity on which the action is performed. This rule simplifies the usage of your cmdlets for advanced PowerShell users.
Also, you can specify things like whether the parameter is mandatory and the position of the parameter:
function Test-Script
{
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$true, Position=0)]
[string]$arg1,
[Parameter(Mandatory=$true, Position=1)]
[string]$arg2
)
Write-Host "`$arg1 value: $arg1"
Write-Host "`$arg2 value: $arg2"
}
To pass the parameter to the function you can either use the position:
Test-Script "Hello" "World"
Or you specify the parameter name:
Test-Script -arg1 "Hello" -arg2 "World"
You don't use parentheses like you do when you call a function within C#.
I would recommend to always pass the parameter names when using more than one parameter, since this is more readable.
If you don't know (or care) how many arguments you will be passing to the function, you could also use a very simple approach like;
Code:
function FunctionName()
{
Write-Host $args
}
That would print out all arguments. For example:
FunctionName a b c 1 2 3
Output
a b c 1 2 3
I find this particularly useful when creating functions that use external commands that could have many different (and optional) parameters, but relies on said command to provide feedback on syntax errors, etc.
Here is a another real-world example (creating a function to the tracert command, which I hate having to remember the truncated name);
Code:
Function traceroute
{
Start-Process -FilePath "$env:systemroot\system32\tracert.exe" -ArgumentList $args -NoNewWindow
}
If you try:
PS > Test("ABC", "GHI") ("DEF")
you get:
$arg1 value: ABC GHI
$arg2 value: DEF
So you see that the parentheses separates the parameters
If you try:
PS > $var = "C"
PS > Test ("AB" + $var) "DEF"
you get:
$arg1 value: ABC
$arg2 value: DEF
Now you could find some immediate usefulness of the parentheses - a space will not become a separator for the next parameter - instead you have an eval function.
I don't see it mentioned here, but splatting your arguments is a useful alternative and becomes especially useful if you are building out the arguments to a command dynamically (as opposed to using Invoke-Expression). You can splat with arrays for positional arguments and hashtables for named arguments. Here are some examples:
Note: You can use positional splats with external commands arguments with relative ease, but named splats are less useful with external commands. They work, but the program must accept arguments in the -Key:Value format as each parameter relates to the hashtable key/value pairs. One example of such software is the choco command from the Chocolatey package manager for Windows.
Splat With Arrays (Positional Arguments)
Test-Connection with Positional Arguments
Test-Connection www.google.com localhost
With Array Splatting
$argumentArray = 'www.google.com', 'localhost'
Test-Connection #argumentArray
Note that when splatting, we reference the splatted variable with an # instead of a $. It is the same when using a Hashtable to splat as well.
Splat With Hashtable (Named Arguments)
Test-Connection with Named Arguments
Test-Connection -ComputerName www.google.com -Source localhost
With Hashtable Splatting
$argumentHash = #{
ComputerName = 'www.google.com'
Source = 'localhost'
}
Test-Connection #argumentHash
Splat Positional and Named Arguments Simultaneously
Test-Connection With Both Positional and Named Arguments
Test-Connection www.google.com localhost -Count 1
Splatting Array and Hashtables Together
$argumentHash = #{
Count = 1
}
$argumentArray = 'www.google.com', 'localhost'
Test-Connection #argumentHash #argumentArray
Function Test([string]$arg1, [string]$arg2)
{
Write-Host "`$arg1 value: $arg1"
Write-Host "`$arg2 value: $arg2"
}
Test("ABC") ("DEF")
I don't know what you're doing with the function, but have a look at using the 'param' keyword. It's quite a bit more powerful for passing parameters into a function, and makes it more user friendly. Below is a link to an overly complex article from Microsoft about it. It isn't as complicated as the article makes it sound.
Param Usage
Also, here is an example from a question on this site:
Check it out.
I stated the following earlier:
The common problem is using the singular form $arg, which is incorrect. It should always be plural as $args.
The problem is not that. In fact, $arg can be anything else. The problem was the use of the comma and the parentheses.
I run the following code that worked and the output follows:
Code:
Function Test([string]$var1, [string]$var2)
{
Write-Host "`$var1 value: $var1"
Write-Host "`$var2 value: $var2"
}
Test "ABC" "DEF"
Output:
$var1 value: ABC
$var2 value: DEF
Function Test {
Param([string]$arg1, [string]$arg2)
Write-Host $arg1
Write-Host $arg2
}
This is a proper params declaration.
See about_Functions_Advanced_Parameters.
And it indeed works.
You can pass parameters in a function like this also:
function FunctionName()
{
Param ([string]$ParamName);
# Operations
}

Using Powershell introspection to find the name of a function passed as a parameter?

Say I'm passing a function as a parameter, is there a way to find out the name of the passed function through introspection in powershell? Or do I just have to pass it along with the rest of the parameters?
(without calling the function in question)
The linked question tries to pass a function by name, as a string, in which case the answer is obvious: the argument itself is the function name.
In case a script block is passed instead, you can use the following technique:
function get-ScriptBlockCommandName {
param(
[scriptblock] $ScriptBlock,
[switch] $Expand
)
# Using the script block's AST, extract the first command name / path token.
$commandName = $ScriptBlock.Ast.EndBlock.
Statements[0].PipelineElements.CommandElements[0].Extent.Text
# Expand (interpolate) the raw name, if requested.
if ($Expand) {
$commandName = $ExecutionContext.InvokeCommand.ExpandString($commandName)
}
# Remove outer quoting, if present.
if ($commandName -match '^([''"])(.+)\1$') {
$commandName = $Matches[2]
if ($Matches[1] -eq "'") { $commandName = $commandName -replace "''", "'" }
}
# Output
$commandName
}
The function returns the (first) command name / path that is called from inside the script block.
Caveats:
An error will occur if you pass an expression (e.g., 1 + 2) as the first statement inside the script block.
Only the first command is analyzed (and its command name / path returned), whereas there is no limit to how many statements you can place inside a script block.
By default, if the command name / path is constructed from variables / other commands, these are not expanded (interpolated), given that doing so can result in execution of commands; to opt into expansion, use the -Expand switch.
Example calls:
PS> get-ScriptBlockCommandName { foo -bar baz -more stuff }
foo
This also works with quoted names / paths (note how & must then be used to invoke the command):
PS> get-ScriptBlockCommandName { & '/dir name/foo' -bar baz -more stuff }
/dir name/foo
However, to avoid potentially unwanted execution of commands, the command name / path is returned as-is, with variable references and subexpressions unexpanded.
You can opt to have these expanded by passing -Expand:
PS> get-ScriptBlockCommandName { & "$HOME/scripts/foo.ps1" -bar baz } -Expand
C:/Users/jdoe/scripts.ps1 # e.g.

Passing arguments as strings to a function in a shell script

Calling a function in another script to delete old files - need to pass $1 as string, and not the eval of that arg (filelist from directory)
Have tried:
- single and double quotes around echo $1 ("$1", '$1')
- single and double quotes around arg ("/tmp/AB*", '/tmp/AB*')
Have read 3 similar questions here, but unsuccessful at understanding the issue...
AIX 6
#!/bin/ksh
#### common load function ######
. /tmp/functions.sh
deletefiles /usr/tmp/AB* 1
#!/bin/sh
# Deletes files from a filelist that are older than X days
deletefiles() {
echo $1
echo $2
#filelist=$1
#days=$2
#execute
#`find ${filelist} -type f -mtime +${days} -exec rm {} + 2>&1`
}
It looks like you want to pass /usr/tmp/AB* as is, without expanding it. This can be done with '/usr/tmp/AB*', "/usr/tmp/AB*", or /usr/tmp/AB\*.
Then, to confirm that you got the right value, you need to use "$1" to prevent wildcard expansion in echo:
deletefiles() {
echo "$1"
echo "$2"
}
deletefiles '/usr/tmp/AB*' 1
I guess your main problem is that you want AB* expanded in deletefiles().
When you don't do something special, how do you find the last parameter?
You can expand the wildcard within deletefiles() with eval, but eval can do more than you wanted. Another method is swithing the order of your parameters (days first) end use shift for deleting days from the paramaterlist when you assigned it to a var.
I'll show both solutions.
deletefiles_notsecure() {
filelist="$(eval echo $1)"
days=$2
echo "Filelist: $filelist"
echo "Days: $days"
}
deletefiles_secure() {
days=$1
shift
filelist="$*"
echo "Filelist: $filelist"
echo "Days: $days"
}
# deletefiles /usr/tmp/AB* 1
deletefiles_notsecure "/tmp/*" 1
echo ===========
deletefiles_secure 1 /tmp/*
As you can see, the second form can be used without quotes from the caller, so that will be easier to use.
Note: It will expand the vars during the call, relative to path you are standing in. When deletefiles_secure() starts with cd "${logdir}" and you are standing in your $HOME when you call deletefiles_secure 1 access*.log* an access.log in your homedir will be found. Use full paths on your local computer!
Don't use eval if you can avoid it: find does have a -name option to specify a file-mask, eg:
deletefiles () {
find "$1" -name "$2" ...
}
deletefiles /somedir 'AB*'

What exactly is a PowerShell ScriptBlock

A PowerShell ScriptBlock is not a lexical closure as it does not close over the variables referenced in its declaring environment. Instead it seems to leverage dynamic scope and free variables which are bound at run time in a lambda expression.
function Get-Block {
$b = "PowerShell"
$value = {"Hello $b"}
return $value
}
$block = Get-Block
& $block
# Hello
# PowerShell is not written as it is not defined in the scope
# in which the block was executed.
function foo {
$value = 5
function bar {
return $value
}
return bar
}
foo
# 5
# 5 is written $value existed during the evaluation of the bar function
# it is my understanding that a function is a named scriptblock
# which is also registered to function:
Calling GetNewClosure() on a ScriptBlock returns a new ScriptBlock which closes over the variables referenced. But this is very limited in scope and ability.
What is a ScriptBlock's classification?
Per the docs, a scriptblock is a "precompiled block of script text." So by default you just a pre-parsed block of script, no more, no less. Executing it creates a child scope, but beyond that it's as if you pasted the code inline. So the most appropriate term would simply be "readonly source code."
Calling GetNewClosure bolts on a dynamically generated Module which basically carries a snapshot of all the variables in the caller's scope at the time of calling GetNewClosure. It is not a real closure, simply a snapshot copy of variables. The scriptblock itself is still just source code, and variable binding does not occur until it is invoked. You can add/remove/edit variables in the attached Module as you wish.
function GetSB
{
$funcVar = 'initial copy'
{"FuncVar is $funcVar"}.GetNewClosure()
$funcVar = 'updated value' # no effect, snapshot is taken when GetNewClosure is called
}
$sb = GetSB
& $sb # FuncVar is initial copy
$funcVar = 'outside'
& $sb # FuncVar is initial copy
$sb.Module.SessionState.PSVariable.Remove('funcVar')
& $sb # FuncVar is outside
A PowerShell ScriptBlock is equivalent to a first-class, anonymous function. Most of the confusion I've seen is not with ScriptBlocks, but with the function keyword.
PowerShell does support function closures, however the function keyword does not.
Examples
Function:
PS> function Hello {
>> param ([string] $thing)
>>
>> return ("Hello " + $thing)
>> }
PS> Hello "World"
"Hello World"
ScriptBlock:
PS> $HelloSB = {
>> param ([string] $thing)
>>
>> return ("Hello " + $thing)
>> }
PS> & $HelloSB "World"
"Hello World"
PS> $HelloRef = $HelloSB
PS> & $HelloRef "Universe"
"Hello Universe"
Closure:
PS> $Greeter = {
>> param ([string] $Greeting)
>>
>> return ( {
>> param ([string] $thing)
>>
>> return ($Greeting + " " + $thing)
>> }.GetNewClosure() )
>> }
PS> $Ahoy = (& $Greeter "Ahoy")
PS> & $Ahoy "World"
"Ahoy World"
PS> $Hola = (& $Greeter "Hola")
PS> & $Hola "Mundo"
"Hola Mundo"
Although you can get around the limitation of the function keyword with the "Set-Item" cmdlet:
PS> function Greeter = { ... } # ✕ Error
PS> function Greeter { ... }.GetNewClosure() # ✕ Error
PS> Set-Item -Path "Function:Greeter" -Value $Greeter # (defined above) ✓ OK
PS> $Hola = Greeter "Hola"
PS> & $Hola "Mundo"
"Hola Mundo"
The Value parameter of the "Set-Item" cmdlet can be any ScriptBlock, even one returned by another function. (The "Greeter" function, for example, returns a closure, as shown above.)
PS> Set-Item -Path "Function:Aloha" -Value (Greeter "Aloha")
PS> Aloha "World"
"Aloha World"
Two other important points:
PowerShell uses dynamic scoping, not lexical scoping.
A lexical closure is closed on its source-code environment, whereas a dynamic closure is closed based on the active/dynamic environment that exists when GetNewClosure() is called. (Which is more appropriate for a scripting language.)
PowerShell may have "functions" and "return" statements, but actually its input/output is based on streams and piping. Anything written out of a ScriptBlock with the "Write-Output" or "write" cmdlet will be returned.

Confusing evaluation of $args in PowerShell

The $args variable should, by definition, contain all arguments passed to a script function. However if I construct a pipeline inside my function, the $args variable evaluates to null. Anyone knows why?
See this example:
function test { 1..3 | % { echo "args inside pipeline: $args" } ; echo "args outside pipeline: $args" }
This is the output, when passing parameter "hello":
PS> test hello
args inside pipeline:
args inside pipeline:
args inside pipeline:
args outside pipeline: hello
Is there a specific reason for this? I know how to work around this, however I wonder if anonye out there can explain the reason for this.
Pipes use $input. Try this:
function test { 1..3 | % { echo "args inside pipeline: $input" } ; echo "args outside pipeline: $args" }