How do I change the call stack in Bash?

Suppose I want to write a smart logging function log

that will read the line that is immediately after being called log

and store it and its output in a log file. A function can find, read, and execute a line of code in question. The problem is, when the function returns, bash executes the string again.

Everything works fine, except that the assignment is BASH_LINENO[0]

silently discarded. Reading http://wiki.bash-hackers.org/syntax/shellvars#bash_lineno I found out that the variable is not readable only.

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;

      

+3


source to share


1 answer


After consulting the wisdom of users on the bug-bash @ gnu.org mailing list, it looks like changing the call stack is ultimately impossible. Here is the answer I got from Chet Rami:

BASH_LINENO

- call stack; assignments to it should be (and are) ignored. This has been the case since at least bash -3.2 (where I quit looking).

There is an indirect way to make bash not execute the following command: set the parameter extdebug

and return the DEBUG trap to non-zero status.

The above technique works very well for my purposes. Finally, I can make a production version of the function 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'

      

The use of the file is intended as follows:

#!/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

      

Output (`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

      

The standard output does not change.



Limitations

The limitations are severe, unfortunately.

Logged command exit code is discarded

First of all, the exit code of the registered command is discarded, so the user cannot validate it in the following statement. The current code exits the script if an error occurs (which is the best behavior in my opinion). You can modify the script to check

Limited support for bash tracing

The function performs bash trace using -x

. If it detects that the user is tracing the output, it temporarily disables the output (since it will interfere with the tracing anyway) and restores it at the end. Unfortunately, it also adds a few extra lines to the trail.

Unless the user disables logging (with $logoff

), there is a significant rate limit for all commands after the first $log_next_line

, even if no logging occurs.

In an ideal world, the function should disable debug capture ( trap - DEBUG

) after each call. Unfortunately I don't know how to do this, so starting from the first macro $log_next_line

, interpreting each line calls the user-defined function.

I use this feature before every key command in my complex bootstrap scripts. With it, I can see exactly what was done and when and what the result was, without having to really understand the logic of long and sometimes messy scripts.

+2


source







All Articles