Как вызывать системные команды из скриптов Python

Раздел: Администрирование -> Команды и администрирование 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 меняет рабочий каталог для дочернего процесса. Обратите внимание, что пути в аргументах команды должны быть скорректированы вручную.

Вызов команд из Python - comments

En
Python call command (python)