Сообщить об ошибке.

Использование декоратора @click.argument() при создании сценариев

Параметры (аргументы) модуля click работают аналогично опциям, но являются позиционными. Они также поддерживают только часть функциональности опций, из-за их синтаксической природы. Модуль click также не будет автоматически генерировать справку по используемым позиционным параметрам. Это делается, чтобы избежать некрасивых страниц справки. Документировать параметры для вывода на справочной странице придется вручную.

Содержание:


Базовое использование параметров в сценарии CLI.

Самый простой вариант создать и использовать параметр в сценарии - это украсить функцию декоратором @click.argument() и передать ей аргумент с именем параметра. Передаваемый тип параметра в декорированную функцию будет определяться следующим образом. Если в декораторе @click.argument() аргумент type не определен, то будет использоваться тип аргумента default, а если аргумент default не определен, то предполагается, что типом является строка str.

import click

@click.command()
@click.argument('x')
@click.argument('y', default=1)
@click.argument('z', type=float)
def test(x, y, z):
    click.echo(f'x = {x} ({type(x)})')
    click.echo(f'y = {y} ({type(y)})')
    click.echo(f'z = {z} ({type(z)})')

if __name__ == '__main__':
    test()

Вывод сценария:

$ python test.py 5 9 7
# x = 5 (<class 'str'>)
# y = 9 (<class 'int'>)
# z = 7.0 (<class 'float'>)

Параметры/аргументов сценария, которые принимают несколько значений.

Вторая наиболее распространенная версия использования - это вариативные параметры/аргументы, которые могут принимать определенное или неограниченное количество значений. Количеством принимаемых значений таких параметров можно управлять с помощью аргумента nargs, передаваемого в декоратор @click.argument(). Если установлено значение -1, то допускается передача неограниченное количество значений.

Переданные таким параметрам значения далее передается как кортеж. Обратите внимание, что только один параметр/аргумент сценария может быть установлен в nargs=-1, так как он поглотит все аргументы.

@click.command()
@click.argument('src', nargs=-1)
@click.argument('dst', nargs=1)
def copy(src, dst):
    """Move file SRC to DST."""
    for f in src:
        click.echo(f"move {f} to folder {dst}")

Вывод сценария:

$ python3 copy.py foo.txt bar.txt my_folder
# move foo.txt to folder my_folder
# move bar.txt to folder my_folder

Обратите внимание, что код этого сценария можно переписать намного лучше. Так как, в этом конкретном примере, аргументы определены как строки. Имена файлов можно принимать как объекты Path, которые сразу проверяют путь на валидность! Смотрите раздел "Передача файла в качестве значения параметра сценария".

Замечание о непустых аргументах с переменным числом значений:

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

Такое поведение, модуль click поддерживает установкой аргумента required=True. Но если сценарий может без этого обойтись, то лучше это поведение вообще не использовать, т.к. разработчики click считают, что если вариативный аргумент пуст, то сценарии должны постепенно превращаться в NoOps. Причина этого в том, что очень часто сценарии вызываются с вводом подстановочного знака из командной строки, и они не должны выдавать ошибку, если подстановочный знак пуст.

Передача файла в качестве значения параметра/аргумента сценария.

Пример с передачей имени файла был в предыдущем подразделе, и теперь имеет смысл объяснить, как правильно обращаться с файлами. Инструменты командной строки более интересны, если они работают с файлами, способом Unix, то есть принимают - как специальный файл, который ссылается на stdin/stdout.

Модуль click поддерживает это с помощью специального типа click.File(), который умеет разумно обрабатывать файлы. Этот тип также корректно обрабатывает Юникод и байты для всех версий Python, поэтому написанный сценарий GLI остается универсальным.

@click.command()
@click.argument('input', type=click.File('rb'))
@click.argument('output', type=click.File('wb'))
def inout(input, output):
    """Copy contents of INPUT to OUTPUT."""
    while True:
        chunk = input.read(1024)
        if not chunk:
            break
        output.write(chunk)

Вывод сценария:

$ python3 inout.py - hello.txt
# hello
# ^D

$ python3 inout.py hello.txt -
# hello

Безопасное открытие файлов.

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

Это поведение может быть изменено передачей аргумента lazy=True или lazy=False конструктору click.File(). Если файл будет открыт для чтения в принудительном режиме ожидания lazy=True, то он провалит свою первую операцию ввода-вывода, вызвав исключение click.FileError.

Файлы, открытые для записи, обычно сразу очищаются от данных, следовательно принудительный режим ожидания следует отключать lazy=False только в том случае, если разработчик абсолютно уверен, что такое поведение - ожидаемое.

Начиная с версии 2.0, также можно открывать файлы в атомарном режиме, передавая atomic=True. В атомарном режиме все записи отправляются в отдельный файл в той же папке, и по завершении файл будет перемещен в исходное расположение. Это полезно, если файл, регулярно читается и/или изменяется другими пользователями.

Передача пути в качестве значения параметра/аргумента сценария.

В предыдущем примере файлы открывались сразу. Но что, если нам нужно просто имя файла? Простейший способ, это считывать путь как строку. Модуль click основан на Unicode, следовательно строка всегда будет значением Unicode. К сожалению, имена файлов могут быть в Юникоде или байтах, в зависимости от того, какая операционная система используется. Таким образом, тип str недостаточен.

В модуле click существует тип click.Path, который автоматически обрабатывает эту неоднозначность. Он не только вернет байты или Unicode, в зависимости от того, что имеет больше смысла, но также сможет выполнять некоторые базовые проверки, такие как проверки существования пути файловой системы.

@click.command()
@click.argument('filename', type=click.Path(exists=True))
def touch(filename):
    """Print FILENAME if the file exists."""
    click.echo(click.format_filename(filename))

Вывод сценария:

$ python3 touch.py hello.txt
# hello.txt

$ python3 touch.py missing.txt
# Usage: touch [OPTIONS] FILENAME
# Try 'touch --help' for help.
# 
# Error: Invalid value for 'FILENAME': Path 'missing.txt' does not exist.

Подстановка значений из environment для параметров сценария.

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

@click.command()
@click.argument('src', envvar='SRC', type=click.File('r'))
def echo(src):
    """Print value of SRC environment variable."""
    click.echo(src.read())

Вывод сценария:

$ export SRC=hello.txt
$ python3 echo.py
# Hello World!

В этом случае это также может быть список различных переменных среды, из которых выбирается первая. Как правило, использовать эту функциональность не рекомендуется, так как она может запутать пользователя.

Обработка значений параметров сценария, похожих на опции.

Иногда нужно обрабатывать параметры/аргументы, которые выглядят как опции. Например, представьте, что есть файл с именем -foo.txt. Если его передать как значение позиционного параметра то, скорее всего, он будет рассматриваться как опция.

Чтобы решить эту проблему, модуль click выполняет то же, что и любой сценарий командной строки в стиле POSIX, а именно принимает строку -- в качестве разделителя для опций и параметров. После маркера -- все остальные опции принимаются в качестве позиционных параметров.

@click.command()
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
    """Print all FILES file names."""
    for filename in files:
        click.echo(filename)

Вывод сценария:

$ python3 touch.py -- -foo.txt bar.txt
# -foo.txt
# bar.txt

Если вдруг не нравится маркер --, то можно установить для ignore_unknown_options значение True, чтобы не проверять неизвестные опции (т.е. опции, которые не определены в сценарии):

@click.command(context_settings={"ignore_unknown_options": True})
@click.argument('files', nargs=-1, type=click.Path())
def touch(files):
    """Print all FILES file names."""
    for filename in files:
        click.echo(filename)

Вывод сценария:

$ python3 touch.py -foo.txt bar.txt
# -foo.txt
# bar.txt