Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Argparse: option taking one or two arguments

I want to have an option which takes one or two arguments, specifically a timestamp and an optional tolerance.

I understand that I should just use nargs='+' and error out if I get more than two values, which is what I am doing.

I am also using metavar=('timestamp', 'tolerance') so the two values can be named.

However, the help message still looks like this:
usage: foo.py [-h] [-t timestamp [tolerance ...]]
Which incorrectly implies that -t can take more than two arguments.

How can I get it to just say [-t timestamp [tolerance]]? My actual code is below:

import argparse
import sys

parser = argparse.ArgumentParser()
parser.add_argument('-t', '--timestamp', nargs='+', metavar=('timestamp', 'tolerance'))
args = parser.parse_args()
if args.timestamp and len(args.timestamp) > 2:
    sys.exit('Argument --timestamp takes one or two values')
like image 411
Barry McNamara Avatar asked May 30 '26 14:05

Barry McNamara


1 Answers

I realize I am really late to the party on this, but I had to accomplish the same thing for a work project. Below is a much simplified version of what I did.

FULL DISCLOSURE: This is obviously hacky as it relies on a private function; this was the only way I saw to do this as there does not appear to be built in support. My solution was part of an application that was packaged to include the specific version of python (3.x) that I needed (as I access a private API to accomplish this) and my project has significant automated testing to catch any breakage in the future. You have been warned.

import argparse
import re as _re

class CustomParser(argparse.ArgumentParser):

    def _match_argument(self, action, arg_strings_pattern):
        if action.dest == 'name':
            # Account for flexible number of arguments. The pattern is copied from the parent class'
            # _get_nargs_pattern() function. 
            narg_pattern = '(-*A{1,2})'
            match = _re.match(narg_pattern , arg_strings_pattern)

            if match:
                return len(match.group(1))
            else:
                raise argparse.ArgumentError(action, "expected {} or {} arguments".format(1, 2))
        else:
            return super()._match_argument(action, arg_strings_pattern)

if __name__ == '__main__':
    parser = CustomParser("Flexible argument number test")
    # nargs must be 2 so that the help output properly formats the metavar argument.
    # Notice that I added '[]' around the optional argument to be consistent with argparse.
    parser.add_argument("--name", nargs=2, metavar=("FIRST", "[LAST]"),
                        help="Your name: FIRST LAST. The last name is optional.")

    args = parser.parse_args()
    print(args)

Example output:

SCRIPT --help
usage: Flexible argument number test [-h] [--name FIRST [LAST]]

optional arguments:
  -h, --help           show this help message and exit
  --name FIRST [LAST]  Your name: FIRST LAST. The last name is optional.

SCRIPT --name John
Namespace(name=['John'])

SCRIPT --name John Smith
Namespace(name=['John', 'Smith'])

SCRIPT --name
usage: Flexible argument number test [-h] [--name FIRST [LAST]]
Flexible argument number test: error: argument --name: expected 1 or 2 arguments

Obviously there are things that you would probably want to address such as:

  • Parameterizing the name of the option that you are looking for so that the 'name' value is not duplicated
  • Have a dictionary of arguments for which the number of arguments can vary and lookup the parameter instead of having the if-statement that only works for a single argument.
like image 59
curob Avatar answered Jun 02 '26 02:06

curob



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!