Интеграция Python и Bash для автоматизации задач

Раздел: Автоматизация -> Скрипты оболочки

Объединение Bash и Python для автоматизации

Bash и Python часто используются совместно, чтобы компенсировать слабые стороны друг друга. Bash отлично справляется с командами оболочки и конвейерами, Python - со сложной логикой, структурами данных и сетевыми запросами. Далее рассмотрены основные подходы к их интеграции.

Как выполнять команды оболочки из Python и обрабатывать их вывод?

Наиболее эффективное решение - использование модуля subprocess. Он предоставляет гибкий и безопасный способ запуска shell-команд, захвата stdout/stderr, управления таймаутами и кодами возврата.

import subprocess

# Простой запуск с получением вывода
result = subprocess.run(['ls', '-lh', '/home'], capture_output=True, text=True)
print(result.stdout)

# Запуск с обработкой ошибок
try:
    subprocess.run(['false'], check=True)
except subprocess.CalledProcessError as e:
    print(f'Команда завершилась с кодом {e.returncode}')

# Передача данных через stdin
proc = subprocess.Popen(['grep', 'error'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
out, _ = proc.communicate(input='line1\nline with error\nline3')
print(out)

Bash скрипты python (bash скрипты с python)

Пояснение:

  • run() - основной способ; capture_output собирает stdout и stderr.
  • check=True вызывает исключение при ненулевом коде возврата.
  • Popen с потоками позволяет взаимодействовать с процессом двусторонне.

Типичные ошибки:

  • Игнорирование кодировки: без text=True вывод возвращается в байтах, что может вызвать ошибки при работе с русским текстом.
  • Использование shell=True без необходимости: увеличивает риск инъекций команд. Лучше передавать аргументы списком.
  • Зависание при большом выводе: нужно использовать communicate() или явно читать потоки.

Как выполнить Python‑код внутри Bash‑скрипта без отдельного файла?

Bash позволяет встраивать Python через heredoc. Это удобно для коротких фрагментов, когда не хочется создавать дополнительный файл.

#!/bin/bash

python3 << 'EOF'
import sys
print(f'Аргументы Bash: {sys.argv}')
for i in range(5):
    print(f'Квадрат {i} = {i**2}')
EOF

Кавычки вокруг EOF предотвращают подстановку переменных Bash внутри Python-кода. Если нужны переменные из оболочки, используйте без кавычек, но экранируйте.

Проблема: при больших фрагментах теряется подсветка синтаксиса и отладка. Ошибки Python выводятся после завершения скрипта. Рекомендуется для коротких задач.

Как автоматически создавать сложные Bash‑сценарии с помощью Python?

Python отлично генерирует код благодаря строкам и шаблонам. Например, сформировать последовательность команд для загрузки данных.

import os

scripts_dir = 'generated'
os.makedirs(scripts_dir, exist_ok=True)

commands = [
    'echo "Скачивание файла 1"',
    'wget -q http://example.com/file1.zip',
    'echo "Распаковка"',
    'unzip -o file1.zip -d data1',
    'rm file1.zip'
]

with open(f'{scripts_dir}/download.sh', 'w') as f:
    f.write('#!/bin/bash\n')
    f.write('set -e\n')
    for cmd in commands:
        f.write(cmd + '\n')

os.chmod(f'{scripts_dir}/download.sh', 0o755)

Такой подход удобен для динамического создания скриптов под разные окружения. Можно использовать шаблонизаторы (Jinja2) для параметризации.

Ошибка: забыть выставить права на выполнение (chmod). Также необходимо проверять корректность генерируемого Bash-синтаксиса.

Как использовать гибридную оболочку Xonsh для слияния Python и Bash?

Xonsh - оболочка, где можно писать на Python и прямо вставлять Bash-команды. Это альтернатива написанию скриптов на чистом Bash с вызовом Python.

# Установка: pip install xonsh
# Пример скрипта test.xonsh
import os

# Python-код
files = os.listdir('.')
for f in files:
    # Bash-команда внутри Python-строки
    !ls -lh @(f)

Синтаксис !command выполняет команду оболочки, @(expr) вставляет значение Python-переменной.

Xonsh требует отдельной установки и не всегда доступен на серверах. При портировании скриптов на другие системы может потребоваться переписывание.

Как сравниваются os.system и subprocess для вызова Bash?

Ранние скрипты используют os.system, но он не возвращает вывод, небезопасен и не гибок. Рекомендуется subprocess.

import os
import subprocess

# Старый способ – os.system
retcode = os.system('ls')
print(f'Код возврата: {retcode}')

# Современный способ – subprocess
result = subprocess.run(['ls'], capture_output=True, text=True)
print(result.stdout)
print(f'Код возврата: {result.returncode}')

Используйте subprocess как более контролируемое и переносимое решение.

Если команда содержит пользовательский ввод, os.system создаёт уязвимость к инъекциям. Всегда экранируйте аргументы или используйте список.

Расширенные примеры интеграции Python и Bash

Мониторинг системы с обработкой данных

Скрипт собирает информацию о процессах, дисках и памяти через Bash, а Python форматирует и отправляет уведомление.

Пример
import subprocess
import json

def get_processes():
    # ps aux --no-headers выдаёт список без заголовка
    result = subprocess.run(
        ['ps', 'aux', '--no-headers'],
        capture_output=True, text=True
    )
    lines = result.stdout.strip().split('\n')
    processes = []
    for line in lines[:5]:  # первые 5 строк для примера
        parts = line.split()
        if len(parts) >= 11:
            proc = {
                'user': parts[0],
                'pid': parts[1],
                'cpu': parts[2],
                'mem': parts[3],
                'command': ' '.join(parts[10:])
            }
            processes.append(proc)
    return processes

def get_disk_usage():
    result = subprocess.run(['df', '-h', '--output=target,used,avail'],
                            capture_output=True, text=True)
    lines = result.stdout.strip().split('\n')[1:]  # пропускаем заголовок
    disks = {}
    for line in lines:
        parts = line.split()
        if len(parts) == 3:
            disks[parts[0]] = {'used': parts[1], 'avail': parts[2]}
    return disks

if __name__ == '__main__':
    data = {
        'processes': get_processes(),
        'disk': get_disk_usage()
    }
    # Преобразуем в JSON для анализа
    print(json.dumps(data, indent=2))
{
  "processes": [
    {
      "user": "root",
      "pid": "1",
      "cpu": "0.0",
      "mem": "0.1",
      "command": "/sbin/init"
    },
    ...
  ],
  "disk": {
    "/": {"used": "12G", "avail": "45G"},
    "/home": {"used": "8G", "avail": "50G"}
  }
}

Пояснение:

  • split() разбивает строки; для ps важен порядок колонок.
  • Используется срез [:5] только для демонстрации; в реальности обрабатывают все строки.
  • JSON-вывод можно перенаправить в другой сервис или сохранить в файл.

Параллельное выполнение Bash‑команд с помощью multiprocessing

Если нужно запустить несколько долгих команд одновременно, Python позволяет распараллелить вызовы через пул процессов.

Пример
import subprocess
from multiprocessing import Pool

def run_cmd(cmd):
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        return (cmd, result.stdout, result.stderr, result.returncode)
    except subprocess.TimeoutExpired:
        return (cmd, None, 'Timeout', -1)
    except Exception as e:
        return (cmd, None, str(e), -1)

if __name__ == '__main__':
    commands = [
        ['ping', '-c', '1', 'google.com'],
        ['dig', '+short', 'example.com'],
        ['curl', '-s', 'https://api.github.com/zen'],
        ['sleep', '2']
    ]
    with Pool(processes=3) as pool:
        results = pool.map(run_cmd, commands)
    for cmd, stdout, stderr, retcode in results:
        print(f'Команда: {" ".join(cmd)}')
        print(f'Код возврата: {retcode}')
        if stdout:
            print(f'stdout: {stdout[:100]}...' if len(stdout) > 100 else f'stdout: {stdout}')
        if stderr:
            print(f'stderr: {stderr}')
        print('---')
Команда: ping -c 1 google.com
Код возврата: 0
stdout: PING google.com (142.250.186.78) 56(84) bytes of data.
64 bytes from 142.250.186.78: ...
---
Команда: dig +short example.com
Код возврата: 0
stdout: 93.184.216.34
---
...

Пояснение:

  • Pool(processes=3) ограничивает параллелизм до 3 одновременных процессов.
  • Аргументы команд передаются в виде списков, безопасно.
  • Обработка таймаута предотвращает зависание.

Безопасное построение команд с помощью shlex

Когда команда формируется из внешних источников (ввод пользователя, файл), shlex.split() корректно разбирает строку как оболочку, избегая инъекций.

Пример
import subprocess
import shlex

# Небезопасно: использование строки с shell=True
user_input = 'file.txt; rm -rf /'
result_unsafe = subprocess.run(f'cat {user_input}', shell=True, capture_output=True)
print('Небезопасный результат:', result_unsafe.returncode)

# Безопасно: разбор строки через shlex
cmd_parts = shlex.split(f'cat {user_input}')
print('Разобранная команда:', cmd_parts)
result_safe = subprocess.run(cmd_parts, capture_output=True)
print('Безопасный результат:', result_safe.returncode)
Небезопасный результат: 0
Разобранная команда: ['cat', 'file.txt; rm -rf /']
Безопасный результат: 1  # ошибка: файл не найден, но команда не выполнилась опасная часть

Пояснение:

  • shlex.split() обрабатывает кавычки и экранирование, превращая строку в список аргументов.
  • Команда выполняется без интерпретации специальных символов оболочки, таких как ; или |.
  • Рекомендуется для любого пользовательского ввода.

Генерация и запуск временных Bash‑скриптов из Python

Иногда удобно создать временный файл скрипта, выполнить его, а после удалить. Модуль tempfile помогает в этом.

Пример
import tempfile
import subprocess
import os

def run_temp_script(script_content: str):
    with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
        f.write('#!/bin/bash\n')
        f.write('set -e\n')
        f.write(script_content)
        temp_path = f.name
    os.chmod(temp_path, 0o755)
    try:
        result = subprocess.run([temp_path], capture_output=True, text=True, check=True)
        return result.stdout
    except subprocess.CalledProcessError as e:
        print(f'Ошибка: {e.stderr}')
        raise
    finally:
        os.unlink(temp_path)

# Пример использования
output = run_temp_script('''
echo "Текущая дата:"
date
for f in /var/log/*.log; do
  echo "Файл: $f"
done
''')
print(output)

Пояснение:

  • NamedTemporaryFile создаёт уникальное имя, delete=False предотвращает автоматическое удаление до нашего вызова.
  • Права на исполнение устанавливаются через os.chmod.
  • Скрипт выполняется как внешняя программа, после чего файл удаляется.

Использование Python как замены сложному Bash с помощью плагина Click

Для создания CLI-инструментов, объединяющих Python и вызовы shell, удобен фреймворк Click. Он позволяет определять аргументы, опции и автоматическую справку.

Пример
# pip install click
import click
import subprocess

@click.group()
def cli():
    pass

@cli.command()
@click.option('--path', default='.', help='Директория для анализа')
@click.option('--sort', is_flag=True, help='Сортировать по размеру')
def diskspace(path, sort):
    """Показать использование диска в указанной папке."""
    cmd = ['du', '-sh', path + '/*']
    if sort:
        cmd = ['du', '-sh', path + '/*']  # на самом деле sort не реализован, пример
    result = subprocess.run(cmd, capture_output=True, text=True, shell=True)
    click.echo(result.stdout)

if __name__ == '__main__':
    cli()

Запуск: python script.py diskspace --path /home --sort

Этот подход позволяет строить надёжные интерфейсы командной строки, внутри которых выполняются Bash‑команды.

Заключение расширенных примеров:

Все эти приёмы демонстрируют, что Python служит мощным дополнением к Bash, позволяя писать безопасные, масштабируемые и понятные сценарии автоматизации.

Bash скрипты с Python - comments

En
Bash скрипты python (python)