53

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.

Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
woter324
  • 2,608
  • 5
  • 27
  • 47

8 Answers8

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
Clijsters
  • 4,031
  • 1
  • 27
  • 37
Duncan
  • 92,073
  • 11
  • 122
  • 156
  • What does "4" represent, as in .Invoke(4)? Thanks. – woter324 Sep 30 '14 at 00:43
  • 3
    Just demonstrating that you can pass arguments when you call the function – Duncan Oct 02 '14 at 09:54
  • 4
    Couldn't you have just wrapped it appropriately to account for the dash? A -functionToCall ${Function:Execute-FunctionWithDash} – dwarfsoft Mar 08 '17 at 10:56
  • 2
    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. – nitzel Nov 02 '17 at 12:31
17

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
Rustam
  • 448
  • 6
  • 10
17

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
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
Frode F.
  • 52,376
  • 9
  • 98
  • 114
  • 2
    No, that is just passing the string "c" to the function, it isn't passing the function object C as a parameter. – Duncan Mar 02 '14 at 16:21
  • question was unclear. see updated answer. Using the scriptblock-property you could also invoke passed on function – Frode F. Mar 02 '14 at 16:33
  • Spot on. Thank you very much @Frode F. Much appreciated. So simple when you know how. – woter324 Mar 02 '14 at 16:39
  • 1
    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. – Duncan Mar 02 '14 at 16:41
  • 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) – Frode F. Mar 02 '14 at 16:49
  • 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). – Duncan Mar 02 '14 at 18:39
  • 1
    `(Get-Item "function:$functionToCall").ScriptBlock.InvokeWithContext($null,$null)` works; or `invoke-command (Get-Item "function:$functionToCall").ScriptBlock` – JohnLBevan Apr 08 '16 at 15:08
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
Derek
  • 967
  • 1
  • 9
  • 16
  • This way you're passing $x but not the function which is undesired behavior. – f0rt Nov 25 '16 at 12:49
  • 1
    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. – Derek Nov 28 '16 at 03:04
  • 4
    Functions with dashes can be accessed like this: `${Function:Do-SomethingCool}` – Jan Hohenheim Jun 12 '18 at 09:08
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
Gregor y
  • 1,762
  • 15
  • 22
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

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.

Tomer
  • 1,594
  • 14
  • 15