15

I know how to check the status of the previously executed command using $?, and we can make that status using exit command. But for the loops in bash are always returning a status 0 and is there any way I can break the loop with some status.

#!/bin/bash
while true
do
        if [ -f "/test" ] ; then
                break ### Here I would like to exit with some status
        fi

done
echo $?  ## Here I want to check the status.
Sriharsha Kalluru
  • 1,743
  • 3
  • 21
  • 27

8 Answers8

28

The status of the loop is the status of the last command that executes. You can use break to break out of the loop, but if the break is successful, then the status of the loop will be 0. However, you can use a subshell and exit instead of breaking. In other words:

for i in foo bar; do echo $i; false; break; done; echo $?  # The loop succeeds
( for i in foo bar; do echo $i; false; exit; done ); echo $? # The loop fails

You could also put the loop in a function and return a value from it. eg:

in() { local c="$1"; shift; for i; do test "$i" = "$c" && return 0; done; return 1; }
William Pursell
  • 204,365
  • 48
  • 270
  • 300
  • 2
    That's why you put the exit in a subshell! – William Pursell Dec 27 '12 at 18:48
  • Thanks a lot, A sub-shell is doing it. But is there any possibility to do it on the same shell? – Sriharsha Kalluru Dec 27 '12 at 18:52
  • 1
    Your example was so helpful for me to understand how loops work, thanks! – Paul Redmond Aug 26 '14 at 00:18
  • 3
    Instead of using a subshell, I'd recommend enclosing your code in a function, because (1) you can use `return $returncode` instead of `exit $returncode` and (2) by using a meaningful name for the function, your make your code more readable. – hagello Dec 03 '15 at 08:40
  • 1
    And (3), a function isn't a subshell, which means it doesn't fork. (Not every sh implementation forks for a subshell, but bash does.) – kojiro Jan 17 '17 at 19:37
3

Something like this?

while true; do
    case $RANDOM in *0) exit 27 ;; esac
done

Or like this?

rc=0
for file in *; do
    grep fnord "$file" || rc=$?
done
exit $rc

The real question is to decide whether the exit code of the loop should be success or failure if one iteration fails. There are scenarios where one make more sense than the other, and other where it's not at all clear cut.

tripleee
  • 175,061
  • 34
  • 275
  • 318
  • I have given example in my question, And in the example you have given if I use it will completely exit form my script instead of loop. – Sriharsha Kalluru Dec 27 '12 at 18:47
  • My second example translates straightforwardly to what you are attempting. The return code is in `$rc`, not in `$?`, though. Put the loop in a function as William Pursell suggests to work around that if it's a problem. – tripleee Dec 28 '12 at 07:20
2

The bash manual says:

while list-1; do list-2; done
until list-1; do list-2; done
  [..]The exit status of the while and until commands is the exit status
  of the last command executed in list-2, or zero if none was executed.[..]

The last command that is executed inside the loop is break. And the exit value of break is 0 (see: help break).

This is why your program keeps exiting with 0.

hagello
  • 2,843
  • 2
  • 27
  • 37
0

I think what you should be asking is: How can I wait until a file or a directory (/test) gets created by another process?

What you are doing up to now is polling with full power. Your loop will allocate up to 100% of the processing power of one core. The keyword is "polling", which is ethically wrong by the standards of computer scientists.

There are two remedies:

  1. insert a sleep statement in your loop; advantage: very simple; disadvantage: the delay will be an arbitrary trade-off between CPU load and responsiveness. ("Arbitrary" is ethically wrong, too).
  2. use a notification mechanism like inotify (see: man inotify); advantage: no CPU load, great responsiveness, no delays, no arbitrary constants in your code; disadvantage: inotify is a kernel API – you need some code to access it: inotify-tools or some C/Perl/Python code. Have a look at inotify and bash!
hagello
  • 2,843
  • 2
  • 27
  • 37
0

The break builtin for bash does allow you to accomplish what you are doing, just break with a negative value and the status returned by $? will be 1:

while true
do
    if [ -f "./test" ] ; then
            break -1
    fi
done
echo $?  ## You'll get 1 here..

Note, this is documented in the help for the break builtin:

help break

break: break [n] Exit for, while, or until loops.

Exit a FOR, WHILE or UNTIL loop. If N is specified, break N enclosing loops.

Exit Status: The exit status is 0 unless N is not greater than or equal to 1.

You can break out of n number of loops or send a negative value for breaking with a non zero return, ie, 1

I agree with @hagello as one option doing a sleep and changing the loop:

#!/bin/bash
timeout=120
waittime=0
sleepinterval=3
until [[ -f "./test" || ($waittime -eq $timeout) ]]
do
   $(sleep $sleepinterval)
   waittime=$((waittime + sleepinterval))
   echo "waittime is $waittime"
done

if [ $waittime -lt $sleepinterval ]; then
    echo "file already exists"
elif [ $waittime -lt $timeout ]; then
    echo "waited between $((waittime-3)) and $waittime seconds for this to finish..."
else
    echo "operation timed out..."
fi
code4cause
  • 119
  • 1
  • 9
  • In Bash 4.4.19 `break` with negative value gives error of counter being out of range. – Cromax May 13 '19 at 12:18
  • `break` works as desired despite or even because of that error. That's how you get the error status that we want. You can do `break -1 2>/dev/null` to avoid seeing the error. That honestly seems like the least cumbersome solution since moving code to a subshells can have undesirable effects too. – ben Aug 27 '21 at 00:41
0

I would like to submit an alternative solution which is simpler and I think more elegant:

(while true
 do
    if [ -f "test" ] ; then
        break
    fi  
 done

Of course of this is part of a script then you could user return 1 instead of exit 1
 exit 1
)
echo "Exit status is: $?" 
0

Git 2.27 (Q2 2020), offers a good illustration of the exit status in a loop, here within the context of aborting a failing test early (e.g. by exiting a loop), which is to say "return 1".

See commit 7cc112d (27 Mar 2020) by Junio C Hamano (gitster).
(Merged by Junio C Hamano -- gitster -- in commit b07c721, 28 Apr 2020)

t/README: suggest how to leave test early with failure

Helped-by: Jeff King

Over time, we added the support to our test framework to make it easy to leave a test early with failure, but it was not clearly documented in t/README to help developers writing new tests.

The documentation now includes:

Be careful when you loop

You may need to verify multiple things in a loop, but the following does not work correctly:

test_expect_success 'test three things' '
    for i in one two three
    do
      test_something "$i"
    done &&
    test_something_else
'

Because the status of the loop itself is the exit status of the test_something in the last round, the loop does not fail when "test_something" for "one" or "two" fails.
This is not what you want.

Instead, you can break out of the loop immediately when you see a failure.
Because all test_expect_* snippets are executed inside a function, "return 1" can be used to fail the test immediately upon a failure:

test_expect_success 'test three things' '
    for i in one two three
    do
      test_something "$i" || return 1
    done &&
    test_something_else
'

Note that we still &&-chain the loop to propagate failures from earlier commands.

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
0

Use artificial exit code

Before breaking the loop set a variable then check the variable as the status code of the loop, like this:

while true; do 
  if [ -f "/test" ] ; then
     {broken=1 && break; };
  fi
done
echo $broken #check the status with [[ -n $broken ]]
Arash
  • 400
  • 4
  • 11