Coding style

This is the official coding style for Shellfu project.

Basic principles

Even though different language, the style is inspired by Python community, more specifically by PEP20 and PEP8.

Especially the main principles (PEP20) are honored throughout the Shellfu project, so it may be easier for you to understand recommendations here if you already know PEP20. So go read it now---as the name hints, it's only 20 lines. You can read it offline:

echo "import this" | python

PEP8 is official coding style, being a Python community way of achieving some of PEP20's principles. So although many things won't make sense for Bash, I recommend to read it or run through it at the very least.

Code layout

Indentation

Indentation is done exclusively by space.

Indentation level is almost exclusively 4 spaces.

for i in $(seq 1 5); do
    something_with "$i"
done

One exception is vertical pipelines:

cat list \
  | while read line; do
        something_with "$line"
        ...
    done \
  | sort \
  | uniq

where each pipe is indented by 2 spaces and followed by 1 space. This makes the pipe float in the indentation space, preserving visual aspect of indentation inside any loops etc (which also may help your editor auto-indent correctly--when inside the loop).

Line length

79 characters.

For docstrings, it's 72 characters including mandatory she-space prefix (#). Add 4 in case of function docstrings.

Imports

Imports come after module docstring, before variable declarations.

#!/bin/bash
# license text

#
# A modest module
#

shellfu import config
shellfu import exit
shellfu import pretty

#
# Some var
#
SOME_VAR=159

Imports should be grouped in this order:

  1. standard library modules,
  2. related third party modules,
  3. local application/library specific imports,

with empty line between the groups, and each group being sorted alphabetically.

Comments

First, don't overuse comments. Don't use comments to state the same what the code states. The question the comment is answering must be why, not what.

If you need too many comments, it may mean that something else is wrong with your code--more comments won't fix it.

Comment formatting

Any comment must start with at least single space.

# hello

Exceptions from this rule are TODO/FIXME comments--see below.

Block comments

Inline comments

Inline comment should be aligned to 4 spaces, and must be separated from the code by at least 2 spaces. Don't do this:

foo # hello
bar1 # world

but rather this:

foo   # hello
bar1  # world

or this (easier when another line is added):

foo     # hello
bar1    # world

TODOs, FIXMEs, workarounds

These must be one-liners, so that they can be easily understood when grepping.

#TODO: Review the loop
#FIXME: We throw away stdout due to bug in Glib
#       http://bugs.gnome.org/bug/12345

Exception is if the note is acompanied with URL, which probably would not be useful to reader, so can be moved to next line.

Note that for both TODOs and FIXMEs, it's always better to have a tracker that is referenced in place--that's especially true if you can't fit the description into one line.

Docstrings

Docstrings are special kind of block comments that usually span many lines and are primary means of providing documentation for functions, modules and variables.

Generic form of docstring is

#
# One-line summary
#
# More detailed explanation
# written in Markdown.
#
# Can have multiple paragraphs etc, as long
# as there is an empty comment at the end
#

In other words, one empty comment, one-line comment for summary, one empty comment, and the rest, followed by one more empty comment. This means that the line preceding the whole block as well as the one following must not be comments.

Assigning docstring to the related item is done by specific juxtaposition to this item.

Module docstring

Module docstring is placed directly after the first empty line in the module file. Typically this will be after shebang, additional lines like Vim editor directives and possibly author and license information.

For example:

#!/bin/bash
#
# Some crazy walll of license text ... or not...
#
# Authors: Me <and@only.me>
#

#
# My cool module
#
# This module is so useful that I can't even start
# describing it!
#

Variable docstring

To add docstring to a global variable, prepend it directly to the variable declaration:

#
# My cool var
#
# This variable means something to me
# but I'm not going to tell you!
#
MY_COOL_VAR=$1

i.e. an empty line, and empty comment, the docstring and immediately the variable and at least one empty line.

It is possible to group related variables together by adding all assignments right after the docstring like this:

#
# I don't have numpad
#
# I prefer using words for numbers, so I will
# always use these variables instead.
#
WORD_ONE=1
WORD_TWO=2
WORD_THREE=3

This is equivalent for copying the same docstring to each of the variables.

Function docstring

For functions, the docstring is part of the body, and follows the first declarative line (the one ending with {):

fooize() {
    #
    # Fooize barrator $1 with bazates $@
    #
    # Fooize using most recent best practices as accepted by
    # academic community.
    #
    local bar=$1    # barrator
    local baz       # bazate
    for baz in "$@"; do
        something_with "$bar" "$baz"
    done
}

This also means that unlike modules and variable docstrings function docstrings:

It is considered good practice to mention positional parameters, or possible inherited variables right in the short description, using naive Bash notation (no quoting) as in above example.

Naming

Module name and namespacing

A valid Shellfu module name consists of lowercase letters, numbers and underscore, ie. [a-z_][a-z0-9_]. This name must be used as name of the module file, without the extension.

Module must not define a global variable or function that does not start with its name. That is, a global variable or function may be:

Private names

Names starting with double underscore are considered strongly private: that is, they must only be used in the same file.

Names starting with single underscore are considered weakly private: they may be used in other modules within the same project. (This can be useful in plugin scenarios.)

When in doubt, use double underscore.

Capitalization

There are three types of variables recognized in terms of this style. The distinction is based on scope:

Functions should be named in snake_case.

Example

For example, a file named greet.sh would be a valid module named greet if it contained following declarations:

#
# Name of this planet
#
GREET__PLANET=${GREET__PLANET:-Earth}

greet() {
    #
    # Greet persom named $1
    #
    local name=$1       # name to greet
    local TimeOfDay     # morning, afternoon or night
    TimeOfDay=$(determine_timeofday)
    __greet__mkgreet "$name"
}

__greet__mkgreet() {
    #
    # Greet user $1 based on $TimeOfDay
    #
    local name=$1       # name to greet
    case $TimeOfDay in
        morning)    echo "Good $GREET__PLANET morning, $name!"   ;;
        afternoon)  echo "Nice $GREET__PLANET afternoon, $name!" ;;
        night)      echo "Sleep well, $name!"                    ;;
    esac
}

Notice that while TimeOfDay is allowed to slip through and be referenced in __greet__mkgreet(), name is purely local to greet(), and then name happens to be declared again in __greet__mkgreet().

Common language constructs

functions

Preferred way of declaration is a variation of most common Bourne shell way with K&R-style brackets:

myfun() {
    foo
    bar
    baz
}

That is,

if, while, for

if foo; then
    bar
elif baz; then
    quux
else
    idk
fi

while foo; do
    bar
    baz
    quux
done

for foo in bar baz; do
    quux $foo
done

That is,

case

case $foo in
    bar)
        baz
        ;;
    *)
        quux
        ;;
esac

That is, three levels:

Let's get idio(ma)tic

condensed case table

However, a "condensed" version is possible --- and often recommended:

case $foo in
    bar)    bar_something_up    ;;
    baz)    do_bazzy_thing      ;;
    *)      quux; exit          ;;
esac
#   ^ 4     ^ 12                ^ 32

That is,

the new case - argument router

This is a cross-breed of infinite while loop and case switch, a construct I call argument router.

To understand how this is useful, I'll show you a more complete example:

verbose=false
file="-"
item=""

while true; do case $1 in
    -f|--file)      file="$2"; shift 2 || show_usage ;;
    -q|--quiet)     verbose=false; shift ;;
    -v|--verbose)   verbose=true; shift  ;;
    --help)         show_help; exit      ;;
    --)             item="$2"; break     ;;
    -*)             show_usage; exit 2   ;;
    *)              item="$1"; break     ;;
esac done

test -n "$item" || show_usage

As you can see it's a variant of condensed case table, wrapped in an infinite while loop. It may break some rules, but for some great advantages: Whole CLI is described in just few lines--you just need to read header to understand how variables may be set.

As opposed to nesting case in while properly, 2 lines (which are always the same) are saved vertically, and 4 spaces per pattern are saved horizontally, which may become extremely useful for longer option/variable names.

As opposed to getopts, you miss some validation mechanisms and option bundling, but OTOH you don't need to understand the getopts syntax--what you see here in Bash is what you get. From my experience (writing rather simple scripts with simple interfaces), this is worth.

Mastodon
published by mdpublish, 2023-05-05