Capture the error stream of a line of code and report it with trap while keeping the stdout untouched

194 Views Asked by At

I have the following (simplified) code attempts in 2 shell scripts below.

The script calls an R script in which code is run that will generate standard output and error stream depending on what happens.

What I'm trying to achieve is to have both the output and error stream display on the console as usual but when the script is run and it fails (e.g. the RScript produces an error stream) I want to save that error message that R generates in a sqlite database but still have the normal output and errors display on the console as well when it exits. I have tried many forms of redirecting output of the Rscript function, but I either end up saving nothing to the database (apart from the line number) or I can save the error message but nothing will be put on the console...

This will not save error message to the DB (line number will), but everything will be on console

updateDBwhenError() {
    sqlite3 "myDB.db" "INSERT INTO logs VALUES('$1')"   
}

err_report() {
    #Tryign to capture both line error and message
    updateDBwhenError "Error Line $1 $2"
    exit
}
trap 'err_report ${LINENO}' ERR

Rscript testScript.R

This will save the error message to the DB, but nothing will be on console anymore

updateDBwhenError() {
    sqlite3 "myDB.db" "INSERT INTO logs VALUES('$1')"   
}

err_report() {
    #Tryign to capture both line error and message
    updateDBwhenError "Error Line $1 $rErr"
    exit
}
trap 'err_report ${LINENO}' ERR

rErr=$(Rscript testScript.R 2>&1)

I looked everywhere for a way to capture just the error stream to a variable and keep the output untouched on the console (both output and error), but I'm stuck.

2

There are 2 best solutions below

4
On

Try this

exec 5>&1
exec 6>&1
rErr=$(Rscript testScript.R 2>&1 1>&6 | tee /dev/fd/5)

Test script

$ cat test
#!/bin/bash
ls
ls sdfgds

Testing with this script

$ exec 5>&1
$ exec 6>&1
$ err=$(./test 2>&1 1>&6 | tee /dev/fd/5)
file  new_file  test  xml
ls: cannot access 'sdfgds': No such file or directory

$ echo "$err"
ls: cannot access 'sdfgds': No such file or directory
3
On

Shortly

Using unnamed fifos (WARNING, this will work because OS do buffering, so only while output stay under 64Kb!), there is a subtle way:

exec {HOLDERR}<> <(:)
ls -ld /t{mp,nt} 2>&${HOLDERR}
read -t 0 -u $HOLDERR && read -ru $HOLDERR errmsg
exec {HOLDERR}<&-

This must output something like:

drwxrwxrwt 4 root root 4096 Jan 01 1970 /tmp

and popilate $errmsg with something like: declare -p errmsg

declare -a errmsg=([0]="ls: cannot access '/tnt': No such file or directory")

One step further

exec {HOLDERR}<> <(:)
ls -ld /t{mp,nt} 2>&${HOLDERR}
errmsg=()
while read -t 0 -u $HOLDERR;do
    read -ru $HOLDERR line
    errmsg+=("$line")
done
exec {HOLDERR}<&-
printf "%s\n" "${errmsg[@]}"

Full usable version

With trap SIGCHLD for flushing fifo.

printmsg() {
    local out=()
    out=("${errmsg[@]}")
    errmsg=("${errmsg[@]:${#out[@]}}")
    [ "${#out[@]}" -gt 0 ] &&
    printf "Err: %s\n" "${out[@]}"
}
checkmsg() {
    local line
    while read -u $HOLDERR -t 0 ;do
    read -ru $HOLDERR line &&
        errmsg+=("$line")
    done
}
msg=()
trap checkmsg CHLD
exec {HOLDERR}<> <(:)

Then

ls -ld /t{mp,nt} 2>&${HOLDERR}

must output something like:

drwxrwxrwt 4 root root 4096 Jan 01 1970 /tmp

followed by:

printmsg

will show

Err: ls: cannot access '/tnt': No such file or directory