Verifying the Backup Command

Learn what can go wrong with our example backup command.

Let’s check if the following command works correctly.

tar -cjf ~/photo.tar.bz2 ~/photo &&
  echo "tar - OK" > results.txt ||
  echo "tar - FAILS" > results.txt &&
cp -f ~/photo.tar.bz2 ~/backup &&
  echo "cp - OK" >> results.txt ||
  echo "cp - FAILS" >> results.txt

We can replace each command call with a Latin letter. Then, we get a convenient form of the boolean expression. The expression looks like this:

B && O1 || F1 && C && O2 || F2

The B and C letters represent the tar and cp calls. O1 is the echo call that prints “tar - OK” line in the log file. F1 is the echo call to print the “tar - FAILS” line. Similarly, O2 and F2 are the commands to log the cp result.

If the tar call succeeds, the B operand of our expression equals true. Then, Bash performs the sequence of the following steps:

  1. B
  2. O1
  3. C
  4. O2 or F2

If the tar fails, the “B” operand equals false. Then Bash does the following steps:

  1. B
  2. F1
  3. C
  4. O2 or F2

This means that the shell calls the cp utility even when the archiving fails. It doesn’t make sense.

Unfortunately, the tar utility makes things even more confusing. It creates an empty archive if it can’t access the target directory or files. Then, the cp utility copies the empty archive successfully. These operations lead to the following output in the log file:

tar - FAILS
cp - OK

Such output is confusing. It doesn’t clarify what went wrong.

Here is our expression again:

B && O1 || F1 && C && O2 || F2

Why does Bash call the cp utility when tar fails? It happens because the echo command always succeeds. It returns zero code, which means true. Thus, the O1, F1, O2 and F2 operands of our expression are always true.

Let’s fix the issue caused by the echo call exit code. We should focus on the tar call and corresponding echo commands. They match the following boolean expression:

B && O1 || F1

We can enclose the “B” and “O1” operands in brackets this way:

(B && O1) || F1

It doesn’t change the expression’s result.

We have a logical OR between the (B && O1) and F1 operands. The F1 operand always equals true. Therefore, the whole expression is always true. The value of (B && O1) does not matter. We want to get another behavior. If the (B && O1) operand equals false, the entire expression should be false.

One possible solution is inverting the F1 operand. The logical NOT operator does that. We get the following expression this way:

B && O1 || ! F1 && C && O2 || F2

Let’s check the behavior that we got. If the B command fails, Bash evaluates F1. It always equals false because of negation. Then, Bash skips the C and O2 commands. This happens because there is a logical AND between them and F1. Finally, Bash comes to the F2 operand. The shell needs its value. Bash knows that the LHS operand of the logical OR equals false. Therefore, it needs to evaluate the RHS operand to deduce the result of the whole expression.

We can make the expression clearer with the following parentheses:

(B && O1 || ! F1 && C && O2) || F2

Now, it’s clear that Bash executes the F2 action when the parenthesized expression equals false. Otherwise, it cannot deduce the final result.

The last command writes this output into the log file:

tar - FAILS
cp - FAILS

This output looks better than the previous one. Now, the cp utility does not copy an empty archive.

The current result still has room for improvement. Let’s imagine that we extended the backup command. Then, it contains 100 actions. If an error occurs at the 50th action, all the remaining operations print their failed results into the log file. Such output makes it complicated to find the problem.

The better solution here is to terminate the command right after the first error occurred. Parentheses can help us to reach this behavior. Here is a possible grouping of the expression’s operands:

(B && O1 || ! F1) && (C && O2 || F2)

Let’s check what happens if the B operand is false. Then, Bash executes the F1 command. The negation inverts the F1 result. Therefore, the entire LHS expression equals false. Here is the LHS expression:

(B && O1 || ! F1)

Then, the short-circuit evaluation happens. It prevents calculating the RHS operand of the logical AND. Then, Bash skips all commands of the RHS expression. Here is the RHS expression:

(C && O2 || F2)

We created the proper behavior of the backup command. We can add one last improvement. The F2 operand should be inverted. Then, the whole expression equals false if the C command fails. Then, the entire backup command fails if tar or cp call fails. Inverting the F2 operand provides the proper non-zero exit status in the error case. Here is the final version of our expression with all improvements:

(B && O1 || ! F1) && (C && O2 || ! F2)

Let’s come back to the real Bash code. The corrected backup command looks like this:

(tar -cjf ~/photo.tar.bz2 ~/photo &&
  echo "tar - OK" > results.txt ||
  ! echo "tar - FAILS" > results.txt) &&
(cp -f ~/photo.tar.bz2 ~/backup &&
  echo "cp - OK" >> results.txt ||
  ! echo "cp - FAILS" >> results.txt)

Run the commands discussed in this lesson in the terminal below.

Get hands-on with 1400+ tech skills courses.