Reputation: 1305
I'm trying out Click for the first time but I've hit a stumbling block.
I want my (two) subcommands to either take a file pathname option or accept file contents from STDIN.
Allowed: Use a path for --compose-file
./docker-secret-helper.py secret-hash-ini --compose-file docker-compose-test.yml
Allowed: Use contents of a file as stdin
cat docker-compose-test.yml | ./docker-secret-helper.py secret-hash-ini
(Should there be an option to indicate stdin, e.g., -i
, or whatever?)
Not Allowed: Neither --compose-file nor stdin passed
./docker-secret-helper.py secret-hash-ini
Should return something like: You must either pass --compose-file or pipe in stdin.
Current Script
My current script accepts (only) the file pathname (via --compose-file
):
#!/usr/bin/env python
import click
from DockerSecretHelper import DockerSecretHelper
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
pass
@cli.command(help="retrieves an ini-style file of variables to be used as env vars for docker-compose commmand")
@click.option('--compose-file', help='compose file to work with', required=True)
def secret_hash_ini(**kwargs):
helper = DockerSecretHelper()
print(helper.get_secret_hash_ini_format_from_compose_file(**kwargs))
# will need some kind of if block to call helper.get_secret_hash_ini_format_from_compose_contents(**kwargs) in the
# case of stdin
@cli.command(help="retrieves names/values of external secrets; to be used by `docker secret set`")
@click.option('--compose-file', help='compose file to work with', required=True)
def external_secret_info_json(**kwargs):
helper = DockerSecretHelper()
print(helper.get_external_secret_info_as_json_from_compost_file(**kwargs))
# will need some kind of if block to call helper.get_external_secret_info_as_json_from_compose_contents(**kwargs) in
# the case of stdin
if __name__ == '__main__':
cli()
How do I implement and enforce either STDIN or a file pathname (but not both).
I'm open to changes to my command's syntax to better follow potential conventions.
This question is similar to Creating command line application in python using Click so it might provide some building blocks (which I'm having trouble assembling).
Upvotes: 5
Views: 5678
Reputation: 312213
I would use click's File
option type:
import click
import sys
@click.group()
def cli():
pass
@cli.command()
@click.option('--compose-file',
help='compose file to work with',
type=click.File('r'),
default=sys.stdin)
def secret_hash_ini(compose_file):
with compose_file:
data = compose_file.read()
print(data)
if __name__ == '__main__':
cli()
Assuming we have a file example.txt
that contains the text:
This is a test.
Then we can specify a file with --compose-file
:
$ python docker-secret-helper.py secret-hash-ini --compose-file example.txt
This is a test.
Or we can read from stdin
:
$ python docker-secret-helper.py secret-hash-ini < example.txt
This is a test.
We can't generate an error in the case that "Neither --compose-file nor stdin passed" because stdin
is always available. If we call docker-secret-helper.py
without providing --compose-file
and without redirecting stdin
, it will simply hang waiting for input.
Upvotes: 13
Reputation: 1797
To make this more universal, you can also do the same thing for output:
#!/usr/bin/env python
"""
Example to read from stdin and write to stdout when no filenames are given.
"""
import sys
from string import Template
import click
@click.command()
@click.option(
"--file",
help="Filename of the template, omit to read from STDIN.",
type=click.File("rt"),
default=sys.stdin,
)
@click.option(
"--out",
help="File to write the Output to, omit to display on screen.",
type=click.File("at"),
default=sys.stdout,
)
def generate(file, out):
"""Read the file, and write to out."""
template = Template(file.read())
click.echo("#Write something", file=out)
...
if __name__ == "__main__":
generate()
so you can do ./my_sript.py < some.tpl > output.txt
as well as ./my_script.py --file=some.tpl --out=output.txt
Upvotes: 3