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 into cat, which concatenates the result AKA standard input (meaning of -, and the contents of the file ("$0"). The result is assigned to the padded variable.

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.