2

I'd like to run a series of tasks but stop should any of them fail, hence I've written (something like):

for task in [TASKS]; do
  process "$task" || break
  commit "$task"
done

This works fine, but (as specified) the exit status of this loop is zero even if we break early. Ideally break-ing would be able to convey the failure.

I know returning 0 is the documented behavior of break, but I'm curious if there are any relatively clean workarounds. The best I can imagine is wrapping this in a function and setting a didBreak variable, and using that as the exit status (of the function). It'd work, but it feels overly-complex.

2

Using ! break works in many shells (all but the pdksh based ones and the sh of FreeBSD (by design) in my tests):

$ zsh -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ bash -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ ksh88 -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ ksh93 -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ dash -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ yash -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ bosh -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
1
$ pdksh -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
0
$ mksh -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
0
$ posh -c 'for i in x; do ! break; echo "$i"; done'; echo "$?"
0

Note that it doesn't trigger errexit in any of them.

It was discussed on the austin-group (the body behind POSIX) mailing list last year. The discussion (which involved the maintainers of bosh, FreeBSD sh and NetBSD sh) died out before a consensus was reached, but the prevalent view seemed to be that POSIX required that behaviour as ! is documented as negating the exit status of commands and break is a special builtin command that exits with 0 exit status.

But if you apply the same reasoning to return for instance, you'll see that fewer shells comply.

In zsh, you can use return in an anonymous function instead of break:

$ () for i in x y; do echo $i; return 1; done
x
$ echo $?
1
  • Hah that's great! TIL. Is this a documented feature at all, or just a total coincidence that it works in most shells? bash --posix also works this way. – dimo414 Sep 20 at 0:44
  • @dimo414, see edit for the POSIX view on that. – Stéphane Chazelas Sep 20 at 7:14
  • Shouldn't it also be possible to just wrap it in a function (not an anonymous function) and use return 1 from there? Or is that not POSIX-compliant? – Wildcard Sep 20 at 8:53
  • @Wildcard, yes using a function would be an obvious way to do it. See also this message of mine in the aforementioned discussion – Stéphane Chazelas Sep 20 at 14:52
  • Thanks for sharing that context! Very interesting. – dimo414 Sep 23 at 3:44
1

You could do something like

failed=false
for task in "${tasks[@]}"; do
  if ! process "$task"; then
      failed=true
      break
  fi
  commit "$task"
done

if "$failed"; then
   echo "Failed something" >&2
fi
  • Yeah, that's pretty much what I'm envisioning (aside from wrapping it in a function), but it feels kludgy to me. Maybe that's the best I can do, though. – dimo414 Jan 27 '17 at 22:24
  • @dimo414 Doesn't feel too kludgy to me. You could even use failed="$t" or something to later be able to tell what task it was that failed. – Kusalananda Jan 27 '17 at 22:27
  • process prints enough information to describe what went wrong; all that's missing is stopping after the loop terminates-early. You're right it's not that bad, I just don't love the separation. – dimo414 Jan 27 '17 at 23:26
0

My imagined solution is:

run_til_failure() {
  local didBreak=0
  for task in [TASKS]; do
    process "$task" || { didBreak=1; break; }
    commit "$task"
  done
  local loopExit=$?
  if (( loopExit )); then return $loopExit; fi
  return $didBreak
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.