Don't parse options after the last positional argument

Viewed 3616

I'm writing a wrapper around the ssh command line client. After the first positional argument that's part of command, all further options should also be treated as positional arguments.

Under optparse, I believe this would be done with disable_interspersed_args.

Presently I have something like this:

parser = argparse.ArgumentParser()
parser.add_argument('--parallel', default=False, action='store_true')
# maybe allow no command? this would ssh interactively into each machine...
parser.add_argument('command', nargs='+')
args = parser.parse_args()

But if options are passed as part of the command (such as my_wrapper ls -l), they're instead interpreted by ArgumentParser as unknown options. error: unrecognized arguments: -l

If I use parse_known_args(), the options may be taken out of order.

p = argparse.ArgumentParser()
p.add_argument('-a', action='store_true')
p.add_argument('command', nargs='+')
print(p.parse_known_args())

$ python3 bah.py -b ls -l -a
(Namespace(a=True, command=['ls']), ['-b', '-l'])

Here you can see that -b's position before ls has been lost, and -a has been parsed out from the command, which is not desired.

How can I:

  • Prevent arguments from being parsed after a certain point?
  • Disable parsing of interspersed arguments?
  • Allow arguments with a prefix to be consumed as positional arguments?
4 Answers

What @dcolish suggested is the universal approach. Here is a sample implementation which also supports the standard -- separator, but its usage is not required for correct parsing.

Result:

# ./parse-pos.py -h
usage: parse-pos.py [-h] [-qa] [-qb] COMMAND [ARGS...]

# ./parse-pos.py -qa ls -q -h aa /bb
try_argv = ['-qa', 'ls']
cmd_rest_argv = ['-q', '-h', 'aa', '/bb']
parsed_args = Namespace(command='ls', qa=True, qb=False)

The code:

#!/usr/bin/python3

import argparse
import sys
from pprint import pprint

class CustomParserError(Exception):
    pass

class CustomArgumentParser(argparse.ArgumentParser):
    def error(self, message):
        raise CustomParserError(message)
    def original_error(self, message):
        super().error(message)

def parse_argv():
    parser = CustomArgumentParser(description='Example')
    parser.add_argument('command', metavar='COMMAND [ARGS...]', help='the command to be executed')
    parser.add_argument('-qa', action='store_true') # "ambiguous option" if you specify just "-q"
    parser.add_argument('-qb', action='store_true') # "ambiguous option" if you specify just "-q"

    def parse_until_positional(parser, _sys_argv = None):
        if _sys_argv is None:
            _sys_argv = sys.argv[1:] # skip the program name
        for i in range(0, len(_sys_argv) + 1):
            try_argv = _sys_argv[0:i]
            try:
                parsed_args = parser.parse_args(try_argv)
            except CustomParserError as ex:
                if len(try_argv) == len(_sys_argv):
                    # this is our last try and we still couldn't parse anything
                    parser.original_error(str(ex)) # sys.exit()
                continue
            # if we are here, we parsed our known optional & dash-prefixed parameters and the COMMAND
            cmd_rest_argv = _sys_argv[i:]
            break
        return (parsed_args, cmd_rest_argv, try_argv)

    (parsed_args, cmd_rest_argv, try_argv) = parse_until_positional(parser)

    # debug
    pprint(try_argv)
    pprint(cmd_rest_argv)
    pprint(parsed_args)

    return (parsed_args, cmd_rest_argv)

def main():
    parse_argv()

main()
Related