如何设置进度条并打印 subprocess.Popen 的输出

How to set up progress bar and also print output from subprocess.Popen

我正在尝试使用进度条来减少使用 python subprocess 时 运行 FPGA 构建脚本的冗长程度。构建脚本可能需要数小时 运行,并且构建输出非常冗长。目前,我为用户打印脚本的输出,以查看正在取得的进展并了解进展情况。我想为用户清理这个,因为冗长掩盖了重要的 python 日志,这真的很烦人。

本质上,我希望用tqdm来表示进度。我想要的是这个(它不必完全匹配下面的示例):

构建开始时,终端可能如下所示:

INFO: starting build
Running Build [                           ] (0:00:01.2345)
@I: compiling file <x>

完成后,它看起来像这样(注意终端输出线不见了)

INFO: starting build
Running Build [...........................] (6:25:48.6251)
<python continues logging stuff here>

为了确定进度,我将检查 subprocess.Popen.stdout 以确定它是否是构建脚本中的一行。这是一些伪代码来展示我正在尝试做的事情:

from subprocess import Popen, PIPE

pb = <initialize progress bar>

lines_in_build_script = [
  "step 1",
  "step 2",
]

p = Popen(cmd, shell=True, stdout=PIPE)
for line in p.stdout:
  pb.update_build_output(line)
  if line in lines_in_build_script:
    pb.increment_progress() # I would prefer if it was just a spinning wheel because looking for line in output is asking for trouble
rc = p.wait()

编辑:tqdm 不是必需的,这只是我知道的一个包。我愿意接受有关如何解决此问题的任何建议。

我对 tqdmalive_progress 功能不满意。我发现 yaspin 并创建了一个自定义的“标题”格式化程序,因为根据我用来确定进度的步骤,设置包的百分比是不可能表征的。相反,我向 yaspin 格式化程序添加了一个计数器以及一些手动更新文本的方法。不完全是我想要的,但它绝对简化了标准输出:

class BarFormat:
    def __init__(self, title: str, total=None, width=None):
        self.title = title
        self._total = total
        self._cnt = 0
        self._start = datetime.datetime.now()
        self._status = ""
        self._width = width if width else os.get_terminal_size(0)[0]

    def __str__(self):
        delta = datetime.datetime.now() - self._start
        if self._total:
            text = f"{self.title} ({self._cnt}/{self._total} elapsed {delta}) {self._status}"
        else:
            text = f"{self.title} (elapsed {delta}) {self._status}"
        # resize if needed
        return text[:self._width-10]

    def current(self) -> int:
        return self._cnt

    def incr(self) -> None:
        self._cnt += 1

    def update(self, status: str) -> None:
        self._status = status.strip()

然后在我的代码中,我有这样的东西:

        fmt = BarFormat(bar_title, total=len(bar_items))
        with yaspin(text=fmt).green.arc as bar:
            for line in p.stdout:
                l = line.decode('utf-8').strip()
                if l:
                    # update status of bar only if there's output
                    fmt.update(l)
                if bar_items:
                    if l == bar_items[fmt.current()]:
                        bar.write("> "+l)
                        fmt.incr()
            rc = p.wait()

            # final spinner status
            if rc in ignore_rcs:
                bar.green.ok()
            else:
                bar.red.fail()

这会产生如下内容:

...
> compile code
> <other steps...>
<spinner> Running Some Tool (125/640 elapsed 0:12:34.56789) @I: some info 

我注意到 yaspin 不处理文本字段中超出终端宽度的行。我这样做是为了在实例化 class 时确定宽度。不过,将其更新为动态的会非常容易。

编辑:我实际上放弃了 yaspin。我开始使用 rich 而不是因为我决定更新我的一些日志以使用 RichHandler 并发现它支持微调器和进度条。这是有关 https://rich.readthedocs.io/en/latest/progress.html

的信息

这是一个工作示例,原始问题使用 enlighten. Enlighten will keep the progress bar at the bottom of the terminal and you can print whatever you like through normal methods. See the docs 如何自定义。

from subprocess import Popen, PIPE

import enlighten


cmd = 'for i in {1..80}; do sleep 0.2; echo $i; [ $((i % 10)) -eq 0 ] && echo step $((i/10)); done'
lines_in_build_script = [
    'step 1\n',
    'step 2\n',
    'step 3\n',
    'step 4\n',
    'step 5\n',
    'step 6\n',
    'step 7\n',
    'step 8\n',
]

manager = enlighten.get_manager()
pb = manager.counter(total=len(lines_in_build_script), desc='Long Process')

p = Popen(cmd, shell=True, stdout=PIPE, text=True)
pb.refresh()
for line in p.stdout:
    print(line, end='')
    if line in lines_in_build_script:
        pb.update()
rc = p.wait()

alive-progress 的作者在这里!

实际上,alive-progress 确实支持将文本放在上面,将它们放在下面没有多大意义,因为终端上的行 运行 从上到下,所以栏必须留下所有像日志一样打印的行。

今天发布了最终收据的隐藏,连同已经支持的旋转器隐藏,你可以这样操作:

是的,所有这些都有很棒的现场旋转器!

我认为显示经过的时间非常酷,但如果您愿意,也可以删除它,以及“请稍候”消息,如下所示: