1

Essentially, I have a program that takes in a few optional/positional arguments but I want an optional argument that can be used on its own. I want it to work similar to how the "--version" option works in that if that argument is given, no positional arguments are required and the code runs.

I've made a couple test files to show what I mean:

import argparse
import os,sys

def main():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    
    parser.add_argument("--print-line", type=str, metavar="file", help="print the first line of a file")
    parser.add_argument("num", type=int, help="an important number")
    
    args = parser.parse_args()

    if args.print_line is not None:
        if not os.path.isfile(args.print_line):
            parser.error("file:%s not found" % args.print_line)
        with open(args.print_line, "r") as infile:
            print("First line:\n%s" % infile.readline().rstrip())
        return
    
    print("Important number: %i" % args.num)
    
    
if __name__ == "__main__":
    main()

The help option gives:

usage: notworks_goodhelp.py [-h] [--print-line file] num

positional arguments:
  num                an important number

optional arguments:
  -h, --help         show this help message and exit
  --print-line file  print the first line of a file (default: None)

Which is perfect! However, if I try and use the optional argument,

$ python notworks_goodhelp.py --print-line file.txt 
usage: notworks_goodhelp.py [-h] [--print-line file] num
notworks_goodhelp.py: error: too few arguments

it won't work because it's missing the positional argument.

I used parse_known_args() to make the script work, but then the help screen is wrong! Shown below:

import argparse
import os,sys

def main():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    
    parser.add_argument("--print-line", type=str, metavar="file", help="print the first line of a file")
    args, leftovers = parser.parse_known_args()
    
    if args.print_line is not None:
        if not os.path.isfile(args.print_line):
            parser.error("file:%s not found" % args.print_line)
        with open(args.print_line, "r") as infile:
            print("First line:\n%s" % infile.readline().rstrip())
        return
    
    parser.add_argument("num", type=int, help="an important number")
    args = parser.parse_args(leftovers)
    
    print("Important number: %i" % args.num)
    
    
if __name__ == "__main__":
    main()

But the help screen is:

usage: works_nohelp.py [-h] [--print-line file]

optional arguments:
  -h, --help         show this help message and exit
  --print-line file  print the first line of a file (default: None)

Which is missing the 'num' positional argument!

Does anyone know a fix for this?

  • `--version` works because it's bound to a `version` action which immediately outputs the version and exits the script, rather than waiting until all options have been parsed. You can do the same for `--print_line`: define a custom subclass of `argparse.Action` that does what you want when the option is parsed, rather than waiting until *all* options have been parsed. – chepner Jun 19 '21 at 17:48
  • The first `parse_args` or `parse_known_args` sees the `help` (or version) and acts. – hpaulj Jun 19 '21 at 17:59
  • Ah, I see. I'm going to try the `argparse.Action` method. – Justin Marquez Jun 19 '21 at 18:49

1 Answers1

0

Based on chepner's comment, I was able to get it working in this program with the proper action and help screen. Thank you!

import argparse
import os,sys

class ExitOnAction(argparse.Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        if nargs != 1 and nargs is not None:
            print("nargs: " + str(nargs))
            raise ValueError("nargs must be 1")
        super(ExitOnAction, self).__init__(option_strings, dest, **kwargs)
    
    def __call__(self, parser, namespace, values, option_string=None):
        setattr(namespace, self.dest, values)
        if not os.path.isfile(values):
            parser.error("file:%s not found" % values)
        with open(values, "r") as infile:
            print("First line:\n%s" % infile.readline().rstrip())
            sys.exit()
        
        

def main():
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    
    parser.add_argument("--print-line", action=ExitOnAction, type=str, metavar="file", help="print the first line of a file")
    parser.add_argument("num", type=int, help="an important number")
    
    print("Parsing args")
    args = parser.parse_args()
    print("Parsed args")
    '''
    if args.print_line is not None:
        if not os.path.isfile(args.print_line):
            parser.error("file:%s not found" % args.print_line)
        with open(args.print_line, "r") as infile:
            print("First line:\n%s" % infile.readline().rstrip())
        return
    '''
    print("Important number: %i" % args.num)
    
    
if __name__ == "__main__":
    main()