MidТеория5 min

os, sys и subprocess

Работа с файловой системой, переменными окружения, аргументами и запуск процессов

os, sys и subprocess — системные операции

Модуль os — операционная система

Работа с путями (pathlib)

В современном Python рекомендуется использовать pathlib:

from pathlib import Path

# Build paths with / operator
config = Path.home() / ".config" / "myapp" / "config.toml"
print(config)

# Path properties
p = Path("/home/user/documents/report.pdf")
print(p.name)        # report.pdf
print(p.stem)        # report
print(p.suffix)      # .pdf
print(p.parent)      # /home/user/documents
print(p.exists())    # False

# Iterate directory
for item in Path("/tmp").iterdir():
    if item.is_file():
        print(f"Файл: {item.name} ({item.stat().st_size} байт)")

# Glob patterns
for py_file in Path(".").rglob("*.py"):
    print(py_file)

# Create directories
data_dir = Path("data") / "output"
data_dir.mkdir(parents=True, exist_ok=True)

# Read/write files
file_path = data_dir / "result.txt"
file_path.write_text("Привет, мир!", encoding="utf-8")
content = file_path.read_text(encoding="utf-8")
print(content)  # Привет, мир!

Переменные окружения

import os

# Read environment variables
home = os.environ.get("HOME", "/tmp")
debug = os.environ.get("DEBUG", "false")

# Safe access with default
db_host = os.getenv("DB_HOST", "localhost")
db_port = int(os.getenv("DB_PORT", "5432"))

# Set environment variable (current process only)
os.environ["MY_APP_MODE"] = "testing"

# Check if variable exists
if "SECRET_KEY" not in os.environ:
    print("WARNING: SECRET_KEY не установлен!")

# Pattern: configuration from environment
from dataclasses import dataclass

@dataclass
class AppConfig:
    """Application configuration from environment."""
    db_host: str = "localhost"
    db_port: int = 5432
    debug: bool = False

    @classmethod
    def from_env(cls) -> "AppConfig":
        return cls(
            db_host=os.getenv("DB_HOST", "localhost"),
            db_port=int(os.getenv("DB_PORT", "5432")),
            debug=os.getenv("DEBUG", "").lower() in ("1", "true", "yes"),
        )

Файловая система

import os
import shutil
from pathlib import Path

# Current working directory
cwd = os.getcwd()

# Walk directory tree
for root, dirs, files in os.walk("."):
    dirs[:] = [d for d in dirs if not d.startswith(".")]
    for f in files:
        if f.endswith(".py"):
            full_path = os.path.join(root, f)
            print(f"{full_path}: {os.path.getsize(full_path)} bytes")

# File operations with shutil
# shutil.copy2("src.txt", "dst.txt")       # Copy with metadata
# shutil.copytree("src_dir", "dst_dir")    # Copy directory tree
# shutil.rmtree("dir_to_remove")           # Remove directory tree
# shutil.move("old_path", "new_path")      # Move file/directory

# Disk usage
usage = shutil.disk_usage("/")
print(f"Всего: {usage.total / (1024**3):.1f} GB")
print(f"Свободно: {usage.free / (1024**3):.1f} GB")

# Temporary files
import tempfile

with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
    f.write("Временные данные")
    print(f"Temp file: {f.name}")

with tempfile.TemporaryDirectory() as tmpdir:
    print(f"Temp dir: {tmpdir}")
    # Directory deleted when exiting context

Модуль sys — параметры интерпретатора

import sys

# Python version
print(sys.version)          # 3.14.0 (main, ...)
print(sys.version_info)     # sys.version_info(major=3, minor=14, ...)

# Check Python version
if sys.version_info < (3, 10):
    print("Требуется Python 3.10+")
    sys.exit(1)

# Platform
print(sys.platform)         # darwin, linux, win32

# Command line arguments
print(sys.argv)             # ['script.py', 'arg1', 'arg2']

# Standard streams
sys.stdout.write("Стандартный вывод\n")
sys.stderr.write("Ошибка\n")

# Memory and limits
print(sys.getsizeof([1, 2, 3]))     # Size in bytes
print(sys.getrecursionlimit())       # Default: 1000

Аргументы командной строки с argparse

import argparse

def main() -> None:
    parser = argparse.ArgumentParser(description="Обработка данных")
    parser.add_argument("input_file", help="Входной файл")
    parser.add_argument(
        "-o", "--output",
        default="result.txt",
        help="Выходной файл (по умолчанию: result.txt)",
    )
    parser.add_argument(
        "-v", "--verbose",
        action="store_true",
        help="Подробный вывод",
    )
    parser.add_argument(
        "-n", "--count",
        type=int,
        default=10,
        help="Количество записей",
    )

    args = parser.parse_args()

    if args.verbose:
        print(f"Входной файл: {args.input_file}")
        print(f"Выходной файл: {args.output}")
        print(f"Количество: {args.count}")

if __name__ == "__main__":
    main()

subprocess — запуск внешних процессов

import subprocess

# Simple command execution
result = subprocess.run(
    ["ls", "-la", "/tmp"],
    capture_output=True,
    text=True,
)
print(f"Return code: {result.returncode}")
print(f"Output: {result.stdout[:200]}")

# Run with timeout
try:
    result = subprocess.run(["sleep", "10"], timeout=3)
except subprocess.TimeoutExpired:
    print("Команда превысила таймаут")

# Check for errors
try:
    subprocess.run(
        ["python", "-c", "import nonexistent"],
        check=True,
        capture_output=True,
        text=True,
    )
except subprocess.CalledProcessError as e:
    print(f"Ошибка (код {e.returncode}): {e.stderr}")

# Pass input to command
result = subprocess.run(
    ["grep", "python"],
    input="python is great\njava is ok\npython rocks\n",
    capture_output=True,
    text=True,
)
print(result.stdout)

# Working directory
result = subprocess.run(
    ["pwd"],
    capture_output=True,
    text=True,
    cwd="/tmp",
)
print(result.stdout.strip())  # /tmp

Безопасность: shell=True

import subprocess

# DANGEROUS: shell=True with user input
# user_input = "file.txt; rm -rf /"
# subprocess.run(f"cat {user_input}", shell=True)  # NEVER!

# SAFE: pass arguments as list (no shell injection)
user_input = "file.txt; rm -rf /"
subprocess.run(["cat", user_input], capture_output=True)
# Safely passes the entire string as one argument

# shell=True is OK for static commands only
result = subprocess.run(
    "echo $HOME | tr '/' '\n'",
    shell=True,
    capture_output=True,
    text=True,
)

Практический пример: утилита бэкапа

import os
import shutil
import subprocess
from datetime import datetime
from pathlib import Path
from dataclasses import dataclass

@dataclass
class BackupConfig:
    source_dir: str
    backup_dir: str
    max_backups: int = 5
    compress: bool = True

    @classmethod
    def from_env(cls) -> "BackupConfig":
        return cls(
            source_dir=os.getenv("BACKUP_SOURCE", "./data"),
            backup_dir=os.getenv("BACKUP_DIR", "./backups"),
            max_backups=int(os.getenv("BACKUP_MAX", "5")),
            compress=os.getenv("BACKUP_COMPRESS", "true").lower() == "true",
        )

def create_backup(config: BackupConfig) -> Path:
    """Create a backup of the source directory."""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_name = f"backup_{timestamp}"
    backup_path = Path(config.backup_dir) / backup_name

    backup_path.parent.mkdir(parents=True, exist_ok=True)
    shutil.copytree(config.source_dir, str(backup_path))
    print(f"Скопировано: {config.source_dir} -> {backup_path}")

    if config.compress:
        archive = f"{backup_path}.tar.gz"
        result = subprocess.run(
            ["tar", "-czf", archive, "-C", config.backup_dir, backup_name],
            capture_output=True, text=True,
        )
        if result.returncode == 0:
            shutil.rmtree(str(backup_path))
            return Path(archive)

    return backup_path

def cleanup_old_backups(config: BackupConfig) -> None:
    """Remove old backups, keeping only max_backups most recent."""
    backup_dir = Path(config.backup_dir)
    if not backup_dir.exists():
        return

    backups = sorted(
        backup_dir.glob("backup_*"),
        key=lambda p: p.stat().st_mtime,
        reverse=True,
    )

    for old_backup in backups[config.max_backups:]:
        if old_backup.is_file():
            old_backup.unlink()
        else:
            shutil.rmtree(str(old_backup))
        print(f"Удалён старый бэкап: {old_backup.name}")

Проверь себя

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

Как безопасно получить переменную окружения с значением по умолчанию?

Что содержит `sys.argv[0]`?

Почему `shell=True` в subprocess.run() может быть опасен?