Параметры (аргументы) модуля click
работают аналогично опциям, но являются позиционными. Они также поддерживают только часть функциональности опций, из-за их синтаксической природы. Модуль click
также не будет автоматически генерировать справку по используемым позиционным параметрам. Это делается, чтобы избежать некрасивых страниц справки. Документировать параметры для вывода на справочной странице придется вручную.
environment
.Самый простой вариант создать и использовать параметр в сценарии - это украсить функцию декоратором @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