Pass function as a parameter
Asked Answered
S

10

54

I've written function 'A' that will call one of a number of other functions. To save re-writing function 'A', I'd like to pass the function to be called as a parameter of function 'A'. For example:

function A{
    Param($functionToCall)
    Write-Host "I'm calling : $functionToCall"
}

function B{
    Write-Host "Function B"
}

Function C{
    write-host "Function C"
}

A -functionToCall C

Returns: I'm calling: C

I am expecting it to return: I'm calling: Function C.

I've tried various things such as:

Param([scriptblock]$functionToCall)

Cannot convert System.String to ScriptBlock

A -functionToCall $function:C

Returns "Write-Host "Function C"

A - functionToCall (&C)

This evaluates before the rest of it:

 Function C
 I'm Calling :

I'm sure this is programming 101, but I can't work out the correct syntax or what it is I'm doing wrong.

Sedulous answered 2/3, 2014 at 15:37 Comment(0)
T
9

Is this what you need?

function A{
    Param($functionToCall)
    Write-Host "I'm calling : $functionToCall"

    #access the function-object like this.. Ex. get the value of the StartPosition property
    (Get-Item "function:$functionToCall").ScriptBlock.StartPosition

}

function B{
    Write-Host "Function B"
}

Function C{
    write-host "Function C"
}


PS> a -functionToCall c

I'm calling : c


Content     : Function C{
                  write-host "Function C"
              }
Type        : Position
Start       : 307
Length      : 43
StartLine   : 14
StartColumn : 1
EndLine     : 16
EndColumn   : 2
Talkfest answered 2/3, 2014 at 15:48 Comment(7)
No, that is just passing the string "c" to the function, it isn't passing the function object C as a parameter.Lentissimo
question was unclear. see updated answer. Using the scriptblock-property you could also invoke passed on functionTalkfest
Spot on. Thank you very much @Frode F. Much appreciated. So simple when you know how.Sedulous
It is still passing in the name of the function and then looking it up in the function namespace from inside A. That will probably work, but it precludes you just defining the function inline at the point of call.Lentissimo
That's true. There's nothing in the code that verifies that the functionname exists atm. He could add a test to see if the get-item command returns anything, or try your approach(although that isn't a console-friendly to write)Talkfest
My approach isn't actually that unfriendly if you omit the spurious braces I had in it (I've removed them from my answer now).Lentissimo
(Get-Item "function:$functionToCall").ScriptBlock.InvokeWithContext($null,$null) works; or invoke-command (Get-Item "function:$functionToCall").ScriptBlockHunger
L
64

I'm not sure this is the best, but:

function A{
    Param([scriptblock]$FunctionToCall)
    Write-Host "I'm calling $($FunctionToCall.Invoke(4))"
}

function B($x){
    Write-Output "Function B with $x"
}

Function C{
    Param($x)
    Write-Output "Function C with $x"
}

PS C:\WINDOWS\system32> A -FunctionToCall $function:B
I'm calling Function B with 4

PS C:\WINDOWS\system32> A -FunctionToCall $function:C
I'm calling Function C with 4

PS C:\WINDOWS\system32> A -FunctionToCall { Param($x) "Got $x" }
I'm calling Got x
Lentissimo answered 2/3, 2014 at 16:29 Comment(4)
What does "4" represent, as in .Invoke(4)? Thanks.Sedulous
Just demonstrating that you can pass arguments when you call the functionLentissimo
Couldn't you have just wrapped it appropriately to account for the dash? A -functionToCall ${Function:Execute-FunctionWithDash}Karylkarylin
If one were to use the result of the scriptblock one might consider InvokeResultAsIs instead of Invoke, since Invoke seems to return a Collection - even if there's only a single object returned.Macromolecule
G
20

If you really want to pass the name of a function, as a string: use &, the call operator, to invoke it:

function A {
  Param($functionToCall)
  # Note the need to enclose a command embedded in a string in $(...)
  Write-Host "I'm calling: $(& $functionToCall)"
}

Function C {
  "Function C"  # Note: Do NOT use Write-Host to output *data*.
}

A -functionToCall C

As for the need to use $(...) inside "...": see this answer, which explains PowerShell's string-expansion (string-interpolation) rules.

The above yields I'm calling: Function C

Note how function C uses implicit output (same as using Write-Output explicitly) to return a value.
Write-Host is generally the wrong tool to use, unless the intent is explicitly to write to the display only, bypassing PowerShell's output streams.

You generally need the & operator in the following scenarios:

  • To invoke a command by name or path, via a variable reference and/or if the name is single- or double-quoted.

  • To invoke a script block.

Script blocks are the preferred way of passing pieces of code around in PowerShell; the above could be rewritten as (note that the invocation mechanism doesn't change, just the argument being passed):

function A {
  Param($scriptBlockToCall)
  Write-Host "I'm calling: $(& $scriptBlockToCall)"
}

Function C {
  "Function C"  # Note: Do NOT use Write-Host to output *data*.
}

A -scriptBlockToCall { C }

In either scenario, to pass arguments, simply place them after: & <commandNameOrScriptBlock>; note how splatting (@<var>) is used to pass the unbound arguments stored in the automatic $args variable through.

function A {
  Param($commandNameOrScriptBlockToCall)
  Write-Host "I'm calling: $(& $commandNameOrScriptBlockToCall @Args)"
}

Function C {
  "Function C with args: $Args"
}


A -commandNameOrScriptBlockToCall C one two # by name
A -commandNameOrScriptBlockToCall { C @Args } one two # by script block

The above yields I'm calling: Function C with args: one two twice.

Note:

  • As JohnLBevan points out, the automatic $args variable is only available in simple (non-advanced) scripts and functions.

  • The use of a [CmdletBinding()] attribute above the param(...) block and/or a per-parameter [Parameter()] attribute is what makes a script or function an advanced one, and advanced scripts and functions additionally only accept arguments that bind to explicitly declared parameters.

  • If you need to use an advanced script or function - such as to support what-if functionality with [CmdletBinding(SupportsShouldProcess)] - you have the following choices for passing arguments through:

    • If it's sufficient to pass positional (unnamed) arguments through, declare a parameter such as [Parameter(ValueFromRemainingArguments)] $PassThruArgs, which implicitly collects all positional arguments passed on invocation.

    • Otherwise, you must explicitly declare parameters for all potential (named) pass-through arguments.

      • You can scaffold parameter declarations based on an existing command with the help of the PowerShell SDK, a technique used to create proxy (wrapper) functions, as shown in this answer.
    • Alternatively, your function could declare a single parameter that accepts a hashtable representing the named pass-through arguments, to be used with splatting; that, of course, requires the caller to explicitly construct such a hashtable.

Granger answered 5/11, 2018 at 2:2 Comment(0)
K
18

Have you thought about passing a ScriptBlock as a parameter?

$scriptBlock = { Write-Host "This is a script block" }
Function f([ScriptBlock]$s) {
  Write-Host "Invoking ScriptBlock: "
  $s.Invoke()
}

PS C:\> f $scriptBlock
Invoking ScriptBlock:
This is a script block
Kasper answered 24/1, 2017 at 11:11 Comment(0)
T
9

Is this what you need?

function A{
    Param($functionToCall)
    Write-Host "I'm calling : $functionToCall"

    #access the function-object like this.. Ex. get the value of the StartPosition property
    (Get-Item "function:$functionToCall").ScriptBlock.StartPosition

}

function B{
    Write-Host "Function B"
}

Function C{
    write-host "Function C"
}


PS> a -functionToCall c

I'm calling : c


Content     : Function C{
                  write-host "Function C"
              }
Type        : Position
Start       : 307
Length      : 43
StartLine   : 14
StartColumn : 1
EndLine     : 16
EndColumn   : 2
Talkfest answered 2/3, 2014 at 15:48 Comment(7)
No, that is just passing the string "c" to the function, it isn't passing the function object C as a parameter.Lentissimo
question was unclear. see updated answer. Using the scriptblock-property you could also invoke passed on functionTalkfest
Spot on. Thank you very much @Frode F. Much appreciated. So simple when you know how.Sedulous
It is still passing in the name of the function and then looking it up in the function namespace from inside A. That will probably work, but it precludes you just defining the function inline at the point of call.Lentissimo
That's true. There's nothing in the code that verifies that the functionname exists atm. He could add a test to see if the get-item command returns anything, or try your approach(although that isn't a console-friendly to write)Talkfest
My approach isn't actually that unfriendly if you omit the spurious braces I had in it (I've removed them from my answer now).Lentissimo
(Get-Item "function:$functionToCall").ScriptBlock.InvokeWithContext($null,$null) works; or invoke-command (Get-Item "function:$functionToCall").ScriptBlockHunger
A
5

Duncan's solution worked great for me. However I run into some issues when the function name had a dash in it.

I was able to get around it by building off his third example:

function A{
    Param([scriptblock]$functionToCall)
    Write-Host "I'm calling $($functionToCall.Invoke(4))"
}

function Execute-FunctionWithDash($x)
{
    Write-Output "Function Execute-FunctionWithDash with $x"
}

PS C:\WINDOWS\system32> A -functionToCall { Param($x) Execute-FunctionWithDash $x }
I'm calling Function Execute-FunctionWithDash with 4
Antonia answered 26/3, 2015 at 14:19 Comment(3)
This way you're passing $x but not the function which is undesired behavior.Incommodity
Actually the last line is calling function A and passes in a function that takes a parameter $x, calls Execute-FunctionWithDash, and passes parameter $x to it.Antonia
Functions with dashes can be accessed like this: ${Function:Do-SomethingCool}Frail
C
2

for passing along a variable number of named parameters

function L($Lambda){
   write-host "`nI'm calling $Lambda"
   write-host "`nWith parameters"; ft -InputObject $Args
   & $Lambda @Args
}

seems to work well with strange function names

function +Strange-Name($NotUsed,$Named1,$Named2){
   ls -filter $Named1 -Attributes $Named2
}

PS C:\>L +Strange-Name -Named1 *.txt -Named2 Archive

and exe files as well

PS C:\>L grep.exe ".*some text.*" *.txt

although it looks like you still need to watch out for injection

function inject($OrigFunction){
   write-host 'pre-run injection'
   & $OrigFunction @Args
   write-host 'post-run injection'
}

PS C:\>L inject +Strange-Name -Named1 *.txt -Named2 Archive
Cahan answered 13/6, 2018 at 0:4 Comment(0)
L
1
    function strdel($a,$b,$c) {
    return ($a.substring(0,$b)+$(substr $a $c $a.length))
}
function substr($a,$b,$c) {
    return $a.substring($b,($c-$b))
}

$string = "Bark in the woods"
$in = $(substr $(strdel $string 0 5) 0 2)
write-host $in

Where Function 'substr' called function 'strdel' as the $a paramater.

Functions from https://github.com/brandoncomputer/vds

Leuko answered 23/2, 2019 at 17:33 Comment(0)
N
1

How about:

function A{
Param($functionToCall)
    $res = Invoke-Command $functionToCall 
    Write-Host "I'm calling : $res"
}

function B{
    "Function B"
}

Function C{
    "Function C"
}

A -functionToCall ${function:C}

Path the function as value using ${function:...}. Invoke the function and save the results to $res.

Nessie answered 26/7, 2019 at 14:38 Comment(0)
M
0

Anon function/applied function (where arguments are values of some variables)

function MyPrint($x,$y) { Write-Host "$x $y"}
$txt="Hi"; $num=101

# 2-stage definition of anon function (don't ask why...)
$_f = {param($x,$y) MyPrint $x $y}

# "copy" existing vars into new scope
$f  = {& $_f $txt $num}.GetNewClosure()

$f.Invoke()            # "Hi 101"
$txt="Nooo"; $num=777
$f.Invoke()            # Still "Hi 101"

MyButton.Add_Click($f) # Use in callbacks
Melosa answered 23/10, 2023 at 5:53 Comment(0)
S
0

Depending on what you mean by passing as a parameter, if you want to use 'function(a,b)' syntax in powershell while passing in the function then you need to wrap the function .i.e. 'a' as {a}.

Where it is declared as 'function({a},b)' {} are known as script blocks. Inside the called function you can invoke it with '& $passedInFunction'.

Stamina answered 2/12, 2023 at 7:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.