Знание того, как выполнить команду оболочки в Python, поможет вам создавать программы для автоматизации задач в вашей системе.
Существует несколько способов выполнить команду оболочки в Python. Простейшие из них используют функции os.system
и os.popen
. Рекомендуемым модулем для запуска команд оболочки является модуль Python subprocess из-за его гибкости в предоставлении вам доступа к стандартному выводу, стандартной ошибке и конвейеризации команд.
Мы начнем это руководство с модуля os, а затем перейдем к модулю subprocess.
Это даст вам полное понимание того, как обрабатывать команды оболочки в Python.
Давайте начнем кодировать!
Использование ОС для выполнения команды в Python
Я создал простой скрипт Python под названием shell_command.py.
Он использует функцию system модуля os для запуска команды Linux date:
import os
os.system('date')
Это вывод функции os.system()
:
$ python shell_command.py
Sun Feb 21 16:01:43 GMT 2021
Давайте посмотрим, что произойдет, если мы запустим тот же код в оболочке Python:
>>> import os
>>> os.system('date')
Sun Feb 21 16:01:43 GMT 2021
0
Мы по-прежнему видим вывод команды date
, но в последней строке мы также видим 0. Это код выхода команды Linux.
Успешно выполненная команда в Linux возвращает код завершения 0, а в случае неудачи возвращается ненулевой код завершения.
Давайте подтвердим это, допустив орфографическую ошибку в команде date
:
>>> os.system('daet')
>>> sh: daet: command not found
>>> 32512
Обратите внимание, как статус выхода отличается от статуса, возвращаемого оболочкой Bash:
$ daet
-bash: daet: command not found
$ echo $?
127
Далее в этой статье мы сравним os.system и другой модуль Python, называемый subprocess.
Использование OS Popen для выполнения команд
Используя os.system()
мы не можем сохранить вывод команды Linux в переменную. И это одна из самых полезных вещей, которую можно сделать, когда вы пишете скрипт.
Обычно вы запускаете команды, сохраняете их вывод в переменной, а затем реализуете в своем скрипте некоторую логику, которая выполняет необходимые вам действия с этой переменной (например, фильтрует ее на основе определенных критериев).
Чтобы сохранить вывод команды в переменной, можно использовать функцию os.popen()
.
Функция popen возвращает открытый файловый объект, и для чтения его значения можно использовать метод read:
>>> import os
>>> output = os.popen('date')
>>> type(output)
<class 'os._wrap_close'>
>>> print(output.__dict__)
{'_stream': <_io.TextIOWrapper name=3 encoding='UTF-8'>, '_proc': }
>>> output.read()
'Sun Feb 21 16:01:43 GMT 2021\n'
Посмотрите, что произойдет, если мы снова применим метод чтения к тому же объекту:
>>> output.read()
''
Результатом является пустая строка, поскольку мы можем прочитать объект файла только один раз.
Мы можем использовать os.popen
и функцию чтения в одной строке:
>>> import os
>>> print(os.popen('date').read())
Sun Feb 21 16:01:43 GMT 2021
При выполнении команд оболочки в Python важно понимать, была ли команда выполнена успешно или нет.
Для этого мы можем использовать метод close объекта файла, возвращаемого os.popen
. Метод close возвращает None, если команда выполнена успешно. Он предоставляет код возврата подпроцесса в случае ошибки.
Успешный сценарий
>>> output = os.popen('date')
>>> print(output.close())
None
Неудачный сценарий
>>> output = os.popen('daet')
>>> /bin/sh: daet: command not found
>>> print(output.close())
32512
Мы можем использовать значение output.close()
для обработки ошибок в наших скриптах Python.
Имеет ли это смысл?
Ожидают ли ОС System и ОС Popen завершения команды?
Прежде чем перейти к другому способу выполнения команд оболочки в Python, я хочу увидеть поведение os.system()
и os.popen()
с командой, выполнение которой занимает несколько секунд.
Мы будем использовать команду ping
с флагом -c
, чтобы остановить выполнение команды после определенного количества пакетов ECHO_RESPONSE (в этом примере 5):
$ ping -c 5 localhost
Выполнение с помощью os.system
>>> os.system('ping -c 5 localhost')
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.051 ms
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.068 ms
64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.091 ms
64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.066 ms
64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.063 ms
--- localhost ping statistics ---
5 packets transmitted, 5 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 0.051/0.068/0.091/0.013 ms
0
При выполнении команды с помощью os.system()
мы видим вывод для каждой попытки ping
, выводимый по одному за раз, точно так же, как мы бы видели это в оболочке Linux.
Выполнение с помощью os.popen
>>> os.popen('ping -c 5 localhost')
<os._wrap_close object at 0x10bc8a190>
>>> os.popen('ping -c 5 localhost').read()
'PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.055 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.059 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.073 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.135 ms\n64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.077 ms\n\n--- localhost ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.055/0.080/0.135/0.029 ms\n'
При использовании os.popen()
мы не видим вывод в оболочке Python немедленно.
Вывод буферизуется, окончательный вывод мы видим только после завершения команды ping
.
Функция os.popen ожидает завершения команды, прежде чем предоставить полный вывод.
Совсем скоро мы рассмотрим разницу между функцией Popen модуля ОС и функцией Popen модуля subprocess.
Методы Read, Readline и Readlines, применяемые к выходным данным OS.Popen
Мы увидели, что:
os.popen()
возвращает открытый файловый объект.- мы можем прочитать содержимое объекта с помощью метода read().
В этом разделе мы сравним поведение методов read()
, readline()
и readlines()
, применяемых к файловому объекту, возвращаемому os.popen
.
Мы уже знаем, что метод read возвращает вывод команды сразу после завершения ее выполнения.
Давайте посмотрим, что происходит с методом readline:
>>> output = os.popen('ping -c 5 localhost')
>>> output.readline()
'PING localhost (127.0.0.1): 56 data bytes\n'
>>> output.readline()
'64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.047 ms\n'
>>> output.readline()
'64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.067 ms\n'
>>> output.readline()
'64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.055 ms\n'
>>> output.readline()
'64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.103 ms\n'
>>> output.readline()
'64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.058 ms\n'
>>> output.readline()
'\n'
>>> output.readline()
'--- localhost ping statistics ---\n'
>>> output.readline()
'5 packets transmitted, 5 packets received, 0.0% packet loss\n'
>>> output.readline()
'round-trip min/avg/max/stddev = 0.047/0.066/0.103/0.020 ms\n'
>>> output.readline()
''
>>> output.readline()
''
С помощью метода readline мы можем выводить вывод команды по одной строке за раз, пока не достигнем конца открытого файлового объекта.
Вот как можно использовать метод readline с циклом while для вывода полного вывода команды:
import os
output = os.popen('ping -c 5 localhost')
while True:
line = output.readline()
if line:
print(line, end='')
else:
break
output.close()
С другой стороны, метод readlines ожидает завершения команды и возвращает список Python:
>>> output = os.popen('ping -c 5 localhost')
>>> output.readlines()
['PING localhost (127.0.0.1): 56 data bytes\n', '64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.044 ms\n', '64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.095 ms\n', '64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.057 ms\n', '64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.078 ms\n', '64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.094 ms\n', '\n', '--- localhost ping statistics ---\n', '5 packets transmitted, 5 packets received, 0.0% packet loss\n', 'round-trip min/avg/max/stddev = 0.044/0.074/0.095/0.020 ms\n']
А с помощью простого цикла for
мы можем вывести полный вывод команды, пройдя по всем элементам в списке, возвращаемом методом readlines()
:
import os
output = os.popen('ping -c 5 localhost')
for line in output.readlines():
print(line, end='')
Все ясно? 🙂
Запуск команд оболочки в Python с помощью subprocess.run
В предыдущем разделе мы увидели, как запустить команду date
с помощью os.system
и os.popen
.
Теперь вы узнаете, как использовать модуль subprocess для выполнения той же команды.
Существует несколько вариантов запуска команд с помощью subprocess, и я начну с рекомендуемого варианта, если вы используете Python 3.5 или более позднюю версию: subprocess.run
.
Давайте передадим команду date в subprocess.run():
>>> import subprocess
>>> subprocess.run('date')
Sun Feb 21 21:44:53 GMT 2021
CompletedProcess(args='date', returncode=0)
Как видите, команда возвращает объект типа CompletedProcess (на самом деле это subprocess.CompletedProcess).
Давайте посмотрим, что произойдет, если я также передам флаг +%a
команде date
(она должна показать день недели):
import subprocess
subprocess.run('date +%a')
Видим следующую ошибку:
$ python subprocess_example.py
Traceback (most recent call last):
File "subprocess_example.py", line 3, in <module>
subprocess.run('date +%a')
File "/Users/codefather/opt/anaconda3/lib/python3.7/subprocess.py", line 472, in run
with Popen(*popenargs, **kwargs) as process:
File "/Users/codefather/opt/anaconda3/lib/python3.7/subprocess.py", line 775, in __init__
restore_signals, start_new_session)
File "/Users/codefather/opt/anaconda3/lib/python3.7/subprocess.py", line 1522, in _execute_child
raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'date +%a': 'date +%a'
Один из способов заставить эту команду работать — передать параметр shell=True
в subprocess.run()
:
import subprocess
subprocess.run('date +%a', shell=True)
Попробуйте и убедитесь, что команда работает так, как и ожидалось.
При передаче параметра shell=True
команда будет вызвана через оболочку.
Примечание: помните о соображениях безопасности, связанных с использованием параметра оболочки.
Когда вы выполняете команду, функция run()
ожидает завершения команды. Я расскажу вам об этом подробнее в одном из следующих разделов.
Если мы хотим выполнить команду «date +%a
» без передачи shell=True
, нам придется передать дату и ее флаги как отдельные элементы массива.
import subprocess
subprocess.run(['date', '+%a'])
[output]
Sun
CompletedProcess(args=['date', '+%a'], returncode=0)
Захват вывода команды с помощью subprocess.run
До сих пор мы выводили вывод команды в оболочке.
Но что, если мы хотим сохранить вывод команды в переменной?
Можем ли мы просто добавить переменную в левую часть вызова subprocess.run()
?
Давайте выясним…
import subprocess
process_output = subprocess.run(['date', '+%a'])
print(process_output)
[output]
Sun
CompletedProcess(args=['date', '+%a'], returncode=0)
Мы по-прежнему видим вывод команды в оболочке, а оператор print
показывает, что переменная process_output является объектом CompletedProcess.
Давайте выясним атрибуты объекта…
Чтобы увидеть пространство имен, связанное с этим объектом Python, мы можем использовать метод __dict__
.
print(process_output.__dict__)
[output]
{'args': ['date', '+%a'], 'returncode': 0, 'stdout': None, 'stderr': None}
Вы можете увидеть атрибуты, в которых хранятся аргументы, код возврата, стандартный вывод и стандартная ошибка.
Код возврата для последней выполненной нами команды — ноль. Еще раз. код возврата для успешно выполненной команды — ноль.
Давайте посмотрим, какой будет код возврата, если мы допустим синтаксическую ошибку в команде:
process_output = subprocess.run(['date', '%a'])
Вот ошибка, которую мы видим после передачи неверного флага команде date
:
$ python subprocess_example.py
date: illegal time format
usage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] …
[-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
{'args': ['date', '%a'], 'returncode': 1, 'stdout': None, 'stderr': None}
Код возврата — 1 (ненулевые коды возврата указывают на сбой).
Кроме того, stdout имеет значение None, поскольку вывод команды отправляется на терминал.
Как можно сохранить stdout в переменной?
В официальной документации subprocess я вижу следующее:
Итак, давайте выясним, что произойдет, если мы установим capture_output
в значение True
…
process_output = subprocess.run(['date', '+%a'], capture_output=True)
Запустив эту команду, вы не увидите вывод команды, напечатанный в оболочке, если только не используете оператор print для отображения значения переменной process_output:
>>> import subprocess
>>> process_output = subprocess.run(['date', '+%a'], capture_output=True)
>>> print(process_output)
CompletedProcess(args=['date', '+%a'], returncode=0, stdout=b'Sun\n', stderr=b'')
На этот раз вы можете видеть, что значение stdout и stderr больше не равно None:
- Стандартный вывод содержит вывод команды.
- Стандартная ошибка — пустая строка, поскольку команда date была выполнена успешно.
Давайте также убедимся, что stderr не пуст в случае ошибки:
>>> import subprocess
>>> process_output = subprocess.run(['date', '%a'], capture_output=True)
>>> print(process_output)
CompletedProcess(args=['date', '%a'], returncode=1, stdout=b'', stderr=b'date: illegal time format\nusage: date [-jnRu] [-d dst] [-r seconds] [-t west] [ v[+|-]val[ymwdHMS]]... \n [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]\n')
Ошибка сохраняется в process_output.stderr.
Формат стандартного вывода и стандартной ошибки команды
В последней команде мы увидели, что stdout и stderr имеют не очень удобный для чтения формат.
Это связано с тем, что они оба захвачены как байты (обратите внимание на все символы новой строки в значениях stdout и stderr).
А что, если мы хотим увидеть их в том же формате, что и вывод команды в оболочке?
Мы можем передать дополнительный текст параметра в метод subprocess.run:
>>> import subprocess
>>> process_output = subprocess.run(['date', '+%a'], capture_output=True, text=True)
>>> print(process_output.stdout)
Sun
Примечание: параметр text был введен в Python 3.7 как более понятная альтернатива параметру universal_newlines.
В качестве альтернативы вы также можете преобразовать байты в строку, используя метод декодирования:
>>> import subprocess
>>> process_output = subprocess.run(['date', '+%a'], capture_output=True)
>>> print(process_output.stdout)
b'Sun\n'
>>> print(process_output.stdout.decode())
Sun
Видите ли вы разницу в формате stdout с декодированием и без него?
Ранее в определении параметра capture_output
мы видели, что его передача эквивалентна передаче stdout=PIPE
и stderr=PIPE
.
Давайте попробуем использовать их, чтобы убедиться, что результат тот же самый…
>>> import subprocess
>>> process_output = subprocess.run(['date', '+%a'], stdout=PIPE, stderr=PIPE, text=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'PIPE' is not defined
У нас возникла ошибка «name ‘PIPE’ is not defined». Почему?
Как вы можете видеть из определения ниже, взятого из официальной документации подпроцесса, PIPE является частью модуля подпроцесса. Это означает, что мы должны использовать subprocess.PIPE в нашей программе.
>>> import subprocess
>>> process_output = subprocess.run(['date', '+%a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
>>> print(process_output.stdout)
Sun
Теперь выглядит лучше 🙂
Как захватить стандартный вывод и стандартную ошибку в один поток
Чтобы захватить стандартный вывод и стандартную ошибку в один поток, нам нужно установить stdout в subprocess.PIPE, а stderr в subprocess.STDOUT:
>>> process_output = subprocess.run(['date', '+%a'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
Вывод:
>> print(process_output.__dict__)
{'args': ['date', '+%a'], 'returncode': 0, 'stdout': 'Sun\n', 'stderr': None}
В stdout содержится вывод, а значение stderr равно None.
А что, если при выполнении команды произойдет ошибка?
>>> process_output = subprocess.run(['date', '%a'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
>>> print(process_output.__dict__)
{'args': ['date', '%a'], 'returncode': 1, 'stdout': 'date: illegal time format\nusage: date [-jnRu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] … \n [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]\n', 'stderr': None}
Как и ожидалось, ошибка является частью потока stdout. Значение атрибута stderr по-прежнему None.
Запись вывода команды в файл на Python
Вы также можете записать вывод команды в файл.
Давайте посмотрим, как использовать оператор with
в Python …
with open('command.out', 'w') as stdout_file:
process_output = subprocess.run(['date', '+%a'], stdout=stdout_file, stderr=subprocess.PIPE, text=True)
print(process_output.__dict__)
Обратите внимание, что на этот раз значение stdout равно None, учитывая, что мы отправляем stdout в файл.
$ python subprocess_example.py
{'args': ['date', '+%a'], 'returncode': 0, 'stdout': None, 'stderr': ''}
$ ls -ltr
total 16
-rw-r--r-- 1 myuser mygroup 208 Feb 21 23:45 subprocess_example.py
-rw-r--r-- 1 myuser mygroup 4 Feb 21 23:46 command.out
$ cat command.out
Sun
С помощью команд ls
и cat
мы подтверждаем, что файл command.out был создан и содержит вывод команды, выполненной в нашей программе Python.
А как насчет записи стандартной ошибки в файл?
Для этого мы можем открыть два файла, используя оператор Python with
.
with open('command.out', 'w') as stdout_file, open('command.err', 'w') as stderr_file:
process_output = subprocess.run(['date', '+%a'], stdout=stdout_file, stderr=stderr_file, text=True)
print(process_output.__dict__)
На этот раз и stdout, и stderr установлены в значение None, и оба файла создаются при вызове команды (файл command.err пуст, поскольку команда выполнена успешно).
$ python subprocess_example.py
{'args': ['date', '+%a'], 'returncode': 0, 'stdout': None, 'stderr': None}
$ ls -ltr
total 16
-rw-r--r-- 1 myuser mygroup 245 Feb 21 23:53 subprocess_example.py
-rw-r--r-- 1 myuser mygroup 0 Feb 21 23:55 command.err
-rw-r--r-- 1 myuser mygroup 4 Feb 21 23:55 command.out
Прежде чем продолжить, попробуйте выполнить команду с неправильным синтаксисом и убедитесь, что ошибка записана в файл command.err.
Перенаправить вывод команды в /dev/null
Возможно, вам потребуется перенаправить вывод команды в /dev/null.
Для этого мы можем использовать специальное значение, предоставленное модулем подпроцесса: DEVNULL.
process_output = subprocess.run(['date', '%a'], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT, text=True)
print(process_output)
Объект, возвращаемый subprocess.run, не включает атрибуты stdout и stderr:
$ python subprocess_example.py
CompletedProcess(args=['date', '%a'], returncode=1)
Как выдать исключение Python при сбое команды оболочки
В одном из предыдущих примеров мы видели, что произойдет, если запустить команду с неправильным синтаксисом:
>>> process_output = subprocess.run(['date', '%a'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
Ошибка сохраняется в потоке stderr, но Python не вызывает никаких исключений.
Чтобы иметь возможность отлавливать эти ошибки, мы можем передать параметр check=True
в subprocess.run.
>>> process_output = subprocess.run(['date', '%a'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=True)
Traceback (most recent call last):
File "", line 1, in
File "/Users/codefather/opt/anaconda3/lib/python3.7/subprocess.py", line 487, in run
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['date', '%a']' returned non-zero exit status 1.
Python вызывает исключение subprocess.CalledProcessError, которое мы можем перехватить как часть блока try и except.
import subprocess
try:
process_output = subprocess.run(['date', '%a'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=True)
except subprocess.CalledProcessError:
print("Error detected while executing the command")
Теперь мы можем лучше обрабатывать ошибки:
$ python subprocess_example.py
Error detected while executing the command
Хорошо 🙂
Как запустить несколько команд с помощью подпроцесса
В Linux очень часто используется конвейер для отправки вывода команды в качестве ввода другой команды.
Мы увидим, как можно сделать то же самое в Python с помощью подпроцесса.
Мы выполним первую команду таким же образом, как и раньше, а затем выполним вторую команду, которая получит дополнительный параметр input.
Значение ввода будет установлено на стандартный вывод первой команды.
Проще показать это на примере…
Я создал файл, содержащий шесть строк:
$ cat test_file
line1
line2
line3
line4
line5
line6
И я хочу выполнить следующую команду на Python:
$ wc -l test_file | awk '{print $1}'
6
Нам придется взять вывод команды wc
и передать его в качестве входных данных команды awk
.
import subprocess
wc_cmd = subprocess.run(['wc', '-l', 'test_file'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("wc_cmd object: {}".format(wc_cmd.__dict__))
awk_cmd = subprocess.run(['awk', '{print $1}'], input=wc_cmd.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print("awk_cmd object: {}".format(awk_cmd.__dict__))
print("The ouput of the command is: {}".format(awk_cmd.stdout.decode()))
Вы можете увидеть две команды, выполненные с помощью subprocess.run.
Мы также передаем входной параметр для выполнения второй команды (awk), а его значение устанавливается на стандартный вывод первой команды (wc).
Окончательный результат:
$ python subprocess_example.py
wc_cmd object: {'args': ['wc', '-l', 'test_file'], 'returncode': 0, 'stdout': b' 6 test_file\n', 'stderr': b''}
awk_cmd object: {'args': ['awk', '{print $1}'], 'returncode': 0, 'stdout': b'6\n', 'stderr': b''}
The ouput of the command is: 6
Мы также могли бы выполнить команду с помощью одного вызова subprocess.run, передав shell=True
:
>>> import subprocess
>>> wc_awk_cmd = subprocess.run("wc -l test_file | awk '{print $1}'", shell=True)
6
shlex.split и модуль подпроцесса
До сих пор мы видели, что для запуска команды с помощью subprocess.run нам необходимо передать список, где первым элементом является команда, а остальными элементами являются флаги, которые вы обычно передаете в оболочке, разделенные пробелом.
Для длинной команды может быть утомительно создавать этот список вручную. Решением для этого является модуль shlex, а именно функция split
.
Давайте возьмем в качестве примера команду wc, которую мы использовали в предыдущем разделе:
wc -l test_file
Вот что произойдет, если применить shlex.split к этой строке:
>>> import shlex
>>> shlex.split('wc -l test_file')
['wc', '-l', 'test_file']
Именно такой формат аргумента нам нужно передать в subprocess.run.
Пришло время запустить нашу команду с помощью shlex.split:
import subprocess, shlex
cmd = 'wc -l test_file'
wc_cmd = subprocess.run(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(wc_cmd.__dict__)
[output]
{'args': ['wc', '-l', 'test_file'], 'returncode': 0, 'stdout': b' 6 test_file\n', 'stderr': b''}
Переменные среды оболочки печати в Python
Возможно, вам захочется использовать переменные среды оболочки в вашей программе Python.
Давайте узнаем, как это сделать…
>>> import subprocess
>>> echo_cmd = subprocess.run(['echo', '$SHELL'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> print(echo_cmd.__dict__)
{'args': ['echo', '$SHELL'], 'returncode': 0, 'stdout': b'$SHELL\n', 'stderr': b''}
Когда я пытаюсь выполнить «echo $SHELL
» с помощью subprocess.run, на стандартном выводе отображается просто строка $SHELL
.
Наша программа не разрешает значение переменной окружения $SHELL
. Чтобы сделать это, нам нужно использовать os.path.expandvars("$SHELL")
.
>>> import os
>>> echo_cmd = subprocess.run(['echo', os.path.expandvars("$SHELL")], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> print(echo_cmd.__dict__)
{'args': ['echo', '/bin/bash'], 'returncode': 0, 'stdout': b'/bin/bash\n', 'stderr': b''}
Использование подпроцесса с SSH
Вы также можете использовать подпроцесс для выполнения команд на удаленной системе через SSH.
Вот как это сделать:
import subprocess, shlex
cmd = "ssh -i ~/.ssh/id_rsa youruser@yourhost"
ssh_cmd = subprocess.Popen(shlex.split(cmd), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
ssh_cmd.stdin.write("date")
ssh_cmd.stdin.close()
print(ssh_cmd.stdout.read())
Посмотрите, как мы используем стандартный ввод для вызова команды date через SSH.
А вот вывод скрипта:
$ python subprocess_example.py
Mon 22 Feb 11:58:50 UTC 2021
Subprocess.run против Subprocess.call
В версиях Python до 3.5 subprocess.run() отсутствует. Вместо него можно использовать subprocess.call()
.
Вот что говорится в официальной документации о функции вызова…
Давайте используем subprocess.call для запуска команды ping, которую мы видели ранее в этом руководстве. На мгновение я предполагаю, что могу запустить команду с помощью subprocess.call, используя тот же синтаксис, что и subprocess.run.
Давайте проверим, правда ли это…
import subprocess, shlex
cmd = 'ping -c 5 localhost'
ping_cmd = subprocess.call(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(ping_cmd)
Но я получаю сообщение об ошибке:
$ python subprocess_example.py
Traceback (most recent call last):
File "subprocess_example.py", line 5, in <module>
print(ping_cmd.__dict__)
AttributeError: 'int' object has no attribute '__dict__'
Это потому, что из subprocess.call мы не получаем обратно объект, а только целое число для кода возврата:
>>> import subprocess, shlex
>>> cmd = 'ping -c 5 localhost'
>>> ping_cmd = subprocess.call(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> print(ping_cmd)
0
Просматривая документацию subprocess.call, я замечаю следующее сообщение:
Как же нам тогда получить вывод subprocess.call?
Официальная документация предполагает, что если вам нужно захватить stdout или stderr, вам следует использовать subprocess.run()
.
Прежде чем закрыть этот раздел, я хотел бы быстро рассмотреть subprocess.check_call, который также присутствует в документации.
Но потом я понял, что в этом случае документация предлагает использовать run()
.
Subprocess.run против Subprocess.Popen
В последнем разделе этого руководства мы протестируем альтернативу subprocess.run: subprocess.Popen.
Причина, по которой я хочу вам это показать, заключается в том, что в поведении subprocess.Popen есть кое-что весьма интересное.
Начнем с запуска команды ping, которую мы уже использовали в других примерах, с помощью subprocess.run:
>>> import subprocess, shlex
>>> cmd = 'ping -c 5 localhost'
>>> ping_cmd = subprocess.run(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> print(ping_cmd.__dict__)
{'args': ['ping', '-c', '5', 'localhost'], 'returncode': 0, 'stdout': b'PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.075 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.056 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.158 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.065 ms\n64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.074 ms\n\n--- localhost ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.056/0.086/0.158/0.037 ms\n', 'stderr': b''}
Subprocess.run ожидает завершения команды, когда вы нажимаете ENTER, чтобы выполнить строку, вызывающую subprocess.run в оболочке Python (я предлагаю запустить это на своей машине, чтобы увидеть это поведение).
Теперь давайте выполним ту же команду, используя subprocess.Popen…
>>> ping_cmd = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> print(ping_cmd.__dict__)
{'_waitpid_lock': <unlocked _thread.lock object at 0x102e60f30>, '_input': None, '_communication_started': False, 'args': ['ping', '-c', '5', 'localhost'], 'stdin': None, 'stdout': <_io.BufferedReader name=3>, 'stderr': <_io.BufferedReader name=5>, 'pid': 35340, 'returncode': None, 'encoding': None, 'errors': None, 'text_mode': None, '_sigint_wait_secs': 0.25, '_closed_child_pipe_fds': True, '_child_created': True}
Subprocess.Popen возвращается немедленно при нажатии ENTER в оболочке Python. Кроме того, возвращаемый объект сильно отличается от объекта subprocess.run.
Чтобы получить стандартный вывод и стандартную ошибку, нам нужно использовать функцию communication()
, которая возвращает кортеж, в котором первым элементом является stdout, а вторым элементом — stderr.
>>> stdout, stderr = ping_cmd.communicate()
>>> print(stdout)
b'PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.060 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.061 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.059 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.103 ms\n64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.119 ms\n\n--- localhost ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.059/0.080/0.119/0.025 ms\n'
>>> print(stderr)
b''
Вернемся к тому факту, что subprocess.Popen был выполнен немедленно (неблокирующим образом) в оболочке Python, даже если команда ping не была завершена немедленно.
С помощью subprocess.Popen мы можем опросить статус длительно выполняемой команды, вот как:
import subprocess, shlex, time
cmd = 'ping -c 5 localhost'
ping_cmd = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while True:
return_code = ping_cmd.poll()
print("Return code: {}".format(return_code))
if return_code is not None:
break
else:
time.sleep(1)
print("Command in progress...\n")
print("Command completed with return code: {}".format(return_code))
print("Command output: {}".format(ping_cmd.stdout.read()))
Функция poll() возвращает None во время выполнения команды.
Мы можем использовать это для выхода из цикла while
только после завершения выполнения команды, основываясь на том факте, что код возврата не равен None.
$ python subprocess_example.py
Return code: None
Command in progress...
Return code: None
Command in progress...
Return code: None
Command in progress...
Return code: None
Command in progress...
Return code: None
Command in progress...
Return code: 0
Command completed with return code: 0
Command output: b'PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.068 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.066 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.088 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.095 ms\n64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.071 ms\n\n--- localhost ping statistics ---\n5 packets transmitted, 5 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 0.066/0.078/0.095/0.012 ms\n'
Имеет ли это смысл?
Заключение
Мы рассмотрели множество различных способов выполнения команд оболочки в Python.
Отличная работа по завершению этого урока!
Рекомендуемый способ вызова команд оболочки — это определенно subprocess.run, если только вы не используете Python 3.5+. В этом случае вы можете использовать subprocess.Popen.
Чтобы привыкнуть к синтаксису модуля подпроцесса, нужна практика, поэтому обязательно попробуйте примеры, рассмотренные в этом руководстве, на своей собственной машине.
Удачного кодирования! 😀