32 调试

例子 32-1. 一个错误脚本

脚本的输出:

上边的脚本究竟哪错了(提示: 注意if的后边)

例子 32-2. 缺少关键字

  1. #!/bin/bash
  2. # missing-keyword.sh
  3. # 这个脚本会提示什么错误信息?
  4. for a in 1 2 3
  5. do
  6. echo "$a"
  7. # done #所需关键字'done'在第8行被注释掉.
  8. exit 0 # 将不会在这退出!
  9. #在命令行执行完此脚本后
  10. 输入:echo $?
  11. 输出:2

脚本的输出:

  1. missing-keyword.sh: line 10: syntax error: unexpected end of file

注意, 其实不必参考错误信息中指出的错误行号. 这行只不过是Bash解释器最终认定错误的地方.
出错信息在报告产生语法错误的行号时, 可能会忽略脚本的注释行.
如果脚本可以执行, 但并不如你所期望的那样工作, 怎么办? 通常情况下, 这都是由常见的逻辑错误所
产生的.

例子 32-3.

  1. #!/bin/bash
  2. # 这个脚本应该删除在当前目录下所有文件名中含有空格的文件
  3. # 它不能正常运行,为什么?
  4. badname=`ls | grep ' '`
  5. # Try this:
  6. # echo "$badname"
  7. rm "$badname"
  8. exit 0

可以通过把echo “$badname”行的注释符去掉,找出例子 29-3中的错误, 看一下echo出来的信息,是否按你期望的方式运行.

在这种特殊的情况下,rm “$badname”不能得到预期的结果,因为$badname不应该加双引号。加上双引号会让rm只有一个参数(这就只能匹配一个文件名).一种不完善的解决办法是去掉$badname外 面的引号, 并且重新设置$IFS, 让$IFS只包含一个换行符, IFS=$’\n’. 但是, 下面这个方法更简单.

总结一下这个问题脚本的症状:
>

  1. 由于”syntax error”(语法错误)使得脚本停止运行,
  2. 或者脚本能够运行, 但是并不是按照我们所期望的那样运行(逻辑错误).
  3. 脚本能够按照我们所期望的那样运行, 但是有烦人的副作用(逻辑炸弹).

如果想调试脚本, 可以用以下方式:

  1. echo语句可以放在脚本中存在疑问的位置上, 观察变量的值, 来了解脚本运行时的情况.

    1. ### debecho (debug-echo), by Stefano Falsetto ###
    2. ### Will echo passed parameters only if DEBUG is set to a value. ###
    3. debecho () {
    4. if [ ! -z "$DEBUG" ]; then
    5. echo "$1" >&2
    6. # ^^^ to stderr
    7. fi
    8. }
    9. DEBUG=on
    10. Whatever=whatnot
    11. debecho $Whatever # whatnot
    12. DEBUG=
    13. Whatever=notwhat
    14. debecho $Whatever # (Will not echo.)
  1. 使用过滤器tee来检查临界点上的进程或数据流.
  2. sh -n scriptname不会运行脚本, 只会检查脚本的语法错误. 这等价于把set -n或set -o noexec插入脚本中. 注意, 某些类型的语法错误不会被这种方式检查出来.

    sh -v scriptname将会在运行脚本之前, 打印出每一个命令. 这等价于把set -v或set -o verbose插入到脚本中.

    选项-n和-v可以同时使用. sh -nv scriptname将会给出详细的语法检查.

    sh -x scriptname会打印出每个命令执行的结果, 但只使用缩写形式. 这等价于在脚本中插入set
    -x或set -o xtrace.

    把set -u或set -o nounset插入到脚本中, 并运行它, 就会在每个试图使用未声明变量的地方给出一个unbound variable错误信息.

    1. set -u # Or set -o nounset
    2. # Setting a variable to null will not trigger the error/abort.
    3. # unset_var=
    4. echo $unset_var # Unset (and undeclared) variable.
    5. echo "Should not echo!"
    6. #sh t2.sh
    7. #t2.sh: line 6: unset_var: unbound variable
  3. 使用“断言”功能在脚本的关键点进行测试的变量或条件。 (这是从C借来的一个想法)

    Example 32-4. Testing a condition with an assert

    ```

    !/bin/bash

    assert.sh

    #

    assert () # If condition false,
    { #+ exit from script

    1. #+ with appropriate error message.

    E_PARAM_ERR=98
    E_ASSERT_FAILED=99

  1. if [ -z "$2" ] # Not enough parameters passed
  2. then #+ to assert() function.
  3. return $E_PARAM_ERR # No damage done.
  4. fi
  5. lineno=$2
  6. if [ ! $1 ]
  7. then
  8. echo "Assertion failed: \"$1\""
  9. echo "File \"$0\", line $lineno" # Give name of file and line number.
  10. exit $E_ASSERT_FAILED
  11. # else
  12. # return
  13. # and continue executing the script.
  14. fi
  15. } # Insert a similar assert() function into a script you need to debug.
  16. #######################################################################
  17. a=5
  18. b=4
  19. condition="$a -lt $b" # Error message and exit from script.
  20. # Try setting "condition" to something else
  21. #+ and see what happens.
  22. assert "$condition" $LINENO
  23. # The remainder of the script executes only if the "assert" does not fail.
  24. # Some commands.
  25. # Some more commands . . .
  26. echo "This statement echoes only if the \"assert\" does not fail."
  27. # . . .
  28. # More commands . . .
  29. exit $?
  30. ```
  1. 使用变量$LINENO和内建命令caller.

  2. 捕获exit返回值.

捕获信号

trap
Specifies an action on receipt of a signal; also useful for debugging.

A signal is a message sent to a process, either by the kernel or another process, telling it to take some specified action (usually to terminate). For example, hitting a Control-C sends a user interrupt, an INT signal, to a running program.

A simple instance:

Example 32-5. Trapping at exit

  1. #!/bin/bash
  2. # Hunting variables with a trap.
  3. trap 'echo Variable Listing --- a = $a b = $b' EXIT
  4. # EXIT is the name of the signal generated upon exit from a script.
  5. #
  6. # The command specified by the "trap" doesn't execute until
  7. #+ the appropriate signal is sent.
  8. echo "This prints before the \"trap\" --"
  9. echo "even though the script sees the \"trap\" first."
  10. echo
  11. a=39
  12. b=36
  13. exit 0
  14. # Note that commenting out the 'exit' command makes no difference,
  15. #+ since the script exits in any case after running out of commands.

Example 32-6. Cleaning up after Control-C

  1. #!/bin/bash
  2. # logon.sh: A quick 'n dirty script to check whether you are on-line yet.
  3. umask 177 # Make sure temp files are not world readable.
  4. TRUE=1
  5. LOGFILE=/var/log/messages
  6. # Note that $LOGFILE must be readable
  7. #+ (as root, chmod 644 /var/log/messages).
  8. TEMPFILE=temp.$$
  9. # Create a "unique" temp file name, using process id of the script.
  10. # Using 'mktemp' is an alternative.
  11. # For example:
  12. # TEMPFILE=`mktemp temp.XXXXXX`
  13. # At logon, the line "remote IP address xxx.xxx.xxx.xxx"
  14. # appended to /var/log/messages.
  15. ONLINE=22
  16. USER_INTERRUPT=13
  17. CHECK_LINES=100
  18. # How many lines in log file to check.
  19. trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
  20. echo
  21. while [ $TRUE ] #Endless loop.
  22. do
  23. tail -n $CHECK_LINES $LOGFILE> $TEMPFILE
  24. # Saves last 100 lines of system log file as temp file.
  25. # Necessary, since newer kernels generate many log messages at log on.
  26. search=`grep $KEYWORD $TEMPFILE`
  27. # Checks for presence of the "IP address" phrase,
  28. #+ indicating a successful logon.
  29. if [ ! -z "$search" ] # Quotes necessary because of possible spaces.
  30. then
  31. echo "On-line"
  32. rm -f $TEMPFILE # Clean up temp file.
  33. exit $ONLINE
  34. else
  35. echo -n "." # The -n option to echo suppresses newline,
  36. #+ so you get continuous rows of dots.
  37. fi
  38. sleep 1
  39. done
  40. # Note: if you change the KEYWORD variable to "Exit",
  41. #+ this script can be used while on-line
  42. #+ to check for an unexpected logoff.
  43. # Exercise: Change the script, per the above note,
  44. # and prettify it.
  45. exit 0
  46. # Nick Drage suggests an alternate method:
  47. while true
  48. do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
  49. echo -n "." # Prints dots (.....) until connected.
  50. sleep 2
  51. done
  52. # Problem: Hitting Control-C to terminate this process may be insufficient.
  53. #+ (Dots may keep on echoing.)
  54. # Exercise: Fix this.
  55. # Stephane Chazelas has yet another alternative:
  56. CHECK_INTERVAL=1
  57. while ! tail -n 1 "$LOGFILE" | grep -q "$KEYWORD"
  58. do echo -n .
  59. sleep $CHECK_INTERVAL
  60. done
  61. echo "On-line"
  62. # Exercise: Discuss the relative strengths and weaknesses
  63. # of each of these various approaches.
  64. Example 32-7. A Simple Implementation of a Progress Bar
  65. #! /bin/bash
  66. # progress-bar2.sh
  67. # Author: Graham Ewart (with reformatting by ABS Guide author).
  68. # Used in ABS Guide with permission (thanks!).
  69. # Invoke this script with bash. It doesn't work with sh.
  70. interval=1
  71. long_interval=10
  72. {
  73. trap "exit" SIGUSR1
  74. sleep $interval; sleep $interval
  75. while true
  76. do
  77. echo -n '.' # Use dots.
  78. sleep $interval
  79. done; } & # Start a progress bar as a background process.
  80. pid=$!
  81. trap "echo !; kill -USR1 $pid; wait $pid" EXIT # To handle ^C.
  82. echo -n 'Long-running process '
  83. sleep $long_interval
  84. echo ' Finished!'
  85. kill -USR1 $pid
  86. wait $pid # Stop the progress bar.
  87. trap EXIT
  88. exit $?

Note
The DEBUG argument to trap causes a specified action to execute after every command in a script. This permits tracing variables, for example.

Example 32-8. Tracing a variable

  1. #!/bin/bash
  2. trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
  3. # Echoes the value of $variable after every command.
  4. variable=29; line=$LINENO
  5. echo " Just initialized \$variable to $variable in line number $line."
  6. let "variable *= 3"; line=$LINENO
  7. echo " Just multiplied \$variable by 3 in line number $line."
  8. exit 0
  9. # The "trap 'command1 . . . command2 . . .' DEBUG" construct is
  10. #+ more appropriate in the context of a complex script,
  11. #+ where inserting multiple "echo $variable" statements might be
  12. #+ awkward and time-consuming.
  13. # Thanks, Stephane Chazelas for the pointer.

Output of script:

VARIABLE-TRACE> $variable = “”
VARIABLE-TRACE> $variable = “29”
Just initialized $variable to 29.
VARIABLE-TRACE> $variable = “29”
VARIABLE-TRACE> $variable = “87”
Just multiplied $variable by 3.
VARIABLE-TRACE> $variable = “87”
Of course, the trap command has other uses aside from debugging, such as disabling certain keystrokes within a script (see Example A-43).

Example 32-9. Running multiple processes (on an SMP box)

  1. #!/bin/bash
  2. # parent.sh
  3. # Running multiple processes on an SMP box.
  4. # Author: Tedman Eng
  5. # This is the first of two scripts,
  6. #+ both of which must be present in the current working directory.
  7. LIMIT=$1 # Total number of process to start
  8. NUMPROC=4 # Number of concurrent threads (forks?)
  9. PROCID=1 # Starting Process ID
  10. echo "My PID is $$"
  11. if [ $PROCID -le $LIMIT ] ; then
  12. ./child.sh $PROCID&
  13. let "PROCID++"
  14. else
  15. wait
  16. exit
  17. fi
  18. }
  19. while [ "$NUMPROC" -gt 0 ]; do
  20. start_thread;
  21. let "NUMPROC--"
  22. done
  23. while true
  24. do
  25. trap "start_thread" SIGRTMIN
  26. done
  27. exit 0
  28. # ======== Second script follows ========
  29. #!/bin/bash
  30. # child.sh
  31. # Running multiple processes on an SMP box.
  32. # This script is called by parent.sh.
  33. # Author: Tedman Eng
  34. temp=$RANDOM
  35. index=$1
  36. shift
  37. let "temp %= 5"
  38. let "temp += 4"
  39. echo "Starting $index Time:$temp" "$@"
  40. sleep ${temp}
  41. echo "Ending $index"
  42. kill -s SIGRTMIN $PPID
  43. exit 0
  44. # ======================= SCRIPT AUTHOR'S NOTES ======================= #
  45. # It's not completely bug free.
  46. # I ran it with limit = 500 and after the first few hundred iterations,
  47. #+ one of the concurrent threads disappeared!
  48. # Not sure if this is collisions from trap signals or something else.
  49. # Once the trap is received, there's a brief moment while executing the
  50. #+ trap handler but before the next trap is set. During this time, it may
  51. #+ be possible to miss a trap signal, thus miss spawning a child process.
  52. # No doubt someone may spot the bug and will be writing
  53. #+ . . . in the future.
  54. # ===================================================================== #
  55. # ----------------------------------------------------------------------#
  56. #################################################################
  57. # The following is the original script written by Vernia Damiano.
  58. # Unfortunately, it doesn't work properly.
  59. #################################################################
  60. #!/bin/bash
  61. # Must call script with at least one integer parameter
  62. #+ (number of concurrent processes).
  63. # All other parameters are passed through to the processes started.
  64. INDICE=8 # Total number of process to start
  65. TEMPO=5 # Maximum sleep time per process
  66. E_BADARGS=65 # No arg(s) passed to script.
  67. if [ $# -eq 0 ] # Check for at least one argument passed to script.
  68. then
  69. echo "Usage: `basename $0` number_of_processes [passed params]"
  70. exit $E_BADARGS
  71. fi
  72. NUMPROC=$1 # Number of concurrent process
  73. shift
  74. PARAMETRI=( "$@" ) # Parameters of each process
  75. function avvia() {
  76. local temp
  77. local index
  78. temp=$RANDOM
  79. index=$1
  80. shift
  81. let "temp %= $TEMPO"
  82. let "temp += 1"
  83. echo "Starting $index Time:$temp" "$@"
  84. sleep ${temp}
  85. echo "Ending $index"
  86. kill -s SIGRTMIN $$
  87. }
  88. function parti() {
  89. if [ $INDICE -gt 0 ] ; then
  90. avvia $INDICE "${PARAMETRI[@]}" &
  91. let "INDICE--"
  92. else
  93. trap : SIGRTMIN
  94. fi
  95. }
  96. trap parti SIGRTMIN
  97. while [ "$NUMPROC" -gt 0 ]; do
  98. parti;
  99. let "NUMPROC--"
  100. done
  101. wait
  102. trap - SIGRTMIN
  103. exit $?
  104. : <<SCRIPT_AUTHOR_COMMENTS
  105. I had the need to run a program, with specified options, on a number of
  106. different files, using a SMP machine. So I thought [I'd] keep running
  107. a specified number of processes and start a new one each time . . . one
  108. of these terminates.
  109. The "wait" instruction does not help, since it waits for a given process
  110. or *all* process started in background. So I wrote [this] bash script
  111. that can do the job, using the "trap" instruction.
  112. --Vernia Damiano

Note
trap ‘’ SIGNAL (two adjacent apostrophes) disables SIGNAL for the remainder of the script. trap SIGNAL restores the functioning of SIGNAL once more. This is useful to protect a critical portion of a script from an undesirable interrupt.