// file : doc/bash-style.cli // license : MIT; see accompanying LICENSE file "\title=Bash Style Guide" // NOTES // // - Maximum
line is 70 characters. // "\h1|Table of Contents|" "\$TOC$" " \h1#intro|Introduction| Bash works best for simple tasks. Needing arrays, arithmetic, and so on, is usually a good indication that the task at hand is too complex for Bash. Most of the below rules can be broken if there is a good reason for it. Besides making things consistent, rules free you from having to stop and think every time you encounter a particular situation. But if it feels that the prescribed way is clearly wrong, then it probably makes sense to break it. You just need to be clear on why you are doing it. See also \l{https://google.github.io/styleguide/shell.xml Google's Bash Style Guide} as well as \l{https://github.com/progrium/bashstyle Let's do Bash right!}; we agree with quite a few (but not all) items in there. In particular, the former provides a lot more rationale compared to this guide. \h1#style|Style| Don't use any extensions for your scripts. That is, call it just \c{foo} rather than \c{foo.sh} or \c{foo.bash}. Use lower-case letters and dash to separate words, for example \c{foo-bar}. Indentation is two spaces (not tabs). Maximum line length is 79 characters (excluding newline). Use blank lines between logical blocks to improve readability. Variable and function names should use lower-case letters with underscores separating words. For \c{if}/\c{while} and \c{for}/\c{do} the corresponding \c{then} or \c{do} is written on the same line after a semicolon, for example: \ if [ ... ]; then fi for x in ...; do done \ For \c{if} use \c{[ ]} for basic tests and \c{[[ ]]} only if the previous form is not sufficient. Use \c{test} for filesystem tests (presence of files, etc). Do use \c{elif}. \h1#struct|Structure| The overall structure of the script should be as follows: \ #! /usr/bin/env bash ## # [ ] # # [ ] # usage=\"usage: $0 \" owd=\"$(pwd)\" trap \"{ cd '$owd'; exit 1; }\" ERR set -o errtrace # Trap in functions. function info () { echo \"$*\" 1>&2; } function error () { info \"$*\"; exit 1; } [ ] [ ] [ ] \ \h#struct-summary|SUMMARY| One-two sentences describing what the script does. \h#struct-func-desc|FUNCTIONALITY-DESCRIPTION| More detailed functionality description for more complex scripts. \h#struct-opt-desc|OPTIONS-DESCRIPTION| Description of command line options. For example: \ # -q # Run quiet. # # -t # Specify the alternative toolchain installation directory. \ \h#struct-opt|OPTIONS| Command line options summary. For example: \ usage=\"usage: $0 [-q] [-t ] \" \ \h#struct-opt-arg-default|OPTIONS-ARGUMENTS-DEFAULTS| Set defaults to variables that will contain option/argument values. For example: \ quiet=\"n\" tools=/usr/local file= \ \h#struct-opt-arg-parse|OPTIONS-ARGUMENTS-PARSING| Parse the command line options/arguments. For example: \ while [ \"$#\" -gt 0 ]; do case \"$1\" in -q) quiet=\"y\" shift ;; -t) shift tools=\"${1%/}\" shift ;; *) if [ -n \"$file\" ]; then error \"$usage\" fi file=\"$1\" shift ;; esac done \ If the value you are expecting from the command line is a directory path, the always strip the trailing slash (as shown above for the \c{-t} option). \h#struct-opt-arg-valid|OPTIONS-ARGUMENTS-VALIDATION| Validate option/argument values. For example: \ if [ -z \"$file\" ]; then error \"$usage\" fi if [ ! -d \"$file\" ]; then fail \"'$file' does not exist or is not a directory\" fi \ \h#struct-func|FUNCTIONALITY| Implement script logic. For diagnostics use the \c{info()} and \c{error()} functions defined above (so that it goes to stderr, not stdout). If using functions, then define them just before use. \h1#quote|Quoting| We quote every variable expansion, no exceptions. For example: \ if [ -n \"$foo\" ]; then ... fi \ This also applies to command substitution (which we always write as \c{$(foo arg)} rather than \c{`foo arg`}), for example: \ list=\"$(cat foo)\" \ Note that a command substitution creates a new quoting context, for example: \ list=\"$(basename \"$1\")\" \ We also quote values that are \i{strings} as opposed to options/file names, paths, or integers. If setting a variable that will contain one of these unquoted values, try to give it a name that reflects its type (e.g., \c{foo_file} rather than \c{foo_name}). Prefer single quotes for \c{sed} scripts, for example: \ proto=\"https\" quiet=\"y\" verbosity=1 dir=/etc out=/dev/null file=manifest seds='s%^./%%' \ Note that quoting will inhibit globbing so you may end up with expansions along these lines: \ rm -f \"$dir/$name\".* \ \N|One exception to this quoting rule is arithmetic expansion (\c{$((\ ))}): Bash treats it as if it was double-quoted and, as a result, any inner quoting is treated literally. For example: \ z=$(($x + $y)) # Ok. z=$((\"$x\" + \"$y\")) # Error. z=$(($x + $(echo \"$y\"))) # Ok. \ | If you have multiple values (e.g., program arguments) that may contain spaces, don't try to handle them with quoting and use arrays instead. Here is a typical example of a space-aware argument handling: \ files=() while [ \"$#\" -gt 0 ]; do case \"$1\" in ... *) shift files=(\"${files[@]}\" \"$1\") shift ;; esac done rm -f \"${files[@]}\" \ In the same vein, never write: \ cmd $* \ Instead always write: \ cmd \"$@\" \ Also understand the difference between \c{@} and \c{*} expansion: \ files=('one' '2 two' 'three') echo \"files: ${files[@]}\" # $1='files: one', $2='2 two', $3='three' echo \"files: ${files[*]}\" # $1='files: one 2 two three' \ \h1#trap|Trap| Our scripts use the error trap to automatically terminate the script in case any command fails. If you need to check the exit status of a command, use \c{if}, for example: \ if grep \"foo\" /tmp/bar; then info \"found\" fi if ! grep \"foo\" /tmp/bar; then info \"not found\" fi \ Note that the \c{if}-condition can be combined with capturing the output, for example: \ if v=\"$(...)\"; then ... fi \ If you need to ignore the exit status, you can use \c{|| true}, for example: \ foo || true \ \h1#bool|Boolean| For boolean values use empty for false and \c{true} for true. This way you can have terse and natural looking conditions, for example: \ first=true while ...; do if [ ! \"$first\" ]; then ... fi if [ \"$first\" ]; then first= fi done \ \h1#function|Functions| If a function takes arguments, provide a brief usage after the function header, for example: \ function dist() # { ... } \ For non-trivial/obvious functions also provide a short description of its functionality/purpose, for example: \ # Prepare a distribution of the specified packages and place it into the # specified directory. # function dist() # { ... } \ Inside functions use local variables, for example: \ function dist() { local x=\"foo\" } \ If the evaluation of the value may fail (e.g., it contains a program substitution), then place the assignment on a separate line since \c{local} will cause the error to be ignore. For example: \ function dist() { local b b=\"$(basename \"$2\")\" } \ For more information on returning data from functions, see \l{https://mywiki.wooledge.org/BashFAQ/084 BashFAQ#084}. For more information on writing reusable functions, see \l{https://stackoverflow.com/questions/11369522/bash-utility-script-library Bash Utility Script Library}. "