27

How do I Start a job of a function i just defined?

function FOO { write-host "HEY" }

Start-Job -ScriptBlock { FOO } |
  Receive-Job -Wait -AutoRemoveJob

Result:

Receive-Job: The term 'FOO' is not recognized as the name of cmdlet,
function ,script file or operable program.

What do I do? Thanks.

mklement0
  • 382,024
  • 64
  • 607
  • 775
Alex58
  • 1,121
  • 2
  • 10
  • 8

9 Answers9

37

As @Shay points out, FOO needs to be defined for the job. Another way to do this is to use the -InitializationScript parameter to prepare the session.

For your example:

$functions = {
    function FOO { write-host "HEY" }
}

Start-Job -InitializationScript $functions -ScriptBlock {FOO}|
    Wait-Job| Receive-Job

This can be useful if you want to use the same functions for different jobs.

Rynant
  • 23,153
  • 5
  • 57
  • 71
13

@Rynant's suggestion of InitializationScript is great

I thought the purpose of (script) blocks is so that you can pass them around. So depending on how you are doing it, I would say go for:

$FOO = {write-host "HEY"}

Start-Job -ScriptBlock $FOO | wait-job |Receive-Job

Of course you can parameterize script blocks as well:

$foo = {param($bar) write-host $bar}

Start-Job -ScriptBlock $foo -ArgumentList "HEY" | wait-job | receive-job
manojlds
  • 290,304
  • 63
  • 469
  • 417
  • I agree that if all @Alex58 wants to do is run a single function a parameterized ScriptBlock is the way to go. But if he is defining multiple functions or an advanced function, `InitializationScript` can be very helpful. For example: you could define functions `FOO` and `BAR` in the initialization script, then run a job with scriptblock `{FOO; BAR}` and another job with `{BAR; FOO}` – Rynant Aug 23 '11 at 15:37
  • @Rynant - I agree. That is why I said "depending on how you are doing it". PS: Got the name wrong in my answer. Meant Rynant and not Matt. – manojlds Aug 23 '11 at 15:44
9

It worked for me as:

Start-Job -ScriptBlock ${Function:FOO}
Alin
  • 350
  • 2
  • 13
  • 3
    This worked for me with also adding `-ArgumentList`. Thanks! – Dávid Laczkó Apr 21 '20 at 17:11
  • Actually this answer is way better then the marked one – managerger May 12 '22 at 14:04
  • 2
    This is great, but it should be noted that this only works if the function in question doesn't call _another_ function in the global scope. As far as I can tell this amounts to using the named `${Function:FOO}`'s contents as the script block, no more no less. This is **really** just the same as the accepted answer, with the one difference that it reuses an existing function definition instead of assigning and then using a (script) block. – 0xC0000022L Nov 02 '22 at 12:11
5

An improvement to @Rynant's answer:

You can define the function as normal in the main body of your script:

Function FOO 
{ 
  Write-Host "HEY" 
} 

and then recycle this definition within a scriptblock:

$export_functions = [scriptblock]::Create(@"
  Function Foo { $function:FOO }
"@)

(makes more sense if you have a substantial function body) and then pass them to Start-Job as above:

Start-Job -ScriptBlock {FOO} -InitializationScript $export_functions| Wait-Job | Receive-Job

I like this way, as it is easier to debug jobs by running them locally under the debugger.

Bob Mortimer
  • 439
  • 7
  • 17
  • I'm unable to get this to work, the job just outputs "$function:FOO" instead of "HEY" – Halfdone Jan 18 '18 at 23:46
  • I've updated it with a version that definitely works. Funny, sure it was working before but can confirm the old version wasn't – Bob Mortimer Jan 22 '18 at 16:45
  • How do you pass parameters using this syntax? – DougN Sep 26 '19 at 21:36
  • Add a `Param` block to the FOO definition and then `-ArgumentList $args` to the `Start-Job` invocation – Bob Mortimer Sep 26 '19 at 22:53
  • Does this technique work with exporting 3 functions, and some code outside a function (which you would call from Start-Job, and that code would then call the functions)? That's my scenario and trying to figure out how variables get passed to the code outside the function (but inside the -ScriptBlock ) – DougN Sep 27 '19 at 18:41
  • Please post a separate question for this - it's not clear from such a short description. Might be easier to dot-source the file – Bob Mortimer Sep 29 '19 at 22:52
  • 1
    I really wish that someone had answered @DougN 's comment here. It was very clear what he was asking for; it's exactly what I wanted to know. I have no idea where to find his question on stackoverflow. – MKANET Apr 15 '22 at 00:06
  • 1
    I see you have posted your [question](https://stackoverflow.com/questions/71879051/reuse-2-functions-in-start-threadjob) and it has been answered – Bob Mortimer Apr 15 '22 at 10:29
3

The function needs to be inside the scriptblock:

Start-Job -ScriptBlock { function FOO { write-host "HEY" } ; FOO } | Wait-Job | Receive-Job
Shay Levy
  • 121,444
  • 32
  • 184
  • 206
  • Mine is completed, not sure why yours is still running. Can you test this again in a fresh instance? – Shay Levy Aug 23 '11 at 18:27
  • I think there's an issue with xp sp3 and posh 2.0. Now I'm on a windows 7 and backgroudjobs work great. I've googled but found nothing. Maybe just my personal issue on xp sp3. Some similar was start-job with import-module with w2k3 or xp sp3... On w2k8 and w2k8r2 no problems. – CB. Aug 23 '11 at 18:51
1

As long as the function passed to the InitializationScript param on Start-Job isn't large Rynant's answer will work, but if the function is large you may run into the below error.

[localhost] There is an error launching the background process. Error reported: The filename or extension is too long"

Capturing the function's definition and then using Invoke-Expression on it in the ScriptBlock is a better alternative.

function Get-Foo {
    param
    (
        [string]$output
    )

    Write-Output $output
}

$getFooFunc = $(Get-Command Get-Foo).Definition

Start-Job -ScriptBlock {
    Invoke-Expression "function Get-Foo {$using:getFooFunc}"
    Get-Foo -output "bar"
}

Get-Job | Receive-Job

PS C:\Users\rohopkin> Get-Job | Receive-Job
bar
hoppy7
  • 11
  • 2
0

A slightly different take. A function is just a scriptblock assigned to a variable. Oh, it has to be a threadjob. It can't be foreach-object -parallel.

$func = { 'hi' } # or
function hi { 'hi' }; $func = $function:hi

start-threadjob { & $using:func } | receive-job -auto -wait

hi
js2010
  • 23,033
  • 6
  • 64
  • 66
0

@Ben Power's comment under the accepted answer was my concern also, so I googled how to get function definitions, and I found Get-Command - though this gets only the function body. But it can be used also if the function is coming from elsewhere, like a dot-sourced file. So I came up with the following (hold my naming convention :)), the idea is to re-build the function definitions delimited by newlines:

Filter Greeting {param ([string]$Greeting) return $Greeting}
Filter FullName {param ([string]$FirstName, [string]$LastName) return $FirstName + " " + $LastName}
$ScriptText = ""
$ScriptText += "Filter Greeting {" + (Get-Command Greeting).Definition + "}`n"
$ScriptText += "Filter FullName {" + (Get-Command FullName).Definition + "}`n"
$Job = Start-Job `
            -InitializationScript $([ScriptBlock]::Create($ScriptText)) `
            -ScriptBlock {(Greeting -Greeting "Hello") + " " + (FullName -FirstName "PowerShell" -LastName "Programmer")}
$Result = $Job | Wait-Job | Receive-Job
$Result
$Job | Remove-Job
Dávid Laczkó
  • 1,091
  • 2
  • 6
  • 25
0

There's good information in the existing answers, but let me attempt a systematic summary:

  • PowerShell's background jobs[1] run in an out-of-process runspace (a hidden child process) and therefore share no state with the caller.

  • Therefore, function definition created by the caller in-session are not visible to background jobs and must be recreated in the context of the job.[2]

  • The simplest way to recreate a function definition is to combine namespace variable notation (e.g, $function:FOO - see this answer) with the $using:scope, as shown below.

    • Sadly, due to a long-standing bug, $using: references do not work in script blocks passed to Start-Job's (as well as Start-ThreadJob's) -InitializationScript parameter, as of this writing (Windows PowerShell, PowerShell (Core) 7.3.6) - see GitHub issue #4530

A self-contained example:

function FOO { "HEY" }

Start-Job -ScriptBlock { 

  # Redefine function FOO in the context of this job.
  $function:FOO = "$using:function:FOO" 
  
  # Now FOO can be invoked.
  FOO

} | Receive-Job -Wait -AutoRemoveJob

The above outputs string HEY, as intended.

Note:

  • Assigning to $function:FOO implicitly creates function FOO (on demand) and makes the assigned value the function's body; the assigned value may either be a [scriptblock] instance or a [string], i.e. source-code text.

  • Referencing $function:FOO retrieves a preexisting FOO function's body as a [scriptblock] instance. Prepending the $using: scope ($using:function:FOO) retrieves the the body of the FOO function from the caller's scope.

  • Note:

    • Due to $using:, $using:function:FOO is not a [scriptblock] instance, but a [string] in the case of Start-Job, due to the - surprising - manner in which [scriptblock] instance are deserialized when undergoing cross-process serialization; the behavior was declared to be by design - see GitHub issue #11698 for a discussion.

    • As such, the "..." around $using:function:FOO are redundant for Start-Job, but not for Start-ThreadJob, where serialization is not involved, and recreating the body from a string is necessary to avoid state corruption (see GitHub issue #16461 for details).

      • The fact that Start-ThreadJob allows $using:function:FOO references at all is probably an oversight, given that they have been explicitly disallowed in script blocks used with ForEach-Object -Parallel (PowerShell v7+) - see GitHub issue #12378.

      • Therefore, with ForEach-Object -Parallel a helper variable that stringifies the function body on the caller's side first is required - see this answer.


[1] This answer doesn't just apply to the child-process-based jobs created by Start-Job, but analogously also to the generally preferable thread-based jobs created with Start-ThreadJob and the thread-based parallelism available in PowerShell (Core) 7+ with ForEach-Object -Parallel, as well as to PowerShell remoting via Invoke-Command - in short: it applies to any scenario in which PowerShell executes out-of-runspace (in a different runspace).

[2] The alternative is to provide such definitions via script files (*.ps1) or modules that the job would have to dot-source (. ) or import.

mklement0
  • 382,024
  • 64
  • 607
  • 775