0
6483

Свой интерфейс командной строки с блэкджеком и Python

Дайте пользователю настраивать программу через интерфейс командной строки. Воспользуйтесь библиотекой argparse для Python.

Свой интерфейс командной строки с блэкджеком и Python

Разбираемся, как это сделать. Библиотека argparse поможет принимать значение конфига в командной строке.

Но как использовать argparse?

Вот вам четыре шага:

  1. Импортируйте библиотеку.
  2. Создайте парсер.
  3. Добавьте в него аргументы.
  4. Запустите .parse_args().

Последний приведёт к объекту Namespace. Он содержит простое свойство для каждого входного аргумента из терминала.

Для подробного разбора этапов посмотрите программу myls.py. Она перечисляет файлы в текущей директории. Ниже – реализация без argparse:

# myls.py
import os
import sys

if len(sys.argv) > 2:
    print('YoВЫu have specified too many arguments')
    sys.exit()

if len(sys.argv) < 2:
    print('You need to specify the path to be listed')
    sys.exit()

input_path = sys.argv[1]

if not os.path.isdir(input_path):
    print('The path specified does not exist')
    sys.exit()

print('\n'.join(os.listdir(input_path)))

Результат:

$ python myls.py
You need to specify the path to be listed

$ python myls.py /mnt /proc /dev
You have specified too many arguments

$ python myls.py /mnt
dir1
dir2

Скрипт работает, но вывод отличается от встроенной команды.

Улучшите код с argparse:

# myls.py
# Импорт библиотеки argparse
import argparse

import os
import sys

# Создание парсера
my_parser = argparse.ArgumentParser(description='List the content of a folder')

# Добавление аргументов
my_parser.add_argument('Path',
                       metavar='path',
                       type=str,
                       help='the path to list')

# Выполнение метода parse_args()
args = my_parser.parse_args()

input_path = args.Path

if not os.path.isdir(input_path):
    print('The path specified does not exist')
    sys.exit()

print('\n'.join(os.listdir(input_path)))

Первое отличие – отсутствие условного оператора if, который проверял бы аргументы. Библиотека берёт эту функцию на себя.

Мы импортировали argparse, создали простой парсер с описанием программы и определили ожидаемые от пользователя аргументы. Последним делом запустили .parse_args() для разбора аргументов на входе и получения объекта Namespace, который содержит пользовательский ввод.

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

$ python myls.py
usage: myls.py [-h] path
myls.py: error: the following arguments are required: path

Отсутствует нужный аргумент пути, поэтому получим ошибку.

А также программа принимает флаг -h, как в примере ниже:

$ python myls.py -h
usage: myls.py [-h] path

List the content of a folder

positional arguments:
path        the path to list

optional arguments:
-h, --help  show this help message and exit

Приложение отвечает на -h выводом справки. И это не требует от нас никаких усилий!

Всего четыре строчки кода превратили переменную args в объект Namespace. Он содержит свойства для аргументов, которые юзер вводит в интерфейс командной строки.

Назовите программу

По умолчанию библиотека использует значение элемента sys.argv[0] для наименования программы, что соответствует названию скрипта. Укажите имя ключевым словом prog:

# Создание парсера
my_parser = argparse.ArgumentParser(prog='myls',
                                    description='List the content of a folder')

Оно будет отображаться в справке:

$ python myls.py
usage: myls [-h] path
myls.py: error: the following arguments are required: path

Теперь программа называется myls, а не myls.py.

Выводите настраиваемую справку в интерфейс командной строки

По умолчанию argparse создаёт помощь своего формата. Настройте её с помощью usage:

# Создание парсера
my_parser = argparse.ArgumentParser(prog='myls',
                                    usage='%(prog)s [options] path',
                                    description='List the content of a folder')

Во время выполнения токен %(prog)s автоматически заменяется именем программы:

$ python myls.py
usage: myls [options] path
myls: error: too few arguments

И справка показывает другую строку использования, где опция -h сменилась универсальными токеном [options].

Отображайте текст до и после аргументов справки

Используйте два ключевых слова для настройки текста до и после справки:

  1. description: описание до вывода помощи
  2. epilog: текст после

Посмотрим, как работает epilog:

# Создание парсера
my_parser = argparse.ArgumentParser(description='List the content of a folder',
                                    epilog='Enjoy the program! :)')

Результат:

$ python myls.py -h usage: myls.py [-h] path List the content of a folder positional arguments: path the path to list optional arguments: -h, --help show this help message and exit Enjoy the program! :)

Задайте символ префикса

По умолчанию, тире – начинает необязательные аргументы. Измените его с помощью ключевого слова prefix_chars:

# Создание парсера
my_parser = argparse.ArgumentParser(description='List the content of a folder',
                                    epilog='Enjoy the program! :)',
                                    prefix_chars='/')

Программа стала поддерживать другой префикс, а справка претерпела изменения:

$ python myls.py
usage: myls.py [/h] path
myls.py: error: too few arguments

Она отображается аргументом /h. Используйте это для разработки под Windows.

Символы префикса для файлов

Сохраняйте аргументы для сложных программ запуска в файле и загружайте из него. argparse выполняет эту работу из коробки.

Для теста напишите программу:

# fromfile_example.py
import argparse

my_parser = argparse.ArgumentParser(fromfile_prefix_chars='@')

my_parser.add_argument('a',
                       help='a first argument')

my_parser.add_argument('b',
                       help='a second argument')

my_parser.add_argument('c',
                       help='a third argument')

my_parser.add_argument('d',
                       help='a fourth argument')

my_parser.add_argument('e',
                       help='a fifth argument')

my_parser.add_argument('-v',
                       '--verbose',
                       action='store_true',
                       help='an optional argument')

# Выполнение parse_args()
args = my_parser.parse_args()

print('If you read this line it means that you have provided '
      'all the parameters')

При создании парсера мы задействовали ключевое слово fromfile_prefix_chars.

Запуск без аргументов приведёт к ошибке:

$ python fromfile_example.py
usage: fromfile_example.py [-h] [-v] a b c d e
fromfile_example.py: error: the following arguments are required: a, b, c, d, e

Создайте args.txt с параметрами в каждой строке:

first
second
third
fourth
fifth

У вас есть специальный символ префикса для указания файла с аргументами. Откройте интерфейс командной строки и запустите предыдущую программу:

$ python fromfile_example.py @args.txt
If you read this line it means that you have provided all the parameters

Видим: argparse считывает аргументы из файла args.txt.

Разрешайте и запрещайте сокращения

Следующая программа выводит указанное вами значение для аргумента --input:

# abbrev_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('--input', action='store', type=int, required=True)
my_parser.add_argument('--id', action='store', type=int)

args = my_parser.parse_args()

print(args.input)

Посмотрите, как argparse обрабатывает сокращения и вызывает программу несколько раз:

$ python abbrev_example.py --input 42
42

$ python abbrev_example.py --inpu 42
42

$ python abbrev_example.py --inp 42
42

$ python abbrev_example.py --in 42
42

А что случится, если передать аргумент --i 42? Библиотека не сможет понять, передавать 42 аргументу --input или --id, и выведет сообщение об ошибке:

$ python abbrev_example.py --i 42
usage: abbrev_example.py [-h] --input INPUT [--id ID]
abbrev_example.py: error: ambiguous option: --i could match --input, --id

Не нравится эта функция? Хотите заставить пользователей вводить полные названия опций? Просто отключите эту возможность ключевым словом allow_abbrev со значением False на этапе создания парсера:

# abbrev_example.py
import argparse

my_parser = argparse.ArgumentParser(allow_abbrev=False)
my_parser.add_argument('--input', action='store', type=int, required=True)

args = my_parser.parse_args()

print(args.input)

Запустите код, и вы увидите, что сокращения недоступны:

$ python abbrev_example.py --inp 42
usage: abbrev_example.py [-h] --input INPUT
abbrev_example.py: error: the following arguments are required: --input

Ошибка сообщает, что пользователь не указал параметр --input потому, что программа не распознала сокращение --inp.

Задавайте имена флагов и аргументов

Вы можете добавить в интерфейс командной строки два типа аргументов:

  • позиционные
  • необязательные

Позиционными командами оперирует программа.

В предыдущем примере path – позиционный аргумент. Без него программа не работает. Они называются позиционными потому, что позиция определяет их функцию.

Рассмотрим cp из Linux:

$ cp [OPTION]... [-T] SOURCE DEST

Первый позиционный аргумент после cp – источник файла для копирования. Второй – место назначения копии.

Необязательные аргументы изменяют поведение команд во время выполнения. В примере с cp необязательный аргумент флаг -r заставляет команду копировать директории рекурсивно.

Два типа аргументов отличаются синтаксисом: необязательные начинаются с - или --.

Хотите добавить необязательный аргумент? Вызовите .add_argument() и назовите новый аргумент, начиная с -.

Измените myls.py:

# myls.py
# Импорт библиотеки argparse
import argparse

import os
import sys

# Создание парсера
my_parser = argparse.ArgumentParser(description='List the content of a folder')

# Добавление аргументов
my_parser.add_argument('Path',
                       metavar='path',
                       type=str,
                       help='the path to list')
my_parser.add_argument('-l',
                       '--long',
                       action='store_true',
                       help='enable the long listing format')

# Выполнение parse_args()
args = my_parser.parse_args()

input_path = args.Path

if not os.path.isdir(input_path):
    print('The path specified does not exist')
    sys.exit()

for line in os.listdir(input_path):
    if args.long:  # Упрощённый длинный список 
        size = os.stat(os.path.join(input_path, line)).st_size
        line = '%10d  %s' % (size, line)
    print(line)

Запустите и проверьте опцию -l:

$ python myls.py -h
usage: myls.py [-h] [-l] path

List the content of a folder

positional arguments:
path        the path to list

optional arguments:
-h, --help  show this help message and exit
-l, --long  enable the long listing format

Теперь программа принимает, но не требует опцию -l.

Задайте действие для аргумента

При добавлении необязательного аргумента можно указать действие. Задайте способ хранения значения в объекте Namespace, который вы получите после выполнения .parse_args().

Некоторые действия предопределены и доступны для использования:

  • store хранит входное значение в объекте Namespace (действие по умолчанию).
  • store_const содержит постоянное значение, когда указаны соответствующие необязательные аргументы.
  • store_true хранит логическое True, когда указан соответствующий необязательный аргумент, и False в других случаях.
  • store_false хранит логическое False, когда указан соответствующий необязательный аргумент, и True в других случаях.
  • append содержит список, добавляя значение каждый раз, когда опция указана.
  • append_const хранит список, добавляя постоянное значение.
  • count хранит int, равный количеству использования опции.
  • help выводит справку.
  • version показывает версию программы.

Рассмотрим вышеуказанные опции на следующем примере:

# actions_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.version = '1.0'
my_parser.add_argument('-a', action='store')
my_parser.add_argument('-b', action='store_const', const=42)
my_parser.add_argument('-c', action='store_true')
my_parser.add_argument('-d', action='store_false')
my_parser.add_argument('-e', action='append')
my_parser.add_argument('-f', action='append_const', const=42)
my_parser.add_argument('-g', action='count')
my_parser.add_argument('-i', action='help')
my_parser.add_argument('-j', action='version')

args = my_parser.parse_args()

print(vars(args))

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

$ python actions_example.py
{'a': None, 'b': None, 'c': False, 'd': True, 'e': None, 'f': None, 'g': None}

Видно, что без указаний аргументов, значения по умолчанию – None.

Действие store хранит передаваемое значение:

$ python actions_example.py -a 42
{'a': '42', 'b': None, 'c': False, 'd': True, 'e': None, 'f': None, 'g': None}

$ python actions_example.py -a "test"
{'a': 'test', 'b': None, 'c': False, 'd': True, 'e': None, 'f': None, 'g': None}

store_const хранит определённую константу, когда предоставлены аргументы. В тесте мы указали аргумент b, и значение args.b стало 42:

$ python actions_example.py -b
{'a': None, 'b': 42, 'c': False, 'd': True, 'e': None, 'f': None, 'g': None}

Действие store_true хранит логическое True при наличии передаваемых аргументов и False в остальных случаях. Нужно противоположное поведение? Воспользуйтесь действием store_false:

$ python actions_example.py
{'a': None, 'b': None, 'c': False, 'd': True, 'e': None, 'f': None, 'g': None}
$ python actions_example.py -c
{'a': None, 'b': None, 'c': True, 'd': True, 'e': None, 'f': None, 'g': None}
$ python actions_example.py -d
{'a': None, 'b': None, 'c': False, 'd': False, 'e': None, 'f': None, 'g': None}

Создавайте список всех переданных значений одним аргументом с помощью append:

$ python actions_example.py -e me -e you -e us
{'a': None, 'b': None, 'c': False, 'd': True, 'e': ['me', 'you', 'us'], 'f': None, 'g': None}

append_const похоже на append, но добавляет одно постоянное значение:

$ python actions_example.py -f -f
{'a': None, 'b': None, 'c': False, 'd': True, 'e': None, 'f': [42, 42], 'g': None}

count считает количество передач аргумента. Оно полезно в реализации уровня подробностей вывода программы. Можно определить уровень как -v – меньше подробностей, чем -vvv:

$ python actions_example.py -ggg
{'a': None, 'b': None, 'c': False, 'd': True, 'e': None, 'f': None, 'g': 3}
$ python actions_example.py -ggggg
{'a': None, 'b': None, 'c': False, 'd': True, 'e': None, 'f': None, 'g': 5}

Действие version просто показывает версию программы:

$ python actions_example.py -j
1.0

Ещё одна возможность: создавайте собственные действия. Для этого наследуйте класс argparse.Action и реализуйте пару методов.

Следующий пример – настраиваемое действие store, которое выводит больше подробностей, чем стандартное:

# custom_action.py
import argparse

class VerboseStore(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        if nargs is not None:
            raise ValueError('nargs not allowed')
        super(VerboseStore, self).__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        print('Here I am, setting the ' \
              'values %r for the %r option...' % (values, option_string))
        setattr(namespace, self.dest, values)

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-i', '--input', action=VerboseStore, type=int)

args = my_parser.parse_args()

print(vars(args))

Результат выполнения:

$ python custom_action.py -i 42
Here I am, setting the values 42 for the '-i' option...
{'input': 42}

Программа вывела линию прежде, чем задать значение 42 параметру -i.

Задавайте количество значений для опции

По умолчанию парсер предполагает один параметр для каждого аргумента. Измените это поведение, указав другое количество значений ключевым словом nargs.

Нужен аргумент, который принимает три значения? Укажите 3 в качестве значения nargs во время добавления параметра в парсер:

# nargs_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('--input', action='store', type=int, nargs=3)

args = my_parser.parse_args()

print(args.input)

Теперь программа принимает три значения для параметра --input:

$ python nargs_example.py --input 42
usage: nargs_example.py [-h] [--input INPUT INPUT INPUT]
nargs_example.py: error: argument --input: expected 3 arguments

$ python nargs_example.py --input 42 42 42
[42, 42, 42]

А значение переменной args.input – это список с тремя значениями.

Ключевое слово nargs также принимает:

  • ?: одно необязательное значение;
  • *: гибкое количество значений, которые собираются в список;
  • +: похоже на *, но требует хотя бы одного значения;
  • argparse.REMAINDER: все значения, которые остаются в командной строке.

В следующей программе позиционный аргумент input принимает одно значение. Если оно отсутствует, программа использует значение ключевого слово default:

# nargs_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('input',
                       action='store',
                       nargs='?',
                       default='my default value')

args = my_parser.parse_args()

print(args.input)

Выбирайте значение для аргумента input. В данном случае будет использовано default:

$ python nargs_example.py 'my custom value'
my custom value

$ python nargs_example.py
my default value

Чтобы принять несколько значений и собрать их в список, укажите * в качестве значения nargs:

# nargs_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('input',
                       action='store',
                       nargs='*',
                       default='my default value')

args = my_parser.parse_args()

print(args.input)

Этот код позволяет задавать гибкое число значений для ожидаемого аргумента:

$ python nargs_example.py me you us
['me', 'you', 'us']

$ python nargs_example.py
my default value

Если вам нужно принять переменное количество значений и убедиться, что указано хотя бы одно значение, используйте + в качестве значений nargs:

# nargs_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('input', action='store', nargs='+')

args = my_parser.parse_args()

print(args.input)

Запустите программу без позиционных аргументов, и увидите точное сообщение об ошибке:

$ python nargs_example.py me you us
['me', 'you', 'us']

$ python nargs_example.py
usage: nargs_example.py [-h] input [input ...]
nargs_example.py: error: the following arguments are required: input

Напоследок представьте, что вам нужно захватить и отправить в список все оставшиеся аргументы, которые были указаны в командной строке. Для этого присвойте значение argparse.REMAINDER ключевому слову nargs:

# nargs_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('first', action='store')
my_parser.add_argument('others', action='store', nargs=argparse.REMAINDER)

args = my_parser.parse_args()

print('first = %r' % args.first)
print('others = %r' % args.others)

Посмотрите, что происходит: первое значение будет связано с первым параметром. Остальные – со вторым:

$ python nargs_example.py me you us
first = 'me'
others = ['you', 'us']

Оставшиеся значения собираются в список.

Установите значение по умолчанию в отсутствии аргумента

Вы уже знаете, что пользователь может не указывать необязательные аргументы в командной строке. Когда аргументы не указаны, соответствующее значение ставится в None.

Но вы можете определить значение по умолчанию для аргумента:

# default_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-a', action='store', default='42')

args = my_parser.parse_args()

print(vars(args))

Запустите программу без опции -a, и вы получите:

$ python default_example.py
{'a': '42'}

Теперь опция -a имеет значение 42, хотя вы не указывали это явно в командной строке.

Установите тип аргумента

По умолчанию все входные значения обрабатываются как строки. У вас есть возможность определять тип соответствующего свойства объекта Namespace, которое вы получаете после вызова .parse_args(). Воспользуйтесь ключевым словом type:

# type_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-a', action='store', type=int)

args = my_parser.parse_args()

print(vars(args))

Указывая значение типа int для аргумента, вы говорите argparse, что свойство .a объекта Namespace должно быть int вместо string:

$ python type_example.py -a 42
{'a': 42}

Теперь значение аргумента проверяется во время выполнения. Если  значение не подходит, выводится чёткая ошибка:

$ python type_example.py -a "that's a string"
usage: type_example.py [-h] [-a A]
type_example.py: error: argument -a: invalid int value: "that's a string"

Установите допустимые значения для определённого аргумента

Ещё одна интересная особенность библиотеки argparse в Python. Предоставьте список принимаемых значений на стадии добавления новой опции:

# choices_ex.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-a', action='store', choices=['head', 'tail'])

args = my_parser.parse_args()

Хотите принимать числовые значения? Используйте range() для определения диапазона принимаемых значений:

# choices_ex.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-a', action='store', type=int, choices=range(1, 5))

args = my_parser.parse_args()

print(vars(args))

Тогда значение, указанное вами в командной строке, автоматически сравнивается со списком допустимых значений:

$ python choices_ex.py -a 4
{'a': 4}

$ python choices_ex.py -a 40
usage: choices_ex.py [-h] [-a {1,2,3,4}]
choices_ex.py: error: argument -a: invalid choice: 40 (choose from 1, 2, 3, 4)

Если его там нет, вы получите сообщение об ошибке.

Установите необходимость аргумента

Хотите заставить пользователя вводить необязательный аргумент? Используйте ключевое слово required:

# required_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-a',
                       action='store',
                       choices=['head', 'tail'],
                       required=True)

args = my_parser.parse_args()

print(vars(args))

Установите значение required в True, и пользователь должен будет указывать значение для этого аргумента:

$ python required_example.py
usage: required_example.py [-h] -a {head,tail}
required_example.py: error: the following arguments are required: -a

$ python required_example.py -a head
{'a': 'head'}

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

Показывайте краткое описание аргумента

Улучшайте справку argparse описаниями аргументов:

# help_example.py
import argparse

my_parser = argparse.ArgumentParser()
my_parser.add_argument('-a',
                       action='store',
                       choices=['head', 'tail'],
                       help='set the user choice to head or tail')

args = my_parser.parse_args()

print(vars(args))

Пользователь увидит больше подробностей в справке:

$ python help_example.py -h
usage: help_example.py [-h] [-a {head,tail}]

optional arguments:
-h, --help      show this help message and exit
-a {head, tail}  set the user choice to head or tail

Такая справка делает программу удобней.

Обязательно попробуйте и делитесь впечатлениями в комментариях ;)

РУБРИКИ В СТАТЬЕ

Комментарии 0

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

BUG