Defensive BASH Programming: The Reprint
The aim of this page📝 is to rewrite Web Archive > Defensive BASH programming > Say what? as it is no more publicly available since the domain is down at the moment.
There’s also Defensive BASH Programming which is older. There’s also a critique on hackernews Defensive BASH programming | Hacker News. Let’s just transcribe. I am also changing the formatting as per shellcheck’s recommendations.
1. Make global variables immutable
- Try to keep globals to minimum
- Globals use UPPER_CASE naming convention
- Use
readonly
declaration
Same as declare -r, sets a variable as read-only, or, in effect, as a constant. Attempts to change the variable fail with an error message. This is the shell analog of the C language const type qualifier.
— https://tldp.org/LDP/abs/html/internal.html
- Use globals to replace the cryptic
$0
,$1
, etc. names passed as arguments into the script call - Globals the author always uses in his programs are as follows
Positional parameters:
$0
is the name of the script itself,$1
is the first argument,$2
the second,$3
the third, and so forth. After$9
, the arguments must be enclosed in brackets, for example,${10}
,${11}
,${12}
. The special variables$*
and$@
denote all the positional parameters.
2. All variables should be local
- the
local
keyword cannot be used within global scope
pkutaj@DESKTOP-LRK1G7U:~$ local foo
-bash: local: can only be used in a function
- Self-documenting parameters
- Usually for loop use the conventional
i
variable ⟹ makes it essential to declare it as local
On Unix-like operating systems, shift is a builtin command of the Bash shell. When executed, it shifts the positional parameters (such as arguments passed to a bash script) to the left, putting each parameter in a lower position.
3. Use the top-level main() function — the only global command in the code is main
- Intuitive for functional programmers
- Helps keeping all variables local
4. Everything except global immutables and calling main is in a function
- As outlined, only 2 things are running globally ..1. Global immutables ..2.
main
which is callingmain()
- We may start with
- but improve this with
- in the second example, finding fines is the problem of
get_temp_files()
and not ofmain()
- the code also becomes testable, by unit testing of
get_temp_files()
5. Instead of using -x for debugging the whole file, wrap problematic sections into set -x
and set +x
for precision.
- Debugging is activated via the
-x
flag
bash -x my_prog.sh
- Don’t do the above
- Instead, debug just a small section of code wrapping it into
set -x
andset +x
- In addition, you can use the
$FUNCNAME
+$@
variables and print them while debugging
- You’ll get something like
temp_files /var/tmp
+ ls /var/tmp
+ grep pid
+ grep -v daemon
+ set +x
6. Extract commands into names with readable functions so that code reads “like newspaper”
- can you always quickly say does the following code do?
- of course, disagreements rage around this one on HN
- Let your code speak
7. Each line does just one thing
- Break expressions with
\
or with natural line continuators - For example
Can be written much cleaner — shellcheck removes backslash and puts the pipe at the end of the line and I am keeping this as opposed to the original doc
- Good example where we see the connection between lines and the connecting rods.
Here is the conflict with shellcheck. See the diff — I suggest that the point is made and both are fine
- for explanation of
&&
and||
see the great Advanced Bash-Scripting Guide > Chapter 26. List Constructs
8. Print usage with heredoc within a dedicated function
- Do not do the following
- It should be a function
- To remove the repetition of
echo
in each line, use Bash Heredoc < Linuxize
9. Command line arguments
- Here is an example to complement the usage function above.
- You use it like this, using the immutable ARGS variable we defined at the top:
main() {
cmdline $ARGS
}
main
10. Unit Testing
- Use shunit2 for unit testing
- Here is another example using df command:
- Here I have an exception, for testing, I declare DF in the global scope not readonly.
- This is because of shunit2 not allowing to change global scope functions.