This is the official coding style for Shellfu project.
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.
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).
79 characters.
For docstrings, it's 72 characters including mandatory she-space
prefix (#
). Add 4 in case of function docstrings.
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:
with empty line between the groups, and each group being sorted alphabetically.
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.
Any comment must start with at least single space.
# hello
Exceptions from this rule are TODO/FIXME comments--see below.
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
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 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 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!
#
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.
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:
#
(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.
Common advice applies to variable naming, but capitalization is decided based on scope:
Global variables must be ALL_CAPS
.
Local variables must be lowercase
or snake_case
, and must not
be used in inherited scope, i.e. in a child function.
Optionally an exception from the previous rule can be granted to
a specific variable. However, such variable must be named using
CamelCase
.
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.
For example:
#
# My global variable
#
PLANET_NAME=Earth
main() {
local name=$1 # name to greet
local TimeOfDay # morning, afternoon or night
TimeOfDay=$(determine_timeofday)
greet "$name"
}
greet() {
#
# Greet user $1 based on $TimeOfDay
#
local name=$1 # name to greet
case $TimeOfDay in
morning) echo "Good $PLANET_NAME morning, $name!" ;;
afternoon) echo "Nice $PLANET_NAME afternoon, $name!" ;;
night) echo "Sleep well, $name!" ;;
esac
}
main "$1"
Notice that while TimeOfDay
is allowed to slip through and be referenced
in greet()
, name
is purely local to main()
, and then again to
greet()
. (It's probably not a good idea to mix variables like that,
the example is ugly, but you get the point.)
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
, do
and done
are always alone, aligned with opening keyword
(if
, while
, for
) or intermediary keyword (elif
, else
);
command part (i.e. condition for if
/elif
/while
, or expansion in
for
) 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
and esac
on level 1,
patterns on level 2,
commands and terminators on level 3.
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.
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.