18

If I call the script below with these options:

--user u1 --password p1 --foo f1   --user u2   --user u3 --password p3

Then it will print:

Namespace(foo=['bar', 'f1'], password=['p1', 'p3'], user=['u1', 'u2', 'u3'])

Question: Is there any way for me to set up a dependency between user and password, so it throws an error, because password for user u2 is not specified?

Less relevant question: How do I specify a default foo value for all users? With the given input I would like foo to equal ['f1','bar','bar'].

A solution for my main question would be to check that the lists user and password have the same length, but it's not quite what I'm looking for.

Here is the script:

import argparse
parser = argparse.ArgumentParser()
group = parser.add_argument_group('authentication')
group.add_argument('--user', action='append', required=True)
group.add_argument('--password', action='append', required=True)
group.add_argument('--foo', action='append', default=['bar'])
print(parser.parse_args())
tommy.carstensen
  • 8,962
  • 15
  • 65
  • 108
  • Typing passwords on the command line could be a security hazard. They could be stolen by someone running `ps`, for example. – unutbu Feb 19 '14 at 13:34
  • 1
    Thanks @unutbu. The "real" arguments are actually not usernames and passwords. I know absolutely nothing about security. – tommy.carstensen Feb 19 '14 at 13:56
  • @unutbu, can you explain what the security risk is and how ps is used maliciously? Like tommy, I know nothing about security. – RyanDay Jan 28 '18 at 06:52
  • 3
    @RyanDay: `ps axuwww` shows the full command (including any arguments) of all current processes. If a user were to type passwords as arguments to a script, then any other user on the same machine would be able to see what those passwords were by running `ps axuwww` while the script is running. – unutbu Jan 28 '18 at 12:41

3 Answers3

13

In your case, since the options must always be specified together, or none of them, you could join them into a unique --user-and-password option with two arguments using nargs=2. This would simplify a lot the handling of the values.

In fact you want to be able to provide multiple pairs, but required=True is satisfied when the first option is found, so it's pretty much useless for checking what you want in your setting.

The other way to do this is to use a custom action. For example:

import argparse


class UserAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if len(namespace.passwords) < len(namespace.users):
            parser.error('Missing password')
        else:
            namespace.users.append(values)


class PasswordAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        if len(namespace.users) <= len(namespace.passwords):
            parser.error('Missing user')
        else:
            namespace.passwords.append(values)


parser = argparse.ArgumentParser()
parser.add_argument('--password', dest='passwords', default=[], action=PasswordAction, required=True)
parser.add_argument('--user', dest='users', default=[], action=UserAction, required=True)

print(parser.parse_args())

Used as:

$python3 ./test_argparse.py --user 1 --password 2 --password 2 --user 3 --password 3
usage: test_argparse.py [-h] --password PASSWORDS --user USERS
test_argparse.py: error: Missing user

And:

$python3 ./test_argparse.py --user 1 --password 2 --user 2 --user 3 --password 3
usage: test_argparse.py [-h] --password PASSWORDS --user USERS
test_argparse.py: error: Missing password

(Note that this solution requires --user to come before --password, otherwise the lengths of the lists don't provide enough information to understand when an option is missing.)

The last solution would be to simply use action='append' and test at the end the lists of values. However this would allow things like --user A --user B --password A --password B which may or may not be something you want to allow.

Bakuriu
  • 98,325
  • 22
  • 197
  • 231
  • I will go for your simple --user-and-password option, as all arguments/keywords are used for each user in my case. But it sure would be great, if argument dependency was added to argparse. Thanks. – tommy.carstensen Feb 19 '14 at 13:18
3

Define a custom user type which holds both username and password.

def user(s):
    try:
        username, password = s.split()
    except:
        raise argparse.ArgumentTypeError('user must be (username, password)')

group.add_argument('--user', type=user, action='append')
Jayanth Koushik
  • 9,476
  • 1
  • 44
  • 52
2

Thank you for the answers. I will accept the one from Bakuriu. Here is one solution (test_argparse2.py):

import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--upf', action='append', nargs=3)
print(parser.parse_args())

Correct usage:

$python3 ./test_argparse2.py --upf u1 p1 bar1 --upf u2 p2 bar2
Namespace(upf=[['u1', 'p1', 'bar1'], ['u2', 'p2', 'bar2']])

Here is another solution (test_argparse3.py) allowing random order of the input arguments:

import argparse
import sys
parser = argparse.ArgumentParser()
parser.add_argument('--upf', nargs='+')
set_required = set(['user','pass','foo',])
for s in parser.parse_args().upf:
    set_present = set(argval.split(':')[0] for argval in s.split(','))
    set_miss = set_required-set_present
    bool_error = False
    if len(set_miss)>0:
        print(set_miss, 'missing for', s)
        bool_error = True
    if bool_error:
        sys.exit()

Incorrect usage:

$python3 ./test_argparse3.py --upf user:u1,pass:p1,foo:bar1 foo:bar,pass:p2
{'user'} missing for foo:bar,pass:p2
tommy.carstensen
  • 8,962
  • 15
  • 65
  • 108