Search code examples
pythondocopt

Option with optional argument


Suppose that I have

My program

Usage:
  myprog [options]

Options:
  -h, --help        Show this screen.
      --version     Show version.
      --files=<arg> Files. [default: foo.txt]

I would like to distinguish in my code:

  • --files not specified.
  • --files specified, but with no argument to accept the default.
  • --files myfile, i.e. --files specified with custom argument.

With the current docstring I can either

  • Not specify --files.
  • Specify --files with an argument.

So I'm missing:

  • The option to specify --files without an argument.
  • Distinguish if --files was specified, or if the user specified --files foo.txt

Solution

  • You will need to specify the --files argument in the main usage string. For example:

    # dopt.py
    from docopt import docopt
    
    dstr = """My program
    
    Usage:
      myprog [--files [FNAME]] [options]
    
    Options:
      -h, --help        Show this screen.
          --version     Show version.
    """
    
    if __name__ == '__main__':
        arguments = docopt(dstr)
        print(arguments)
    

    This essentially makes --files a true/false argument and adds another argument FNAME to hold the file name. Usage:

    $ python dopt.py
    {'--files': False,
     '--help': False,
     '--version': False,
     'FNAME': None}
    
    $ python dopt.py --files
    {'--files': True,
     '--help': False,
     '--version': False,
     'FNAME': None}
    
    $ python dopt.py --files abc.txt
    {'--files': True,
     '--help': False,
     '--version': False,
     'FNAME': 'abc.txt'}
    

    Then, you can use the value of --files and FNAME from the returned dict to infer what to do:

    if not arguments['--files']:
        print("Not using files")
    elif not arguments['FNAME']:
        print("Using default file foo.txt")
    else:
        print(f"Using file {arguments['FNAME']}")
    

    A pitfall to remember: you can also specify FNAME independently of --files. So this also works, and it might interfere with other arguments, so be sure to test all combinations thoroughly:

    $ python dopt.py abc.txt
    {'--files': False,
     '--help': False,
     '--version': False,
     'FNAME': 'abc.txt'}
    Not using files
    

    Personally, I prefer using argparse because it's less ambiguous. It builds the doc string from the prescribed arguments, not the other way round.

    In argparse, an argument can have a default value and you can specify that it can take zero or one argument using nargs="?". Then, you can specify a const="foo.txt" value which the argument will take if no values are given. For example:

    # dopt.py
    import argparse
    
    parser = argparse.ArgumentParser()
    parser.add_argument("--files", required=False, default=None, nargs="?", const="foo.txt")
    p = parser.parse_args()
    print(p)
    

    And running this:

    $ python dopt.py
    Namespace(files=None)
    
    $ python dopt.py --files
    Namespace(files='foo.txt')
    
    $ python dopt.py --files abc.txt
    Namespace(files='abc.txt')
    

    And it even handles the "no --files" case correctly:

    $ python dopt.py abc.txt
    usage: dopt.py [-h] [--files [FILES]]
    dopt.py: error: unrecognized arguments: abc.txt