39

I have a program that uses a default name and password. I'm using argparse to allow the user to specify command line options, and I would like to enable the user to provide the program with a different name and password to use. So I have the following:

parser.add_argument(
    '-n',
    '--name',
    help='the login name that you wish the program to use'
    )

parser.add_argument(
    '-p',
    '--password',
    help='the password to log in with.'
    )

But it doesn't make any sense to specify only the name or only the password, but it would make sense to specify neither one. I noticed that argparse does have the ability to specify that two arguments are mutually exclusive. But what I have are two arguments that must appear together. How do I get this behavior? (I found "argument groups" mentioned in the docs, but they don't appear to solve my problem http://docs.python.org/2/library/argparse.html#argument-groups)

tadasajon
  • 14,276
  • 29
  • 92
  • 144
  • And I assume that post-processing the arguments is out the the question? – mgilson Jan 16 '13 at 02:14
  • 1
    Nothing is out of the question. I just want argparse to do the work for me and tell the user that the options must appear together. – tadasajon Jan 16 '13 at 02:20

3 Answers3

26

I believe that the best way to handle this is to post-process the returned namespace. The reason that argparse doesn't support this is because it parses arguments 1 at a time. It's easy for argparse to check to see if something was already parsed (which is why mutually-exclusive arguments work), but it isn't easy to see if something will be parsed in the future.

A simple:

parser.add_argument('-n','--name',...,default=None)
parser.add_argument('-p','--password',...,default=None)
ns = parser.parse_args()

if len([x for x in (ns.name,ns.password) if x is not None]) == 1:
   parser.error('--name and --password must be given together')

name = ns.name if ns.name is not None else "default_name"
password = ns.password if ns.password is not None else "default_password"

seems like it would suffice.

mgilson
  • 300,191
  • 65
  • 633
  • 696
  • This is correct, but 'name = ns.name or "default_name"' is simpler and in my opinion easier to read that 'ns.name if ns.name is not None else ...'. However, as mgilson points out elsewhere, if the user passes an empty string for either value, the empty string will evaluate false and the default will be used instead of the provided empty string. – James Polley Oct 21 '14 at 23:47
  • `sum(1 for x in (ns.name, ns.password) if x is not None)` also works in the place of `len([x for x in (ns.name,ns.password) if x is not None])`. – wecsam Apr 17 '18 at 12:30
  • 1
    Wouldn't `ns.name is None ^ ns.password is None` be enough? :D – Danon Nov 18 '19 at 12:20
26

I know this is more than two years late, but I found a nice and concise way to do it:

if bool(ns.username) ^ bool(ns.password):
    parser.error('--username and --password must be given together')

^ is the XOR operator in Python. To require both arguments given at the command line is essentially an XOR test.

Ting Qian
  • 361
  • 3
  • 3
8

This is probably how I'd do it. Since you have existing defaults with the option to change, define the defaults, but don't use them as your argument defaults:

default_name = "x"
default_pass = "y"
parser.add_argument(
    '-n',
    '--name',
    default=None,
    help='the login name that you wish the program to use'
    )

parser.add_argument(
    '-p',
    '--password',
    default=None,
    help='the password to log in with.'
    )
args = parser.parse_args()
if all(i is not None for i in [args.name, args.password]):
    name = args.name
    passwd = args.password
elif any(i is not None for i in [args.name, args.password]):
    parser.error("Both Name and Password are Required!")
else:
    name = default_name
    passwd = default_pass
monkut
  • 42,176
  • 24
  • 124
  • 155