Issue
imagine a command line like:
my-script -foo $(no-such-cmd)
the $(no-such-cmd)
is going to going to cause an error, but I can't find a way to let the script know there's an issue. set -e
doesn't seem to work. is there a way?
This is part of a problem I came across when trying to process arguments from a file. I was reading a file line by line and trying to detect errors.
here's an example. I've hardcoded in a couple of pretend lines from a file.
function err_handler
{
ehs=1
echo "bad thing happened"
}
function fxn
{
echo fxn "$@"
return 0
}
function doit
{
ehs=0
trap err_handler ERR
eval fxn "$@"
eval_status=$?
trap "" ERR
echo ehs $ehs ev $eval_status
}
line='-x good'
doit $line
line='-x $(no-such-cmd)'
doit $line
and the output:
> ./line.sh
fxn -x good
ehs 0 ev 0
./line.sh: line 17: no-such-cmd: command not found
fxn -x
ehs 0 ev 0
notice how the error message shows up but the trap didn't trigger nor did the exit status reflect it.
=============== answer to some of the questions asked below ===============
@Barmar - the expansion is happening before doit is called, but it's working because of the doit_init. the errors get caught. I can add this code to the loop in my real code.
@user1934428 - I'm want to catch any an all errors that come in from the switches I'm reading from a file. This $(no-such-cmd) was just an easy way to demonstrate a hard one to catch.
@charles duffy - I'm not trying to put code in a variable, I'm reading switches/arguments to a command from a file.
@Willian Pursell - I don't have control over input. I need to rely on the users to have quoted the args correctly.
Solution
So never call my-script -foo $(no-such-cmd)
. First do variable assignment, then call the command.
if ! arg=$(no-such-cmd); then
echo "no-such-cmd failed!"
exit 2
fi
my-script -foo "$arg"
Hacking your solution around eval
and doit
and trap err
with interprocess communication between $(..)
subshell and parent shell will be unmaintainable and unreadable. Anyway, you have to share your trap ERR
with subshells. And also, you have to do interprocess communicate from the subshell to parent shell - they are separate processes. In the example below a signal is used.
Additionally, below, I have split parsing the command arguments and executing the command in separate stages. Note that variable assignment is equal to the last command executed. In the case of args=($(no-such-cmd))
the last command executed is no-such-cmd
in which case the assignment fails with 127 exit code. However, args=($(no-such-cmd) $(true))
the last command executed is true
, in which case the first subshell sends SIGUSR1 to the parent shell to communicate about error.
#!/bin/bash
err_handler() {
# Just communicate to parent process that we failed.
kill -s SIGUSR1 "$parent"
}
fxn() {
echo fxn "$@"
}
doit_init() {
trap err_handler ERR
# just set variable err on SIGUSR1
trap err=1 SIGUSR1
# inherit ERR
set -E
}
doit() {
echo "+ fxn $*"
err=0
local args=()
parent=$$
eval "args=($*)"
ret=$?
if ((ret)); then
echo "parsing args failed with $ret"
return 1
fi
if ((err)); then
echo "any command substitution while expanding arguments failed"
return 1
fi
fxn "${args[@]}"
ret=$?
if ((ret)); then
echo "Calling fxn failed $ret"
return 1
fi
}
doit_init
line='-x good'
doit "$line"
# shellcheck disable=SC2016
line='-x $(no-such-cmd)'
doit "$line"
# shellcheck disable=SC2016
line='-x $(no-such-cmd) $(true)'
doit "$line"
Execution of the script results in:
$ ./script.sh
+ fxn -x good
fxn -x good
+ fxn -x $(no-such-cmd)
./script.sh: line 30: no-such-cmd: command not found
parsing args failed with 127
+ fxn -x $(no-such-cmd) $(true)
./script.sh: line 30: no-such-cmd: command not found
any command substitution while expanding arguments failed
Check your scripts with shellcheck. Do not store command arguments in a string - prefer bash arrays. Prefer quoting command substitution.
Answered By - KamilCuk Answer Checked By - Gilberto Lyons (WPSolving Admin)