Search code examples
pythonpython-click

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


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


Solution

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