I’ve tried to make a function, which accepts a command whose result is piped into a progress dialog
, while preserving the exit status
return value and also being able to capture the command’s result into a var, from stdout
.
Due to vars not being properly evaluated in command substitutions
after their expansion, as in $("${command[@]}")
, so far, I couldn’t manage to realize the following functionality without eval
, though.
Many state, that eval
is ‘evil’ and completely avoidable. While this may be true for many cases, others say, that its usage is safe, as long as no ‘untrusted user input’ is involved and that there are other, non-obvious implicit evaluations performed by shells, which may be equally dangerous.
How can I alter the following function in a way, that avoids eval
and other evaluating techniques, that could pose a problem in its scope, or can the code be properly hardened and used like that?
The function’s eval
uated ‘input’, may come in the form of explicitly crafted command strings
, as expanded var
or as var name reference
to these command strings. In this case, that would comprise of git
and dialog
commands and a worktree
name from a var
inside the git
command. The command input could involve sth. like ls
,find
, grep
or cat
.
So there shouldn’t be any direct user input
involved, other than a dynamic ‘worktreeName’ var, which might either be extracted from a git worktree list
or from a previous input dialog
.
I presume, the dynamic worktree name could only be potentially problematic here in the latter case; would the var expansion with single-quoting via ${worktreeName@Q}
(which should happen before eval
) sufficient, or are there additional sanitizing measures required?
Also, is the double-quoting in the form eval "${diaCom[@]@E} >/dev/tty"
sufficient, does wrapping it inside a process substitution
{...}
and @E
escaping improve the security, or would there be other measures required, to harden the expression?
# Evaluates a command to process and a dialog command to display progress with, each either from a var name reference, a var or a string
#
# arg1: -q/-Q for quiet mode
# arg1/2: command to evaluate and process
# arg2/3: dialog command evaluate result with
getDialogCmdResultAndExitStatus() {
local quiet
# Set quiet mode, if 1st flag is '-q/-Q'
for arg; do
if [[ $arg =~ ^-[[:alnum:]] ]]; then
if [[ ${arg^^} != '-Q' ]]; then
echo -e "Invalild argument: $arg"
exit 1
fi
quiet='TRUE'
shift
fi
done
local -a com
local -n cRef
# Get command from string or var
if ! declare -p "$1" &>/dev/null; then
com="$1"
# From var name reference
else
cRef=$1
com="${cRef[@]}"
fi
local -a diaCom
local -n diaComRef
# Get dialog command from string or var
if ! declare -p "$2" &>/dev/null; then
diaCom="$2"
# From var name reference
else
diaComRef=$2
diaCom="${diaComRef[@]}"
fi
# Evaluate command to process and result with dialog command
# Quiet, only return exit status
if [[ $quiet ]]; then
{ eval "${com[@]@E} 2>&1" | eval "${diaCom[@]@E} >/dev/tty"; return ${PIPESTATUS[0]}; } 3>&1
# Output command result to stdout and return exit status
else
{ eval "${com[@]@E} 2>&1" | tee >(cat - >&3) | eval "${diaCom[@]@E} >/dev/tty"; return ${PIPESTATUS[0]}; } 3>&1
fi
}
# Prepare commands (utilizing functions from examples above)
declare worktreeName='release/r3.14'
declare command="gitRemoveWtCmd ${worktreeName@Q}"
declare dialogCmd="dialog_programbox 'ZbZ1Deleting local worktree...' 20 120"
# Store and limit IFS to newlines
_IFS="$IFS"; IFS=$'n'
# Capture command's stdout and stderr + real-time displaying with dialog;
# first line after output capturing: get pipe/exit status of processed command
cmdOutput=( $(getDialogCmdResultAndExitStatus command dialogCmd) )
exitStatus=$?
if [[ $exitStatus -ne 0 ]]; then
echo -e "Operation failed with exit code: $exitStatus"
fi
# Restore IFS to defaults
IFS="$_IFS"