Python 执行 Bash 命令得到实时输出

有些时候,我们需要在 Python 里执行一些 Bash / Shell 命令,比如调用外部的构建工具、Git 或者是其它程序。这时候,用 Python 自带的 subprocess 模块可以很方便的实现。在实际使用中,真正让人头大的有两点:如何执行像脚本一样运行多行 Shell 命令;以及如何实时得到命令的输出。

对于非常简单的 Shell 命令,只需要使用 subprocess.Popen 即可:

1
2
3
4
5
import subprocess
process = subprocess.Popen(['echo', 'Hello World'],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
stdout, stderr = process.communicate()

上面的代码很好懂,即使用 Popen 开启一个新的进程执行命令,并将 stdoutstderr 通过 PIPE 记录。这个方式非常简单,适用于大多数情况。

但如果你的命令需要很长时间才能执行完成,会需要实时的输出。最简单的方式便是不将 stdoutstderr 通过 PIPE 记录,如此一来便无法得到 stdout 中的内容;此外,还可以通过 tee 命令配合 2>&1,比如 echo hello 2>&1 | tee dev/tty,这个方法可行但是对于每一行 Shell 命令都得使用。

经过了一番探索,最后找到了一个既可以执行多行 Shell 命令,并且能够得到实时输出的方法:

首先用 Popen 打开 /bin/bash 实例,将我们需要执行的 Shell 指令通过 stdin 传入,这样子做的好处是可以让 bash 逐行执行每一条命令。之后在一个 while 循环中,持续地读子进程 stdout 的内容,通过 proc.poll() 判断子进程是否终止来终止循环,最后通过 proc.wait() 得到子进程退出的代码,如果非 0 的话表示异常退出,进而选择下一步的操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
proc = subprocess.Popen(
    "/bin/bash",
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE,
    shell=True
)

proc.stdin.write(commands.encode())
proc.stdin.flush()
proc.stdin.close()

# Real time stdout of subprocess
stdout = []
while True:
    line = proc.stdout.readline().decode()
    stdout.append(line)

    if line == "" and proc.poll() is not None:
        break

    # Print outputs
    sys.stdout.write(line)

# Wait for child process and get return code
return_code = proc.wait()

这样便可以像 GitHub Action 一样 run 多行的命令了

1
2
3
4
5
6
7
jobs:
  say_hello:
    runs-on: ubuntu-latest
    steps:
    - run: |
        echo "Hello ${{ github.event.inputs.name }}!"
        echo "- in ${{ github.event.inputs.home }}!"

参考:

加载评论