1

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.

0

2 Answers 2

4

I'd do:

cmd1_output=$(cmd1) && cmd2_output=$(cmd2) &&
  printf '%s\n' "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 '%s\n' "output of cmd1: $(cmd1) and of cmd2 (not run if cmd1 failed) $([ "$?" -eq 0 ] && cmd2)"
1
  • Shell polluting is not an issue in my case as the shell-lifetime is very short and non-interactive. But it does make for a better more complete answer, thanks
    – user5211
    Commented Feb 4 at 16:36
0

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.

8
  • 1
    Except some entries in /dev/* are subdirectories and ls (without -d) lists their contents but echo doesn't. Although Q said 'a listing of /dev' -- apparently meaning one listing -- suggesting this wasn't actually desired. Commented Feb 4 at 1:33
  • @dave_thompson_085, oh yes, that's true!
    – ilkkachu
    Commented Feb 4 at 8:31
  • I checked first before posting $() is called "inline execution" on other questions on Stack and here Bash shell basics — inline execution from march 2023'
    – user5211
    Commented Feb 4 at 15:58
  • @user5211, it's called command substitution in e.g. the POSIX description of the shell language and in Bash's reference manual. There's also a tag with that name on unix.SE (plus further links). It's clear from the context what you mean, just that, well, I've never heard of that other name before, and "command substitution" appears to be the standard phrase.
    – ilkkachu
    Commented Feb 4 at 16:02
  • _Note that $(ls /dev/*) is a bit silly _ of course it is, I made a simple example anyone can understand/run. since most Unix systems have a /dev and most don't have a /devb (thus a failure) To get an answer they don't need to know my servers IP, or that I'm using wget/ftp/telnet/freerdp/ssh/GOPHER instead of the made up Connect_to_foreign_system program, as it doesn't matter to the question.
    – user5211
    Commented Feb 4 at 16:04

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .