redirecting stderr to err function - bash

I want to trap errors with my err function, which writes to the stderr. But in this example I am getting two error lines. How can I remove the first error msg not generated by my err function? I want to prevent something like
_get_error_msg 2> /dev/null
or
exec 2> /dev/null
err() {
echo -e "[$(date +'%H:%M:%S')] \e[31mERROR\e[0m $*" >&1
}
test.sh
#!/bin/bash
set -Eeo pipefail
err() {
echo -e "[$(date +'%H:%M:%S')] \e[31mERROR\e[0m $*" >&2
}
trap 'err line "${LINENO}": "${BASH_COMMAND}"' ERR
_get_error_msg
I am getting this output:
./test.sh: line 11: _get_error_msg: command not found
[04:18:36] ERROR line 11: _get_error_msg
How can I remove
./test.sh: line 11: _get_error_msg: command not found
Or pipe this line also to my err() function to get something like this
[04:18:36] ERROR line 11: _get_error_msg: command not found
just one line written to the stderr?
EDIT:
Something like this:
#!/bin/bash
set -Eeo pipefail
err() {
echo -e "[$(date +'%H:%M:%S')] \e[31mERROR\e[0m $*"
}
err_msg() {
while IFS= read -r msg; do
err "${msg}"
done
}
exec 2> >(err_msg)
trap 'err line "${LINENO}": "${BASH_COMMAND}"' ERR
_get_error_msg

Related

How to keep return value while processing a command's output?

In Bash environment, I have a command, and I want to detect if it fails.
However it is not failing gracefully:
# ./program
do stuff1
do stuff2
error!
do stuff3
# echo $?
0
When it runs without errors (successful run), it returns with 0. When it runs into an error, it can either
return with 1, easily detectable
return with 0, but during run it prints some error messages
I want to use this program in a script with these goals:
I need the output to be printing to stdout normally (not at once after it finished!)
I need to catch the output's return value by $? or similar
I need to grep for "error" string in the output and set a variable in case of presence
Then I can evaluate by checking the return value and the "error" output.
However, if I add tee, it will ruin the return value.
I have tried $PIPESTATUS[0] and $PIPESTATUS[1], but it doesn't seem to work:
program | tee >(grep -i error)
Even if there is no error, $PIPESTATUS[1] always returns 0 (true), because the tee command was successful.
So what is the way to do this in bash?
#!/usr/bin/env bash
case $BASH_VERSION in
''|[0-3].*|4.[012].*) echo "ERROR: bash 4.3+ required" >2; exit 1;;
esac
exec {stdout_fd}>&1
if "$#" | tee "/dev/fd/$stdout_fd" | grep -i error >/dev/null; then
echo "Errors occurred (detected on stdout)" >&2
elif (( ${PIPESTATUS[0]} )); then
echo "Errors detected (via exit status)" >&2
else
echo "No errors occurred" >&2
fi
Tested as follows:
$ myfunc() { echo "This is an ERROR"; return 0; }; export -f myfunc
$ ./test-err myfunc
This is an ERROR
Errors occurred (detected on stdout)
$ myfunc() { echo "Everything is not so fine"; return 1; }; export -f myfunc
$ ./test-err myfunc
Everything is not so fine
Errors detected (via exit status)
$ myfunc() { echo "Everything is fine"; }; export -f myfunc
$ ./test-err myfunc
Everything is fine
No errors occurred

Bash script: Redirect error of command to function that receives an argument

how can I achieve to redirect the error to a function that receives a string as an argument?
This is the code:
function error {
echo "[ERROR]: $1"
}
# This does works:
terraform apply myplan || { echo -e '\n[ERROR]: Terraform apply failed. Fix errors and run the script again!' ; exit 1; }
# Output: [ERROR]: Terraform apply failed. Fix errors and run the script again!
# This does NOT work:
terraform apply myplan || { error 'Terraform apply failed. Fix errors and run the script again!' ; exit 1; }
# Output: [ERROR]
I do not understand why.
Example:
#!/bin/bash
# simulate terraform commands
function terraform_ok {
echo "this is on stdout from terraform_ok"
exit 0
}
function terraform_warning {
echo "this is on stdout from terraform_warning"
echo "this is on stderr from terraform_warning" >&2
exit 0
}
function terraform_error {
echo "this is on stdout from terraform_error"
echo "this is on stderr from terraform_error" >&2
echo "this is line two on stderr" >&2
exit 1
}
function catch_error {
rv=$?
if [[ $rv != 0 ]]; then
echo -e "[ERROR] >>>\n$#\n[ERROR] <<<"
elif [[ "$#" != "" ]]; then
echo -e "[WARNING] >>>\n$#\n[WARNING] <<<"
fi
# exit subshell with the same exit code the terraform command had
exit $rv
}
function swap_stdout_and_stderr {
"$#" 3>&2 2>&1 1>&3
}
function perform {
(catch_error "$(swap_stdout_and_stderr "$#")") 2>&1
}
function die {
rv=$?
echo "\"$#\" failed with exit code $rv."
exit $rv
}
function perform_or_die {
perform "$#" || die "$#"
}
perform_or_die terraform_ok apply myplan
perform_or_die terraform_warning apply myplan
perform_or_die terraform_error apply myplan
echo "this will never be reached"
Output (all on stdout):
this is on stdout from terraform_ok
this is on stdout from terraform_warning
[WARNING] >>>
this is on stderr from terraform_warning
[WARNING] <<<
this is on stdout from terraform_error
[ERROR] >>>
this is on stderr from terraform_error
this is line two on stderr
[ERROR] <<<
"terraform_error apply myplan" failed with exit code 1.
Explanation:
The swapping of stdout and stderr (3>&2 2>&1 1>&3) is done because when you do variable=$(command) the variable will get assigned whatever comes on stdout from command. The same applies in catch_error "$(command)". Whatever comes on stdout from command will be assigned to $# in the function catch_error. In your case you I assume you want to catch what comes on stderr instead, hence the swapping.
The final 2>&1 on the line is done to redirect stderr (which is the old stdout) back to stdout so that the expected behavior of greping in the output from this script can be done as usual.
Since the catch_error ... command is running in a subshell I've used || to execute another command in case the subshell returns an error. That command is die "$#" to exit the whole script with the same error code that the command exited with and to be able to show the command that failed.
The simplest way I can think of; this will save all output to a file:
terraform apply --auto-approve -no-color -input=false \
2>&1 | tee /tmp/tf-apply.out
I believe the expression &> would save only errors to the file.

Bash: how to trap set -e, but not exit

My scripts have as first instruction:
set -e
So that whenever an error occurs, the script aborts. I would like to trap this situation to show an information message, but I do not want to show that message whenever the script exits; ONLY when set -e triggers the abortion. Is it possible to trap this situation?
This:
set -e
function mytrap {
echo "Abnormal termination!"
}
trap mytrap EXIT
error
echo "Normal termination"
Is called in any exit (whether an error happens or not), which is not what I want.
Instead of using trap on EXIT, use it on ERR event:
trap mytrap ERR
Full Code:
set -e
function mytrap {
echo "Abnormal termination!"
}
trap mytrap ERR
(($#)) && error
echo "Normal termination"
Now run it for error generation:
bash sete.sh 123
sete.sh: line 9: error: command not found
Abnormal termination!
And here is normal exit:
bash sete.sh
Normal termination

Pass bash syntax (pipe operator) correctly to function

How is it possible that operator >> and stream redirection operator are passed to the function try() which catches errors and exits...
When I do this :
exitFunc() { echo "EXIIIIIIIIIIIIIIIIT" }
yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exitFunc 111; }
try() { "$#" || die "cannot $*"; }
try commandWhichFails >> "logFile.log" 2>&1
When I run the above, also the exitFunction echo is output into the logFile...
How do I need to change the above that the try command does basically this
try ( what ever comes here >> "logFile.log" 2>&1 )
Can this be achieved with subshells?
If you want to use stderr in yell and not have it lost by your redirection in the body of the script, then you need to preserve it at the start of the script. For example in file descriptor 5:
#!/bin/bash
exec 5>&2
yell() { echo "$0: $*" >&5; }
...
If your bash supports it you can ask it to allocate the new file descriptor for you using a new syntax:
#!/bin/bash
exec {newfd}>&2
yell() { echo "$0: $*" >&$newfd; }
...
If you need to you can close the new fd with exec {newfd}>&-.
If I understand you correctly, you can't achieve it with subshells.
If you want the output of commandWhichFails to be sent to logFile.log, but not the errors from try() etc., the problem with your code is that redirections are resolved before command execution, in order of appearance.
Where you've put
try false >> "logFile.log" 2>&1
(using false as a command which fails), the redirections apply to the output of try, not to its arguments (at this point, there is no way to know that try executes its arguments as a command).
There may be a better way to do this, but my instinct is to add a catch function, thus:
last_command=
exitFunc() { echo "EXIIIIIIIIIIIIIIIIT"; } #added ; here
yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exitFunc 111; }
try() { last_command="$#"; "$#"; }
catch() { [ $? -eq 0 ] || die "cannot $last_command"; }
try false >> "logFile.log" 2>&1
catch
Depending on portability requirements, you can always replace last_command with a function like last_command() { history | tail -2 | sed -n '1s/^ *[0-9] *//p' ;} (bash), which requires set -o history and removes the necessity of the try() function. You can replace the -2 with -"$1" to get the N th previous command.
For a more complete discussion, see BASH: echoing the last command run . I'd also recommend looking at trap for general error handling.

Mysterious LINENO in bash trap ERR

I was just playing with bash to bypass this summer afternoon heat, when suddenly I've got a mysterious result for which I cannot determine it's origin.
Let me explain it bit a bit.
I'm playing with trap ERR to create some debugging functions for my bash scripts.
This is the script that runs fine:
traperror () {
local err=$? # error status
local line=$1 # LINENO
[ "$2" != "" ] && local funcstack=$2 # funcname
[ "$3" != "" ] && local linecallfunc=$3 # line where func was called
echo "<---"
echo "ERROR: line $line - command exited with status: $err"
if [ "$funcstack" != "" ]; then
echo -n " ... Error at function ${funcstack[0]}() "
if [ "$linecallfunc" != "" ]; then
echo -n "called at line $3"
fi
echo
fi
echo "--->"
}
#trap 'traperror $LINENO ${FUNCNAME}' ERR
somefunction () {
trap 'traperror $LINENO ${FUNCNAME} $BASH_LINENO' ERR
asdfas
}
somefunction
echo foo
The output is (stderr goes to /dev/null for clarity; the bash error is of course foo.sh: line 23: asdfas: command not found which is as you know error code 127)
~$ bash foo.sh 2> /dev/null
<---
ERROR: line 21 - command exited with status: 127
... Error at function somefunction() called at line 24
--->
foo
All the line numbers are right, line 21 is where starts the function "somefunction" and line 24 is where it is called.
However if I uncomment the first trap (the one in main) I get this output:
~$ bash foo.sh 2> /dev/null
<---
ERROR: line 21 - command exited with status: 127
... Error at function somefunction() called at line 24
--->
<---
ERROR: line 15 - command exited with status: 127
--->
foo
In case I uncomment the first trap and comment the second one I get that the error is in line 23 which is right too because it is the absolute line where the wrong command is placed.
~$ bash foo.sh
<---
ERROR: line 23 - command exited with status: 127
--->
foo
So my question is: why line 15? where does that line number come from? Line 15 is the last line in the trap function. Can anyone explain in plain English why trap returns the last line of the function it calls as the line that produced the error in line 21?
Thanks in advance!
EDIT
Just in case someone is interested in the debug function. This is the production version:
# Copyright (c): Hilario J. Montoliu <hmontoliu#gmail.com>
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.
set -o errtrace
trap 'traperror $? $LINENO $BASH_LINENO "$BASH_COMMAND" $(printf "::%s" ${FUNCNAME[#]})' ERR
traperror () {
local err=$1 # error status
local line=$2 # LINENO
local linecallfunc=$3
local command="$4"
local funcstack="$5"
echo "<---"
echo "ERROR: line $line - command '$command' exited with status: $err"
if [ "$funcstack" != "::" ]; then
echo -n " ... Error at ${funcstack} "
if [ "$linecallfunc" != "" ]; then
echo -n "called at line $linecallfunc"
fi
else
echo -n " ... internal debug info from function ${FUNCNAME} (line $linecallfunc)"
fi
echo
echo "--->"
}
somefunction () {
asdfasdf param1
}
somefunction
echo foo
Which will work as:
~$ bash foo.sh 2> /dev/null
<---
ERROR: line 26 - command 'asdfasdf param1' exited with status: 127
... Error at ::somefunction::main called at line 29
--->
<---
ERROR: line 22 - command 'asdfasdf param1' exited with status: 127
... internal debug info from function traperror (line 0)
--->
foo
Some relevant facts/background info:
Traps on ERR are not inherited by shell functions even though they get the rest of the environment, unless errtrace is set.
The exit status of a function is that of its last command.
My guess as to what is happening:
In the case where both traps are active,
The nonexistent command triggers the ERR trap in the function. LINENO is that of the nonexistent command.
The trap finishes executing. Since the nonexistent command was the last command, the return status of the function is nonzero, so the ERR trap in the shell is triggered. LINENO is still set to the last line of traperror since it was the last line to execute and is still the current line, as no new line has been executed yet.
In the case where only the shell trap is active (the one in the function is commented out)
The nonexistent command is the last command in the function, so causes the function to return non-zero, thus causing the shell's ERR trap to trigger. For the same reason above, LINENO is the last line of the function as it was the last line to execute and is still the current line.
To make sure in your first version of your traperror function that the ERR signal handler will not be executed twice, you can ignore or reset the ERR signal handler to its default action for the rest of your program - within the definition of the ERR signal handler itself. And this should always be done for a custom EXIT signal handler as well.
trap "" EXIT ERR # ignore
trap - EXIT ERR # reset
# for the first version of your traperror function
- trap 'traperror $LINENO ${FUNCNAME}' ERR
- trap 'traperror $LINENO ${FUNCNAME} $BASH_LINENO' ERR
+ trap 'traperror $LINENO ${FUNCNAME}; trap - ERR' ERR
+ trap 'traperror $LINENO ${FUNCNAME} $BASH_LINENO; trap - ERR' ERR

Resources