How do I include a locally defined function when using PowerShell's Invoke-Command for remoting?
Asked Answered
B

5

39

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 [email protected]

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?

Bergquist answered 6/7, 2012 at 17:58 Comment(0)
S
44

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 [email protected]

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.

Shrader answered 6/7, 2012 at 20:23 Comment(2)
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?Bergquist
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.Shrader
R
36

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} }"
Rennet answered 30/5, 2014 at 6:44 Comment(3)
Exactly what I was looking for! Your solution in combination with $Using:Var did the trick :) Thank you very much!Antonina
It works as long as your function's name does not contain "-" like Get-Foo. I don't find the syntax.Coppinger
Note that for multiline functions $fooDef = @'(function with many lines)'@ is the way to go.Catgut
D
7

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.

Diplomatic answered 1/7, 2015 at 1:12 Comment(2)
This is the best option. Reduces clutter and is easy. Write the script just as you want it executed and voila!Darceydarci
That is running s local script. Not using locally defined function.Prospective
I
3

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
Inapplicable answered 11/4, 2018 at 6:11 Comment(1)
genius, thank you. Did not realise that you could convert a function into a scriptblockMeister
P
2

I've found yet another solution to this question, using Set-Item Function:

function Foo {
  param(
    [string]$x
  )

  Write-Output $x
}

$FooDef = ${Function:Foo} # or $Function:Foo as function name doesn't contain '-'

Invoke-Command -ScriptBlock {
  Set-Item Function:Foo $Using:FooDef

  Foo "Bye!"
} -ComputerName someserver.example.com -Credential [email protected]
Prospective answered 15/1 at 22:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.