详细的错误处理在Bash

在Bash中的详细错误处理

作者:Willem Bogaerts, Kratz Business Solutions的应用程序史密斯

概要

Shell脚本通常作为后台进程运行,无需运行在可见的外壳中即可实现有用的功能。 例如,请考虑从Web服务器上的程序触发的cron作业或脚本。 编写这样的脚本可能相当痛苦,因为所有错误也出现在视线之外。 当然,您可以使用日志文件,但是很难找到理想的日志记录级别。 当脚本运行正常,并且意外失败时,方法太少,您通常会记录方式太多。 虽然日志文件可以容纳大量信息,但找到相关信息有点棘手。

我的解决方案是将所有细节的错误记录到一个小数据库。 此数据库包含消息的表,相应的跟踪和重要的环境变量。 我已经在这个howto中选择了一个SQLite数据库,但同样的原则也适用于其他数据库。

数据库

SQLite需要一些设置才能正常工作,这些设置可以放在初始化脚本中。 这些设置包括SQLite本身及其外键处理的错误行为:

.bail ON
.echo OFF
PRAGMA foreign_keys = TRUE;

当然,我们还需要一个数据库,我不想依赖一个数据库。 因此,bash脚本的第一件事是在数据库文件上运行一个SQL“revive”脚本:如果数据库不存在,它将被创建,如果没有,它将不执行任何操作:

CREATE TABLE IF NOT EXISTS ErrorLog
      (intErrorLogId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
       strMessage TEXT NOT NULL,
       tstOccurredAt DATE NOT NULL DEFAULT(CURRENT_TIMESTAMP) );
CREATE INDEX IF NOT EXISTS idxELOccurredAt ON ErrorLog(tstOccurredAt);

CREATE TABLE IF NOT EXISTS ErrorStackTrace
      (intErrorStackTraceId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
       intErrorLogId INTEGER NOT NULL,
       strSourceFile TEXT NOT NULL,
       strFunction TEXT NOT NULL,
       intLine INTEGER NOT NULL,
       FOREIGN KEY(intErrorLogId) REFERENCES ErrorLog(intErrorLogId)
               ON DELETE CASCADE
               ON UPDATE CASCADE );
CREATE INDEX IF NOT EXISTS idxESTTraceErrorLogId ON ErrorStackTrace(intErrorLogId);

CREATE TABLE IF NOT EXISTS ErrorEnvironment
      (intErrorEnvironmentId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
       intErrorLogId INTEGER NOT NULL,
       strVariable TEXT NOT NULL,
       strValue TEXT NULL,
       FOREIGN KEY(intErrorLogId) REFERENCES ErrorLog(intErrorLogId)
               ON DELETE CASCADE
               ON UPDATE CASCADE );
CREATE INDEX IF NOT EXISTS idxEEErrorLogId ON ErrorEnvironment(intErrorLogId);

机制

现在我们已经定义了数据库,我们可以专注于错误处理程序本身。 Bash有一个陷阱命令,除其他外,可以用来捕获非零退出代码的命令。 这些非零退出代码通常表示错误。 在脚本的“被困”部分中,您仍然可以对非零退出代码做出反应,而不使用错误处理程序通过使用“或构造”来接管:

false || echo "non-zero exit"

上述行将不会调用错误处理程序,但是以下行中的任何一行(命令false将始终以非零退出代码退出,命令true将退出,退出代码为零):

false
false;true

如果我们定义一个记录所有错误数据的函数,我们可以将该函数传递给trap命令。 从该功能,我们可以通过调用caller命令来访问调用。 caller命令将返回一个包含行号,子程序名和文件名的数组。

我将从我感兴趣的变量的预定义列表中读取环境,因为我的(Xubuntu)系统有这么多预定义的变量,记录它们都会将我淹没在无用的信息中。 我也对写入STDERR的消息感兴趣,所以我将定义一个可以用来发送STDERR流的文件位置。 在记录错误后,我仍然会将该文件的内容写入STDERR,因此用户不会因为交互式运行的脚本而处于黑暗中。 现在我有我要登录的所有信息。

警告

记录的命令trap命令级别的命令 。 这意味着如果您在主脚本中放置一个陷阱语句并调用函数,该函数将完成,只有在完成之后,如果函数以错误状态结束,则可以调用错误处理程序。 如果您想在函数内部进行错误处理,也可以在其中放置陷阱命令。

调用错误处理程序

要使我的错误处理脚本可用作一种库,我将使用source命令从要监视的脚本中调用它。 这将有效地包含在我想要监视的脚本中。 调用脚本需要在包含错误处理脚本之前定义一些设置:

ERROR_CLEANUP_ACTION
可选的。 错误处理程序运行后执行的命令,例如清理临时文件。
ERROR_ENVIRONMENT_VARIABLES
要记录的变量列表。 该列表将使用EXITCODE BASH_COMMAND BASH_LINENO BASH_ARGV进行扩充
ERRORDB
数据库的位置。 数据库文件不需要存在,但该位置确实需要由运行该脚本的用户写入。
ERROROUTPUT
一个文件的位置来捕获STDERR的内容。 可选的。 默认为 “/var/tmp/$$.err”
SQLITE3_EXECUTABLE
SQLite3命令行客户机的位置。 可选的。 默认为 “which sqlite3”

因为转义INSERT查询的所有可能的错误消息可能是一个很大的挑战,我定义了两个辅助函数Error_HexitError_Hexit_File 。 这允许我将所有异国字符串“转义”为十六进制字符串。 SQLite将所有十六进制字符串解释为二进制对象,因此我将其转换为INSERT语句中的 TEXT。

完整的脚本

把它放在一起,我有这个脚本,我可以包括:

#!/bin/bash
# Needed variable: ERRORDB
# Optional variables: ERROROUTPUT, SQLITE3_EXECUTABLE, ERROR_CLEANUP_ACTION, ERROR_ENVIRONMENT_VARIABLES

if [ -z $ERROROUTPUT ];then
   ERROROUTPUT=/var/tmp/$$.err
fi
if [ -e $SQLITE3_EXECUTABLE ] ;then
   SQLITE3_EXECUTABLE=`which sqlite3`
fi
# The settings file for SQLite3:
ERROR_INIT_SQLITE=/var/tmp/$$_init.sql
# For convenience, a function to create the settings for SQLite3:
function Error_Create_Init_File
        {
         cat > $ERROR_INIT_SQLITE <<'ERROR_SQLITE_SETTINGS'
.bail ON
.echo OFF
PRAGMA foreign_keys = TRUE;
ERROR_SQLITE_SETTINGS
        }
# Create the database:
if [ -z $ERRORDB ] ;then
   echo "Error database is not defined."
   exit 1
else
   Error_Create_Init_File
   $SQLITE3_EXECUTABLE -batch -init $ERROR_INIT_SQLITE $ERRORDB <<'ERROR_TABLE_DEFINITION'
CREATE TABLE IF NOT EXISTS ErrorLog
      (intErrorLogId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
       strMessage TEXT NOT NULL,
       tstOccurredAt DATE NOT NULL DEFAULT(CURRENT_TIMESTAMP) );
CREATE INDEX IF NOT EXISTS idxELOccurredAt ON ErrorLog(tstOccurredAt);

CREATE TABLE IF NOT EXISTS ErrorStackTrace
      (intErrorStackTraceId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
       intErrorLogId INTEGER NOT NULL,
       strSourceFile TEXT NOT NULL,
       strFunction TEXT NOT NULL,
       intLine INTEGER NOT NULL,
       FOREIGN KEY(intErrorLogId) REFERENCES ErrorLog(intErrorLogId)
               ON DELETE CASCADE
               ON UPDATE CASCADE );
CREATE INDEX IF NOT EXISTS idxESTTraceErrorLogId ON ErrorStackTrace(intErrorLogId);

CREATE TABLE IF NOT EXISTS ErrorEnvironment
      (intErrorEnvironmentId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
       intErrorLogId INTEGER NOT NULL,
       strVariable TEXT NOT NULL,
       strValue TEXT NULL,
       FOREIGN KEY(intErrorLogId) REFERENCES ErrorLog(intErrorLogId)
               ON DELETE CASCADE
               ON UPDATE CASCADE );
CREATE INDEX IF NOT EXISTS idxEEErrorLogId ON ErrorEnvironment(intErrorLogId);
ERROR_TABLE_DEFINITION
   rm -f $ERROR_INIT_SQLITE
fi
# Helper functions to "escape" strings tohexadecimal strings
function Error_Hexit # StringToHex
        {
         echo -n "$1" | hexdump -v -e '1 1 "%02X"'
        }

function Error_Hexit_File # FileToHex
        {
         if [ -e $1 -a -s $1 ] ;then
            hexdump -v -e '1 1 "%02X"' < $1
         else
            Error_Hexit '(No message)'
         fi
        }
# The error handling function:
# Enable with: trap Error_Handler ERR
# Disable with: trap '' ERR
function Error_Handler
        {
         local EXITCODE=$?
         trap '' ERR # switch off error handling to prevent wild recursion.
         local ARRAY=( `caller 0` )
         Error_Create_Init_File
         # Write the error message (from STDERR) and read the generated autonumber:
         local INSERT_ID=`$SQLITE3_EXECUTABLE -batch -init $ERROR_INIT_SQLITE $ERRORDB "INSERT INTO ErrorLog(strMessage) VALUES(CAST(x'$(Error_Hexit_File $ERROROUTPUT)' AS TEXT));SELECT last_insert_rowid();"`
         # Write the stack trace:
         local STACKLEVEL=0
         local STACK_ENTRY=`caller $STACKLEVEL`
         until [ -z "$STACK_ENTRY" ];do
               local STACK_ARRAY=( $STACK_ENTRY )
               $SQLITE3_EXECUTABLE -batch -init $ERROR_INIT_SQLITE $ERRORDB "INSERT INTO ErrorStackTrace(intErrorLogId,strSourceFile,strFunction,intLine) VALUES($INSERT_ID, CAST(x'$(Error_Hexit ${STACK_ARRAY[2]})' AS TEXT), '${STACK_ARRAY[1]}', ${STACK_ARRAY[0]})"
               let STACKLEVEL+=1
               STACK_ENTRY=`caller $STACKLEVEL`
         done
         # Write the error environment:
         for VAR in EXITCODE BASH_COMMAND BASH_LINENO BASH_ARGV $ERROR_ENVIRONMENT_VARIABLES ;do
             local CONTENT=$(Error_Hexit "${!VAR}")
             $SQLITE3_EXECUTABLE -batch -init $ERROR_INIT_SQLITE $ERRORDB "INSERT INTO ErrorEnvironment(intErrorLogId,strVariable,strValue) VALUES($INSERT_ID, '$VAR', CAST(x'$CONTENT' AS TEXT));"
         done
         # Clean up and provide feedback:
         if [ -e $ERROROUTPUT ] ;then
            cat $ERROROUTPUT 1>&2
         fi
         rm -f $ERROR_INIT_SQLITE
         rm -f $ERROROUTPUT
         if [ -n "$ERROR_CLEANUP_ACTION" ] ;then
            $ERROR_CLEANUP_ACTION
         fi
         exit $EXITCODE
        }

用法示例

#!/bin/bash

# Find out where I am to load the library from the same directory:
HERE=`dirname $0`

# Error handling settings:
ERRORDB=~/test.sqlite
ERROR_ENVIRONMENT_VARIABLES='USER TERM PATH HOSTNAME LANG DISPLAY NOTEXIST'
ERROR_CLEANUP_ACTION="echo I'm cleaning up!"

source $HERE/liberrorhandler.bash
trap Error_Handler ERR
# The above trap statement will do nothing in this example,
# unless you comment out the other trap statement.

function InnerFunction
        {
         trap Error_Handler ERR
         # The above trap statement will cause the error handler to be called.
         cat $1 2> $ERROROUTPUT
         # This will fail, because the file passed in $1 does not exist.
        }

function OuterFunction
        {
         InnerFunction /doesnot.exist
        }

OuterFunction 2>$ERROROUTPUT

参考文献

高级Bash脚本指南
http://www.tldp.org/LDP/abs/html/index.html
陷阱
http://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html#index-trap
呼叫者
http://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-caller
SQLite3 SQL语法
http://sqlite.org/lang.html
SQLite3 Pragma命令
http://sqlite.org/pragma.html
SQLite3命令行客户端
http://www.linuxcommand.org/man_pages/sqlite31.html
SQLite Manager(Firefox插件)
http://sqlite-manager.googlecode.com/
赞(52) 打赏
未经允许不得转载:优客志 » 系统运维
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏