How to make bash abort a command on an inline execution error

Command substitution, or Inline Execution as I call it, $() in Bash allows for text returns; before the execution of a main command. How do I abort the main command, if a $() returns an error code

More specifically I want to stop/prevent a secondary Inline Execution from being attempted.

Echo $(ls /devb/*) is better than $(ls /dev/*)

i.e. if "ls /devb/" returns a non-zero exit code, I don’t want to run a listing of /dev,

My real example gets to be rather wordy, and the specifics aren’t super important to the question, but a simplified version is

Connect_to_foreign_system 1.1.1.1 /u:$(zenity <prompt for user>) /p:$(zenity <prompt for password>)

If the prompt for user is canceled, zenity returns with a exit code of 1, but the main command continues, and then prompts for a password.

If the first prompt errors I really don’t want to prompt for the password, and it would be nice to not run Connect_to_foreign_system.

I’m not running this in a script, this is pure command-line, but even if I put it in a script with set -e both the second inline and main command, still runs anyway.

I know that I can write a script to get each text string and then run the command and test for exit-codes as I go. I’m asking how to do it as a standalone statement.

Asked By: user5211

||

That inline execution is called a command substitution. To check the exit status of one, you need to have it on a command that has no other commands, just an assignment to capture the output. E.g.:

if a=$(foo); then
    echo >&2 "first command failed, exiting"
    exit 1
fi
b=$(bar)
echo "output of first command: '$a', output of second: '$b'"

Note that $(ls /dev/*) is a bit silly in that it’s the shell that expands the glob /dev/* and all ls has to do is to print the filenames the shell gives it. (Unless you use -l or -F to get other information, too.) Though ls would list the contents of any subdirectories the glob matches. Anyway, something like that might warrant some reconsideration. If you just want the first-level contents of /dev, you just use ls /dev, or echo /dev/* instead.

Answered By: ilkkachu

I’d do:

cmd1_output=$(cmd1) && cmd2_output=$(cmd2) &&
  printf '%sn' "Both cmd1 and cmd2 succeeded and their output was respectively $cmd1_output and $cmd2_output"

So in your case:

user=$(zenity <prompt for user>) &&
  password=$(zenity <prompt for password>) &&
  Connect_to_foreign_system 1.1.1.1 /u:"$user" /p:"$password"

Note that while the $(...)s above don’t need to be quoted in bash as they are used in a scalar variable assignments, like for the $user and $password above they do need to be quoted when used in command arguments as they would be subject to split+glob if left unquoted.

Note that passing passwords on the command line is very bad practice in general as command line arguments are generally public within a system (show in the output of ps -Af for instance). With most commands, there’s usually a safer way to pass secrets to them, whether that’s via environment variables or though some file or file descriptor.

To avoid polluting the variable namespace with those $user and $password variables which are used only once, you can run the whole and-list in a subshell ((...)):

(user=$(zenity <prompt for user>) &&
  password=$(zenity <prompt for password>) &&
  Connect_to_foreign_system 1.1.1.1 /u:"$user" /p:"$password")

Or if switching from bash to zsh is an option, you can use the positional parameters of an anonymous function instead of variables:

() {
  1=$(zenity <prompt for user>) &&
    2=$(zenity <prompt for password>) &&
    Connect_to_foreign_system 1.1.1.1 /u:$1 /p:$2
}

(in zsh, split+glob is not performed implicitly upon unquoted parameter expansions, so quoting those $1/$2 is not necessary).


If it was only about not running cmd2 if cmd1 fails but still use the output of cmd1, in some shells including ksh93, zsh and bash, but not dash nor busybox sh nor yash nor mksh, you can also do:

printf '%sn' "output of cmd1: $(cmd1) and of cmd2 (not run if cmd1 failed) $([ "$?" -eq 0 ] && cmd2)"
Answered By: Stéphane Chazelas
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.