Python Command-line arguments: Part4

Subscribe to my newsletter and never miss my upcoming articles

Before we catapult command-line arguments to infinity and beyond, we should go via one more item as suggested by an avid reader, Paddy. The docopt. In case you've just popped in, here's what we have done to this moment:

  1. argv
  2. argparse
  3. getopt

So far so good, but here's the pick-up line; a bonus section.

docopt has a lot of similarities to the argparse. Like its counterpart, it comes with helper functionality, that way it's easier to know if you did something wrong in your use or if your program is being used by a new user for the first time somewhere.

The difference, however, comes with how docopt parses. It relies on the docstring you give it as a description.

The basic idea is that a good help message has all the necessary information in it to make a parser.

In my walks through the internet, I happened to come across this one comment, and I quote:

I feel you, man. It's so simple and unorthodox it's confusing.

And I had to crack or two reading it. I laughed so hard, partially because it is true, such that you might need a background of command-line argument parsing prior to using it.

This said, docopt is an external module, meaning it has to be installed. Hence, in a virtual environment:

pip install docopt

In this particular instance, we are going to make a calculator. Nothing fancy, just something to advance from the previous programs we wrote so that you can see the subtle differences.

Our program will do the below,

  • Addition
  • Multiplication
  • Squares
  • Root

So go ahead and create a file: my_calculator.py, and paste the below in. We will go through this step at a time.

"""My Advanced Calculator v1.0

Usage:
  my_calculator.py add (<number><number>)...
  my_calculator.py mult (<number><number>)...
  my_calculator.py square [--verbose] <number>
  my_calculator.py root <number>
  my_calculator.py (-h | --help)
  my_calculator.py --version

Examples:
  my_calculator.py add 9 4 67 101
  my_calculator.py mult 88 43 20458 1 134 
  my_calculator.py square --verbose 9 

Options:
  -h --help        Show this screen.
  -v --version     Show version.
     --verbose         Show details verbosely.

"""

import math
from docopt import docopt

class MyCalculator:
    def get_options(self):
        self.args = docopt(__doc__)

# loop via commands getting what is needed
        if self.args["add"]:
            self.addition()
        elif self.args["mult"]:
            self.multiply()
        elif self.args["square"]:
            self.get_square()
        else:
            self.get_root()

    def addition(self):
        """
        Get the summ of all numbers passed
        """
        summation = sum([int(number) for number in self.args["<number><number>"]])
        print(f"{summation}")

    def multiply(self):
        """Get the product of the list of numbers"""
        product = math.prod([int(number) for number in self.args["<number><number>"]])
        print(f"{product}")
<another-argument>
    def get_square(self):
        number = int(self.args["<number>"])
        if self.args["--verbose"]:
            print(f"{number} * {number} = {number*number}")
        else:
            print(f"{number*number}")

    def get_root(self):
        """
      Get the square root
      """
        number = self.args["<number>"]
        print(f"{math.sqrt(int(number))}")


if __name__ == "__main__":
    arguments = docopt(__doc__, version="MyCalculator 1.0")
    calculator = MyCalculator()
    calculator.get_options()

Now, docopt accepts the below :

docopt(doc, argv=None, help=True, version=None, options_first=False)

The first argument, being doc, which essentially, is this, the program description.

"""My Advanced Calculator v1.0
Usage:
  my_calculator.py add (<number><number>)...
  my_calculator.py mult (<number><number>)...
  my_calculator.py square [--verbose] <number>
  my_calculator.py root <number>
  my_calculator.py (-h | --help)
  my_calculator.py --version

Examples:
  my_calculator.py add 9 4 67 101
  my_calculator.py mult 88 43 20458 1 134 
  my_calculator.py square --verbose 9 

Options:
  -h --help        Show this screen.
  -v --version     Show version.
     --verbose         Show details verbosely.

"""

It is here that we define the logic of our parser, following the rule that a good help message has all the necessary information in it to make a parser.

In the docstring, we give a Usage description, followed by various examples, and the Options available in our program for the values passed. Breaking it down a bit further,

  my_calculator.py add (<number><number>)...

Do take note, add, mult, square, and root are commands and not arguments nor are they options. They tell the user what is going to happen to the arguments parsed if any.

For our addition, we specify that if a user wants to add, they should pass two or more values to the program. To tell the program to create such a parser, we use (<number><number>). Wrapping in () is just a specification that lets the program know that it should accept two numbers. The ... after tells the program, Hey, create the parser accepting two arguments, but remember, there may be two or more! , hence the ellipses. The same goes for multiplication, which we have shortened as mult.

For the squares and roots, however, our program accepts only one number. By default, arguments passed to docopt are required, therefore, we can safely ignore the ( ). If we wanted the user to give the argument as an option (i.e, not mandatory), we would wrap our argument(s) in [ ]. Something you may have seen as well is this:

  my_calculator.py square [--verbose] <number>

This particular description, along with having a command square, has the option --verbose in square brackets. The great part about this is that it does not matter where you place this as you get the square of your number. So running my_calculator.py square --verbose 9 would give the same exact value as running my_calculator.py square 9 --verbose. You can tell the difference from how the other options are used (they reused independent of arguments):

my_calculator.py (-h | --help)
my_calculator.py --version

A quick run of our program, without positional arguments, would give something like this:

 python my_calculator.py

Output:


Usage:
  my_calculator.py add (<number><number>)...
  my_calculator.py mult (<number><number>)...
  my_calculator.py square [--verbose] <number>
  my_calculator.py root <number>
  my_calculator.py (-h | --help)
  my_calculator.py --version

Out of the box, we would get the usage detail, from where we would be able to get the commands and options available. Running with the -h option for help would give the whole docstring. Back to the main call:

    arguments = docopt(__doc__, version="MyCalculator 1.0") # give the program version number/name
    calculator = MyCalculator()
    calculator.get_options()

We create an instance of MyCalculator and call its get_options. from where we can get all its arguments and options. So we can call python my_calculator.py add 1 23 4556

# get the arguments and options parsed
self.args = docopt(__doc__)

self.args == {'--help': False,
 '--verbose': False,
 '--version': False,
 '<number>': None,
 '<number><number>': ['1', '23', '4556'], # can accept two or more numbers because of the ellipse
 'add': True,
 'mult': False,
 'root': False,
 'square': False}

You can see that this is a dictionary or as web developers may call it, a JSON object. The list of numbers, <number><number> contains the numbers as expected, but as strings, So to sum them, we would need to convert each into integers before.

# convert each item in the list to an integer and sum hte resultant list
sum([int(number) for number in self.args["<number><number>"]])

So go ahead and run python my_calculator.py add 1 23 4556, see the results for yourself.

As we are not getting into the details of list comprehension in this piece, I will leave the calculations for you to ponder upon. We are getting back to our package:

docopt(doc, argv=None, help=True, version=None, options_first=False)

We already gave it the doc option,(document description). For argv, docopt will take everything passed to it excluding the program name, that is, sys.argv[1:]. Do take a look at argv if you need some clarification with this. The help option is set to True by default for the sake of the help message when needed while the options_first is disabled and hence we can position our options either before or after or between our arguments while parsing.

It is not just using commands that does all the talking. We may well just use options and parameters. For example:

"""
Usage:
 arguments_example.py [-vrh] [FILE] ...
"""

An example program taking in multiple optional options with a FILE that is also optionally passed. You can default it to a file in the local directory, much like our file organizer in the File management series

We can combine this as far as we want (optional arguments inside optional, required arguments inside optional or even mutually exclusive).

# have an optional command taking optional arguments
my_program [command --option <argument>]

# arguments are optional, but if <one-argument> is passed, <another-argument> should be present.
my_program [(<one-argument> <another-argument>)]

On and one it could go, but this is up to you to do more digging to find the particular use-case you need. Paddy McCarthy I hope I got the Knitty details, I know it took a while.

For any lover of the command-line out there, you can get docopt working for your language of preference be it

  • C++
  • C
  • PHP
  • Haskel

The full list of supported languages can be found here. Till next time, @codes_green. Code, as usual, can be found from TheGreenCodes.

Comments (1)

Paddy McCarthy's photo

Docopt is so good for me, I like to tell others about it. I hope it works for you too. 👍