Self-documenting scripts

For writing python scripts that are best served with user inputs at command line, docopt is like magic. It transforms a simple comment block describing a script’s use into actual argument1 code, which developers don’t need to write. To the point that those unfamiliar with docopt are likely to erroneously presume the script incomplete, when they realise it contains almost no code for processing arguments. Here’s an example snippet from one of my notes for illustration:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

"""Wind speed plots based on ISO 19901-1:2005.
2016 ckunte.

Usage: isowind.py --speed=S [--height=H]
       isowind.py --help
       isowind.py --version

Options:
  -h, --help  Show this help.
  --height=H  Maximum height (m). [default: 140.0]
  --speed=S   1h mean wind speed at zr ref. height (m/s).

"""
from docopt import docopt

def main():
    args = docopt(__doc__, version='ISO 19901-1 wind speed plots, v0.1')
    Uo = float(args['--speed']) # m/s 1h mean wind speed at zr
    h = float(args['--height']) # m, max height (e.g., a flare tower)

The script isowind.py takes a mandatory argument --speed (e.g. --speed=30 for, say, 30m/s mean wind speed at reference height) like so:

$ python isowind.py --speed=30

But that’s not all, there is another argument called --height in the block. These are enclosed in square brackets, indicating it’s optional, for which a default value is provided in the block. So, for the above command line construct (without an explicit optional height input, the script uses 140m). Instead, if the user provides the height as an argument, like so, then the script uses the height as 120 instead of the default 140.

$ python isowind.py --speed=30 --height=120

The script defaults to show the help block in brief when the user fails to provide any arguments at the command line:

$ python isowind.py
Usage: isowind.py --speed=S [--height=H]
       isowind.py --help
       isowind.py --version

With the --help argument, it produces the full help block like so:

$ python isowind.py --help
Wind speed plots based on ISO 19901-1:2005.
2016 ckunte.

Usage: isowind.py --speed=S [--height=H]
       isowind.py --help
       isowind.py --version

Options:
  -h, --help  Show this help screen.
  --height=H  Maximum height (m). [default: 140.0]
  --speed=S   1h mean wind speed at zr ref. height (m/s).

Not long ago, I discovered another such self documenting snippet, which I now use in my makefiles. Here’s an example Makefile:

# Makefile for t1 backup routine
# 2018 ckunte

# Source on Windows
WSRC=/cygdrive/c/work/proj/t1

# Source on Mac
MSRC=/Users/ckunte/Desktop/t1

# Using pre-configured hosts + ids in ~/.ssh/config
SRV=$(IP)

# Flags + port
WFLAGS=-auz --exclude=".*" --log-file=$(WSRC)/log.txt
MFLAGS=-auz --exclude=".*" --log-file=$(MSRC)/log.txt
PORT=-e 'ssh -p 22'

help: ## Show this help
    @echo 'Makefile for backing-up t1'
    @echo 'Usage: make <opt> IP=<ipaddr>'
    @echo '<opt>:'
    @rg -I '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-6s\033[0m %s\n", $$1, $$2}'

m2w: ## Mac -> Win (run from Win)
    @echo -n 'Syncing from Mac to Windows...'
    @rsync $(WFLAGS) $(PORT) $(SRV):$(MSRC)/ $(WSRC)/
    @echo 'done.'

w2m: ## Win -> Mac (run from Win)
    @echo -n 'Syncing from Windows to Mac...'
    @rsync $(WFLAGS) $(WSRC)/ $(PORT) $(SRV):$(MSRC)/
    @echo 'done.'

m2m: ## Mac -> Mac (run from Mac)
    @echo -n 'Syncing from Mac to Mac...'
    @rsync $(MFLAGS) $(MSRC)/ $(PORT) $(SRV):$(MSRC)/
    @echo 'done.'

.PHONY: help

When this Makefile is run without an argument, predictably it produces this following neat help section:

$ make
Makefile for backing-up t1
Usage: make <opt> IP=<ipaddr>
<opt>:
 help   Show this help
 m2m    Mac -> Mac (run from Mac)
 m2w    Mac -> Win (run from Win)
 w2m    Win -> Mac (run from Win)

To backup from a Windows computer to a Mac in this example, I run the following command line:

$ make w2m IP=10.0.1.50

There’s no need to run secure shell server on Windows. rsync works bi-directionally, and so reverse push is simple and easy. The handshake between two computers occurs using public key cryptography, and the files and folders are pumped from one encrypted computer drive to another. The IP address in the above corresponds to the target (receiving) computer’s (IPv4) network address. This argument makes it pretty easy to sync it to any computer I have access to. The part of interest in the above Makefile is again the help section, which uses a few lines of magic to pull commented line descriptions, sorts them as options, and presents them as part of the help screen. It can be used with either egrep -h or rg -I. (I like ripgrep.)

Self documenting scripts are cool and they’re a joy to use. Give these a try, if you haven’t already.


  1. An argument in this context is a variable or input furnished by the user at command line.