Статьи

Всегда полезный и аккуратный модуль подпроцесса

Первоначально автор Shrikant Sharat
Модуль подпроцесса

Python является одним из моих любимых модулей в стандартной библиотеке. Если вы когда-либо выполняли какое-то приличное количество кода на python, вы могли с этим столкнуться. Этот модуль используется для работы с внешними командами, предназначенными для замены старой системы os.s и т.п.

Самым тривиальным использованием может быть получение выходных данных небольшой команды оболочки, такой как ls или ps. Не то чтобы это был лучший способ получить список файлов в каталоге (например, os.listdir ), но вы поняли .

Я собираюсь разместить свои заметки и опыт об этом модуле здесь. Обратите внимание, я написал это с учетом Python 2.7. Вещи могут немного отличаться в других версиях (даже 2.6). Если вы обнаружите какие-либо ошибки или предложения, пожалуйста, дайте мне знать.

Простое использование

Для обеспечения контекста, давайте запустим команду ls из подпроцесса и получим ее вывод

import subprocess
ls_output = subprocess.check_output(['ls'])

Я подробно расскажу о получении результата от команды позже. Чтобы дать больше аргументов командной строки,

subprocess.check_output(['ls', '-l'])

Первый элемент в списке — это исполняемый файл, а остальные — его аргументы командной строки (эквивалент argv). Никаких изворотливых цитирований оболочки и сложных правил вложенных цитат для переваривания. Просто простой список питонов.

Однако отсутствие цитирования оболочки означает, что у вас также нет тонкостей оболочки. Как трубопровод для одного. Следующее не будет работать так, как можно было бы ожидать.

subprocess.check_output(['ls', '|', 'wc', '-l'])

Здесь команда ls получает свою первую команду как | и я понятия не имею, что ls сделает с этим. Возможно, жалуются, что такого файла не существует. Поэтому вместо этого мы должны использовать логический аргумент оболочки. Подробнее позже в статье.

Попен Класс

Если в модуле подпроцесса есть только одна вещь, которая вас должна заинтересовать, это класс Popen . Другие функции, такие как call , check_output и check_call, используют Popen внутренне. Вот подпись из документов.

class subprocess.Popen(args, bufsize=0, executable=None, stdin=None,
    stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False,
    cwd=None, env=None, universal_newlines=False, startupinfo=None,
    creationflags=0)

Я предлагаю вам прочитать документы для этого класса. Как и во всех документах Python, это действительно хорошо.

Запуск через оболочку

Подпроцесс также может запускать инструкции командной строки через программу оболочки. Обычно это dash / bash в Linux и cmd в windows.

subprocess.call('ls | wc -l', shell=True)

Обратите внимание, что в этом случае мы передаем строку, а не список. Это потому, что мы хотим, чтобы оболочка интерпретировала всю нашу команду. Вы можете даже использовать цитирование в стиле оболочки, если хотите. Оболочка должна решить, как лучше разбить командную строку на аргументы исполняемого файла и командной строки.

В Windows, если вы передадите список аргументов, он будет преобразован в строку с использованием тех же правил, что и среда выполнения MS C. Посмотрите строку документа для subprocess.list2cmdline для получения дополнительной информации об этом. В то время как в Unix-подобных системах, даже если вы передаете строку, она превращается в список из одного элемента :).

Поведение аргумента оболочки иногда может сбивать с толку, поэтому я попытаюсь немного прояснить это здесь. Что-то, что я хотел бы иметь, когда я впервые столкнулся с этим модулем.

Во-первых, давайте рассмотрим случай, когда оболочка имеет значение False, по умолчанию. В этом случае, если args является строкой, предполагается, что это имя исполняемого файла. Даже если он содержит пробелы. Учтите следующее.

subprocess.call('ls -l')

Это не сработает, потому что подпроцесс ищет исполняемый файл с именем ls -l, но, очевидно, не может его найти. Однако если args является списком, то первый элемент в этом списке считается исполняемым, а остальные элементы в списке передаются программе в качестве аргументов командной строки.

subprocess.call(['ls', '-l'])

делает то, что вы думаете, что будет.

subprocess.call(['ls', '-l'], shell=True)

Во втором случае, когда для оболочки установлено значение True, программа, которая фактически запускается, является оболочкой по умолчанию для ОС, / bin / sh в Linux и cmd.exe в Windows. Это можно изменить с помощью исполняемого аргумента.

При использовании оболочки args обычно является строкой, которая будет проанализирована программой оболочки. Строка args передается в качестве аргумента командной строки в оболочку (с параметром -c в Linux), так что оболочка интерпретирует ее как последовательность команд оболочки и обрабатывает ее соответствующим образом. Это означает, что вы можете использовать все встроенные функции и вкусности, которые предлагает ваша оболочка.

subprocess.call('ls -l', shell=True)

похож на

$ /bin/sh -c 'ls -l'

В том же духе, если вы передаете список в виде аргументов с установленной в True оболочкой, все элементы списка передаются в качестве аргументов командной строки в оболочку.

subprocess.call(['ls', '-l'], shell=True)

похож на

$ /bin/sh -c ls -l

который так же, как

$ /bin/sh -c ls

поскольку / bin / sh принимает только аргумент рядом с -c в качестве командной строки для выполнения.

Получение кода возврата (он же статус выхода)

Если вы хотите запустить внешнюю команду и ее код возврата — это все, что вас интересует, то функции call и check_call — это то, что вам нужно. Они оба возвращают код возврата после выполнения команды. Разница в том, что check_call вызывает CalledProcessError, если код возврата не равен нулю.

Если вы прочитали документы по этим функциям, вы увидите, что не рекомендуется использовать stdout = PIPE или stderr = PIPE. А если нет, то stdout и stderr команды просто перенаправляются в родительские потоки (в данном случае Python VM).

Если это не то, что вы хотите, вы должны использовать класс Popen.

proc = Popen('ls')

В момент создания класса Popen команда начинает работать. Вы можете подождать его и после его завершения получить доступ к коду возврата через атрибут returncode .

proc.wait()
print proc.returncode

Если вы попробуете это в python REPL, вы не увидите необходимости вызывать .wait (), так как вы можете просто подождать себя в REPL, пока команда не закончится, и затем получить доступ к коду возврата. Сюрприз!

>>> proc = Popen('ls')
>>> file1 file2

>>> print proc.returncode
None
>>> # wat?

Команда определенно закончена. Почему у нас нет кода возврата?

>>> proc.wait()
0
>>> print proc.returncode
0

Причиной этого является то, что код возврата не устанавливается автоматически после завершения процесса. Вы должны вызвать .wait или .poll, чтобы понять, завершена ли программа, и установить атрибут returncode.

IO Streams

Самый простой способ получить выходные данные команды, как было показано ранее, — использовать функцию check_output .

output = subprocess.check_output('ls')

Обратите внимание на префикс check_ в имени функции? Звонить в любой колокол? Это верно, эта функция вызовет CalledProcessError, если код возврата не равен нулю.

Это не всегда может быть лучшим решением для получения результата от команды. Если вы получили CalledProcessError от этого вызова функции, если у вас нет содержимого stderr, вы, вероятно, мало знаете, что пошло не так. Вы захотите узнать, что написано в stderr команды.

Ошибка чтения потока

Есть два способа получить сообщение об ошибке. Во-первых, это перенаправление stderr в stdout и только работа с stdout. Это можно сделать, установив аргумент stderr в subprocess.STDOUT .

Второе — создать объект Popen с установленным для stderr значением subprocess.PIPE (необязательно вместе с аргументом stdout) и считывать его атрибут stderr, который является читаемым файловым объектом. Существует также вспомогательный метод класса Popen, который называется .communicate, который необязательно принимает строку для отправки в stdin процесса и возвращает кортеж (stdout_content, stderr_content).

Смотря как stdout и stderr

Однако все они предполагают, что команда выполняется в течение некоторого времени, выводит пару строк вывода и завершается, поэтому вы можете получить выходные данные в строках. Иногда это не так. Если вы хотите запустить интенсивную по сети команду, такую ​​как svn checkout, которая печатает каждый файл по мере загрузки, вам нужно что-то лучше.

Первоначальное решение, о котором можно подумать, заключается в следующем.

proc = Popen('svn co svn+ssh://myrepo', stdout=PIPE)
for line in proc.stdout:
    print line

Это работает, по большей части. Но, опять же, если есть ошибка, вы тоже захотите прочитать stderr. Было бы неплохо читать stdout и stderr одновременно. Также как оболочка, кажется, делает. Увы, на сегодняшний день это остается не такой простой проблемой, по крайней мере, в системах, отличных от Linux.

В Linux (и там, где это поддерживается) вы можете использовать модуль select, чтобы следить за множеством файловых потоковых объектов. Но это не доступно на окнах. Я обнаружил, что более независимое от платформы решение работает с использованием потоков и очереди .

from subprocess import Popen, PIPE
from threading import Thread
from Queue import Queue, Empty

io_q = Queue()

def stream_watcher(identifier, stream):

    for line in stream:
        io_q.put((identifier, line))

    if not stream.closed:
        stream.close()

proc = Popen('svn co svn+ssh://myrepo', stdout=PIPE, stderr=PIPE)

Thread(target=stream_watcher, name='stdout-watcher',
        args=('STDOUT', proc.stdout)).start()
Thread(target=stream_watcher, name='stderr-watcher',
        args=('STDERR', proc.stderr)).start()

def printer():
    while True:
        try:
            # Block for 1 second.
            item = io_q.get(True, 1)
        except Empty:
            # No output in either streams for a second. Are we done?
            if proc.poll() is not None:
                break
        else:
            identifier, line = item
            print identifier + ':', line

Thread(target=printer, name='printer').start()

Немного кода. Это типичная вещь производитель-потребитель. Два потока производят строки вывода (по одному из stdout и stderr) и помещают их в очередь. Один поток наблюдает за очередью и печатает строки, пока сам процесс не завершится.

Передача окружающей среды

Аргумент env для Popen (и других) позволяет вам настроить среду запускаемой команды. Если он не задан или имеет значение «Нет», используется среда текущего процесса, как описано в документации.

Вы можете не согласиться со мной, но я чувствую некоторые тонкости с этим аргументом, которые должны были быть упомянуты в документации.

Объединить с текущей средой

Во-первых, если вы предоставляете сопоставление для env, все, что находится в этом сопоставлении, — это все, что доступно для запускаемой команды. Например, если вы не зададите TOP_ARG в отображении env, команда не увидит TOP_ARG в своей среде. Итак, я часто оказываюсь этим

p = Popen('command', env=dict(os.environ, my_env_prop='value'))

Это имеет смысл, как только вы поймете это, но я бы хотел, чтобы это было хотя бы намекало в документации.

Unicode

Еще один, это связано с Unicode (сюрприз-сюрприз!). И окна. Если вы используете юникоды в отображении env, вы получите сообщение об ошибке, в котором говорится, что вы можете использовать только строки в отображении среды. Хуже всего то, что эта ошибка возникает только в Windows, а не в Linux. Если использование юникодов в этом месте является ошибкой, я бы хотел, чтобы он ломался на обеих платформах.

Этот вопрос является очень болезненным , если вы похожи на меня и использование Юникода все время .

from __future__ import unicode_literals

Эта строка присутствует во всех моих исходных файлах Python. В сообщении об ошибке даже не упоминается, что в вашей среде enico есть юникоды, поэтому очень трудно понять, что происходит не так.

Выполнить в другом рабочем каталоге

Это обрабатывается аргументом cwd. Вы устанавливаете местоположение каталога, который вы хотите, в качестве рабочего каталога программы, которую вы запускаете.

В документах упоминается, что рабочий каталог изменяется еще до того, как команда начинает выполняться. Но вы не можете указать путь к программе относительно cwd. На самом деле я обнаружил, что вы можете сделать это.

Либо я что-то упустил с этим, либо документы действительно неточны. Во всяком случае, это работает

subprocess.call('./ls', cwd='/bin')

Распечатывает все файлы в / bin. Конечно, следующее не работает, если рабочим каталогом не является / bin.

subprocess.call('./ls')

Так что, если вы даете что-то явно в cwd и используете относительный путь к исполняемому файлу, об этом следует помнить.

Убийство и смерть

Простой

proc.terminate()

или для какой-то драматической эмфы!

proc.kill()

сделает трюк, чтобы закончить процесс. Как отмечено в документации, первый отправляет SIGTERM, а затем отправляет SIGKILL в Unix, но оба выполняют некоторые собственные функции Windows в Windows.

Авто-убийство на Смерти

Процессы, которые вы запускаете в своей программе на Python, продолжают работать даже после выхода из программы. Это , как правило , то , что вы хотите, но если вы хотите , чтобы все ваши вложенные процессы убитых автоматически при выходе с помощью Ctrl + C или тому подобное, вы должны использовать atexit модуль.

procs = []

@atexit.register
def kill_subprocesses():
    for proc in procs:
        proc.kill()

И добавьте все объекты Popen, созданные в список процедур. Это единственное решение, которое я нашел, которое работает лучше всего.

Команды запуска в эмуляторе терминала

Однажды мне пришлось написать скрипт, который запускал бы несколько проверок svn, а затем запускал много сборок ant (~ 20-35) для извлеченных проектов. На мой взгляд, лучший и самый простой способ сделать это — запустить несколько окон эмулятора терминала, каждое из которых запускает отдельную проверку / сборку муравья. Это позволяет нам отслеживать каждый процесс и даже отменять любой из них, просто закрывая соответствующее окно эмулятора терминала.

Linux

Это довольно тривиально на самом деле. В Linux вы можете использовать xterm для этого.

Popen(['xterm', '-e', 'sleep 3s'])

Windows

На окнах это не так просто. Первое решение для этого было бы

Popen(['cmd', '/K', 'command'])

 

Параметр / K указывает cmd запускать команду и не закрывать окно команд. Вместо этого вы можете использовать / C, чтобы закрыть окно команды после ее завершения.

Как бы просто это ни выглядело, у него странное поведение. Я не совсем понимаю, но я постараюсь объяснить, что у меня есть. Когда вы пытаетесь запустить скрипт Python с помощью вышеуказанного вызова Popen, в командном окне, как это

python main.py

Вы не видите всплывающее окно новой команды. Вместо этого подкоманда запускается в том же командном окне. Я понятия не имею, что происходит, когда вы запускаете несколько подкоманд таким образом. (У меня есть только ограниченный доступ к окнам).

Если вместо этого вы запустите его в чем-то вроде IDE или IDLE (F5), у вас откроется новое командное окно. Я считаю, что по одному на каждую команду, которую вы запускаете таким образом. Просто так, как вы ожидаете.

Но я отказался от cmd.exe для этой цели и научился использовать утилиту mintty, которая поставляется с cygwin (я думаю, 1.7+). мятный это круто. В самом деле. Давненько я так не думал об утилите командной строки в Windows.

Popen(['mintty', '--hold', 'error', '--exec', 'command'])

Этот. При запуске команды открывается новое окно консоли mintty, и оно автоматически закрывается, если команда завершает работу с нулевым статусом (это то, что делает —hold error). В противном случае он остается включенным. Очень полезно.

Вывод

Модуль подпроцесса — очень полезная вещь. Потратьте некоторое время на понимание этого лучше. Это моя попытка помочь людям с этим, и она оказалась намного дольше, чем я ожидал. Если в этом есть какие-либо неточности или если у вас есть что добавить, оставьте комментарий.