Supply either STDIN or a file pathname option to Python Click CLI script
Asked Answered
D

2

5

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).

Davisdavison answered 20/1, 2020 at 19:36 Comment(0)
D
12

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.

Damoiselle answered 20/1, 2020 at 19:57 Comment(1)
You can prevent the hang with: docs.python.org/3/library/os.html#os.isatty This can be used to insure stdin is a pipe...Sahaptin
M
3

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

Magma answered 11/6, 2021 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.