简体   繁体   English

如何从 python 中运行 bash 脚本并获取所有输出?

[英]How to run a bash script from within python and get all the output?

This is a direct clarification question to the answer in here which I thought it worked, but it does not!这是对这里答案的直接澄清问题,我认为它有效,但它没有!

I have the following test bash script ( testbash.sh ) which just creates some output and a lot of errors for testing purposes (running on Red Hat Enterprise Linux Server release 7.6 (Maipo) and also Ubuntu 16.04.6 LTS):我有以下测试 bash 脚本 ( testbash.sh ),它只是创建一些输出和许多用于测试目的的错误(在 Red Hat Enterprise Linux Server 7.6 (Maipo) 和 Ubuntu 16.04.6 LTS 上运行):

export MAX_SEED=2
echo "Start test"
pids=""

for seed in `seq 1 ${MAX_SEED}`
do
  python -c "raise ValueError('test')" &
  pids="${pids} $!"
done
echo "pids: ${pids}"
wait $pids
echo "End test"

If I run this script I get the following output:如果我运行这个脚本,我会得到以下输出:

Start test
pids:  68322 68323
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: test
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: test
[1]-  Exit 1                  python -c "raise ValueError('test')"
[2]+  Exit 1                  python -c "raise ValueError('test')"
End test

That is the expected outcome.这就是预期的结果。 That is fine.那没关系。 I want to get errors!我想得到错误!

Now here is the python code that is supposed to catch all the output:现在这里是应该捕获所有输出的python代码:

from __future__ import print_function

import sys
import time
from subprocess import PIPE, Popen, STDOUT
from threading  import Thread

try:
    from queue import Queue, Empty
except ImportError:
    from Queue import Queue, Empty  # python 2.x    

ON_POSIX = 'posix' in sys.builtin_module_names

def enqueue_output(out, queue):
    for line in iter(out.readline, b''):
        queue.put(line.decode('ascii'))
    out.close()

p = Popen(['. testbash.sh'], stdout=PIPE, stderr=STDOUT, bufsize=1, close_fds=ON_POSIX, shell=True)
q = Queue()
t = Thread(target=enqueue_output, args=(p.stdout, q))
t.daemon = True # thread dies with the program
t.start()

# read line without blocking
while t.is_alive():
    #time.sleep(1)
    try:
        line = q.get(timeout=.1)
    except Empty:
        print(line)
        pass
    else:
        # got line
        print(line, end='')

p.wait()
print('returncode = {}'.format(p.returncode))

But when I run this code I only get the following output:但是当我运行这段代码时,我只会得到以下输出:

Start test
pids:  70191 70192
Traceback (most recent call last):
returncode = 0

or this output (without the line End test ):或此输出(没有行End test ):

Start test
pids:  10180 10181
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: test
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: test
returncode = 0

Most of the above output is missing!上面的大部分输出都丢失了! How can I fix this?我怎样才能解决这个问题? Also, I need some way to check if any command in the bash script did not succeed.另外,我需要一些方法来检查 bash 脚本中的任何命令是否失败。 In the example this is the case, but the errorcode printed out is still 0. I expect an errorcode != 0.在示例中就是这种情况,但打印出的错误代码仍然是 0。我期望错误代码 != 0。

It is not important to immediately get the output.立即获得输出并不重要。 A delay of some seconds is fine.延迟几秒钟就可以了。 Also if the output order is a bit mixed up this is of no concern.此外,如果输出顺序有点混乱,这也无关紧要。 The important thing is to get all the output ( stdout and stderr ).重要的是获取所有输出( stdoutstderr )。

Maybe there is a simpler way to just get the output of a bash script which is started from python?也许有一种更简单的方法来获取从 python 启动的 bash 脚本的输出?

To be run with python3与 python3 一起运行

from __future__ import print_function
import os
import stat
import sys
import time
from subprocess import PIPE, Popen, STDOUT
from threading  import Thread
try:
    from queue import Queue, Empty
except ImportError:
    from Queue import Queue, Empty  # python 2.x
ON_POSIX = 'posix' in sys.builtin_module_names
TESTBASH = '/tmp/testbash.sh'
def create_bashtest():
    with open(TESTBASH, 'wt') as file_desc:
        file_desc.write("""#!/usr/bin/env bash
export MAX_SEED=2
echo "Start test"
pids=""
for seed in `seq 1 ${MAX_SEED}`
do
  python -c "raise ValueError('test')" &
  pids="${pids} $!"
  sleep .1 # Wait so that error messages don't get out of order.
done
wait $pids; return_code=$?
sleep 0.2 # Wait for background messages to be processed.
echo "pids: ${pids}"
echo "End test"
sleep 1 # Wait for main process to handle all the output
exit $return_code
""")
    os.chmod(TESTBASH, stat.S_IEXEC|stat.S_IRUSR|stat.S_IWUSR)

def enqueue_output(queue):
    pipe = Popen([TESTBASH], stdout=PIPE, stderr=STDOUT,
                 bufsize=1, close_fds=ON_POSIX, shell=True)
    out = pipe.stdout
    while pipe.poll() is None:
        line = out.readline()
        if  line:
            queue.put(line.decode('ascii'))
        time.sleep(.1)
    print('returncode = {}'.format(pipe.returncode))

create_bashtest()
C_CHANNEL = Queue()

THREAD = Thread(target=enqueue_output, args=(C_CHANNEL,))
THREAD.daemon = True
THREAD.start()

while THREAD.is_alive():
    time.sleep(0.1)
    try:
        line = C_CHANNEL.get_nowait()
    except Empty:
        pass # print("no output")
    else:
        print(line, end='')

Hope this helps :希望这可以帮助 :

First, looks like buffers are not being flushed.首先,看起来缓冲区没有被刷新。 Redirecting (and, to be safe, appending) stdout/stderr to a file(s) rather than to the terminal, may help.将 stdout/stderr 重定向(并且,为了安全起见,附加)到文件而不是终端,可能会有所帮助。 You can always use tee (or tee -a ) if you really want both.如果你真的想要两者,你总是可以使用tee (或tee -a )。 Using context managers 'might' help.使用上下文管理器“可能”有帮助。

As far as the zero return code, $!至于零返回码, $! https://unix.stackexchange.com/questions/386196/doesnt-work-on-command-line ! https://unix.stackexchange.com/questions/386196/doesnt-work-on-command-line ! may be invoking history invoking history, thereby $!可能正在调用历史调用历史,因此$! resulting in an empty value.导致空值。

If you somehow end up with just a bare wait the return code will be a zero.如果您以某种方式最终只是wait ,返回码将为零。 Regardless, return codes can be tricky, and you might be picking a successful return code from elsewhere.无论如何,返回代码可能很棘手,您可能会从其他地方选择一个成功的返回代码。

Take a look at stdbuf command to change the buffer sizes for stdout and stderr: Is there a way to flush stdout of a running process That may also help with getting the rest of your expected output.查看 stdbuf 命令以更改 stdout 和 stderr 的缓冲区大小: 是否有办法刷新正在运行的进程的 stdout这也可能有助于获得其余的预期输出。

Rewrite the while block this way:以这种方式重写while块:

# read line without blocking
while t.is_alive():
    try:
        line = q.get(block=False)
    except Empty:
        # print(line)
        pass
    else:
        # got line
        print(line, end='')

You don't want to block on getting a line from the Queue when there's none, and you don't need a timeout in this case, as it's only used when blocking the thread is required.当没有Queue时,您不想阻塞从Queue获取一行,并且在这种情况下您不需要超时,因为它仅在需要阻塞线程时使用。 Consequently, if the Queue.get() throws Empty , there's no line to print, and we just pass .因此,如果Queue.get()抛出Empty ,则没有要打印的行,我们只需pass

=== ===

Also, let's clarify the script execution logic.另外,让我们澄清一下脚本执行逻辑。

Since you're using Bash expressions, and the default shell used by Popen is /bin/sh , you'd probably want to rewrite the invokation line this way:由于您使用的是 Bash 表达式,并且Popen使用的默认 shell 是/bin/sh ,您可能希望以这种方式重写调用行:

p = Popen(['/usr/bin/bash','-c', './testbash.sh'], stdout=PIPE, stderr=STDOUT, bufsize=1, close_fds=ON_POSIX)

It won't hurt to add a shebang to your shell script, too:在你的 shell 脚本中添加一个 shebang 也没有什么坏处:

#!/usr/bin/env bash
<... rest of the script ...>

If you're looking for these lines:如果您正在寻找这些行:

[1]-  Exit 1                  python -c "raise ValueError('test')"
[2]+  Exit 1                  python -c "raise ValueError('test')"

This is a function of the bash shell that's typically only available in interactive mode, ie when you're typing commands into a terminal.这是 bash shell 的一项功能,通常仅在interactive模式下可用,即当您在终端中键入命令时。 If you check the bash source code , you can see that it explicitly checks the mode before printing to stdout/stderr.如果您检查bash 源代码,您可以看到它在打印到 stdout/stderr 之前显式检查了模式。

In the more recent versions of bash, you can't set this inside a script: see https://unix.stackexchange.com/a/364618 .在较新版本的 bash 中,您无法在脚本中进行设置:请参阅https://unix.stackexchange.com/a/364618 However, you can set this yourself when starting the script:但是,您可以在启动脚本时自行设置:

p = Popen(['/bin/bash -i ./testbash.sh'], stdout=PIPE, stderr=STDOUT, bufsize=1, close_fds=ON_POSIX, shell=True)

I will note that this is only working for me on Python3 - Python2 is only getting part of the output.我会注意到这仅在 Python3 上对我有用 - Python2 只获得部分输出。 It isn't clear version of Python you're using, but considering Python2 is end of life now we should probably all be trying to switch to Python3.您正在使用的 Python 版本尚不清楚,但考虑到 Python2 现在已经结束,我们可能都应该尝试切换到 Python3。

As for the bash script, even with interactive mode set it seems you have to change how you wait to get that output:至于 bash 脚本,即使设置了交互模式,您似乎也必须更改wait获得该输出的方式:

#!/bin/bash
export MAX_SEED=2
echo "Start test"
pids=""

for seed in `seq 1 ${MAX_SEED}`
do
    python -c "raise ValueError('test')" &
    pids="${pids} $!"
done
echo "pids: ${pids}"
wait -n $pids
wait -n $pids
ret=$?
echo "End test"
exit $ret

Normal wait wasn't working for me (Ubuntu 18.04), but wait -n seemed to work - but as it only waits for the next job to complete, I had inconsistent output just calling it once.正常wait对我不起作用(Ubuntu 18.04),但wait -n似乎有效 - 但由于它只等待下一个工作完成,我只调用一次就得到了不一致的输出。 Calling wait -n for each job launched seems to do the trick, but the program flow should probably be refactored to loop over the wait the same number of times you spin up the job.为每个启动的作业调用wait -n似乎可以解决问题,但可能应该重构程序流以循环等待与启动作业相同的次数。

Also note that to change the return code of the script, Philippe's answer has the right approach - the $?另请注意,要更改脚本的返回码,Philippe 的答案有正确的方法 - $? variable has the return code of the latest command that failed, which you can then pass to exit .变量具有失败的最新命令的返回代码,然后您可以将其传递给exit (Yet another difference in Python versions: Python2 is returning 127 while Python3 returns 1 for me.) If you need the return values for each job, one way might be to parse out the values in the interactive job exit lines. (Python 版本的另一个区别:Python2 返回127而 Python3 为我返回1 )如果您需要每个作业的返回值,一种方法可能是解析交互式作业退出行中的值。

Just guessing - could it be that a line that starts with an empty character / space is not recognized as a line by your logic.只是猜测 - 以空字符/空格开头的行是否不会被您的逻辑识别为行。

Maybe this indent is the issue.也许这个缩进是问题所在。 Another option is, that there is a tab or something like that and the ascii decode might fail.另一种选择是,有一个选项卡或类似的东西,ascii 解码可能会失败。

This is how I usually use subprocess:这就是我通常使用子流程的方式:

import subprocess

with subprocess.Popen(["./test.sh"], shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) as p:
    error = p.stderr.read().decode()
    std_out = p.stdout.read().decode()
    if std_out:
        print(std_out)
    if error:
        print("Error message: {}".format(error))

Here you decode and read both the stdout and the stderr.在这里,您可以解码和读取标准输出和标准错误。 You get everything but not in the same order, I don't if that's an issue.你得到所有东西,但顺序不同,如果这是一个问题,我不知道。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM