From CMD, is it possible to execute a command after a for loop using start to launch several parallel tasks, but only AFTER all tasks have completed?

huangapple go评论79阅读模式
英文:

From CMD, is it possible to execute a command after a for loop using start to launch several parallel tasks, but only AFTER all tasks have completed?

问题

我运行一个修改文件夹中所有音频文件的命令,由于 ffmpeg 中的一个错误,我需要将 ffmpegsox 命令链接在一起,但 sox 很慢,所以我并行运行它们,这显著加快了进程(大约快了10倍,可能你CPU内核越多,速度越快)。这一切都运行良好。

问题在于,因为这些都是作为单独的任务启动的,它在完成任务之前返回并运行FOR循环之后的命令。在我的情况下,这是删除临时中间文件(在下面的示例中为 del int_*.wav)。这是一个简化的例子(删除了大部分参数以便更容易阅读):

md "Ready" & (for %x in ("*.mp3") do (start "Convert" cmd /c "ffmpeg -i "%x" -f wav "int_%x.wav" & sox "int_%x.wav" -r 44100 "Ready\ready-%x"")) && del int_*.wav

它可以正确运行,除了最后一部分的 del int_*.wav,因为它在所有 START 线程完成之前尝试运行,所以什么也没做。

它报错:

Could Not Find C:\<parent directory name>\int_*.wav

这是预期的,因为它在创建要删除的文件之前就到了 DEL。因此,我需要让它暂停,直到 FOR 循环完成并且所有启动的任务都关闭。如果我去掉 START 命令并尝试不并行运行它们,它会正确运行 DEL 命令,但速度很慢(这是我最初的做法,然后想出了上面的解决方案来加快速度):

md "Ready" & (for %x in ("*.mp3") do (ffmpeg -i "%x" -f wav "int_%x.wav" & start sox "int_%x.wav" -r 44100 "Ready\ready-%x")) & del int_*.wav

请注意,此示例只在末尾有 DEL 命令,但在我的实际用例中,循环后有多个命令,包括一个批处理文件,所有这些都用 & 符号链接在一起。因此,我不能使用仅提供不同删除文件方式的解决方案。我需要在双括号关闭后的所有内容等待循环完成和所有启动的任务关闭后才运行。

还请注意,我故意将此命令作为命令提示符中的一个命令运行,而不是通过批处理文件运行。这是因为我必须更改许多参数的一个小变化子集,并且通常仅对目录中的文件的一个子集运行它。因此,在这种特定情况下,只需粘贴命令并使用通配符调整相关参数和文件名比制作批处理文件更容易。如果可能的话,我想保持它作为单个 CMD 行。

有没有办法做到这一点?

英文:

I'm running a command that modifies all the audio files in a folder, due to a bug in ffmpeg, I need to chain together an ffmpeg and sox command, but sox is really slow, so I run them in parallel, which dramatically speeds up the process (makes it about 10 times faster, probably the more cores in your CPU, the faster it will go). That all works fine.

The problem is that because those are all spawned as separate tasks, it returns and runs the commands after the FOR loop before it has completed the tasks. In my case, that's to delete the intermediate temp files (del int_*.wav in the example below). Here's a simplified example (removed most parameters to make it easier to read):

md "Ready" & (for %x in ("*.mp3") do (start "Convert" cmd /c "ffmpeg -i "%x" -f wav "int_%x.wav" & sox "int_%x.wav" -r 44100 "Ready\ready-%x"")) && del int_*.wav

That converts all the MP3 files in the current directory per my parameters to a different set of MP3 files in the destination directory (the Ready subdirectory). It uses .WAV files as intermediaries because those are lossless and fast.

The above runs correctly, except for the last part, del int_*.wav, because it tries to run that before all the START threads have finished, and so it does nothing.

It errors with:

Could Not Find C:\<parent directory name>\int_*.wav

Which is expected, because it gets to the DEL before it has created the files to delete. Hence my need to have it pause until the FOR loop has completed and all the START tasks have closed.

It does run the DEL command correctly if I leave out the START command and don't try to run them in parallel, but then it's very slow (this is how I originally did it, then came up with the above solution to make it faster):

md "Ready" & (for %x in ("*.mp3") do (ffmpeg -i "%x" -f wav "int_%x.wav" & start sox "int_%x.wav" -r 44100 "Ready\ready-%x")) & del int_*.wav

And note that this example only has the DEL command at the end, but in my actual use case, I have multiple commands after the loop, including a batch file, all chained together with & signs. So I can't use a solution that just provides a different way to delete the files. I need to have everything after the double closing parentheses wait to run until the loop has completed and all the started tasks have closed.

Also note that I'm intentionally running this as one command from the command prompt rather than via a batch file. This is because I have to change a small varying subset of many parameters and generally run it on a subset of the files in a directory. So in this particular case, it's much easier to just paste the command and tweak the relevant parameters and filenames with wildcards than make a batch file and have to pass it everything every time. I'd like to keep it as a single CMD line if possible.

Is there any way to do that?

答案1

得分: 1

避免在这些日子里使用cmd。在PowerShell中,一切都更容易,许多在cmd中无法实现的事情在PowerShell中都可以实现。

foreach (f in Get-ChildItem -Filter "*.mp3") {
    Start-Job -ScriptBlock {
        ffmpeg -i "$f" -f wav "int_$f.wav"
        sox "int_$f.wav" -r 44100 "Ready\ready-$f"
    }
}
Get-Job | Wait-Job
Remove-Item int_*.wav

或者你可以像这样直接使用管道而不需要 Get-Job

foreach (f in ls "*.mp3") { sajb {
    ffmpeg -i "$f" -f wav "int_$f.wav"; sox "int_$f.wav" -r 44100 "Ready\ready-$f"
} } | wjb
del int_*.wav

或者甚至可以将其变成一行:

foreach (f in ls "*.mp3") { sajb { ffmpeg -i "$f" -f wav "int_$f.wav"; sox "int_$f.wav" -r 44100 "Ready\ready-$f" } } | wjb; rm int_*.wav

在这种特殊情况下,只需粘贴命令并调整相关参数和带通配符的文件名比创建批处理文件并每次都要传递所有参数要容易得多。

没有什么可以阻止你在参数未设置时设置默认值。这在任何Shell脚本语言中都是可能的。只需传递需要更改的参数。但是在批处理中,你需要读取参数并手动设置标志,而在PowerShell中,只需使用 Param() 定义你想要的一切,一切都会自动处理。

或者,你可以使用 Start-Process/Wait-Process 替代 Start-Job/Wait-Job,但这会更长,不太可读,并且可能对某些命令不起作用。

Start-Process -PassThru {
    cmd /c "ffmpeg -i `"$f`" -f wav `"`int_$f.wav`" & sox `"`int_$f.wav`" -r 44100 `"`Ready\ready-$f`""
} | Wait-Process
rm int_*.wav
英文:

Avoid cmd these days. Everything is much easier in PowerShell, and many things that are impossible to do in cmd can be achievable in PowerShell

foreach (f in Get-ChildItem -Filter "*.mp3") {
    Start-Job -ScriptBlock {
        ffmpeg -i "$f" -f wav "int_$f.wav"
        sox "int_$f.wav" -r 44100 "Ready\ready-$f"
    }
}
Get-Job | Wait-Job
Remove-Item int_*.wav

Or you can pipe directly without Get-Job like this

foreach (f in ls "*.mp3") { sajb {
    ffmpeg -i "$f" -f wav "int_$f.wav"; sox "int_$f.wav" -r 44100 "Ready\ready-$f"
} } | wjb
del int_*.wav

Or even make it a one-liner

foreach (f in ls "*.mp3") { sajb { ffmpeg -i "$f" -f wav "int_$f.wav"; sox "int_$f.wav" -r 44100 "Ready\ready-$f" } } | wjb; rm int_*.wav

> So in this particular case, it's much easier to just paste the command and tweak the relevant parameters and filenames with wildcards than make a batch file and have to pass it everything every time

Nothing prevents you from setting a default value if the parameter is not set. This is possible in any shell scripting languages. Just pass the arguments that you need to change. However in batch you'll need to read arguments and set the flags manually while in PowerShell just define what you want with Param() and everything will be handled automatically


Alternatively you can use Start-Process/Wait-Process instead of Start-Job/Wait-Job but this is longer, less readable and may not work for some commands

Start-Process -PassThru {
    cmd /c "ffmpeg -i `"$f`" -f wav `"int_$f.wav`" & sox `"int_$f.wav`" -r 44100 `"Ready\ready-$f`""
} | Wait-Process
rm int_*.wav

答案2

得分: 0

[untested]

for %%e in ('dir int_* 2^>nul^|find /c "int"') do set starting=%%e
set /a converted=0
md "Ready" & (for %%x in ("*.mp3") do (set /a converted+=1&ffmpeg -i "%%x" -f wav "int_%%x.wav" & start sox "int_%%x.wav" -r 44100 "Ready\ready-%%x"))
:wait
timeout /t 1 >nul
for %%e in ('dir int_* 2^>nul^|find /c "int"') do set /a current=%%e-starting
if %current% neq %converted% goto wait
英文:

[untested]

for %%e in ('dir int_* 2^>nul^|find /c "int") do set starting=%%e
set /a converted=0
md "Ready" & (for %x in ("*.mp3") do (set /a converted+=1&ffmpeg -i "%x" -f wav "int_%x.wav" & start sox "int_%x.wav" -r 44100 "Ready\ready-%x"))
:wait
timeout /t 1 >nul
for %%e in ('dir int_* 2^>nul^|find /c "int") do set /a current=%%e-starting
if %current% neq %converted% goto wait

This shoud set starting to the number of int_* files originally in the directory, then count the conversions to be done in converted.

Then calculate the number completed in current and if not the same as the number in converted, wait another 1 second.

答案3

得分: 0

PowerShell 似乎提供了最佳解决方案,因为 wjb 选项会忠实地等待并行任务全部完成,然后立即运行删除操作。感谢 @phuclv 引导我走上这条路。但是,这需要一些额外的步骤:

首先,升级到 PowerShell 7(Windows 11 默认只包含 5.2 版本,它在并行/StartJob 函数方面存在限制和问题。可以从 Microsoft Store 运行搜索 PowerShell 进行安装,或者在 PowerShell 提示符下运行以下命令:winget install --id Microsoft.Powershell --source winget

然后启动一个新的 PowerShell 7 终端窗口。旧的 PowerShell 版本 5 仍然会与版本 7 安装在一起。要打开新的版本,请启动一个新的终端窗口,然后打开一个新选项卡,选择 "PowerShell" 而不是 "Windows PowerShell"(Windows Powershell 是旧版本 5)。

现在,您可以像这样使用 "ForEach-Object -parallel" 运行命令:

md "Ready"; Get-ChildItem -Filter "*.mp3" | ForEach-Object -parallel { ffmpeg -i $.name -f flac -af dynaudnorm -id3v2_version 3 ($.name+".flac"); sox --norm=-3.25 ($.fullname + ".flac") -c 2 -C 192 -r 44100 (".\Ready\ready-" + $.name) } | wjb; rm *.flac

与 PowerShell 相关的其他注意事项:

虽然在版本 5 下可以使用 StartJob 函数,但生成的任务会丧失对当前目录的感知。可以使用 ($using:) 替代 $,但我在尝试这个方法时遇到了问题(升级到版本 7 并使用 -parallel 更容易)。

还有一个新的 Start-ThreadJob,可能比 Start-Job 更快且更高效,但至少对于我的用途来说,ForEach-Object -parallel" 选项完美运行。

英文:

PowerShell does seem to provide the best solution to this, because the wjb option dutifully waits for the running parallel tasks to all finish, then immediately runs the delete. Thanks to @phuclv for steering me down this path. However, this requires a few additional steps:

First, upgrade to PowerShell 7 (only 5.2 included by default with Windows 11, which has limits and problems with its parallel / StartJob functions. Either run from Microsoft Store (search for PowerShell) or, at a PS prompt, run: winget install --id Microsoft.Powershell --source winget

Then launch a new PowerShell 7 Terminal window. The old PowerShell version 5 will still be installed alongside version 7. To open the new one, launch a new Terminal window, then open a new tab, selecting "PowerShell" instead of "Windows PowerShell" (Windows Powershell is the old version 5).

Now you can run a command with the "ForEach-Object -parallel" like this:

md "Ready"; Get-ChildItem -Filter "*.mp3" | ForEach-Object -parallel { ffmpeg -i $.name -f flac -af dynaudnorm -id3v2_version 3 ($.name+".flac"); sox --norm=-3.25 ($.fullname + ".flac") -c 2 -C 192 -r 44100 (".\Ready\ready-" + $.name) } | wjb; rm *.flac

Additional considerations specific to PowerShell:

While you can use the StartJob function under version 5, that spawned tasks lose awareness of the current directory. In place of $, you can use ($using:), but I had trouble getting this to work (upgrading to version 7 and using -parallel was easier).

There is also a newer Start-ThreadJob which may be faster and more efficient than Start-Job, but at least for my purposes, the ForEach-Object -parallel" option worked perfectly.

huangapple
  • 本文由 发表于 2023年6月29日 04:14:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/76576453.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定