37

I feel like I'm missing something that should be obvious, but I just can't figure out how to do this.

I have a ps1 script that has a function defined in it. It calls the function and then tries using it remotely:

function foo
{
    Param([string]$x)

    Write-Output $x
}

foo "Hi!"

Invoke-Command -ScriptBlock { foo "Bye!" } -ComputerName someserver.example.com -Credential someuser@example.com

This short example script prints "Hi!" and then crashes saying "The term 'foo' is not recognized as the name of a cmdlet, function, script file, or operable program."

I understand that the function is not defined on the remote server because it is not in the ScriptBlock. I could redefine it there, but I'd rather not. I'd like to define the function once and use it either locally or remotely. Is there a good way to do this?

David Hogue
  • 1,791
  • 1
  • 14
  • 23

4 Answers4

42

You need to pass the function itself (not a call to the function in the ScriptBlock).

I had the same need just last week and found this SO discussion

So your code will become:

Invoke-Command -ScriptBlock ${function:foo} -argumentlist "Bye!" -ComputerName someserver.example.com -Credential someuser@example.com

Note that by using this method, you can only pass parameters into your function positionally; you can't make use of named parameters as you could when running the function locally.

Community
  • 1
  • 1
alroc
  • 27,574
  • 6
  • 51
  • 97
  • Okay, that sounds similar to something I'd read. So taking that a step farther: is there a good way to include the function and some additional lines of script that use the function? – David Hogue Jul 06 '12 at 20:53
  • I haven't tested this yet, but if you want to format the output of the function (for example), this should work: `-ScriptBlock {$function:foo|format-table -auto}`. Basically any `ScriptBlock` can be any "chunk" of valid PowerShell code, so as long as you format it properly (or use semicolons at the end of each "line"), you should be good. Earlier today I was mucking around with `measure-command {$x=[xml](get-content file.xml);$x.selectsinglenode("//thing");}, for example. – alroc Jul 06 '12 at 23:36
35

You can pass the definition of the function as a parameter, and then redefine the function on the remote server by creating a scriptblock and then dot-sourcing it:

$fooDef = "function foo { ${function:foo} }"

Invoke-Command -ArgumentList $fooDef -ComputerName someserver.example.com -ScriptBlock {
    Param( $fooDef )

    . ([ScriptBlock]::Create($fooDef))

    Write-Host "You can call the function as often as you like:"
    foo "Bye"
    foo "Adieu!"
}

This eliminates the need to have a duplicate copy of your function. You can also pass more than one function this way, if you're so inclined:

$allFunctionDefs = "function foo { ${function:foo} }; function bar { ${function:bar} }"
JamesQMurphy
  • 4,214
  • 1
  • 36
  • 41
  • 2
    Exactly what I was looking for! Your solution in combination with `$Using:Var` did the trick :) Thank you very much! – DarkLite1 Jan 30 '15 at 14:05
  • It works as long as your function's name does not contain "-" like Get-Foo. I don't find the syntax. – Yann Greder Mar 13 '21 at 16:31
  • 1
    Note that for multiline functions `$fooDef = @'`(function with many lines)`'@` is the way to go. – ndemou Jan 16 '23 at 16:35
6

You can also put the function(s) as well as the script in a file (foo.ps1) and pass that to Invoke-Command using the FilePath parameter:

Invoke-Command –ComputerName server –FilePath .\foo.ps1

The file will be copied to the remote computers and executed.

Keith Hill
  • 194,368
  • 42
  • 353
  • 369
1

Although that's an old question I would like to add my solution.

Funny enough the param list of the scriptblock within function test, does not take an argument of type [scriptblock] and therefor needs conversion.

Function Write-Log 
{
    param(
        [string]$Message
    )

    Write-Host -ForegroundColor Yellow "$($env:computername): $Message"
}

Function Test
{
    $sb = {
        param(
            [String]$FunctionCall
        )

        [Scriptblock]$WriteLog = [Scriptblock]::Create($FunctionCall) 
        $WriteLog.Invoke("There goes my message...")               
    }

    # Get function stack and convert to type scriptblock 
    [scriptblock]$writelog = (Get-Item "Function:Write-Log").ScriptBlock 

    # Invoke command and pass function in scriptblock form as argument 
    Invoke-Command -ComputerName SomeHost -ScriptBlock $sb -ArgumentList $writelog
}

Test

Another posibility is passing a hashtable to our scriptblock containing all the methods that you would like to have available in the remote session:

Function Build-FunctionStack 
{
    param([ref]$dict, [string]$FunctionName)

    ($dict.Value).Add((Get-Item "Function:${FunctionName}").Name, (Get-Item "Function:${FunctionName}").Scriptblock)
}

Function MyFunctionA 
{
    param([string]$SomeValue)

    Write-Host $SomeValue
}

Function MyFunctionB
{
    param([int]$Foo)

    Write-Host $Foo
}

$functionStack = @{}

Build-FunctionStack -dict ([ref]$functionStack) -FunctionName "MyFunctionA"
Build-FunctionStack -dict ([ref]$functionStack) -FunctionName "MyFunctionB" 

Function ExecuteSomethingRemote
{
    $sb = {
        param([Hashtable]$FunctionStack)

        ([Scriptblock]::Create($functionStack["MyFunctionA"])).Invoke("Here goes my message");
        ([Scriptblock]::Create($functionStack["MyFunctionB"])).Invoke(1234);

    }

    Invoke-Command -ComputerName SomeHost -ScriptBlock $sb -ArgumentList $functionStack
}

ExecuteSomethingRemote
Matthias Güntert
  • 4,013
  • 6
  • 41
  • 89