1. Overview

In this tutorial, we’ll look at the various techniques to debug Bash shell scripts. The Bash shell doesn’t provide any built-in debugger. However, there are certain commands and constructs that can be utilized for this purpose.

First, we’ll discuss the usages of the set command for debugging scripts. After that, we’ll check a few debugging specific use-cases using the set and trap commands. Finally, we’ll present some methods to debug already running scripts.

2. Bash Debugging Options

The debugging options available in the Bash shell can be switched on and off in multiple ways. Within scripts, we can either use the set command or add an option to the shebang line. However, another approach is to explicitly specify the debugging options in the command-line while executing the script. Let’s dive into the discussion.

2.1. Enabling verbose Mode

We can enable the verbose mode using the -v switch, which allows us to view each command before it’s executed.

To demonstrate this, let’s create a sample script:

#! /bin/bash
read -p "Enter the input: " val
zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi

This script checks whether or not the number entered as input is positive.

Next, let’s execute our script:

$ bash -v ./positive_check.sh
#! /bin/bash
read -p "Enter the input: " val
Enter the input: -10
zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi
The input value is not positive.

As we can notice, it prints every line of the script on the terminal before it’s processed.

We can also add the -v option in the shebang line:

#! /bin/bash -v

This has the same effect as explicitly calling a script using bash -v. Another equivalent is to enable the mode within a script using set command:

#! /bin/bash
set -v

In fact, we can use either of the ways discussed above to enable the various switches that we’ll discuss henceforth.

2.2. Syntax Checking Using noexec Mode

There can be situations where we may want to validate the script syntactically prior to its execution. If so, we can use the noexec mode using the -n option. As a result, Bash will read the commands but not execute them.

Let’s execute our positive_check.sh script in noexec mode:

$ bash -n ./positive_check.sh

This produces a blank output since there are no syntax errors. Now, we’ll modify our script a bit and remove the then statement:

#! /bin/bash
read -p "Enter the input: " val
zero_val=0
if [ "$val" -gt "$zero_val" ]
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi

Next, we’ll validate it syntactically with -n option:

$ bash -n ./positive_check_noexec.sh
./positive_check_noexec.sh: line 6: syntax error near unexpected token `else'
./positive_check_noexec.sh: line 6: `  else'

As expected, it threw an error since we missed the then statement in the if condition.

2.3. Debugging Using xtrace Mode

In the previous section, we tested the script for syntax errors. But for identifying logical errors, we may want to trace the state of variables and commands during the execution process. In such instances, we can execute the script in xtrace (execution trace) mode using the -x option.

This mode prints the trace of commands for each line after they are expanded but before they are executed.

Let’s execute our positive_check.sh script in execution trace mode:

$ bash -x ./positive_check.sh
+ read -p 'Enter the input: ' val
Enter the input: 17
+ zero_val=0
+ '[' 17 -gt 0 ']'
+ echo 'Positive number entered.'
Positive number entered.

Here we can see the expanded version of variables on stdout before execution. It’s important to note that the lines preceded by + sign are generated by the xtrace mode.

2.4. Identifying Unset Variables

Let’s run an experiment to understand the default behavior of unset variables in Bash scripts:

#! /bin/bash
five_val=5
two_val=2
total=$((five_val+tow_val))
echo $total

We’ll now execute the above script:

$ ./add_values.sh
5

As we can notice, there’s an issue: The script executed successfully, but the output is logically incorrect.

We’ll now execute the script with the -u option:

$ bash -u ./add_values.sh
./add_values.sh: line 4: tow_val: unbound variable

Certainly, there’s a lot more clarity now!

The script failed to execute since the variable tow_val is not defined. We had mistakenly typed two_val as tow_val while calculating the total.

The -u option treats unset variables and parameters as an error when performing parameter expansion. Consequently, we get an error notification that a variable is not bound to value while executing the script with -u option

3. Use Cases to Debug Shell Scripts

So far, we saw the various switches for debugging scripts. Henceforth, we’ll look at some use-cases and methods to implement these in shell scripts.

3.1. Combining Debugging Options

To get better insights, we can further combine the various options of the set command.

Let’s execute our add_values.sh script with both -v and -u options enabled:

$ bash -uv ./add_values.sh
#! /bin/bash
five_val=5
two_val=2
total=$((five_val+tow_val))
./add_values.sh: line 4: tow_val: unbound variable

Here, by enabling the verbose mode with the -u option, we could easily identify the statement triggering the error.

Similarly, we can combine the verbose and xtrace mode to get more precise debug information.

As discussed previously, the -v option shows each line before it is evaluated, and the -x option shows each line after they are expanded. Hence, we can combine both -x and -v options to see how statements look like before and after variable substitutions.

Now, let’s execute our positive_check.sh script with -x and -v mode enabled:

$ bash -xv ./positive_check.sh
#! /bin/bash
read -p "Enter the input: " val
+ read -p 'Enter the input: ' val
Enter the input: 5
zero_val=0
+ zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi
+ '[' 5 -gt 0 ']'
+ echo 'Positive number entered.'
Positive number entered.

We can observe that the statements are printed on stdout before and after variable expansion.

3.2. Debugging Specific Parts of the Script

Debugging with -x or -v option shell scripts generates an output for every statement on stdout. However, there can be situations where we may want to reduce debug information to only specific parts of the script. We can achieve that by enabling the debug mode before the code block starts, and later reset it using the set command.

Let’s check it with an example:

#! /bin/bash
read -p "Enter the input: " val
zero_val=0
set -x
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi
set +x
echo "Script Ended"
 

Here, we could debug only the if condition using the set statement before the condition starts. Later, we could reset the xtrace mode after the if  block ends using the set +x command.

Let’s validate it with the output:

$ ./positive_debug.sh
Enter the input: 7
+ '[' 7 -gt 0 ']'
+ echo 'Positive number entered.'
Positive number entered.
+ set +x
Script Ended

Certainly, the output looks less cluttered.

3.3. Redirecting Only the Debug Output to a File

In the previous section, we examined how we can restrict debugging to only certain parts of the script. Consequently, we could restrict the amount of output on stdout.

Furthermore, we can redirect the debug information to another file and let the script output print on stdout.

Let’s create another script to check it:

#! /bin/bash
exec 5> debug.log 
PS4='$LINENO: ' 
BASH_XTRACEFD="5" 
read -p "Enter the input: " val
zero_val=0
if [ "$val" -gt "$zero_val" ]
then
   echo "Positive number entered."
else
   echo "The input value is not positive."
fi

First, we opened the debug.log file on File Descriptor (FD) 5 for writing using the exec command.

Then we changed the special shell variable PS4. The PS4 variable defines the prompt that gets displayed when we execute a shell script in xtrace mode. The default value of PS4 is  +. We changed the value of the PS4 variable to display line numbers in the debug prompt. To achieve this, we used another special shell variable LINENO.

Later, we assigned the FD 5 to Bash variable BASH_XTRACEFD. In effect, Bash will now write the xtrace output on FD5  i.e. debug.log file. Let’s execute the script:

$ bash -x ./debug_logging.sh
+ exec
+ PS4='$LINENO: '
4: BASH_XTRACEFD=5
Enter the input: 2
Positive number entered.

As expected, the debug output is not written on the terminal. Although, the first few lines, until FD 5 is assigned to debug output were printed.

Additionally, the script also creates an output file debug.log, which contains the debug information:

$ cat debug.log
5: read -p 'Enter the input: ' val
6: zero_val=0
7: '[' 2 -gt 0 ']'
9: echo 'Positive number entered.'

4. Debugging Scripts Using trap

We can utilize the DEBUG trap feature of Bash to execute a command repetitively.  The command specified in the arguments of trap command is executed before each subsequent statement in the script.

Let’s illustrate this with an example:

#! /bin/bash
trap 'echo "Line- ${LINENO}: five_val=${five_val}, two_val=${two_val}, total=${total}" ' DEBUG
five_val=5
two_val=2
total=$((five_val+two_val))
echo "Total is: $total"
total=0 && echo "Resetting Total"

In this example, we specified the echo command to print the values of variables  five_val, two_val, and total. Subsequently, we passed this echo statement to the trap command with the DEBUG signal. In effect, prior to the execution of every command in the script, the values of variables get printed.

Let’s check the generated output:

$ ./trap_debug.sh
Line- 3: five_val=, two_val=, total=
Line- 4: five_val=5, two_val=, total=
Line- 5: five_val=5, two_val=2, total=
Line- 6: five_val=5, two_val=2, total=7
Total is: 7
Line- 7: five_val=5, two_val=2, total=7
Line- 7: five_val=5, two_val=2, total=0
Resetting Total

5. Debugging Already Running Scripts

So far, we presented methods to debug shell scripts while executing them. Now, we’ll look at ways to debug an already running script.

Consider a sample running script which executes sleep in an infinite while loop:

#! /bin/bash
while :
do
 sleep 10 &
 echo "Sleeping for 4 seconds.."
 sleep 4
done

With the help of pstree command, we can check the child processes forked by our script sleep.sh:

$ pstree -p
init(1)─┬─init(148)───bash(149)───sleep.sh(372)─┬─sleep(422)
        │                                        ├─sleep(424)
        │                                        └─sleep(425)
        ├─init(213)───bash(214)───pstree(426)
        └─{init}(7)

We used an additional option -p to print the process ids along with the process names. Hence, we’re able to realize that the script is waiting for the child processes (sleep) to complete.

Sometimes we may want to have a closer look at the operations performed by our processes. In such cases, we can use the strace command to trace the Linux system calls in progress:

$ sudo strace -c -fp 372
strace: Process 372 attached
strace: Process 789 attached
strace: Process 790 attached
^Cstrace: Process 372 detached
strace: Process 789 detached
strace: Process 790 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
100.00    0.015625        5208         3           wait4
  0.00    0.000000           0         6           read
  0.00    0.000000           0         1           write
  0.00    0.000000           0        39           close
  0.00    0.000000           0        36           fstat
  0.00    0.000000           0        38           mmap
  0.00    0.000000           0         8           mprotect
  0.00    0.000000           0         2           munmap
  0.00    0.000000           0         6           brk
  0.00    0.000000           0        16           rt_sigaction
  0.00    0.000000           0        20           rt_sigprocmask
  0.00    0.000000           0         1           rt_sigreturn
  0.00    0.000000           0         6         6 access
  0.00    0.000000           0         1           dup2
  0.00    0.000000           0         2           getpid
  0.00    0.000000           0         2           clone
  0.00    0.000000           0         2           execve
  0.00    0.000000           0         2           arch_prctl
  0.00    0.000000           0        37           openat
------ ----------- ----------- --------- --------- ----------------
100.00    0.015625                   228         6 total

Here we used the option -p to attach to the process id (372) i.e. our script in execution. Additionally, we also used the -f option to attach to all its child processes. Note that, the strace command generates output for every system call. Hence, we used the -c option to print a summary of the system calls at the termination of strace.

6. Conclusion

In this tutorial, we studied multiple techniques to debug a shell script.

In the beginning, we discussed the various options of set command and their usage for debugging scripts. After that, we implemented several case-studies to study a combination of debugging options. Alongside this, we also explored ways to restrict debug output and redirect it to another file.

Next, we presented a use-case of the trap command and DEBUG signal for debugging scenarios. Finally, we offered a few approaches to debug already running scripts.

guest
0 Comments
Inline Feedbacks
View all comments