Processing CLI Arguments with Python

Lets do a little coding today, simple solution to take command-line arguments and act on them. We’ll look at two methods, the legacy way of reading sys.argv, and using the argparse module to simplify the reading and processing of such arguments.

We all know when a library or module exists, that’s probably the way to go. Don’t reinvent the wheel right? Lets try it anyway. Why you ask? Because I can!

The Legacy Method

Akin to the bash $@ the sys.argv variable once imported contains all passed command-line arguments and stores them in the sys.argv list variable.

github.com/IPyandy/Blog-Coding on  master [?]
100% [I]  python3
Python 3.7.0 (default, Jun 29 2018, 20:13:13)
[Clang 9.1.0 (clang-902.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from sys import argv as argv
>>> type(argv)
<class 'list'>

Now let’s write a simple script to iterate through the arguments passed, the default value at argv[0] is the name of the program or script being run.

#!/usr/bin/env python3
"""Simple example of using sys.argv to process CLI arguments."""

from sys import argv as argv

def main():
  """Just a main function."""
  for i,a in enumerate(argv):
    # test len of arguments passed and make sure
    #  only print arguments after the script name
    if len(argv) > 1 and a != argv[0]:
      print("Argument #{} passed is: {}".format(i,a))
    elif len(argv) == 1:
      print("No arguments passed")

if __name__ == '__main__':
  """just calls main()"""
  main()

If we run this with just python3 sys-argv-01.py then we’ll get a message of No arguments passed since the elif clause in the main() function is executed. Though if we run with python3 sys-argv-01.py this is a test. What do you think the output is? Lets find out.

github.com/IPyandy/Blog-Coding on  master [?]
•100% [I] ➜ python3 python/argparse/sys-argv-01.py this is a test
Argument #1 passed is: this
Argument #2 passed is: is
Argument #3 passed is: a
Argument #4 passed is: test

Our second call matches the if inside the for loop, which basically says, run..

  • if arguments is greater than 1 (it will always be at least 1)
  • and if a which is our individual argument does NOT equal the program name
    • This is simplify the output as there’s no point in printing our own program name for this

Make it Useful

Like all CLI arguments, these can be taken by your script/app and take certain actions on specific flags. Install a plugin from --plugin-one for example. Removed the app all together with --uninstall or similar. There’s

I won’t go into details, because really it’s not recommended to do it this way. Why reinvent the wheel when the round shape works? We’ll cover argparse below which is a powerfull module to handle argument parsing and take actions.

Enter Argparse

Disclaimer: we will not cover everything (not even close) on argparse as it’s a comprehensive package. See the docs for the more complete list of possible use cases.

What is argparse? Lets see what the documentation has to say.

The argparse module makes it easy to write user-friendly command-line interfaces. The program defines what arguments it requires, and argparse will figure out how to parse those out of sys.argv. The argparse module also automatically generates help and usage messages and issues errors when users give the program invalid arguments.

All this means is we can take the guessing and invention game out of getting user input from the command line.

First Example

Lets take a quick example of all it takes to utilize the module.

# argparse version 1
import argparse


def main():
    """Just a main function."""
    parser = argparse.ArgumentParser()
    parser.parse_args()


if __name__ == '__main__':
    main()

Lets run the program and see what happens.

Blog-Coding/python/argparse on  master [?]
•100% [I] ➜ python3 argparse-01.py --help
usage: argparse-01.py [-h]

optional arguments:
  -h, --help  show this help message and exit

Wait! we haven’t declared any arguments to pass we already get the --help or -h? Yup, that’s a freebie and included with the module itself.

Positional Arguments

Positional arguments are not optional and must appear in the order in which they’re added to the parser.

Lets add some of those arguments to our little program.

# argparse version 2
import argparse


def main():
    """Just a main function."""
    parser = argparse.ArgumentParser()
    parser.add_argument("name", help="Enter your full name")
    parser.add_argument("age", help="Enter your age in years", type=int)
    parser.parse_args()


if __name__ == '__main__':
    main()

Now if we run the program with no arguments passed, we get a friendly error message.

Blog-Coding/python/argparse on  master [⇡?]
•100% [I] ➜ python3 argparse-02.py
usage: argparse-02.py [-h] name age
argparse-02.py: error: the following arguments are required: name, age

The message tells you exactly what the none optional arguments are. Now lets make this a little more useful to do something with this information.

# argparse version 3
import argparse


def main():
    """Just a main function."""
    parser = argparse.ArgumentParser()
    parser.add_argument("name", help="Enter your full name")
    parser.add_argument("age", help="Enter your age in years", type=int)
    args = parser.parse_args()
    print(type(args))
    print(f"Hello seems like your name is {args.name} and you're {args.age} years old.")


if __name__ == '__main__':
    main()

The above sample code does a few things.

  • Creates the parser as before
  • Adds two new arguments to look for
  • These arguments are not optional and must be in the order provided
  • The age argument is given and explicit type of int as the default is string

Whateve you enter into the command line gets stored in the variable args which is of type <class 'argparse.Namespace'>. We can use the dot notation to get the arguments from this variable and pass them on to our print function. I left the print(type(args)) just to see the actual type of args in the output.

Blog-Coding/python/argparse on  master [⇡?]
•100% [I] ➜ python3 argparse-03.py "Inigo Montoya" 31
<class 'argparse.Namespace'>
Hello seems like your name is Inigo Montoya and you're 31 years o

As you can see we need to wrap the name in quotes if providing more then the first name. This is because the cli parser will interpret as two separate arguments otherwise. That behaviour is not python specific, it is a shell behaviour and expected.

I keep saying these must be in order; right? So what happens if we swap those values?

Blog-Coding/python/argparse on  master [⇡?]
•100% [I] ➜ python3 argparse-03.py 31 "Inigo Montoya"
usage: argparse-03.py [-h] name age
argparse-03.py: error: argument age: invalid int value: 'Inigo Montoya'

There’s a reason I set the type of age to int in order to catch this error. Since we’re doing any calculations, the simplest way was to just make sure it’s caught early on.

Optional Arguments

Optional cli arguments are added to the parser starting with - for single letter arguments and -- for full word arguments.

# argparse version 4
import argparse


def main():
    """Just a main function."""
    parser = argparse.ArgumentParser()
    parser.add_argument("name", help="Enter your full name")
    parser.add_argument(
        "-a",
        "--age",
        help="Enter your age in years",
        type=int,
        nargs="?",
        const=1)
    args = parser.parse_args()

    print(
        f"Hello seems like your name is {args.name} and you're {args.age} years old."
    )
    if "inigo montoya" in args.name.lower():
        print("I did not kill your father.")


if __name__ == '__main__':
    main()

Here we left the name as a positional mandatory argument and age as an optional argument with a default value of 1 if the argument is passed but no value specified.

Lets try it out leaving the --age parameter off.

Blog-Coding/python/argparse on  master [?]
•100% [I] ➜ python3 argparse-04.py "Inigo Montoya"
Hello seems like your name is Inigo Montoya and you're None years old.
I did not kill your father.

We should really add checking to make sure proper actions are taken, though the point of this is to show that age now is optional. The default value of None is assigned if not specified at all.

Now lets add the argument --age but leave it with no values.

Blog-Coding/python/argparse on  master [?]
•100% [I] ➜ python3 argparse-04.py "Bob Lee" --age
Hello seems like your name is Bob Lee and you're 1 years old.

You can see the given default value of 1 is assigned to it. Lets try it out specifying a value with both -a and --age

Blog-Coding/python/argparse on  master [?]
•100% [I] ➜ python3 argparse-04.py "Inigo Montoya" -a 31
Hello seems like your name is Inigo Montoya and you're 31 years old.
I did not kill your father.

Blog-Coding/python/argparse on  master [?]
•100% [I] ➜ python3 argparse-04.py "Inigo Montoya" --age=31
Hello seems like your name is Inigo Montoya and you're 31 years old.
I did not kill your father.

As you can see the default value of 1 is overwritten by the value entered at the command line. I show both cases just to prove that you can use either method and still reference it the same way.

Notice that for positional arguments the actual argument name is left off and for optional ones it’s provided. This makes sense, since optional ones the parser needs to know where to add the specific values and for positional ones, they’re stored in the order provided.

Recap

This was a simplistic example, but even then shows the power and value of the argparse module. The beauty of python is usually, if you’re attempting to do something, someone has written a module/library for it.

The code for this post can be found in Github, that repository will be used for all coding examples in this blog. Well, anything outside an actual project or things that require their own attention. Even then, I will attempt to add those as git submodules so they’re easily accessible.

Follow me on Twitter and on Github.

comments powered by Disqus