Welcome, guest | Sign In | My Account | Store | Cart

For many of my Python modules I find it convenient to provide a small if __name__ == "__main__": ... block to be able to call individual methods from the command line. This requires some kind of translation of command-line string arguments to args and kwargs for the method call. This recipe uses a few conventions to do that:

  • the first argument is the method name
  • positional args are positional (duh)
  • "key=value" is a keyword argument
  • an attempt is made to interpret arguments as JSON to allow specifying types other than string
Python, 39 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def _method_info_from_argv(argv=None):
    """Command-line -> method call arg processing.
    
    - positional args:
            a b -> method('a', 'b')
    - intifying args:
            a 123 -> method('a', 123)
    - json loading args:
            a '["pi", 3.14, null]' -> method('a', ['pi', 3.14, None])
    - keyword args:
            a foo=bar -> method('a', foo='bar')
    - using more of the above
            1234 'extras=["r2"]'  -> method(1234, extras=["r2"])
    
    @param argv {list} Command line arg list. Defaults to `sys.argv`.
    @returns (<method-name>, <args>, <kwargs>)
    """
    import json
    import sys
    if argv is None:
        argv = sys.argv

    method_name, arg_strs = argv[1], argv[2:]
    args = []
    kwargs = {}
    for s in arg_strs:
        if s.count('=') == 1:
            key, value = s.split('=', 1)
        else:
            key, value = None, s
        try:
            value = json.loads(value) 
        except ValueError:
            pass
        if key:
            kwargs[key] = value
        else:
            args.append(value)
    return method_name, args, kwargs

Usage goes something like this:

if __name__ == "__main__":
    import sys
    method_name, args, kwargs = _method_info_from_argv(sys.argv)
    rv = globals()[method_name](*args, **kwargs)
    sys.exit(rv)

3 comments

Alexander G Morano 11 years, 3 months ago  # | flag

Not to diminish the work, but why over complicate?

import sys

def testFunction(*args, **kwargs):
    for x in args:
        print x

    for x in kwargs.keys():
        print x, kwargs[x]

exec(''.join(sys.argv[1:]))

can run from the cmd line:

R:\develop\nas\data\dev>python cmdline.py testFunction(1, 4, nice={4:'what', 'test': 5}, more=45, less=[1,2,3])

output:

1 4 less [1,2,3] more 45 nice {'test': 5, 4: 'what'}

kesten 7 years, 7 months ago  # | flag

Very nice. I had a further use case of needing to execute a file or command within a file remotely by passing a stringified json dictionary. It might be nicer to create a module that can be imported since i will probably be controlling all the python files, but in case I don't, I've written it as a file "cli.py" that can be copied along with the file to be executed. Called like this

python cli.py my_actual_file.py method_name --arg1 <arg1> --arg2 <arg2> ...

if method_name == "main" then call execfile passing in the appropriate sys.argv my_actual_file.py must accept the args either using argparse.parser.pare_args() in a main() function or as args to the "metnod_name"

With this, I can do things like

python cli.py copy_db_schema.py main --source_db "{'host': 'server1', 'port': 5432, 'user': 'me', 'pwd': '*'}" --dest_db ...similar...

or

python cli.py copy_db_schema.py list_tables --source_db "{'host': 'server1', 'port': 5432, 'user': 'me', 'pwd': '*'}"

where list_tables is a method accepting source_db dict params

Essentially, i'm wrapperizing your _method_info_from_argv

Listing