Как вызывать системные команды из скриптов Python
Основные подходы к вызову команд из Python
В этой части статьи рассматриваются различные способы запуска внешних процессов из Python. Каждый вариант сопровождается вопросом, на который он отвечает, описанием, примером кода и типичными проблемами.
Какое решение является наиболее современным и рекомендуемым для вызова команд?
Начиная с Python 3.5, основным и предпочтительным способом является использование функции subprocess.run(). Она заменяет устаревшие os.system() и os.popen(), предоставляя единый интерфейс для запуска процессов, захвата вывода, проверки кода возврата и обработки ошибок.
import subprocess
# Запуск команды 'ls -la' в текущем каталоге
result = subprocess.run(['ls', '-la'], capture_output=True, text=True)
print(result.stdout)Python call command (вызов команд из python)
Пояснение: аргумент capture_output=True позволяет захватить stdout и stderr. text=True возвращает строки (а не байты). После завершения result содержит stdout, stderr и returncode. Если код возврата не равен нулю, можно вызвать исключение с помощью check=True.
Типичная ошибка:
Забывают указать shell=True при передаче команды строкой, или наоборот передают список, ожидая shell-синтаксиса. При передаче списка ['ls', '-la'] shell не используется, что безопаснее. Если требуется shell (например, для перенаправлений), нужно использовать shell=True и передавать команду строкой, но это может быть небезопасно при вводе пользователя.
Как выполнить команду, дождаться её завершения и получить только код возврата?
Функция os.system(command) запускает подпроцесс через командную оболочку и возвращает код завершения (целое число). Вывод команды направляется в терминал Python.
import os
retcode = os.system('mkdir /tmp/test_dir')
print(f'Код возврата: {retcode}')Python pip commands (команды pip)
Проблема:
Нет возможности захватить вывод команды, и код возврата не всегда надёжен в Windows. Также os.system использует простую оболочку, что может привести к проблемам с экранированием.
Как выполнить команду и получить её вывод в виде строки?
Функция subprocess.check_output() (доступна в Python 2.7+/3.x) выполняет команду и возвращает её stdout (в виде байтов или строки при использовании universal_newlines=True). Если код возврата не ноль, выбрасывается CalledProcessError.
import subprocess
output = subprocess.check_output(['echo', 'Hello'], universal_newlines=True)
print(output) # Hello
Типичная ошибка:
Ожидание, что check_output вернёт и stderr. По умолчанию stderr не захватывается. Для захвата обоих потоков нужно указать stderr=subprocess.STDOUT.
Как работать с процессами в реальном времени, например, считывать вывод построчно?
Класс subprocess.Popen предоставляет максимальную гибкость. С его помощью можно запустить процесс, обмениваться с ним данными через stdin/stdout/stderr и управлять его выполнением асинхронно.
import subprocess
proc = subprocess.Popen(['ping', '-c', '4', 'google.com'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True)
for line in proc.stdout:
print(line.strip())
proc.wait()
Распространённая проблема:
Необходимо закрывать PIPE, иначе может возникнуть взаимная блокировка (deadlock), если процесс пишет много данных в stderr, а читается только stdout. Лучше использовать communicate() или читать оба потока в потоках.
Как выполнить команду с проверкой на успех и автоматическим исключением при ошибке?
Функция subprocess.check_call() работает как call(), но выбрасывает CalledProcessError, если код возврата не равен нулю. Это удобно для критичных команд.
import subprocess
try:
subprocess.check_call(['false'])
except subprocess.CalledProcessError as e:
print(f'Команда завершилась с кодом {e.returncode}')
Особенность:
Не возвращает вывод, а только проверяет код возврата. Для получения вывода используйте check_output или run(..., check=True, capture_output=True).
Как выполнить команду через оболочку с поддержкой подстановок и пайпов?
Использование shell=True позволяет передавать команду как строку, которая интерпретируется оболочкой. Это удобно для сложных конвейеров.
import subprocess
result = subprocess.run('grep error /var/log/syslog | tail -10',
shell=True, capture_output=True, text=True)
print(result.stdout)
Опасность:
При передаче строки, полученной из внешнего источника (пользовательский ввод), возможна инъекция команд. Всегда экранируйте аргументы с помощью shlex.quote() или используйте список аргументов без shell.
Как правильно обрабатывать аргументы с пробелами и специальными символами без shell?
Передавайте команду в виде списка строк. Python сам экранирует аргументы для вызова системного вызова execve. Никакого shell не требуется.
import subprocess
subprocess.run(['cp', 'file with spaces.txt', '/tmp/']) # безопасно
Типичная ошибка:
Новички часто используют shell=True и экранируют пробелы кавычками внутри строки, что путает. Лучше сразу привыкнуть к списку.
Как запустить команду на Windows с подавлением окна консоли?
Для GUI-приложений или фоновых задач можно создать процесс с флагом CREATE_NO_WINDOW (0x08000000) через creationflags.
import subprocess
subprocess.Popen(['notepad.exe'], creationflags=subprocess.CREATE_NO_WINDOW)
Замечание:
Флаг доступен только на Windows. На Linux он игнорируется.
Как выполнить команду с таймаутом на выполнение?
Параметр timeout в run() и Popen.wait() позволяет ограничить время работы. Если время превышено, выбрасывается TimeoutExpired.
import subprocess
try:
subprocess.run(['sleep', '10'], timeout=2)
except subprocess.TimeoutExpired:
print('Команда не завершилась за 2 секунды')
Важно:
После таймаута процесс может остаться висеть. В run его можно принудительно завершить через kill=True (Python 3.9+).
Расширенные примеры вызова команд из Python
В этом разделе представлены нестандартные и углубленные примеры работы с внешними процессами. Каждый пример снабжен кодом и выводом (если применимо).
Пример 1. Запуск конвейера из нескольких команд без shell
import subprocess
# Эквивалент: ls /var/log | grep *.log | head -5
p1 = subprocess.Popen(['ls', '/var/log'], stdout=subprocess.PIPE, text=True)
p2 = subprocess.Popen(['grep', '\\.log'], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p3 = subprocess.Popen(['head', '-5'], stdin=p2.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close() # разрешаем p1 получить SIGPIPE, если p2 завершится раньше
p2.stdout.close()
output = p3.communicate()[0]
print('Вывод конвейера:')
print(output)
Вывод конвейера: Xorg.0.log Xorg.0.log.old alternatives.log dpkg.log fontconfig.log
Пояснение: каждый процесс подключается к stdout предыдущего. Важно закрывать ненужные дескрипторы, чтобы избежать зависания.
Пример 2. Передача данных на stdin и получение stdout/stderr
import subprocess
proc = subprocess.Popen(['tr', 'a-z', 'A-Z'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True)
out, err = proc.communicate(input='hello world\nfrom python')
print('stdout:', out)
print('stderr:', err)
stdout: HELLO WORLD FROM PYTHON stderr:
Метод communicate удобен, когда нужно передать данные в процесс целиком. Он считывает весь вывод и ждёт завершения.
Пример 3. Использование shlex для безопасной разборки командной строки
import subprocess
import shlex
command_line = 'cat /etc/passwd | grep -v ^#'
args = shlex.split(command_line) # ['cat', '/etc/passwd', '|', 'grep', '-v', '^#']
# Но это не пайп! Правильнее обрабатывать как конвейер отдельно.
# Пример: просто вызов одной команды с аргументами:
result = subprocess.run(shlex.split('ls -la --color=never'), capture_output=True, text=True)
print(result.stdout[:200])
shlex.split строго разбирает команду как shell, но не создаёт пайпы. Для пайпов нужен Popen. Полезно для безопасной разбивки на аргументы без shell.
Пример 4. Запуск процесса с переменными окружения
import subprocess
import os
env = os.environ.copy()
env['MY_VAR'] = 'value123'
result = subprocess.run(['printenv', 'MY_VAR'], env=env, capture_output=True, text=True)
print('MY_VAR:', result.stdout.strip())
MY_VAR: value123
Параметр env позволяет задать полное окружение для дочернего процесса. Копируйте os.environ, чтобы не потерять системные переменные.
Пример 5. Асинхронное выполнение с обработкой STDOUT и STDERR в реальном времени (с помощью потоков)
import subprocess
import threading
def reader(stream, label):
for line in iter(stream.readline, ''):
print(f'[{label}] {line.strip()}')
stream.close()
proc = subprocess.Popen(['bash', '-c', 'for i in 1 2 3; do echo $i; sleep 0.5; done'],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
t1 = threading.Thread(target=reader, args=(proc.stdout, 'OUT'))
t2 = threading.Thread(target=reader, args=(proc.stderr, 'ERR'))
t1.start()
t2.start()
t1.join()
t2.join()
proc.wait()
[OUT] 1 [OUT] 2 [OUT] 3
Использование потоков позволяет читать оба потока одновременно, избегая блокировок. Для реальных задач можно использовать asyncio.
Пример 6. Запуск команды с подавлением вывода и логированием в файл
import subprocess
with open('output.log', 'w') as f:
result = subprocess.run(['ls', '-la'], stdout=f, stderr=subprocess.STDOUT, text=True)
# Код возврата можно проверить после
print('Код возврата:', result.returncode)
Вывод команды направляется прямо в файл, минуя Python. Это эффективно для больших объёмов данных.
Пример 7. Использование sh (вызов shell-команд в стиле bash) через библиотеку sh (сторонняя)
# require: pip install sh
import sh
# Вызов ls как функции
print(sh.ls('-la', '/tmp')) # возвращает строку вывода
# Конвейеры через sh
wc = sh.wc(sh.ls('/var/log'), '-l')
print('Количество файлов:', wc.stdout.strip())
(вывод ls /tmp) Количество файлов: 15
Библиотека sh упрощает вызов команд до стиля вызова функций, поддерживает пайпы и захват вывода. Однако она не входит в стандартную библиотеку и может быть избыточной.
Пример 8. Запуск процесса с привязкой к конкретному рабочему каталогу
import subprocess
result = subprocess.run(['pwd'], cwd='/tmp', capture_output=True, text=True)
print('Текущий каталог в процессе:', result.stdout.strip())
Текущий каталог в процессе: /tmp
Параметр cwd меняет рабочий каталог для дочернего процесса. Обратите внимание, что пути в аргументах команды должны быть скорректированы вручную.