2

This script just tries to copy file foo to bar/, and if it`s unsuccessful, retries 2 more times.

EDIT: as it turns out, the reason the second example works and the first does not, is because the use of (...) to run a group of commands after the && or || branch is considered a subshell and does not affect the outer variable.

So revised question: how can I group some commands like the first example, but not use a subshell to do so?

EDIT 2: the answer is to replace ( ... ) with { ...; }!

#!/usr/bin/env bash
#

# Be safe
set -euo pipefail

# Init variables
__file="foo"
output_dir="bar"
logfile="test.log"
errorlevel=8
count=0

# Try three times to copy a file
until (( errorlevel == 0 || count == 3 )); do

  cp -Lv --no-preserve=mode \
    "$__file" \
    "$output_dir/" \
    | tee -a "$logfile" \
    && (errorlevel=$?; echo "zero return, $errorlevel") \
    || (errorlevel=$?; echo "non-zero return, $errorlevel")

  echo "errorlevel $errorlevel"

  (( ++count ))

  echo "count $count"

done

unset errorlevel
unset count

The problem:

When the copy fails, or is successful, the || or && branch catches correctly and sets variable errorlevel to 1 or 0, respectively, as evidenced with the echo $errorlevel contained within the branch.

But the very next command, also an echo, returns the initial variable value of 8! I don't understand why this is happening.

A few notes:

  1. I'm using && and || explicitly because I have set -e to make this script as safe as possible (this is actually a small routine in a 1500-line script). If I don't use || to catch a non-zero return, the script exits. Alternatively I could set +e, do the copy routine without having to use && and || (by catching the errorlevel once), and then set -e again, but I'd rather not because it doesn't match the style of the rest of my script.
  2. I'm using set -o pipefail to pass a non-zero errorlevel down the pipeline so that my tee -a "$logfile" doesn't always override my exit errorlevel.
  3. I'm using ++count instead of count++ because the former returns a zero errorlevel, and the latter returns a non-zero errorlevel (and I would have to do an ugly (( count++ )) || true because of the set -e explained in note #1).

I have worked around it for now by setting the errorlevel variable explicitly to 1 or 0, since I actually don't care what the non-zero errorlevel is, but I would still really like to know why the above isn`t working as I expect.

Here's the workaround:

#!/usr/bin/env bash
#

set -euo pipefail

__file="foo"
output_dir="bar"
logfile="test.log"

errorlevel=1
count=0
until (( errorlevel == 0 || count == 5 )); do
  cp -Lv --no-preserve=mode \
    "$__file" \
    "$output_dir/" \
    | tee -a "$logfile" \
    && errorlevel=0 \
    || errorlevel=1
  (( ++count ))
  sleep 1
done

unset errorlevel
unset count

Any insight is greatly appreciated!

muru
  • 72,889

1 Answers1

1

I'd prefer to use an if block:

errorlevel=0
if cp -Lv --no-preserve=mode \
    "$__file" \
    "$output_dir/" \
    | tee -a "$logfile"
then
    echo "zero return, $errorlevel"
else
    errorlevel=$?
    echo "non-zero return, $errorlevel"
fi

The if protects a non-zero exit status from triggering the -e exit-on-error action.

(The original problem, of course, is that ( ) creates a subshell, so the variable assignment is never affects the parent shell.)

muru
  • 72,889