I see many examples along the lines of:
Get-ChildItem -Filter "*.txt" | ForEach-Object { sajb {ren $_.fullname ($_.directoryname + "\" + "temp_" + $_.name + ".newext") } }
or what I think should be equivalent, using start-job -scriptblock:
Get-ChildItem -Filter "*.txt" | ForEach-Object { Start-Job -ScriptBlock {ren $_.fullname ($_.directoryname + "\" + "temp_" + $_.name + ".newext") } }
But these don't work for me. I get output like this and nothing happens to the files:
or this for my actual usecase:
If I remove the sajb block, then it works fine in series and does exactly what you'd expect. It's only when I try to run all commands in the loop in parallel that it fails.
The same operations do work fine from the Command prompt using:
for %x in ("*.txt") do (start "Convert" cmd /c "ren "%x" "temp_%x.newext"")
My purpose is to do this for some commands that run slowly in series but would run quickly in parallel using ffmpeg and sox, which also work fine in the for loop from the command prompt. I just can't get it working in PowerShell, even in the simple case like the file rename example above. What am I doing wrong with the start-job / sajb?
If it matters, I want to run this as a single PowerShell command from the PS prompt. I do not want to create a PowerShell script.
I see other posts with what look like similar questions, but I don't think they ever received functional answers, or if those answer are correct, I don't understand how to apply them to my situation:
sajb
is simply a built-in alias of the Start-Job
cmdlet.
Two asides:
The Start-ThreadJob
cmdlet offers a lightweight, much faster thread-based alternative to the child-process-based regular background jobs created with Start-Job
.
It comes with PowerShell (Core) 7+ and in Windows PowerShell can be installed on demand with, e.g., Install-Module ThreadJob -Scope CurrentUser
.
In most cases, thread jobs are the better choice, both for performance and type fidelity - see the bottom section of this answer for why.
In PowerShell (Core) 7+, the simplest solution is to use ForEach-Object
with the -Parallel
parameter, which combines parallel execution with direct access to pipeline input via the automatic $_
variable:
1..3 |
ForEach-Object -Parallel { "`$_ is: $_" }
As Santiago Squarzon notes, any Start-Job
(as well as Start-ThreadJob
) call inside a (non-parallel) ForEach-Object
call will not automatically see the automatic $_
variable reflecting the current pipeline input object, given that it executes in a different runspace (in the case of Start-Job
, a runspace in a different process); therefore, you must reference / pass its value explicitly:
Either: use $using:_
, via the $using:
scope:
1..3 |
ForEach-Object { Start-Job { $using:_ } } |
Receive-Job -Wait -AutoRemoveJob
$using:
references requires enclosure in (...)
- see GitHub issue #10876Or: pass any values to the job via the -ArgumentList
(-Args
) parameter, which the job can then access via the automatic $args
variable:
1..3 |
ForEach-Object { Start-Job { $args[0] } -ArgumentList $_ } |
Receive-Job -Wait -AutoRemoveJob
Additionally, note that in Windows PowerShell (the legacy PowerShell edition whose latest and last version is 5.1), Start-Job
script blocks use a default working directory rather than inheriting the caller's - see the bottom section of this answer for details.