Как выполнить команду shell в Python

Знание того, как выполнить команду оболочки в 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 я вижу следующее:

Команда оболочки в Python - subprocess.run

Итак, давайте выясним, что произойдет, если мы установим 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 в нашей программе.

Команда оболочки в Python - 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().

Вот что говорится в официальной документации о функции вызова…

Команда оболочки в Python - 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, я замечаю следующее сообщение:

Подпроцесс Python - stdout=PIPE или stderr=PIPE

Как же нам тогда получить вывод 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.

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

Удачного кодирования! 😀

Автор

Фото аватара

Владимир Михайлов

Программист на Python с большим количеством опыта и разнообразных проектов.