Как запустить bash скрипт из python и получить весь вывод? - PullRequest
3 голосов
/ 30 января 2020

Это прямой уточняющий вопрос к ответу в здесь , который, как я думал, сработал, но это не так!

У меня есть следующий тестовый 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"

Если я запускаю этот сценарий я получаю следующий вывод:

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

Это ожидаемый результат. Это хорошо. Я хочу получить ошибки!

Теперь вот код 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))

Но когда я запускаю этот код, я получаю только следующий вывод :

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

или этот вывод (без строки 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

Большая часть вышеприведенного вывода отсутствует! Как я могу это исправить? Кроме того, мне нужен способ проверить, не завершилась ли какая-либо команда в сценарии bash. В данном примере это так, но распечатанный код ошибки по-прежнему равен 0. Я ожидаю код ошибки! = 0.

Не важно сразу получить вывод. Задержка в несколько секунд в порядке. Также, если порядок вывода немного перепутан, это не имеет значения. Важно получить на выходе all (stdout и stderr).

Может быть, есть более простой способ получить вывод сценария bash, который началось с python?

Ответы [ 6 ]

1 голос
/ 08 февраля 2020

Для запуска с 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='')

Надеюсь, это поможет:

0 голосов
/ 08 февраля 2020

Если вы ищете следующие строки:

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

Это функция оболочки bash, которая обычно доступна только в режиме interactive, т.е. когда вы вводите команды в Терминал. Если вы проверите исходный код bash , вы увидите, что он явно проверяет режим перед печатью в stdout / stderr.

В более поздних версиях bash, вы не можете установить это внутри скрипта: см. https://unix.stackexchange.com/a/364618. Однако вы можете установить это самостоятельно при запуске скрипта:

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

Я отмечу, что это работает только для меня на Python3 - Python2 только получает часть вывода. Непонятная версия Python, которую вы используете, но учитывая, что Python2 - это конец жизни, теперь нам, вероятно, следует попытаться перейти на Python3.

Что касается 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

Обычный wait не работал для меня (Ubuntu 18.04), но wait -n казалось, работает - но так как он только ждет завершения следующей работы, у меня был непоследовательный вывод, просто вызвав его один раз. Вызов wait -n для каждого запущенного задания, кажется, делает свое дело, но поток программы, вероятно, должен быть реорганизован до l oop за время ожидания столько же раз, сколько вы запускаете задание.

Также обратите внимание, что чтобы изменить код возврата сценария, ответ Филиппа имеет правильный подход - переменная $? содержит код возврата последней неудачной команды, которую можно затем передать exit. (Еще одно отличие в Python версиях: Python2 возвращает 127, тогда как Python3 возвращает 1 для меня.) Если вам нужны возвращаемые значения для каждого задания, одним из способов может быть анализ значений в интерактивные строки выхода на работу.

0 голосов
/ 04 февраля 2020

Перепишите блок 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='')

Вы не хотите блокировать получение строки из Queue, когда ее нет, и вам не нужен тайм-аут в этот случай, так как он используется только тогда, когда требуется блокировка потока. Следовательно, если Queue.get() выбрасывает Empty, строка для печати отсутствует, и мы просто pass.

===

Кроме того, давайте уточним логи выполнения скрипта c.

Поскольку вы используете Bash выражений, а оболочкой по умолчанию, используемой Popen, является /bin/sh, вы, вероятно, захотите переписать строку вызова следующим образом:

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

Также не помешает добавить шебанг в ваш скрипт:

#!/usr/bin/env bash
<... rest of the script ...>
0 голосов
/ 04 февраля 2020

Во-первых, похоже, что буферы не очищаются. Перенаправление (и, для безопасности, добавление) stdout / stderr в файл (ы), а не в терминал, может помочь. Вы всегда можете использовать tee (или tee -a), если вы действительно хотите оба. Использование менеджеров контекста может помочь.

Что касается нулевого кода возврата, $! https://unix.stackexchange.com/questions/386196/doesnt-work-on-command-line ! может вызывать историю, вызывая историю, в результате чего $! приводит к пустое значение.

Если вы каким-то образом получите только голое wait, код возврата будет нулевым. В любом случае, коды возврата могут быть хитрыми, и вы, возможно, выбираете успешный код возврата из других источников.

Посмотрите на команду stdbuf, чтобы изменить размеры буфера для stdout и stderr: Есть ли способ flu sh стандартный вывод запущенного процесса Это также может помочь получить остальную часть ожидаемого результата.

0 голосов
/ 30 января 2020

Обычно я использую подпроцесс:

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))

Здесь вы декодируете и читаете как stdout, так и stderr. Вы получаете все, но не в том же порядке, я не знаю, если это проблема.

0 голосов
/ 30 января 2020

Просто догадываюсь - может ли быть так, что строка, начинающаяся с пустого символа / пробела, не распознается вашей логикой c.

Возможно, этот отступ является проблемой. Другой вариант заключается в том, что имеется вкладка или что-то в этом роде, и декодирование ascii может завершиться неудачей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...