- Home »

Как использовать 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-программа должна быть интуитивно понятной, надежной и хорошо документированной.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.