I want to write a script based on dialog programbox
(or others) and capture a command's stderr
and the stdout
passed to dialog
into an array (if any), as well as the command's exit status (as last array element). For this, I'm using prepared command strings, command substitution
and fd3 redirection
.
Basically, this works nicely, as long as there're no chained commands with if
involved - then I get error messages, like, in this case 'command not found'. Here's a (simplified) example:
# Prepare command strings
declare cmd0='if [[ -e . ]]; then echo TRUE; fi'
cmd1=($("$cmd0") 2>&1)
cmd2=(dialog --colors --no-collapse --programbox "\Zb\Z1List local folders..." 20 160)
# Redirect stdout and stderr to dialog, while executing cmd1 > cmd2, capturing cmd1 exit status
exec 3>&1
tmp=($(${cmd1[@]} 2>&1 | "${cmd2[@]}" 1>&3; echo ${PIPESTATUS[0]}))
exec 3>&-
# Print results
printf '%s\n' "${tmp[@]}"
The command declaration seems to require an array declaration, but then special chars like ';' will also add to the problem. This is what I tried; directly using c
instead of cmd1
will print the whole line as error.
What needs to be changed, to make the command substituion
for chained commands (including if
) in cmd1 work?
With the hints in the comments above I could come up with a solution, albeit not completely satisfactory, due to the need of defining functions
for each complex/nested command to parse/pass to dialog programbox
.
dialog_programbox
can accept and parse simple commands via argument, or, for complex commands, process input from stdin
instead:
There may be a way to extract piped input into the captured result var (here: exitStatus), e. g. per usage of tee
- if someone knows, how, please feel free to post a solution.
For other dialog
variants, which accept input from a var and which do not consume stdin
, the output from dialog_programbox
can be printed to stdout
as newline-separated string, with exit/pipe status as the last or first element, for easy splitting from a captured result var. I'll probably extend the solution accordingly, at a later point.
# Function example for complex commands to pipe to dialog_programbox()
complexCmdFunc1() {
# Requires explicit stderr redirection to stdout for each command
# Could also split cmd execution and store exit status in var for reporting
if ! git worktree remove -- "$1" 2>&1; then
echo -e "\n\nREMOVAL BLOCKED:" "Worktree may not exist or is unlcean/locked (try force) - ABORTING."
# Need to explicitly return exit status to report
return 1
fi
echo -e "Successfully removed worktree:\n$1"
return 0
}
dialog_programbox() {
local -i height
local -i width
local -n cmd
local cmdTmp
local title
for arg; do
if [[ $arg == +([[:digit:]]) ]]; then
if [[ $height -gt 0 ]]; then
width=$arg
continue
fi
height=$arg
elif [[ $title ]]; then
if [[ -R $arg ]]; then
cmd=$arg
# For direct CMD arg injection, has bug
elif ! declare -p "$arg" 2>&1 1>/dev/null; then
cmdTmp=$arg
cmd=cmdTmp
elif [[ $(declare -p "$arg" 2>/dev/null) == 'declare -a'* ]]; then
cmd=$arg
elif [[ -z $cmd ]]; then
cmd=$arg
fi
else
title="$arg"
fi
done
[[ -z $height ]] && height=30
[[ -z $width ]] && height=120
# CMD ARG INJECTION
if [[ $cmd ]]; then
$( echo "${cmd[@]}" ) 2>&1 | dialog --title "$title" --colors --no-collapse --programbox $height $width 1>&3
echo ${PIPESTATUS[0]}
# stdin command substitution - would work but swallow pipe/exit status
#dialog --title "$title" --colors --no-collapse --programbox $height $width 1>&3 < <($( echo ${cmd[@]} ) 2>&1)
# PIPING STDIN
else
dialog --title "$title" --colors --no-collapse --programbox $height $width </dev/stdin 1>&3
# Temp var would address empty stdin but waste real-time progress report
#local tmp="$(grep . </dev/stdin)"
# if [[ $tmp ]] ; then
# dialog --title "$title" --colors --no-collapse --programbox $height $width <<<"$tmp"
# else
# echo 'Nothing to process.' | dialog --title "$title" --colors --no-collapse --programbox $height $width
# fi
fi
}
# COMPLEX CMD output piping usage - requires explicit echo of PIPESTATUS
exitStatus1=$(complexCmdFunc1 'unreleased' | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 160; echo ${PIPESTATUS[0]})
# More efficient, since no additional subshell invocation is needed:
complexCmdFunc1 'unreleased' | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 160
exitStatus2=${PIPESTATUS[0]}
# Empty pipe, will render an empty dialog, of which addressing would require storing cmd result in temp var and losing real-time progress reporting
#exitStatus=$(echo | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 160; echo ${PIPESTATUS[0]})
# CMD var injection examples (for simple/non-nested commands)
#declare -a c=(ls foo bar)
declare c='ls foo bar'
exitStatus3=$(dialog_programbox '\Zb\Z1Deleting local worktree...' c 20 160)
# direct CMD injection - HAS BUG
#exitStatus=$(dialog_programbox '\Zb\Z1Deleting local worktree...' 'ls foo bar' 20 160)
echo -e "EXIT_STATUS1: $exitStatus1"
echo -e "EXIT_STATUS2: $exitStatus2"
echo -e "EXIT_STATUS3: $exitStatus3"
Another solution, based on this answer from Michał Górny (thanks), an example, on how to get the exit status AND the (displayed) output from a command defined in a function and processed by dialog programbox
, involving coproc
and global vars:
# Result array for storing processed command's output
declare -ga cmdOutput
declare -g exitStatus
worktreeName='unreleased'
# Command to process and get exit status + results from
gitRemoveWtCmd() {
_IFS="$IFS"; IFS=$'\n'
cmdOutput=( $(git worktree remove -- "$worktreeName" 2>&1) )
exitStatus=$?
IFS="$_IFS"
if [[ $exitStatus -ne 0 ]]; then
printf '%s\n' "${cmdOutput[@]}"
echo -e "\nCannot remove unclean or non-existent worktree"
else
echo -e "\nSuccessfully removed worktree: [$worktreeName]";
fi
}
# Co-process with dialog to send command results to, redirected output to fd3
coproc diaProgramboxCop {
dialog --colors --no-collapse --programbox '\Zb\Z1List local folders...' 20 160 1>&3
}
# Redirecting processed stderr to output and linking fd3 there
{ exec 3>&1
gitRemoveWtCmd 2>&1
exec 3>&-
} >&${diaProgramboxCop[1]}
exec {diaProgramboxCop[1]}>&-
wait ${diaProgramboxCop_PID}
printf '%s%s\n' "OUTPUT: " "${cmdOutput[@]}"
echo -e "EXIT_STATUS: $exitStatus"
A real drawback here is, that global vars are required for cmdOutput
and exitStatus
and the capturing of the command's output eradicates the real-time displaying of the command's progress, due to storing it via subshell into the var before actually sending it to the dialog
.
So, this approach would better be used with widgets
like msgbox
, which consume a pre-filled var
.
According to the suggestion here, using fd3
, redirected from tee + cat
in a pipe
, can be used for channelling all command output to the var assignment (stdout
and stderr
) and together with redirection
of the dialog
output directly to /dev/tty
, this is another way of capturing the processed command's output, while displaying it via dialog
in real-time, provided, the commands can be hard-coded and it can even return the no direct pipe/exit status:
worktreeName='unreleased'
_IFS="$IFS"; IFS=$'\n'
cmdOutput=( $({ gitRemoveWtCmd ${worktreeName@Q} 2>&1 | tee >(cat >&3) | dialog_programbox '\Zb\Z1Deleting local worktree...' 20 120 >/dev/tty;} 3>&1 ; echo -e ${PIPESTATUS[0]}) )
printf '%s\n%s\n' "CMD_OUTPUT:" "${cmdOutput[@]:0:((${#cmdOutput[@]} - 1))}"
echo -e "EXIT_STATUS: ${cmdOutput[@]: -1}"
IFS="$_IFS"
The PIPESTATUS
can still be printed from within the command substitution
, but needs to be split off the captured output. But it's even better, to just use ;exit ${PIPESTATUS[0]}
from within the same process substitution curly braces, do without array splitting and simply use the exit status
code from $?
immediately in the next line.
The final solution allows the application of varying commands to the dialog
, per use of eval
inside the function getDialogCmdResultAndExitStatus()
, which accepts a flag to silence any tty
or stdout
output and a var
or name reference
for the command to process and the dialog
command to use for displaying, while by default also passing all command output to stdout
(for capturing into a var) and always returning the processed command's internal pipe status
as exit status
, to enable follow-up decisions in scripts:
getDialogCmdResultAndExitStatus() {
local quiet
if [[ $1 =~ ^-[[:alnum:]] ]]; then
if [[ ${1^^} != '-Q' ]]; then
echo -e "Invalild argument: $1"
exit 1
fi
quiet='TRUE'
shift
fi
local -a com
local -n cRef
if ! declare -p "$1" &>/dev/null; then
com="$1"
else
cRef=$1
com="${cRef[@]}"
fi
local -a diaCom
local -n diaComRef
if ! declare -p "$2" &>/dev/null; then
diaCom="$2"
else
diaComRef=$2
diaCom="${diaComRef[@]}"
fi
if [[ $quiet ]]; then
{ eval "${com[@]@E} 2>&1" | eval "${diaCom[@]@E} >/dev/tty"; return ${PIPESTATUS[0]}; } 3>&1
else
{ eval "${com[@]@E} 2>&1" | tee >(cat - >&3) | eval "${diaCom[@]@E} >/dev/tty"; return ${PIPESTATUS[0]}; } 3>&1
fi
}
Used like this - using the processed command's exit status
(internally pipe status
) requires inline-usage together with if
or capturing $?
for further scripting decisions, right after the output capturing line:
# Prepare commands (utilizing functions from examples above)
declare command="gitRemoveWtCmd ${worktreeName@Q}"
declare dialogCmd="dialog_programbox '\Zb\Z1Deleting 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
cmdOutput=( $(getDialogCmdResultAndExitStatus command dialogCmd) )
# First line after output capturing: get pipe/exit status of processed command
echo -e "EXIT_STATUS: $?"
# Restore IFS to defaults
IFS="$_IFS"
or
if ! getDialogCmdResultAndExitStatus command dialogCmd; then
# do sth else...
fi