26

When I use Python's argparse or optparse command line argument parser, any unique prefix of an argument is considered valid, e.g.

$ ./buildall.py --help
usage: buildall.py [-h] [-f]

Build all repositories

optional arguments:
  -h, --help   show this help message and exit
  -f, --force  Build dirty repositories

works with --help, --hel, --he for the help option as well as --forc and --fo for the force option.

Can this behavior be turned off somehow? I want to get an error message for incomplete arguments.

Simon Warta
  • 10,850
  • 5
  • 40
  • 78

3 Answers3

25

The ability to disable abbreviated long options was only added in Python 3.5. From the argparse documentation:

The parse_args() method by default allows long options to be abbreviated to a prefix, if the abbreviation is unambiguous (the prefix matches a unique option) ... This feature can be disabled by setting allow_abbrev to False.

So if you're on Python 3.5, you can create your parser with allow_abbrev=False:

parser = argparse.ArgumentParser(..., allow_abbrev=False)

If you're on optparse or pre-3.5 argparse, you just have to live with abbreviated options.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • Great, do you know of an analogue option for optparse? – Simon Warta Nov 24 '15 at 18:24
  • @SimonWarta: Nope. There's a reason it's deprecated. – user2357112 Nov 24 '15 at 18:27
  • 7
    Note that `allow_abbrev` is not available prior to Python 3.5. – chepner Nov 24 '15 at 18:49
  • @chepner: Oh dammit, I didn't see that. It doesn't look like there's any way to get the behavior in pre-3.5. That sucks. – user2357112 Nov 24 '15 at 18:51
  • 1
    You could grab the `argparse.py` file from the latest Python release, and put it in your own directories (where it will have load priority). This module is self contained, so it's a drop-in replacement. The only Py2/3 incompatibility that I'm aware of is a `yield from get_subactions()` line in the `HelpFormatter` code. – hpaulj Nov 24 '15 at 20:35
  • `optparse` is still around, and will be around for along time, but don't expect any changes. `argparse` is being changed, but at a very slow pace. Developers are very cautious about backward compatibility problems, so 3.5 only got a couple of `argparse` patches. – hpaulj Nov 24 '15 at 20:41
5

For those of us still stuck on python2.7 for whatever reason, this is a minimal change to locally disable prefix matching:

class SaneArgumentParser(_argparse.ArgumentParser):
  """Disables prefix matching in ArgumentParser."""
  def _get_option_tuples(self, option_string):
    """Prevent argument parsing from looking for prefix matches."""
    return []

Now instead of using argparse.ArgumentParser, just use SaneArgumentParser. Unlike chepner's answer, this does not require any modification to the argparse module. It is also a much smaller change. Hopefully other people stuck in python's past will find this useful.

itfische
  • 696
  • 1
  • 5
  • 14
  • 3
    Even simpler approach is to modify the parser instance directly: `parser._get_option_tuples = lambda option: []`. – Nikita Nemkin Feb 07 '19 at 12:41
  • I downvoted this answer because it breaks short option handling... and then found that the current `allow_abbrev` implementation breaks short option handling the same way, and there are several related bug reports languishing on the tracker about it being broken. – user2357112 Apr 26 '19 at 20:49
3

Prior to Python 3.5, you would have to monkeypatch an undocumented ArgumentParser method. Don't actually use this; it is untested and may not work with all versions (or any version) of Python. For entertainment purposes only.

import argparse

# This is a copy from argparse.py, with a single change
def _get_option_tuples(self, option_string):
    result = []

    # option strings starting with two prefix characters are only
    # split at the '='
    chars = self.prefix_chars
    if option_string[0] in chars and option_string[1] in chars:
        if '=' in option_string:
            option_prefix, explicit_arg = option_string.split('=', 1)
        else:
            option_prefix = option_string
            explicit_arg = None
        for option_string in self._option_string_actions:
            # === This is the change ===
            # if option_string.startswith(option_prefix):
            if option_string == option_prefix:
                action = self._option_string_actions[option_string]
                tup = action, option_string, explicit_arg
                result.append(tup)

    # single character options can be concatenated with their arguments
    # but multiple character options always have to have their argument
    # separate
    elif option_string[0] in chars and option_string[1] not in chars:
        option_prefix = option_string
        explicit_arg = None
        short_option_prefix = option_string[:2]
        short_explicit_arg = option_string[2:]

        for option_string in self._option_string_actions:
            if option_string == short_option_prefix:
                action = self._option_string_actions[option_string]
                tup = action, option_string, short_explicit_arg
                result.append(tup)
            elif option_string.startswith(option_prefix):
                action = self._option_string_actions[option_string]
                tup = action, option_string, explicit_arg
                result.append(tup)

    # shouldn't ever get here
    else:
        self.error(_('unexpected option string: %s') % option_string)

    # return the collected option tuples
    return result

argparse.ArgumentParser._get_option_tuples = _get_option_tuples
p = argparse.ArgumentParser()
p.add_argument("--foo")
print p.parse_args("--f 5".split())
chepner
  • 497,756
  • 71
  • 530
  • 681
  • The change isn't very big - if you are willing to modify the guts of your `argparse.py`. If you want the behavior to be switchable you'll have make more changes to pass some sort of switch parameter. – hpaulj Nov 24 '15 at 22:38