如何在Bash中修改调用堆栈?

时间:2014-12-02 21:34:42

标签: bash

假设我想编写一个智能日志记录函数log,它将读取log调用之后的行,并将其及其输出存储在日志文件中。该函数可以查找,读取和执行有问题的代码行。问题是,当函数返回时,bash再次执行该行。

除了BASH_LINENO[0]的赋值被静默丢弃外,一切正常。阅读http://wiki.bash-hackers.org/syntax/shellvars#bash_lineno我已经知道变量不是只读的。

function log()
{
        BASH_LINENO[0]=$((${BASH_LINENO[0]}+1))

        file=${BASH_SOURCE[1]##*/}
        linenr=$((${BASH_LINENO[0]} + 1 ))
        line=`sed "1,$((${linenr}-1)) d;${linenr} s/^ *//; q" $file`
        if [ -f /tmp/tmp.txt ]; then
            rm /tmp/tmp.txt
        fi
        exec 3>&1 4>&2 >>/tmp/tmp.txt 2>&1 
        set -x
        eval $line 
        exitstatus=$?
        set +x
        exec 1>&3 2>&4 4>&- 3>&-
        #Here goes the code that parses the /tmp/tmp.txt and stores it in the log
        if [ "$exitstatus" -ne "0" ]; then
            exit $exitstatus
        fi
}

#Test case:
log
echo "Unfortunately this line gets appended twice" | tee -a bla.txt;

1 个答案:

答案 0 :(得分:2)

在bug-bash@gnu.org邮件列表中查阅用户的智慧之后,似乎无法修改调用堆栈。以下是我从Chet Ramey那里得到的答案:

  

BASH_LINENO是一个调用堆栈;对它的任务应该是(并且是)   忽略。自从至少bash-3.2就是这种情况(那就是我   退出了。)

     

有一种间接的方法可以强制bash不执行下一个   command:设置extdebug选项并让DEBUG陷阱返回a   非零状态。

上述技术对我的目的非常有效。我终于能够生成log函数的生产版本。

#!/bin/bash
shopt -s extdebug
repetition_count=0

_ERR_HDR_FMT="%.8s %s@%s:%s:%s"
_ERR_MSG_FMT="[${_ERR_HDR_FMT}]%s \$ "

msg() {
    printf "$_ERR_MSG_FMT" $(date +%T) $USER $HOSTNAME $PWD/${BASH_SOURCE[2]##*/} ${BASH_LINENO[1]}
    echo ${@}
}

function rlog()
{
    case $- in *x*) USE_X="-x";; *) USE_X=;; esac
    set +x
    if [ "${BASH_LINENO[0]}" -ne "$myline" ]; then
        repetition_count=0
        return 0; 
    fi
    if [ "$repetition_count" -gt "0" ]; then
        return -1; 
    fi
    if [ -z "$log" ]; then
        return 0
    fi
    file=${BASH_SOURCE[1]##*/}
    line=`sed "1,$((${myline}-1)) d;${myline} s/^ *//; q" $file`
    if [ -f /tmp/tmp.txt ]; then
        rm /tmp/tmp.txt
    fi
    echo "$line" > /tmp/tmp2.txt
    mymsg=`msg`
    exec 3>&1 4>&2 >>/tmp/tmp.txt 2>&1 
    set -x
    source /tmp/tmp2.txt
    exitstatus=$?
    set +x
    exec 1>&3 2>&4 4>&- 3>&-
    repetition_count=1 #This flag is to prevent multiple execution of the current line of code. This condition gets checked at the beginning of the function
    frstline=`sed '1q' /tmp/tmp.txt`
    [[ "$frstline" =~ ^(\++)[^+].*$ ]]
#   echo "BASH_REMATCH[1]=${BASH_REMATCH[1]}"
    eval 'tmp="${BASH_REMATCH[1]}"'
    pluscnt=$(( (${#tmp} + 1) *2 ))
    pluses="\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+"
    pluses=${pluses:0:$pluscnt}
    commandlines="`awk \" gsub(/^${pluses}\\s/,\\\"\\\")\" /tmp/tmp.txt`"
    n=0
    #There might me more then 1 command in the debugged line. The next loop appends each command to the log.
    while read -r line; do
        if [ "$n" -ne "0" ]; then
            echo "+ $line" >>$log
        else
            echo "${mymsg}$line" >>$log
            n=1
        fi
    done <<< "$commandlines"
    #Next line extracts all lines that are prefixed by sufficent number of "+" (usually 3), that are immidiately after the last line prefixed with $pluses, i.e. after the last command line.
    awk "BEGIN {flag=0} /${pluses}/ { flag=1 } /^[^+]/ { if (flag==1) print \$0; }" /tmp/tmp.txt | tee -a $log
    if [ "$exitstatus" -ne "0" ]; then
        echo "## Exit status: $exitstatus" >>$log
    fi
    echo >>$log
    if [ "$exitstatus" -ne "0" ]; then
        exit $exitstatus
    fi
    if [ -n "$USE_X" ]; then
        set -x
    fi
    return -1
}

log_next_line='eval if [ -n "$log" ]; then myline=$(($LINENO+1)); trap "rlog" DEBUG; fi;'
logoff='trap - DEBUG'

该文件的用法如下:

#!/bin/bash

log=mylog.log

if [ -f mylog.log ]; then
    rm mylog.log
fi

. ./log.sh
a=example
x=a

$log_next_line
echo "KUKU!"
$log_next_line
echo $x
$log_next_line
echo ${!x}
$log_next_line
echo ${!x} > /dev/null
$log_next_line
echo "Proba">/tmp/mtmp.txt
$log_next_line
touch ${!x}.txt
$log_next_line
if [ $(( ${#a} + 6 )) -gt 10 ]; then echo "Too long string"; fi
$log_next_line
echo "\$a and \$x">/dev/null
$log_next_line
echo $x
$log_next_line
ls -l
$log_next_line
mkdir /ddad/adad/dad #Generates an error

输出(`mylog.log):

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:14] $ echo 'KUKU!'
KUKU!

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:16] $ echo a
a

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:18] $ echo example
example

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:20] $ echo example

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:22] $ echo 1,2,3

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:24] $ touch example.txt

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:26] $ '[' 13 -gt 10 ']'
+ echo 'Too long string'
Too long string

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:28] $ echo '$a and $x'

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:30] $ echo a
a

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:32] $ ls -l
total 12
-rw-rw-r-- 1 adam adam   0 gru  4 13:39 example.txt
lrwxrwxrwx 1 adam adam  66 gru  4 13:29 log.sh -> /home/Adama-docs/Adam/Adam/MyDocs/praca/Puppet/bootstrap/common.sh
-rwxrwxr-x 1 adam adam 520 gru  4 13:29 log-test-case.sh
-rw-rw-r-- 1 adam adam 995 gru  4 13:39 mylog.log

[13:39:51 adam@adam-N56VZ:/home/Adama-docs/Adam/Adam/linux/tmp/log/log-test-case.sh:34] $ mkdir /ddad/adad/dad
mkdir: cannot create directory ‘/ddad/adad/dad’: No such file or directory
## Exit status: 1

标准输出不变。

限制

不幸的是,限制是严重的。

记录命令的退出代码被丢弃

首先,丢弃已记录命令的退出代码,因此用户无法在下一个语句中对其进行测试。如果出现错误(我相信这是最好的行为),当前代码将退出脚本。可以修改脚本以进行测试

对bash跟踪的有限支持

该功能支持使用-x进行bash跟踪。如果它发现用户跟踪输出,它会暂时禁用输出(因为它会干扰跟踪),并在结束时将其恢复。不幸的是,它还在追踪上添加了一些额外的线条。

除非用户关闭日志记录(使用$logoff),否则在第一个$log_next_line之后所有命令都会受到相当大的速度损失,即使没有进行日志记录。

在理想世界中,该函数应在每次调用后禁用调试捕获(trap - DEBUG)。不幸的是我不知道怎么做,所以从第一个$log_next_line宏开始,每行的解释调用一个自定义函数。

我在复杂的bootstrapping脚本中的每个键命令之前使用此函数。有了它,我可以看到究竟什么时候执行什么,什么是输出,而不需要真正理解漫长而有时凌乱的脚本的逻辑。