J.doe
J.doe

Reputation: 67

Python3 Argparse optional positional argument

Currently when I try to run one of my options in argparse I have to write something before, example:

python3 main.py https://example.com ping-history

This is fine for my other options (ping and others) since I need to specify which website I want to access. However I need it to make an exception for one of my options. So it would look like this instead:

python3 main.py ping-history

I've tried adding:

nargs="?"

to:

parser.add_argument('filename', help="input file") 

This solves the problem, however when I try to run --verbose or --silent:

python3 main.py https://example.com -v title

I get this:

main.py: error: argument commands: invalid choice: 'https://example.com'

I'm not sure what is happening, could someone please explain?

edit: Parser file:

import argparse


VERSION = "v1.0.0 (2017-06-16)"



def add_options(parser):
    """add options"""
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-s", "--silent", dest="silent", help="Less output", action="store_true")
    group.add_argument("-v", "--verbose", dest="verbose", help="More output", action="store_true")
    parser.add_argument("-V", "--version", action="version", version=VERSION)
    parser.add_argument('filename', help="input file", nargs="?")


def add_commands(parser):
    """Adds commands"""
    subparser = parser.add_subparsers(title="commands  (positional arguments)", help="Available commands",\
    dest="commands")

    subparser.add_parser("lines", help="count lines in textfile")
    subparser.add_parser("words", help="count words in textfile")
    subparser.add_parser("letters", help="count letters in textfile")
    subparser.add_parser("all", help="count all")
    subparser.add_parser("word_frequency", help="count word frequency")
    subparser.add_parser("letter_frequency", help="count letter frequency")
    #subparser.add_parser("filename", help= "filename to analyze")
    subparser.add_parser("ping", help="ping a website")
    subparser.add_parser("ping-history", help="show past status code")
    subparser.add_parser("quote", help="retrieve todays quote")
    subparser.add_parser("title", help="retrive titel from page")


def parse_options():
    """add options"""

    parser = argparse.ArgumentParser()
    add_options(parser)
    add_commands(parser)


    arg, unknown_args = parser.parse_known_args()

    options = {}
    options["known_args"] = vars(arg)
    options["unknown_args"] = unknown_args




    return options

Upvotes: 0

Views: 1502

Answers (1)

hpaulj
hpaulj

Reputation: 231355

In my comment I guessed that you had an optional positional last. But on further thought your error is more consistent with an initial optional positional.

In [42]: parser=argparse.ArgumentParser()
In [43]: a1 = parser.add_argument('name');
In [44]: parser.add_argument('-v',action='store_true');
In [45]: parser.add_argument('cmds',choices=['one','two'])

In [46]: parser.parse_args('foo one'.split())
Out[46]: Namespace(cmds='one', name='foo', v=False)
In [47]: parser.parse_args('foo -v one'.split())
Out[47]: Namespace(cmds='one', name='foo', v=True)

Change the first argument to be optional:

In [48]: a1.nargs
In [49]: a1.nargs='?'

In [50]: parser.parse_args('foo one'.split())
Out[50]: Namespace(cmds='one', name='foo', v=False)

In [51]: parser.parse_args('foo -v one'.split())
usage: ipython3 [-h] [-v] [name] {one,two}
ipython3: error: argument cmds: invalid choice: 'foo' (choose from 'one', 'two')
...

I get the same error if I just give it one string:

In [54]: parser.parse_args('foo'.split())
usage: ipython3 [-h] [-v] [name] {one,two}
ipython3: error: argument cmds: invalid choice: 'foo'

parse_args alternates between evaluating positionals and optionals. When doing positionals it tries to handle as many strings as possible (using regex style pattern matching). A '?' positional can match an empty string. So when it sees just one string 'foo', it matches [] with 'name' and tries to match 'foo' with 'cmd'.

There are bug/issues about making the parsing look further ahead, and anticipate that the 2nd positional might satisfied with a string after the flag. But for now, don't mix flags and positionals when one or more of positionals is 'optional'.

In [55]: parser.parse_args('foo one -v'.split())
Out[55]: Namespace(cmds='one', name='foo', v=True)
In [56]: parser.parse_args('-v foo one'.split())
Out[56]: Namespace(cmds='one', name='foo', v=True)

edit

In your full parser, the second positional is the subparser. As I demonstrated above, it tries to apply ['https://example.com'] to both a '?' argument and the subparser.

So the working call is:

python3 main.py -v https://example.com title

If only some of the commands need a filename, consider assigning that argument to those subparsers rather than make it an optional one for the parent parser.

In general parent positionals are tricky when using subparsers. It's easier to make all the parent arguments flagged.

Upvotes: 1

Related Questions