Best practices [1]
Prevent code injection
In comparison to Python, Bash scripts are already executed while reading them. This makes it impossible to change them at runtime. This example demonstrates a resulting problem:
1#!/bin/bash
2echo "This row is only printed once"
3padded=$(printf '%*s' 256 ' ' | cat - "$0")
4echo "$padded" > "$0"
- line 3:
printf defines a minimum amount of characters to print (256), in this case using a wildcard (
%*s) and passing in spaces (’ ‘). The result is then piped intocat, which concatenates the result AKA standard input (meaning of-, and the contents of the file ("$0"). The result is assigned to thepaddedvariable.- line 4:
Output of padded content (line 3) overwrites the script file
Bash scripts are read character by character, bash remembers the current position via an index. When everything is pushed to a higher index position because of the proceeding spaces, the current position which bash remembers to be is then not the same position in the actual script. Because of that, the bash executes the remaining lines over and over again, resulting in an endless script.
This is a security issue, known as code injection, which attackers can use the manipulate important scripts, for instance in order to gain access rights.
To prevent code injection, put the entire script into a command group, which is then read into the memory all at once:
1#!/bin/bash
2{
3 echo "This row is only printed once"
4 padded=$(printf '%*s' 256 ' ' | cat - "$0")
5 echo "$padded" > "$0"
6 exit 0
7}
The exit 0 at the end guarantees that the script is ended at the end of the
group. Otherwise an attacker could still append content, which is then executed after
the group finishes.
Running script instances cannot be altered, but to prevent any changes to the script on the files system, prevent other users from editing the file:
$ chmod 755 /path/to/script.sh
Use functions
Functions are also read in one go, so another possibility is to use a main function, which is called by a short command group. A basic structure:
1#!/bin/bash
2main() {
3 return 0
4}
5{
6 main "$@"
7 exit "$?"
8}
- line 6:
The main function is called passing all script arguments (
"$@"stands for “$1” “$2” … and so on). See bash manual for more.- line 7:
Script exits with the exit status of the most recently executed foreground pipeline (here: 0 from main()).
Mind variable scopes
Functions have the advantage of being able to declare local variables.
Important
All variables that are not declared via local or declare
(without -g option) function as global variables.
Variables passed into functions inherit the scope of the callee
Global variables can be replaced by equally named local variable
If a local variable, declared by the callee, is changed within the function, it was passed into, the variable of the callee is changed as well
Demonstration:
1#!/bin/bash
2servus_local() {
3 local_gruss="Servus"
4 moin_local
5 echo "[$FUNCNAME] $gruss $USER"
6}
7moin_local() {
8 local_gruss="Moin"
9 echo "[$FUNCNAME] $gruss $USER"
10}
11servus_dynamic() {
12 local_gruss="Servus"
13 moin_dynamic
14 echo "[$FUNCNAME] $gruss $USER"
15}
16moin_dynamic() {
17 gruss="Moin"
18 echo "[$FUNCNAME] $gruss $USER"
19}
20servus_global() {
21 gruss="Servus"
22 moin_global
23 echo "[$FUNCNAME] $gruss $USER"
24}
25moin_global() {
26 gruss="Moin"
27 echo "[$FUNCNAME] $gruss $USER"
28}
29main() {
30 servus_local
31 echo "[$FUNCNAME] $gruss $USER"
32 servus_dynamic
33 echo "[$FUNCNAME] $gruss $USER"
34 servus_global
35 echo "[$FUNCNAME] $gruss $USER"
36}
37{
38 main "$@"
39 exit "$?"
40}
which outputs (for $USER=”arnewohletz”):
[moin_local] Moin arnewohletz
[servus_local] Servus arnewohletz
[main] arnewohletz
[moin_dynamic] Moin arnewohletz
[servus_dynamic] Moin arnewohletz
[main] arnewohletz
[moin_global] Moin arnewohletz
[servus_global] Moin arnewohletz
[main] Moin arnewohletz
Always declare your variables with local and declare (see
built-in commands).
To enable warning messages for undeclared variables, run your script via the
-u option. You may also check your script using the ShellCheck.
Properly name function arguments
In Bash, function don’t have arguments but positional parameters, which are numbered, but don’t have names. When a method uses more than five or more arguments, it is hard to keep track of the position for each of them.
To overcome this issue, declare all positional parameter as local variable at the beginning of each function:
send_email() {
local address="$1"
local title="$2"
local message="$3"
}
Always separate declaration and assignment of variables
Be careful not to declare and assign a variable in one line: both actions are executed sequentially and not in a defined order. For direct assignments, this is not critical, but it is when the assigned value comes from a subshell:
1file_contains() {
2 local file="$1"
3 local search_pattern="$2"
4 local result=$(grep -F "$search_patern" "$file")
5 if (( $? != 0 )); then
6 # nothing found
7 return 1
8 fi
9 return 0
10}
In line 5, the $? variable, the return value of tha previous command (line 4) does not receive the grep return value, but the one from local, which is always 0 except if the command received illegal parameters.