In a script: make command line argument required with no options and not required with option

Hello!
I am trying to configure a Python 3.10 script, launched from command-line, as follows:

  1. If no command-line options are specified, the script requires a string as a mandatory parameter;
  2. If option -o is specified, it does not require any other parameters.

I am struggling when trying to apply this to the code. So far, I have:

parser = argparse.ArgumentParser(description="My test script")
parser.add_argument("input_string_1", type=str, nargs=1, help="Insert the string")
parser.add_argument("-o", "--my-option", help="This does not require a string", action="store_true")
args = parser.parse_args()

If I try to run the script as ./myscript.py -o, I get:

error: the following arguments are required: input_string_1

How can I deactivate the requirement for input_string_1 when using option -o?

It’s probably simplest and most readable to write a little extra condition after the parse (as long as input_string_1 has a default value like None that cannot be reproduced by a command line arg), and throw an error if it equals that unachievable default without -o.

Maybe there’s a better way with click or some other third party framework, or with custom Actions or something. Otherwise the cleanest way I can think of using argparse for everything except an if statement, is to have two parsers. The first one parses -o, and all the other normal args. Call that one’s .parse_known_args method an d test the namespace for -o. An if statement again could dispatch two different parsers (a total of three) on the remaining args, or dynamically add the real default value for input_string_1 if -o is there, using .set_defaults, and then parse the rest.

2 Likes

I suspect one of the reasons you are struggling with this is that this pattern is not common, and may not be a good pattern for users of your script to understand. If the optional argument and positional argument are mutually exclusive, you may want to find another way to express this.

For example, if the positional argument is not required when -o is supplied because the script will read from standard input, then make that explicit by requiring the user to supply - as the positional argument.

1 Like

The code below just about works, but as Kevin pointed out, the error messages are confusing for the user (if left unchanged).

Another simpler possibility is to flip the logic of -o, but not to “store_false”, but to take one subsequent positional arg if present; otherwise giving it a default value.

import argparse

opt_parser = argparse.ArgumentParser(description="My test script")
opt_parser.add_argument("-o", "--my-option", action="store_true", help="This does not require a string")

main_parser = argparse.ArgumentParser(description="My test script")

opt_namespace, rest = opt_parser.parse_known_args()
if opt_namespace.my_option:
    kwargs = dict(nargs= '?', default = None)
else:
    kwargs = dict(nargs=1)

main_parser.add_argument("input_string_1", type=str, help="Insert the string", **kwargs)

main_namespace = main_parser.parse_args(rest)


print(f'{opt_namespace=}')
print(f'{main_namespace=}')
1 Like

Thanks for your help and all your suggestions. Yes, it is maybe an unusual requirement.
Instead of adding more code, I tried to rework the script as follows, avoiding the use of -o at all and checking instead just if input_string_1 is present:

  • the input_string_1 is not mandatory
  • if input_string_1 is provided as argument, the script uses it
  • if input_string_1 is not present, the script implicitly assumes that the condition corresponding to option -o is automatically true, without requiring in this case that option -o is mandatory.

It is probably trivial, but this way it is not necessary to rethink the argparse code.

parser = argparse.ArgumentParser(description="My test script")
parser.add_argument("input_string_1", type=str, nargs='?', help="Insert the string")
args = parser.parse_args()

if args.input_string_1 is None:
    # do as if option -o was active
else:
    # use args.input_string_1
1 Like