argparse: Ignore positional arguments if a flag is set?

Viewed 924

I want to provide the command with 3 arguments: <version>, <input file> and <output file> under normal usage. Except there's a specific flag --init, which will basically run the program without any input and output file specifications required.

I would therefore ideally have a command which usage is:

py program.py --init

OR

py program.py <version> <input file> <output file>

However, since positional arguments are always required (because all 3 are required in any other circumstance than --init), there seems to be no way to cleanly get this syntax and all I could think of would be to make the 3 positional arguments into an optional flag, raise an exception if the optional flag isn't there when --init is not called. And that all seems ugly.

My code so far:

def get_args():
    parser = argparse.ArgumentParser(description="Tool for FOO-ing a BAR.")
    parser.add_argument(dest="version", help="The version.")
    parser.add_argument(dest="input", help="The input file.")
    parser.add_argument(dest="output", help="The output file.")

    parser.add_argument("-i", "--init", dest="init", action="store_true", help="Foo Init.")

    return parser.parse_args()

To Clarify:
Either all 3 arguments (<version> <input> <output>) MUST be specified.
OR
The program is only ran with --init flag and 0 arguments should be specified.

The program should NOT accept with 0, 1 or 2 arguments specified (without the --init flag).

4 Answers

You can define your own action class:

class init_action(argparse.Action):
    def __init__(self, option_strings, dest, **kwargs):
        return super().__init__(option_strings, dest, nargs=0, default=argparse.SUPPRESS, **kwargs)
    
    def __call__(self, parser, namespace, values, option_string, **kwargs):
        # Do whatever should be done here
        parser.exit()

def get_args():
    parser = argparse.ArgumentParser(description="Tool for FOO-ing a BAR.")
    parser.add_argument(dest="version", help="The version.")
    parser.add_argument(dest="input", help="The input file.")
    parser.add_argument(dest="output", help="The output file.")

    parser.add_argument("-i", "--init", action=init_action, help="Foo Init.")

    return parser.parse_args()

I had the exact same problem, and fixed it by adding nargs="?" to all my positional arguments

def get_args():
    parser = argparse.ArgumentParser(description="Tool for FOO-ing a BAR.")
    parser.add_argument(dest="version", nargs="?", help="The version.")
    parser.add_argument(dest="input", nargs="?", help="The input file.")
    parser.add_argument(dest="output", nargs="?", help="The output file.")

    parser.add_argument("-i", "--init", dest="init", action="store_true", help="Foo Init.")

    return parser.parse_args()

but I'm not completely satisfied with it since when you run

py program.py -h

the synopsys is printed as

usage: program.py [-h] [--init] [version] [input] [output]

while I would want it to be

usage: program.py [-h] [--init] version input output

since version, input and output are required, just that they are not required when you use the --init flag.

Another downside is that you do not get the help text printed if you do not provide the positional arguments correctly.

I think it is sad that argparse have no way to set ignore_positional_args=True or something on flags. Especially since this property already is true for the -h flag. At least I'm not able to find a way to do it.

EDIT:

I actually found a way to better way to do it, but put it in a separate answer because I think this can be a useful approach as well.

parser.add_argument has a default parameter (docs) which can be used here for version, input and output parameters. Now you won't need third init param

One possible solution is to use sys.argv to determine whether the flag is enabled. Using your example,

import sys

if '--init' not in sys.argv:
    parser.add_argument(dest="version", help="The version.")
    parser.add_argument(dest="input", help="The input file.")
    parser.add_argument(dest="output", help="The output file.")

Argparse will allow you to run the program with either --init flag without any arguments or force you to include other arguments if --init flag is not present.

One downside to this approach is that you will see the following output, when you run py program.py -h:

usage: program.py [--init] version input output

positional arguments:
  version - The version.
  input - The input file.
  output - The output file.

optional arguments:
  --init - ...

which makes it hard for the user to understand the logic unless you clarify it in the description in the help section.

Related