Home » Как использовать argparse для написания CLI программ на Python
Как использовать argparse для написания CLI программ на Python

Как использовать argparse для написания CLI программ на Python

Знаете это болезненное чувство, когда очередной скрипт для мониторинга серверов превращается в лапшу из sys.argv с кучей условий? Или когда коллега просит “добавить хотя бы –help” к вашему крутому tool’у для деплоя? Встречайте argparse — встроенный в Python модуль, который превратит ваши скрипты в полноценные CLI-программы с человеческим интерфейсом.

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

Как это работает под капотом

argparse — это парсер аргументов командной строки, который входит в стандартную библиотеку Python начиная с версии 2.7. Принцип работы простой:

  • Создаете объект ArgumentParser
  • Добавляете описания аргументов через add_argument()
  • Вызываете parse_args() — получаете объект с атрибутами
  • Используете результат в своем коде

Вот минимальный пример:

import argparse

parser = argparse.ArgumentParser(description='Проверка статуса сервера')
parser.add_argument('hostname', help='Имя хоста для проверки')
parser.add_argument('--port', type=int, default=22, help='Порт для проверки')

args = parser.parse_args()
print(f"Проверяем {args.hostname}:{args.port}")

Запуск: python check_server.py example.com --port 80

Быстрая настройка: от нуля до рабочей программы

Давайте пошагово создадим полезную утилиту для мониторинга дискового пространства на серверах:

Шаг 1: Базовая структура

#!/usr/bin/env python3
import argparse
import subprocess
import sys

def main():
    parser = argparse.ArgumentParser(
        description='Мониторинг дискового пространства',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Примеры использования:
  %(prog)s /var/log --threshold 80
  %(prog)s /home --format json
        """
    )
    
    # Позиционный аргумент
    parser.add_argument('path', help='Путь для проверки')
    
    # Опциональные аргументы
    parser.add_argument('--threshold', '-t', type=int, default=90,
                       help='Порог заполнения в процентах (по умолчанию: 90)')
    parser.add_argument('--format', choices=['text', 'json'], default='text',
                       help='Формат вывода')
    parser.add_argument('--verbose', '-v', action='store_true',
                       help='Подробный вывод')
    
    args = parser.parse_args()
    
    # Здесь будет логика программы
    check_disk_usage(args.path, args.threshold, args.format, args.verbose)

if __name__ == '__main__':
    main()

Шаг 2: Добавляем функциональность

import shutil
import json
import os

def check_disk_usage(path, threshold, output_format, verbose):
    try:
        total, used, free = shutil.disk_usage(path)
        usage_percent = (used / total) * 100
        
        data = {
            'path': path,
            'total': total,
            'used': used,
            'free': free,
            'usage_percent': round(usage_percent, 2),
            'threshold': threshold,
            'status': 'WARNING' if usage_percent >= threshold else 'OK'
        }
        
        if output_format == 'json':
            print(json.dumps(data, indent=2))
        else:
            print(f"Путь: {path}")
            print(f"Использовано: {data['usage_percent']}%")
            print(f"Статус: {data['status']}")
            
            if verbose:
                print(f"Всего: {total // (1024**3)} GB")
                print(f"Свободно: {free // (1024**3)} GB")
        
        # Возвращаем код выхода
        sys.exit(1 if usage_percent >= threshold else 0)
        
    except Exception as e:
        print(f"Ошибка: {e}", file=sys.stderr)
        sys.exit(2)

Продвинутые возможности argparse

Группы аргументов и взаимоисключающие опции

parser = argparse.ArgumentParser(description='Управление сервисами')

# Группа взаимоисключающих действий
action_group = parser.add_mutually_exclusive_group(required=True)
action_group.add_argument('--start', action='store_true', help='Запустить сервис')
action_group.add_argument('--stop', action='store_true', help='Остановить сервис')
action_group.add_argument('--restart', action='store_true', help='Перезапустить сервис')

# Логическая группа для опций логирования
log_group = parser.add_argument_group('Опции логирования')
log_group.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'])
log_group.add_argument('--log-file', type=argparse.FileType('w'))

Subcommands для сложных программ

Отлично подходит для создания инструментов типа docker или git:

import argparse

def create_parser():
    parser = argparse.ArgumentParser(prog='server-tool')
    parser.add_argument('--config', help='Файл конфигурации')
    
    subparsers = parser.add_subparsers(dest='command', help='Доступные команды')
    
    # Команда deploy
    deploy_parser = subparsers.add_parser('deploy', help='Развертывание приложения')
    deploy_parser.add_argument('app_name', help='Имя приложения')
    deploy_parser.add_argument('--env', choices=['dev', 'prod'], default='dev')
    deploy_parser.add_argument('--force', action='store_true', help='Принудительное развертывание')
    
    # Команда status
    status_parser = subparsers.add_parser('status', help='Статус сервисов')
    status_parser.add_argument('--service', help='Конкретный сервис')
    status_parser.add_argument('--json', action='store_true', help='Вывод в JSON')
    
    # Команда logs
    logs_parser = subparsers.add_parser('logs', help='Просмотр логов')
    logs_parser.add_argument('service', help='Имя сервиса')
    logs_parser.add_argument('--tail', type=int, default=100, help='Количество строк')
    logs_parser.add_argument('--follow', '-f', action='store_true', help='Следить за логами')
    
    return parser

def main():
    parser = create_parser()
    args = parser.parse_args()
    
    if args.command == 'deploy':
        deploy_app(args.app_name, args.env, args.force)
    elif args.command == 'status':
        show_status(args.service, args.json)
    elif args.command == 'logs':
        show_logs(args.service, args.tail, args.follow)
    else:
        parser.print_help()

Практические примеры для серверного администрирования

Утилита для бэкапов

#!/usr/bin/env python3
import argparse
import subprocess
import datetime
import os

def create_backup_tool():
    parser = argparse.ArgumentParser(
        description='Инструмент для создания бэкапов',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    
    parser.add_argument('source', help='Исходный каталог для бэкапа')
    parser.add_argument('destination', help='Каталог назначения')
    
    parser.add_argument('--compress', choices=['gzip', 'bzip2', 'xz'], 
                       help='Тип сжатия')
    parser.add_argument('--exclude', action='append', dest='excludes',
                       help='Исключить файлы/каталоги (можно указать несколько раз)')
    parser.add_argument('--dry-run', action='store_true',
                       help='Показать что будет сделано, но не выполнять')
    parser.add_argument('--quiet', '-q', action='store_true',
                       help='Тихий режим')
    parser.add_argument('--retention', type=int, default=7,
                       help='Количество дней хранения старых бэкапов')
    
    return parser

def perform_backup(args):
    timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_name = f"backup_{timestamp}.tar"
    
    if args.compress:
        backup_name += f".{args.compress}"
    
    backup_path = os.path.join(args.destination, backup_name)
    
    # Формируем команду tar
    cmd = ['tar', '-cf', backup_path, '-C', os.path.dirname(args.source), 
           os.path.basename(args.source)]
    
    if args.compress == 'gzip':
        cmd[1] = '-czf'
    elif args.compress == 'bzip2':
        cmd[1] = '-cjf'
    elif args.compress == 'xz':
        cmd[1] = '-cJf'
    
    # Добавляем исключения
    if args.excludes:
        for exclude in args.excludes:
            cmd.extend(['--exclude', exclude])
    
    if not args.quiet:
        print(f"Создание бэкапа: {backup_path}")
    
    if args.dry_run:
        print(f"Будет выполнена команда: {' '.join(cmd)}")
        return
    
    try:
        subprocess.run(cmd, check=True)
        if not args.quiet:
            print("Бэкап создан успешно")
    except subprocess.CalledProcessError as e:
        print(f"Ошибка создания бэкапа: {e}")
        return False
    
    return True

if __name__ == '__main__':
    parser = create_backup_tool()
    args = parser.parse_args()
    
    if perform_backup(args):
        cleanup_old_backups(args.destination, args.retention)

Мониторинг сетевых соединений

#!/usr/bin/env python3
import argparse
import socket
import concurrent.futures
import time

def create_network_monitor():
    parser = argparse.ArgumentParser(description='Мониторинг сетевых соединений')
    
    parser.add_argument('hosts', nargs='+', help='Список хостов для проверки')
    parser.add_argument('--port', '-p', type=int, default=80, 
                       help='Порт для проверки')
    parser.add_argument('--timeout', type=float, default=5.0,
                       help='Таймаут соединения в секундах')
    parser.add_argument('--interval', type=int, default=60,
                       help='Интервал между проверками в секундах')
    parser.add_argument('--continuous', '-c', action='store_true',
                       help='Непрерывный мониторинг')
    parser.add_argument('--threads', type=int, default=10,
                       help='Количество потоков для проверки')
    
    return parser

def check_host(host, port, timeout):
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        start_time = time.time()
        result = sock.connect_ex((host, port))
        end_time = time.time()
        sock.close()
        
        if result == 0:
            return {
                'host': host,
                'port': port,
                'status': 'UP',
                'response_time': round((end_time - start_time) * 1000, 2)
            }
        else:
            return {
                'host': host,
                'port': port,
                'status': 'DOWN',
                'response_time': None
            }
    except Exception as e:
        return {
            'host': host,
            'port': port,
            'status': 'ERROR',
            'error': str(e)
        }

def monitor_hosts(args):
    while True:
        print(f"\n=== Проверка в {time.strftime('%Y-%m-%d %H:%M:%S')} ===")
        
        with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads) as executor:
            futures = [executor.submit(check_host, host, args.port, args.timeout) 
                      for host in args.hosts]
            
            for future in concurrent.futures.as_completed(futures):
                result = future.result()
                if result['status'] == 'UP':
                    print(f"✓ {result['host']}:{result['port']} - {result['response_time']}ms")
                else:
                    print(f"✗ {result['host']}:{result['port']} - {result['status']}")
        
        if not args.continuous:
            break
        
        time.sleep(args.interval)

if __name__ == '__main__':
    parser = create_network_monitor()
    args = parser.parse_args()
    
    try:
        monitor_hosts(args)
    except KeyboardInterrupt:
        print("\nМониторинг прерван пользователем")

Сравнение с альтернативными решениями

Библиотека Сложность Возможности Размер Когда использовать
argparse Средняя Полный набор функций Встроенная Стандартные CLI программы
click Низкая Декораторы, цепочки команд ~500KB Сложные CLI с множеством команд
fire Очень низкая Автоматическое создание CLI ~100KB Быстрое превращение функций в CLI
docopt Средняя Описание через docstring ~50KB Когда важен декларативный подход
typer Низкая Type hints, автодополнение ~2MB Современные CLI с type hints

Полезные трюки и продвинутые техники

Валидация аргументов

import argparse
import re
import ipaddress

def validate_ip(value):
    try:
        ipaddress.ip_address(value)
        return value
    except ValueError:
        raise argparse.ArgumentTypeError(f"'{value}' не является валидным IP-адресом")

def validate_port(value):
    ivalue = int(value)
    if ivalue < 1 or ivalue > 65535:
        raise argparse.ArgumentTypeError(f"Порт должен быть в диапазоне 1-65535")
    return ivalue

def validate_email(value):
    pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    if not re.match(pattern, value):
        raise argparse.ArgumentTypeError(f"'{value}' не является валидным email")
    return value

parser = argparse.ArgumentParser()
parser.add_argument('--ip', type=validate_ip, required=True)
parser.add_argument('--port', type=validate_port, default=22)
parser.add_argument('--email', type=validate_email)

Работа с конфигурационными файлами

import argparse
import configparser
import os

def load_config(config_file):
    config = configparser.ConfigParser()
    config.read(config_file)
    return config

def merge_config_with_args(args, config):
    # Приоритет: аргументы командной строки > конфиг > значения по умолчанию
    if hasattr(args, 'host') and args.host is None:
        args.host = config.get('server', 'host', fallback='localhost')
    
    if hasattr(args, 'port') and args.port == 22:  # значение по умолчанию
        args.port = config.getint('server', 'port', fallback=22)
    
    return args

parser = argparse.ArgumentParser()
parser.add_argument('--config', default='~/.myapp.conf',
                   help='Путь к файлу конфигурации')
parser.add_argument('--host', help='Хост сервера')
parser.add_argument('--port', type=int, default=22, help='Порт сервера')

args = parser.parse_args()

# Загружаем конфиг если он существует
config_path = os.path.expanduser(args.config)
if os.path.exists(config_path):
    config = load_config(config_path)
    args = merge_config_with_args(args, config)

Прогресс-бары и интерактивность

import argparse
import sys
from getpass import getpass

def confirm_action(message):
    while True:
        response = input(f"{message} (y/n): ").lower()
        if response in ['y', 'yes']:
            return True
        elif response in ['n', 'no']:
            return False
        else:
            print("Пожалуйста, введите 'y' или 'n'")

parser = argparse.ArgumentParser(description='Управление пользователями')
parser.add_argument('action', choices=['create', 'delete', 'modify'])
parser.add_argument('username', help='Имя пользователя')
parser.add_argument('--force', action='store_true', 
                   help='Не спрашивать подтверждение')
parser.add_argument('--password', help='Пароль (будет запрошен если не указан)')

args = parser.parse_args()

if args.action == 'delete' and not args.force:
    if not confirm_action(f"Удалить пользователя {args.username}?"):
        print("Операция отменена")
        sys.exit(0)

if args.action == 'create':
    if args.password:
        password = args.password
    else:
        password = getpass("Введите пароль: ")
    
    # Создаем пользователя
    print(f"Создание пользователя {args.username}...")

Интеграция с другими инструментами

Логирование

import argparse
import logging
import sys

def setup_logging(level, log_file=None):
    log_format = '%(asctime)s - %(levelname)s - %(message)s'
    
    if log_file:
        logging.basicConfig(
            level=getattr(logging, level),
            format=log_format,
            handlers=[
                logging.FileHandler(log_file),
                logging.StreamHandler(sys.stdout)
            ]
        )
    else:
        logging.basicConfig(
            level=getattr(logging, level),
            format=log_format
        )

parser = argparse.ArgumentParser()
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
                   default='INFO', help='Уровень логирования')
parser.add_argument('--log-file', help='Файл для записи логов')

args = parser.parse_args()
setup_logging(args.log_level, args.log_file)

logger = logging.getLogger(__name__)
logger.info("Программа запущена")

Работа с переменными окружения

import argparse
import os

def env_or_default(env_var, default=None):
    return os.environ.get(env_var, default)

parser = argparse.ArgumentParser()
parser.add_argument('--host', default=env_or_default('SERVER_HOST', 'localhost'))
parser.add_argument('--port', type=int, default=int(env_or_default('SERVER_PORT', '22')))
parser.add_argument('--token', default=env_or_default('API_TOKEN'))

# Можно также использовать required=True если переменная обязательна
if not os.environ.get('API_TOKEN'):
    parser.add_argument('--token', required=True, help='API токен (или установите переменную API_TOKEN)')

Автоматизация и CI/CD

argparse отлично подходит для создания инструментов автоматизации, особенно в контексте серверного администрирования. Вот пример скрипта для автоматического развертывания:

#!/usr/bin/env python3
import argparse
import subprocess
import sys
import json
import time

def create_deployment_tool():
    parser = argparse.ArgumentParser(
        description='Инструмент автоматического развертывания',
        formatter_class=argparse.RawDescriptionHelpFormatter
    )
    
    parser.add_argument('environment', choices=['staging', 'production'],
                       help='Окружение для развертывания')
    parser.add_argument('version', help='Версия для развертывания')
    
    parser.add_argument('--config', required=True,
                       help='Файл конфигурации развертывания')
    parser.add_argument('--rollback', action='store_true',
                       help='Откатить к предыдущей версии')
    parser.add_argument('--dry-run', action='store_true',
                       help='Показать план без выполнения')
    parser.add_argument('--skip-tests', action='store_true',
                       help='Пропустить тесты')
    parser.add_argument('--parallel', type=int, default=1,
                       help='Количество параллельных процессов')
    
    return parser

def deploy(args):
    if args.dry_run:
        print(f"ПЛАН: Развертывание {args.version} в {args.environment}")
        return
    
    print(f"Начинаем развертывание {args.version} в {args.environment}")
    
    # Здесь будет реальная логика развертывания
    steps = [
        "Проверка доступности серверов",
        "Загрузка новой версии",
        "Остановка старых сервисов",
        "Обновление конфигурации",
        "Запуск новых сервисов",
        "Проверка работоспособности"
    ]
    
    for step in steps:
        print(f"Выполняется: {step}")
        time.sleep(1)  # Имитация работы
    
    print("Развертывание завершено успешно")

if __name__ == '__main__':
    parser = create_deployment_tool()
    args = parser.parse_args()
    
    try:
        deploy(args)
    except KeyboardInterrupt:
        print("\nРазвертывание прервано пользователем")
        sys.exit(1)
    except Exception as e:
        print(f"Ошибка развертывания: {e}")
        sys.exit(1)

Отладка и тестирование CLI программ

import argparse
import unittest
from unittest.mock import patch
import sys

class TestCLIProgram(unittest.TestCase):
    def setUp(self):
        self.parser = argparse.ArgumentParser()
        self.parser.add_argument('--host', default='localhost')
        self.parser.add_argument('--port', type=int, default=22)
        self.parser.add_argument('--verbose', action='store_true')
    
    def test_default_arguments(self):
        args = self.parser.parse_args([])
        self.assertEqual(args.host, 'localhost')
        self.assertEqual(args.port, 22)
        self.assertFalse(args.verbose)
    
    def test_custom_arguments(self):
        args = self.parser.parse_args(['--host', 'example.com', '--port', '80', '--verbose'])
        self.assertEqual(args.host, 'example.com')
        self.assertEqual(args.port, 80)
        self.assertTrue(args.verbose)
    
    @patch('sys.argv', ['program.py', '--host', 'test.com'])
    def test_with_mocked_argv(self):
        args = self.parser.parse_args()
        self.assertEqual(args.host, 'test.com')

if __name__ == '__main__':
    unittest.main()

Интересные факты и нестандартные применения

  • argparse может работать с файлами: используйте type=argparse.FileType('r') для автоматического открытия файлов
  • Можно создавать собственные actions: наследуйтесь от argparse.Action для кастомного поведения
  • Поддержка completion: с библиотекой argcomplete можно добавить автодополнение в bash
  • Можно парсить из списка: parse_args(['--host', 'example.com']) удобно для тестирования

Пример кастомного action

import argparse

class ValidateRangeAction(argparse.Action):
    def __init__(self, option_strings, dest, min_val=None, max_val=None, **kwargs):
        self.min_val = min_val
        self.max_val = max_val
        super().__init__(option_strings, dest, **kwargs)
    
    def __call__(self, parser, namespace, values, option_string=None):
        if self.min_val is not None and values < self.min_val:
            raise argparse.ArgumentError(self, f"Значение должно быть >= {self.min_val}")
        if self.max_val is not None and values > self.max_val:
            raise argparse.ArgumentError(self, f"Значение должно быть <= {self.max_val}")
        setattr(namespace, self.dest, values)

parser = argparse.ArgumentParser()
parser.add_argument('--threads', type=int, action=ValidateRangeAction, 
                   min_val=1, max_val=100, help='Количество потоков (1-100)')

Создание пакетов с entry points

Для создания полноценных консольных утилит используйте setup.py:

# setup.py
from setuptools import setup, find_packages

setup(
    name='my-server-tools',
    version='1.0.0',
    packages=find_packages(),
    entry_points={
        'console_scripts': [
            'disk-monitor=mytools.disk_monitor:main',
            'net-check=mytools.network_monitor:main',
            'backup-tool=mytools.backup:main',
        ],
    },
    install_requires=[
        'requests',
        'psutil',
    ],
)

После установки через pip ваши инструменты будут доступны как обычные консольные команды.

Производительность и оптимизация

argparse довольно быстр, но для программ с тысячами аргументов стоит учитывать:

  • Используйте fromfile_prefix_chars для чтения аргументов из файла
  • Ленивая загрузка subparsers для больших программ
  • Кэширование результатов парсинга для повторных вызовов

Пример оптимизированного парсера:

import argparse
from functools import lru_cache

@lru_cache(maxsize=1)
def get_parser():
    parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
    # ... настройка парсера
    return parser

def main():
    parser = get_parser()
    args = parser.parse_args()
    # ... логика программы

Заключение и рекомендации

argparse — это мощный и гибкий инструмент для создания CLI программ, который должен быть в арсенале каждого системного администратора и DevOps-инженера. Он позволяет быстро превратить простые скрипты в полноценные утилиты с человеческим интерфейсом.

Используйте argparse когда:

  • Создаете инструменты для администрирования серверов
  • Нужна валидация и типизация аргументов
  • Важна совместимость (встроенная библиотека)
  • Требуется детальный контроль над поведением CLI

Рассмотрите альтернативы когда:

  • Нужно очень быстро создать простой CLI (fire)
  • Планируется сложная программа с множеством команд (click)
  • Важна современная типизация (typer)

Лучшие практики:

  • Всегда добавляйте описание программы и аргументов
  • Используйте валидацию типов и значений
  • Предусматривайте значения по умолчанию
  • Группируйте логически связанные аргументы
  • Тестируйте парсинг аргументов

Для тестирования ваших CLI-утилит на реальных серверах рекомендую использовать VPS хостинг или выделенные серверы — так вы сможете проверить работу в реальных условиях, не рискуя production-окружением.

argparse поможет вам создать инструменты, которыми будет приятно пользоваться и вам, и вашим коллегам. Главное — помнить, что хорошая CLI-программа должна быть интуитивно понятной, надежной и хорошо документированной.


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked