Script debugging

This chapter describes how to debug shell scripts.

Common mistakes

When writing Shell scripts, you must take into account the failure of the command, otherwise it is easy to make mistakes.

#! /bin/bash

dir_name=/path/not/exist

cd $dir_name
rm *

In the above script, if the directory $dir_name does not exist, the cd $dir_name command will fail to execute. At this time, the current directory will not be changed, and the script will continue to execute, causing the rm * command to delete all files in the current directory.

If it is changed to the following, there will be problems.

cd $dir_name && rm *

In the above script, only cd $dir_name is executed successfully, will rm * be executed. However, if the variable $dir_name is empty, cd will enter the user's home directory, thus deleting all the files in the user's home directory.

The following wording is correct.

[[ -d $dir_name ]] && cd $dir_name && rm *

In the above code, first determine whether the directory $dir_name exists, and then perform other operations.

If you are not worried about what file to delete, you can print it out first to see.

[[ -d $dir_name ]] && cd $dir_name && echo rm *

In the above command, echo rm * will not delete the file, only the file to be deleted will be printed out.

-x parameter of bash

The -x parameter of bash can print the command before executing each command. Once an error occurs, it is easier to track down.

Below is a script script.sh.

# script.sh
echo hello world

With the -x parameter, the command will be displayed before executing each command.

$ bash -x script.sh
+ echo hello world
hello world

In the above example, the line with + at the beginning of the line shows that this line is the command to be executed, and the next line is the execution result of the command.

Let's look at an example where -x is written inside the script.

#! /bin/bash -x
# trouble: script to demonstrate common errors

number=1
if [$number = 1 ]; then
  echo "Number is equal to 1."
else
  echo "Number is not equal to 1."
fi

After the above script is executed, each line of commands will be output.

$ trouble
+ number=1
+'[' 1 = 1']'
+ echo'Number is equal to 1.'
Number is equal to 1.

The + sign before the output command is determined by the system variable PS4, which can be modified.

$ export PS4='$LINENO + '
$ trouble
5 + number=1
7 +'[' 1 = 1']'
8 + echo'Number is equal to 1.'
Number is equal to 1.

In addition, the set command can also set shell behavior parameters, which is helpful for script debugging. For details, see the chapter "set command".

Environment variables

There are some environment variables that are often used for debugging.

LINENO

The variable LINENO returns its line number in the script.

#!/bin/bash

echo "This is line $LINENO"

Execute the above script test.sh, $LINENO will return 3.

$ ./test.sh
This is line 3

FUNCNAME

The variable FUNCNAME returns an array with the current function call stack. The 0th member of the array is the currently called function, the 1st member is the function that calls the current function, and so on.

#!/bin/bash

function func1()
{
  echo "func1: FUNCNAME0 is ${FUNCNAME[0]}"
  echo "func1: FUNCNAME1 is ${FUNCNAME[1]}"
  echo "func1: FUNCNAME2 is ${FUNCNAME[2]}"
  func2
}

function func2()
{
  echo "func2: FUNCNAME0 is ${FUNCNAME[0]}"
  echo "func2: FUNCNAME1 is ${FUNCNAME[1]}"
  echo "func2: FUNCNAME2 is ${FUNCNAME[2]}"
}

func1

Execute the above script test.sh, the results are as follows.

$ ./test.sh
func1: FUNCNAME0 is func1
func1: FUNCNAME1 is main
func1: FUNCNAME2 is
func2: FUNCNAME0 is func2
func2: FUNCNAME1 is func1
func2: FUNCNAME2 is main

In the above example, when func1 is executed, member 0 of the variable FUNCNAME is func1, and member 1 is the main script main that calls func1. When func2 is executed, the 0th member of the variable FUNCNAME is func2, and the 1st member is func1 that calls func2.

BASH_SOURCE

The variable BASH_SOURCE returns an array with the current script call stack. The 0th member of the array is the currently executing script, the 1st member is the script that calls the current script, and so on, there is a one-to-one correspondence with the variable FUNCNAME.

There are two sub-scripts lib1.sh and lib2.sh below.

# lib1.sh
function func1()
{
  echo "func1: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
  echo "func1: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
  echo "func1: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
  func2
}
# lib2.sh
function func2()
{
  echo "func2: BASH_SOURCE0 is ${BASH_SOURCE[0]}"
  echo "func2: BASH_SOURCE1 is ${BASH_SOURCE[1]}"
  echo "func2: BASH_SOURCE2 is ${BASH_SOURCE[2]}"
}

Then, the main script main.sh calls the above two sub-scripts.

#!/bin/bash
# main.sh

source lib1.sh
source lib2.sh

func1

Execute the main script main.sh, you will get the following result.

$ ./main.sh
func1: BASH_SOURCE0 is lib1.sh
func1: BASH_SOURCE1 is ./main.sh
func1: BASH_SOURCE2 is
func2: BASH_SOURCE0 is lib2.sh
func2: BASH_SOURCE1 is lib1.sh
func2: BASH_SOURCE2 is ./main.sh

In the above example, when the function func1 is executed, member 0 of the variable BASH_SOURCE is the script lib1.sh where func1 is located, and member 1 is the main script main.sh; when the function func2 is executed , The member 0 of the variable BASH_SOURCE is the script lib2.sh where func2 is located, and the member 1 is the script lib1.sh that calls func2.

BASH_LINENO

The variable BASH_LINENO returns an array, the content is the line number corresponding to each call. ${BASH_LINENO[$i]} has a one-to-one relationship with ${FUNCNAME[$i]}, which means that ${FUNCNAME[$i]} is calling its script file ${BASH_SOURCE[$ i+1]} The line number inside.

There are two sub-scripts lib1.sh and lib2.sh below.

# lib1.sh
function func1()
{
  echo "func1: BASH_LINENO is ${BASH_LINENO[0]}"
  echo "func1: FUNCNAME is ${FUNCNAME[0]}"
  echo "func1: BASH_SOURCE is ${BASH_SOURCE[1]}"

  func2
}
# lib2.sh
function func2()
{
  echo "func2: BASH_LINENO is ${BASH_LINENO[0]}"
  echo "func2: FUNCNAME is ${FUNCNAME[0]}"
  echo "func2: BASH_SOURCE is ${BASH_SOURCE[1]}"
}

Then, the main script main.sh calls the above two sub-scripts.

#!/bin/bash
# main.sh

source lib1.sh
source lib2.sh

func1

Execute the main script main.sh, you will get the following result.

$ ./main.sh
func1: BASH_LINENO is 7
func1: FUNCNAME is func1
func1: BASH_SOURCE is main.sh
func2: BASH_LINENO is 8
func2: FUNCNAME is func2
func2: BASH_SOURCE is lib1.sh

In the above example, the function func1 is called on the 7th line of main.sh, and the function func2 is called on the 8th line of lib1.sh.