16

I'm trying to get the exit code of the function that I'm repeatedly calling in the "condition" part of a Bash while loop:

while <function>; do 
    <stuff> 
done 

When this loop terminates due to an error, I need the exit code of <function>. Any thoughts on how I can get that?

0

4 Answers 4

22

You can capture the exit value from the condition and propagate that forward:

while rmdir FOO; ss=$?; [[ $ss -eq 0 ]]
do
    echo in loop
done
echo "out of loop with ?=$? but ss=$ss"

Output

rmdir: failed to remove 'FOO': No such file or directory
out of loop with ?=0 but ss=1

In this instance the exit status from rmdir FOO has been captured in the variable ss and is 1. (Try replacing rmdir FOO with ( exit 4 ). You'll find that ss=4.)

How does this work? Remember that the syntax is actually while list-1; do list-2; done, and not the much more usual expectation of while command; do list; done. The list-1 can be a sequence of semicolon-separated commands, and the documentation states that the "while command continuously executes the list list-2 as long as the last command in the list list-1 returns an exit status of zero."

As an alternative presentation of the messy-looking while condition, it is possible to assign a variable while inside an expression (( ... )), and then to use the result. This gives the harder-to-read but more compact assign-and-test structure:

while rmdir FOO; ((! (ss=$?)))
do
    echo in loop
done
echo "out of loop with ?=$? but ss=$ss"

Alternatively you can use while rmdir FOO; ! (( ss=$? )). These work because ((1)) evaluates arithmetically to 1, which is generally associated with true, and so the exit code of that evaluation is 0 (success). On the other hand, ((0)) evaluates arithmetically to 0, which is generally associated with false, and so the exit code of that evaluation is 1 (failure). This may seem confusing, as after all both evaluations ((.)) are "successful", but this is a hack to bring the value of arithmetic expressions representing true/false in line with bash's exit codes of success/failure, and make conditional expressions like if ...; then ...; fi, while ...; do ...; done, etc, work correctly, whether based on exit codes or arithmetic values.

18
  • 1
    The brackets in (ss=$?) are purely to force the assignment before anything else. Your [[ $(ss=$?) -eq 0 ]] can't work because it tries to evaluate ss=$? as a command
    – roaima
    Nov 10, 2021 at 0:00
  • 2
    @Diagon (( (ss=$!) )) has no subshells; it's all expression evaluation. Like (( (1+2)*3 )) where the * isn't a filename glob but a multiplication symbol
    – roaima
    Nov 10, 2021 at 8:01
  • 2
    Yes, I found it. Under ARITHMETIC EVALUATION, "Operators are evaluated in order of precedence. Sub-expressions in parentheses are evaluated first and may override the precedence rules above."
    – Diagon
    Nov 10, 2021 at 19:41
  • 1
    if you want to golf it, use ! ((ss=$?)) and save the parens...
    – ilkkachu
    Nov 11, 2021 at 16:59
  • 1
    @Diagon, because the shell is weird. In C, and in most programming language, if you convert a number to an boolean (a true/false value), a zero becomes false, and anything else becomes true. So, in C, if (x) foo(); will call foo() if x is nonzero. The arithmetic context that the shell uses inside (( .. )) mimics that behaviour, it exits with a falsy status if the final value inside is zero. That's actually ok in something like while (( i-- )), which would count down until i is zero.
    – ilkkachu
    Nov 12, 2021 at 16:38
5

One way to do this is to make the test explicitly instead of delegating it to while. So, the following should do:

err=0
while true
do
    <function>
    (( (err=$?) > 0 )) && break
    <stuff>
done
echo "$err"

At the beginning of the (a priori infinite) loop you would execute the condition command "manually" and store the exit code, check it, and break out of the loop if it is non-zero (indicating failure). Only if it was zero you execute the "actual" loop code.

The syntax can be "compacted" because (as noted by @roaima) you can have variable assignments inside the (( ... )) arithmetic test construct.

0
4

Create a wrapping function that sets a variable with the result of the exit code of your function. Example:

e=0

# file does not exist
foo() { 
  rm xyz
}

baz() { 
  foo
  e=$?
  [[ $e -ne 0 ]] && return 1
  return 0
}

# create a file
touch xyz
while baz; do
  # first loop succeeds, the file exists
  # second loop exits with error, file doesn't exists
  echo in loop
done
in loop
rm: cannot remove 'xyz': No such file or directory
# then
$ echo $e
1
2
  • 1
    I can't get this to work. Define foo() and baz() as above. The loop becomes touch xyz; while baz; do echo in loop; done; echo $e. I don't ever get an "in loop" message. It looks like the [[ $? -ne 0 ]] && return should be replaced as [[ $e -ne 0 ]] && return 1; return 0, which in turn can be replaced as [[ $e -eq 0]] with no explicit return statement
    – roaima
    Nov 10, 2021 at 0:04
  • @roaima I edited with your suggestions, if you think it can be improved, feel free to edit if you want :) Nov 10, 2021 at 0:21
1

With zsh, you could use return from an anonymous function:

() while ((1)) {
  <function> || return
  <stuff>
}
print $?

With any Bourne-like shell, you can always use a named function:

repeat_until_fail()
  while true; do
    <function> || return
    <stuff>
  done

repeat_until_fail
echo "$?"

You must log in to answer this question.

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