1

What is the best option to parse "mount style" options in python?

I need to parse argument=value lists associated with a specific argument, as in the following example:

myprogram --database type=mysql,user=root,passwd=secret,database=mydb --mail type=imap,server=imap.my.domain,user=myself,passwd=othersecret,port=999 --whatever key=val,key=val2,etcetera

My first guess would be to specify a single argument and then hand-parse it (example-only code):

from argparse import ArgumentParser

parser = ArgumentParser(description='Example only')
parser.add_argument('-D', '--database', nargs=1)
parser.add_argument('-M', '--mail', nargs=1)
parser.add_argument('-W', '--whatever', nargs=1)

args = parser.parse_args('--database type=mysql,user=root,passwd=secret,database=mydb --mail type=imap,server=imap.my.domain,user=myself,passwd=othersecret,port=999 --whatever key=val,key=val2,etcetera'.split())

def parse(a):
    d = {}
    for x in a.split(','):
        try:
            k, v = x.split('=')
        except ValueError:
            k = x
            v = None
        d[k] = v
    
args.database = parse(args.database)
args.mail = parse(args.mail)
args.whatever = parse(args.whatever)

... but I'm wondering if there's a better way.

ZioByte
  • 2,690
  • 1
  • 32
  • 68
  • It seems like your problem is just parsing the ``type=mysql,...`` format, is that correct? This seems unrelated to argparse. What exactly is the format you need to support? – MisterMiyagi Jul 13 '20 at 10:20
  • @MisterMiyagi: I gave an example in OP. essentially I have a series of possible `key=val` pairs associated with a `--option` each `--option` has a list of possible keys (with suitable defaults if missing); of course different `--option`s have different key sets. As said, I can implement specific classes, but I feel I'm overlapping `argparse` functionality, so I was wondering if there's a better way to do this. – ZioByte Jul 13 '20 at 11:00

1 Answers1

0

It turns out the right way to handle this is to implement a custom argparse.Action.

In the specific case this is something working reasonably:

!/bin/env python3

from argparse import ArgumentParser, Action


class KeypairAction(Action):
    def __init__(self, option_strings, dest, keys=None, **kwargs):
        super().__init__(option_strings, dest, **kwargs)
        if keys is None:
            raise ValueError('must define "keys" argument')
        if isinstance(keys, dict):
            self.keys = keys
        elif isinstance(keys, (list, tuple)):
            self.keys = {}
            for k in keys:
                if isinstance(k, tuple):
                    k, v = k
                else:
                    v = None
                self.keys[k] = v

    def __call__(self, parser, namespace, values, option_string=None):
        d = {}
        for x in values.split(','):
            try:
                k, v = x.split('=')
            except ValueError:
                k = x
                v = None
            d[k] = v
        setattr(namespace, self.dest, d)


ap = ArgumentParser(description="My nice program")
ap.add_argument('-D', '--database', action=KeypairAction, keys=('type', 'host', 'port', 'user', 'pass', 'db'))
ap.add_argument('-M', '--mail', action=KeypairAction, keys=('type', 'host', 'port', 'user', 'pass'))
ap.add_argument('-W', '--whatever', action=KeypairAction, keys=('key1', 'key2', 'key3', 'etcetera'))

args = ap.parse_args('--database type=mysql,user=root,pass=secret,db=mydb '
                         '--mail type=imap,host=imap.my.domain,user=myself,pass=othersecret,port=999 '
                         '--whatever key1=val,key2=val2,etcetera'.split())
print(args)

This could be trivially expanded to handle defaults.

ZioByte
  • 2,690
  • 1
  • 32
  • 68