Большие приложения#

Для более сложных приложений, интерфейс командной строки, как правило, тоже усложняется. С помощью Click можно создавать сложные интерфейсы командной строки, но для этого надо разобраться с понятием контекста.

Контекст (Context) это внутренний объект Click, который создается при выполнении команды click и содержит информацию о том какая команда была вызвана, с какими параметрами. В простых случаях с контекстом не нужно работать напрямую, но можно посмотреть на него, если добавить декоратор click.pass_context к функции:

import click


@click.command()
@click.argument("ip_address")
@click.option("--count", "-c", default=2, type=int, help="Number of packets")
@click.pass_context
def ping_ip(ctx, ip_address, count):
    """
    Ping IP address and return True/False
    """
    print(ctx.command)
    print(ctx.params)


if __name__ == "__main__":
    ping_ip()

Вывод будет таким:

$ python example_01_ping_function.py 8.8.8.8
<Command ping-ip>
{'ip_address': '8.8.8.8', 'count': 2}

В выводе видно какая команда была вызвана - ping-ip и какие параметры были указаны: тут и значение адреса 8.8.8.8 и значение опции по умолчанию - 2.

Кроме того, что контекст содержит много информации для самого click, ему можно присваивать произвольные значение в атрибут obj. Этот атрибут используется для передачи информации между группой команд и командами. Когда значений несколько, как правило, используется словарь.

click.group#

click.group это декоратор, который работает так же как click.command, но при этом позволяет создавать подкоманды. Пример интерфейса с подкомандами - git. В зависимости от того какая команда пишется после git, появляются разные аргументы и опции.

Пример интерфейса с группой:

import click


@click.group()
def pomodoro_cli():
    pass


@pomodoro_cli.command()
@click.option("--day", "-d", is_flag=True)
@click.option("--week", "-w", is_flag=True)
@click.option("--month", "-m", is_flag=True)
def stats(day, week, month):
    print("STATS")


@pomodoro_cli.command()
@click.option("--pomodoros_to_run", "-r", default=5, show_default=True, type=int)
@click.option("--work_minutes", "-w", default=25, show_default=True, type=int)
@click.option("--short_break", "-s", default=5, show_default=True, type=int)
@click.option("--long_break", "-l", default=30, show_default=True, type=int)
@click.option("--set_size", "-p", default=4, show_default=True, type=int)
def work(pomodoros_to_run, work_minutes, short_break, long_break, set_size):
    pass


if __name__ == "__main__":
    pomodoro_cli()

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

@click.group()
def pomodoro_cli():
    pass

Следующие две функции создают подкоманды скрипта stats и work (как commit и add в git). Имена функций будут именами команд. У этих функций могут быть свои аргументы и опции. Единственное отличие от предыдущих примеров - это то, что вместо декоратора click.command используется pomodoro_cli.command. Таким образом указывается, что команда относится к группе pomodoro_cli:

@pomodoro_cli.command()
@click.option("--day", "-d", is_flag=True)
@click.option("--week", "-w", is_flag=True)
@click.option("--month", "-m", is_flag=True)
def stats(day, week, month):
    print("STATS")


@pomodoro_cli.command()
@click.option("--pomodoros_to_run", "-r", default=5, show_default=True, type=int)
@click.option("--work_minutes", "-w", default=25, show_default=True, type=int)
@click.option("--short_break", "-s", default=5, show_default=True, type=int)
@click.option("--long_break", "-l", default=30, show_default=True, type=int)
@click.option("--set_size", "-p", default=4, show_default=True, type=int)
def work(pomodoros_to_run, work_minutes, short_break, long_break, set_size):
    pass

При такой настройке help скрипта выглядит таким образом:

$ python example_10_click_group_basics.py --help
Usage: example_10_click_group_basics.py [OPTIONS] COMMAND [ARGS]...

Options:
  --help  Show this message and exit.

Commands:
  stats
  work

У каждой команды есть свой help:

$ python example_10_click_group_basics.py stats --help
Usage: example_10_click_group_basics.py stats [OPTIONS]

Options:
  -d, --day
  -w, --week
  -m, --month
  --help       Show this message and exit.


$ python example_10_click_group_basics.py work --help
Usage: example_10_click_group_basics.py work [OPTIONS]

Options:
  -r, --pomodoros_to_run INTEGER  [default: 5]
  -w, --work_minutes INTEGER      [default: 25]
  -s, --short_break INTEGER       [default: 5]
  -l, --long_break INTEGER        [default: 30]
  -p, --set_size INTEGER          [default: 4]
  --help                          Show this message and exit.

В этом случае у каждой команды свои параметры, плюс команд мало.

click.pass_context#

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

Пример скрипта (example_11_click_context.py):

import click


@click.group()
@click.option("--db-filename", "-n", help="db filename")
@click.pass_context
def dhcp_db(context, db_filename):
    context.obj = {"db_filename": db_filename}


@dhcp_db.command()
@click.option("--db-schema", "-s", help="db schema filename")
@click.pass_context
def create(context, db_schema):
    """
    create DB
    """


@dhcp_db.command()
@click.argument("filename", nargs=-1, required=True)
@click.option("--switch-data", "-s", default=False, is_flag=True)
@click.pass_context
def add(context, filename, switch_data):
    """
    add data to db from FILENAME
    """


@dhcp_db.command()
@click.option("--key", "-k", type=click.Choice(["mac", "ip", "vlan"]))
@click.option("--value", "-v", help="value of key")
@click.option("--show-all", "-a", is_flag=True, help="show db content")
@click.pass_context
def get(context, key, value, show_all):
    """
    get data from db
    """


if __name__ == "__dhcp_db__":
    dhcp_db()