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:
- standard library modules,
- related third party modules,
- 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:
- are indented with 4 additional spaces,
- are not preceded by empty line (but the declaration instead),
- and may, or may not be followed by empty line---here anything that
is not
#
(i.e. normal code as well) counts as delimiter.
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:
Just module name itself.
Module name itself followed by two underscores* (
__
) and an id consisting of one or more letters, numbers or underscores.Any of above, prefixed by one or more underscores.
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:
Global variables must be
ALL_CAPS
.Function-local variables must be
lowercase
orsnake_case
. These variables must not be used in inherited scope, i.e. in a child function.Inheritable-local variables must be
CamelCase
. These variables may be used in inherited scope, i.e. in a child function.Both kinds of local variables (inherited and non-inherited) should be defined in header of the function, that is, before actual code. It's also recommended to comment variables here.
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,
multi-line;
no
function
keyword;single space between parentheses and opening curly bracket;
closing curly bracket is alone -- no redirection here.
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,
then
anddo
are always on same line as opening keyword (if
,while
,for
) or intermediary keyword (elif
);command part (i.e. condition for
if
/elif
/while
, or expansion infor
) is terminated by semicolon;commands in loop body are not terminated by semicolon.
case
case $foo in
bar)
baz
;;
*)
quux
;;
esac
That is, three levels:
case
andesac
on level 1,patterns on level 2,
commands and terminators on level 3.
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,
a "table" layout, where pattern, commands and terminator form a "row";
each "column" is aligned 4 spaces after
case
;commands aligned together, to next 4-space boundary after longest pattern;
just as terminators, after longest command;
in case of multiple commands, semicolon and space is used as delimiter.
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.