In the past years, I built a corpus of well documented and reusable bash scripts for both my main work and my sideprojects.

This is how it happened.

A couple of years ago I went through a long period where writing many scripts was the only way to help me at work. Testing some modems and command line utilities, I needed to group many commands at once to, as example, establish a full data connection.

Honestly, my scripts were really hard to reuse, and months after writing them, always took me time to understand the meaning of all the variables in place.

The main problems were:

  1. no usage/help message: I was forced to dig into the code to see what it was supposed to do
  2. no flexibility: hard-coded values and no command line arguments, so the script was able to do only one specific thing

Since I loved Docopt - the Python library which parses the help message and provides all the command line options you need - I looked for a similar project for bash scripting, and I found one.

At that time Docopts for bash was written in Python (now it is written in go), but I prefer to avoid dependencies in my scripts. In python you have the Python Package Index and it is perfectly normal to install a python dependency, but for bash, at least from my point of view, I prefer that each script depends only on the commands it calls, not on other scripts for extended functionalities.

So I wrote CLInt, a CLI generator from the help message… ehm… so, what’s the difference?

CLInt works at writing time.

CLInt auto-generates all the bash code needed to parse the command line options, so there is no external dependency.

As example, consider a script with this usage description:

## Test script to show how clint.sh works
##
## usage: yourscript ... # the usage message (CLInt ignores this part)
##
## options:              # The options part (CLInt parses this part. Be aware of the format)
##      -f, --file <string>      Example of a valued flag. The flag's value is stored in a variable named "$_file"
##      -d, --default <string>   Example of a valued flag with a default value (_this_is_the_default) [default: _this_is_the_default]
##      -b, --boolean            Example of a no-argument (boolean) flag: its value (0 or 1) is stored in a variable named "$_boolean"
##      -v, --verbose            Example of a no-argument (boolean) flag with a default value "0" [default: 0]

Run clint.sh and you’ll get:

$ clint.sh -s test-script.sh

# GENERATED_CODE: start
# Default values
_verbose=0
_default=_this_is_the_default

# No-arguments is not allowed
[ $# -eq 0 ] && sed -ne 's/^## \(.*\)/\1/p' $0 && exit 1

# Converting long-options into short ones
for arg in "$@"; do
    shift
    case "$arg" in
"--file") set -- "$@" "-f";;
"--default") set -- "$@" "-d";;
"--boolean") set -- "$@" "-b";;
"--verbose") set -- "$@" "-v";;
    *) set -- "$@" "$arg"
    esac
done

function print_illegal() {
    echo Unexpected flag in command line \"$@\"
}

# Parsing flags and arguments
while getopts 'hbvf:d:' OPT; do
    case $OPT in
        h) sed -ne 's/^## \(.*\)/\1/p' $0
            exit 1 ;;
        b) _boolean=1 ;;
        v) _verbose=1 ;;
        f) _file=$OPTARG ;;
        d) _default=$OPTARG ;;
        \?) print_illegal $@ >&2;
            echo "---"
            sed -ne 's/^## \(.*\)/\1/p' $0
            exit 1
            ;;
    esac
done
# GENERATED_CODE: end

Copy paste this code into test-script.sh, that’s it.

I then added some sweets:

  • if xclip is installed, CLInt’s output is available in system clipboard as well
  • if you use VIM, the following configuration let CLInt works in the current open buffer without the hassle of opening a terminal, execute CLInt, and copy the CLInt’s autogenerated code.
command! CLInt r !/path/to/CLInt/clint.sh -s %

This totally changed the quality of my scripts, and it was not because I became a better Bash developer.

All my scripts started to be well documented just because the documentation was the only thing I needed to have options and flexibility, and it was super easy and fast to do it (especially on VIM).

CLInt development has stopped some time ago, because it already satisfies all my needs, but it is still young and far from perfect, so I hope that some more people will try it and help me find all the bugs hidden in the project.