Как использовать библиотеку Python SH для работы с сервером

Если вы когда-либо писали скрипты для автоматизации на Linux, то наверняка сталкивались с модулем subprocess. Это мощный инструмент, но его синтаксис часто называют громоздким, а обработку потоков вывода — неинтуитивной. Вы когда-нибудь ловили себя на мысли: «Почему нельзя просто вызвать команду как функцию?»

Хорошая новость: можно. Библиотека sh (также известная как python-sh) создана именно для этого. На текущий момент (февраль 2026 года) это одно из самых элегантных решений для интеграции системных команд в код Python.

В этой статье мы разберем, почему начинающим (да и опытным) разработчикам стоит обратить внимание на sh вместо subprocess, и как использовать её для повседневной работы с Linux-сервером.

Что такое sh и почему это лучше subprocess?

Библиотека sh — это не набор заново реализованных команд, а полноценная "обёртка" (wrapper). Она позволяет вызывать любые программы в вашем системном PATH так, словно это обычные Python-функции.

Сравните два подхода:

Классический subprocess:

import subprocess
result = subprocess.run(['ls', '-l', '/home'], capture_output=True, text=True)
print(result.stdout)

Магия sh:

from sh import ls
print(ls("-l", "/home"))

Уже на этом примере видно, что код стал чище и читаемее. Вы просто работаете с командой, а не с процессом.

Установка: всё стандартно

Поскольку мы говорим о работе на Linux (а sh поддерживает Linux, macOS и BSD, но не поддерживает Windows), установка проходит штатно через pip:

pip install sh

Для актуальных версий Python (3.8 — 3.12) библиотека полностью совместима и активно поддерживается.

Основы: вызываем команды как функции

После импорта любая системная команда становится доступной.

Простой пример с git:

from sh import git

# Вызов git status
status = git("status")
print(status)

# Вызов git log --oneline
log = git.log("--oneline")
print(log)

Обратите внимание на второй пример: git.log("--oneline"). Точечная нотация позволяет обращаться к дочерним командам (субкомандам) — это невероятно удобно для работы с такими инструментами, как git, systemctl или docker.

Работа с аргументами и флагами

Одна из самых частых задач — передача аргументов. sh понимает и позиционные аргументы, и ключевые слова.

Длинные и короткие флаги

Допустим, нам нужно скачать файл с помощью curl. В bash это было бы:

curl -o page.html --silent http://example.com

В sh это превращается в:

from sh import curl

curl("http://example.com", o="page.html", silent=True)
# Или более традиционно:
curl("http://example.com", "-o", "page.html", "--silent")

Правила просты: - Короткий флаг (-o) передаётся как именованный аргумент o="имя". - Длинный флаг (--silent) передаётся как silent=True.

Автоматическое преобразование имен

В Python имена переменных не могут содержать дефисы. Но команды Linux их обожают (например, --no-create-home). sh умно решает эту проблему: дефисы в именах аргументов заменяются на подчеркивания.

from sh import adduser

# В терминале: adduser amoffat --system --shell=/bin/bash --no-create-home
adduser("amoffat", system=True, shell="/bin/bash", no_create_home=True)

Библиотека автоматически преобразует no_create_home в --no-create-home.

Пайпы (конвейеры) как композиция функций

В Shell символ | (пайп) передает вывод одной программы на ввод другой. В sh это реализовано через композицию функций с помощью аргумента _in.

Хотите узнать, сколько файлов в директории /etc?

from sh import ls, wc

# Аналог: ls -1 /etc | wc -l
count = wc(ls("-1", "/etc"), "-l")
print(count)

Или более сложный пример: найти 10 самых больших файлов в текущей директории.

from sh import du, sort, head

# du -sb * | sort -rn | head -10
result = head(sort(du("-sb", "*"), "-rn"), "-n", "10")
print(result)

Это читается почти как английский язык.

Перенаправление вывода (Redirection)

В subprocess чтобы сохранить вывод в файл, нужно открывать файл и работать с хендлерами. В sh это делается через специальные именованные аргументы: _out (stdout) и _err (stderr).

Сохраняем в файл

from sh import ls

# Сохраняем список файлов в documents.txt
ls("-l", "/home/user/documents", _out="documents.txt")

Работа с файловыми объектами Python

Вы можете передать не только строку с именем файла, но и любой файлоподобный объект:

from sh import ifconfig
from io import StringIO

buffer = StringIO()
ifconfig(_out=buffer)
print("Команда выполнена, результат в буфере:", buffer.getvalue())

Объединение потоков

Если нужно, чтобы stderr писался туда же, куда и stdout (как 2>&1 в bash), используйте _err_to_out=True:

from sh import find
find("/root", name="config", _out="search.log", _err_to_out=True)

Фоновые задачи (Background Processes)

Если команда выполняется долго, можно запустить её в фоне с помощью _bg=True. Это позволяет скрипту не ждать её завершения, а делать другие дела.

from sh import sleep

# Запускаем таймер на 10 секунд в фоне
p = sleep(10, _bg=True)

print("Спим в фоне, а мы выполняем другой код...")

# Делаем что-то ещё
print("Считаем овец...")

# В какой-то момент ждем завершения фоновой задачи
p.wait()
print("10 секунд прошло!")

Объект p, который возвращается, позволяет управлять процессом: ждать его (wait()), проверить, запущен ли он, или получить статус выхода.

Магия Baking: готовим команды заранее

"Выпекание" (baking) — одна из killer-фич библиотеки. Она позволяет зафиксировать часть аргументов для команды, создавая новый вызываемый объект. Это похоже на functools.partial, но для shell-команд.

Представьте, что вы часто подключаетесь по SSH к удаленному серверу:

from sh import ssh

# Обычный вызов (каждый раз писать порт и юзера - утомительно)
ssh("myserver.com", "-p", "2222", "whoami")

# Используем baking
remote = ssh.bake("myserver.com", p=2222)

# Теперь remote — это готовая команда ssh с запеченными параметрами
whoami = remote.whoami()
uptime = remote.uptime()
remote.tail("/var/log/nginx/access.log", n=50)

Код стал не только короче, но и выразительнее. Мы работаем с объектом remote, вызывая его методы.

Ещё пример: работа в определенной директории

from sh import ls

ls_custom = ls.bake("-la") # Теперь ls_custom всегда вызывает ls -la
print(ls_custom("/var/log")) # Выполнится ls -la /var/log

Обработка ошибок (Exit Codes)

В мире Linux ненулевой код возврата команды обычно означает ошибку. sh превращает это в исключения, что делает обработку ошибок более питоничной.

from sh import ls, ErrorReturnCode

try:
    ls("/some/non/existent/folder")
except ErrorReturnCode as e:
    print(f"Команда упала с кодом {e.exit_code}")
    print(f"Stdout: {e.stdout}")
    print(f"Stderr: {e.stderr}")

Вы даже можете ловить конкретные коды ошибок, например ErrorReturnCode_2 (код 2 часто означает "нет такого файла").

Интерактивный режим и Foreground

Некоторые программы (текстовые редакторы, htop) требуют полного контроля над терминалом. Если вы попытаетесь запустить vim обычным способом, скрипт зависнет, пытаясь агрегировать вывод. Для таких случаев используйте _fg=True (foreground) .

from sh import vim

# Редактор откроется как обычно, а Python будет ждать его закрытия
vim("/home/user/script.py", _fg=True)

Это передает управление терминалом дочернему процессу.

Итоговый пример: автоматизация бэкапа

Давайте соберем всё вместе и напишем небольшой скрипт для создания архива и его отправки на удаленный сервер.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sh
from datetime import datetime

# 1. Задаем переменные
BACKUP_DIR = "/var/www/site"
ARCHIVE_NAME = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.tar.gz"
REMOTE_SERVER = "backup@example.com"
REMOTE_PATH = "/backups/"

# 2. Создаем архив (проверяем ошибки)
print("Создание архива...")
try:
    # Используем команду tar с пайпом к gzip? Нет, просто tar с флагом -z
    sh.tar("-czf", ARCHIVE_NAME, BACKUP_DIR, _out="archive.log", _err_to_out=True)
    print(f"Архив {ARCHIVE_NAME} создан.")
except sh.ErrorReturnCode as e:
    print(f"Ошибка при создании архива: {e.stderr}")
    exit(1)

# 3. "Выпекаем" команду scp для удобства
print("Отправка архива на сервер...")
scp_to_remote = sh.scp.bake(ARCHIVE_NAME, f"{REMOTE_SERVER}:{REMOTE_PATH}")

try:
    # Можно было бы добавить ключи, например, port=2222, если нужно
    scp_to_remote()
    print("Архив успешно отправлен.")
except sh.ErrorReturnCode:
    print("Не удалось отправить файл. Проверьте соединение или пути.")
    exit(1)

print("Бэкап завершен!")

Согласитесь, такой код читается как хорошая инструкция, а не как набор низкоуровневых вызовов.

Заключение

Библиотека sh — это блестящий пример того, как можно улучшить взаимодействие Python с операционной системой. Для начинающих программистов она предлагает:

  1. Интуитивный синтаксис. Не нужно думать в терминах процессов и пайпов, думайте в терминах функций и объектов.
  2. Меньше кода. Забудьте про бесконечные subprocess.Popen и communicate().
  3. Питоничный подход. Исключения, именованные аргументы, композиция — всё это делает код надежнее.

Конечно, subprocess останется стандартом и иногда нужен для очень специфичных сценариев. Но для 90% повседневных задач автоматизации на Linux библиотека sh (текущая версия 2.x) является более чистым и продуктивным выбором .

Попробуйте заменить сегодня один из ваших старых скриптов на sh, и вы почувствуете разницу. Установка простая (pip install sh), а документация на Read the Docs подробная и понятная.

Зависимости: Python, sh