1

I have a script similar to this:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

while read -r l; do echo "${l}" done <<< "$(cat input.txt)"

echo Success

The command cat input.txt is just an example here to simplify my question.

I expected that if input.txt does not exist, the script would exit immediately thanks to set -euo pipefail. But it's not the case, instead the script ends successfully with the following output:

cat: input.txt: No such file or directory

Success

Is there a way to make the script fail as I expected?

  • I recommend checking beforehand if input.txt exists and would then respond accordingly. – Cyrus Dec 16 '22 at 02:20
  • Thanks but that's what I meant by cat input.txt is just an example. My actual command is long running process and I can't predict if it will fail or not. But I want my wrapping script to fail if the command fails. – Thomas Leplus Dec 16 '22 at 03:00

2 Answers2

1

I understand here cat input.txt is a place holder for some arbitrary command. For cat input.txt specifically, the solution would be easy: just use input redirection from input.txt instead.

The exit code will be available in next expansions in the here string. For instance:

cat <<< "$(exit 3)$?"

Outputs 3. So you could do something like:

#! /bin/bash -
set -o nounset -o pipefail -o errexit

unset -v ret { [ "$ret" -eq 0 ] || exit "$ret" while IFS= read -r line || [ -n "$line" ]; do printf '%s\n' "$line" || exit done } <<< "$(cat input.txt)"${ret[1+(ret=$?)]}" echo success

Which appears to work with bash 5.0 at least.

Or you could use a temporary variable:

#! /bin/bash -
set -o nounset -o pipefail -o errexit

output_minus_trailing_newlines=$(cat input.txt) while IFS= read -r line || [ -n "$line" ]; do printf '%s\n' "$line" || exit done <<< "$output_minus_trailing_newlines"

echo success

In either case, if the command fails, none of its output will be processed.

You could also use a pipe and the lastpipe option if you don't want the loop to run in a subshell.

#! /bin/bash -
set -o nounset -o pipefail -o errexit
shopt -s lastpipe

cat input.txt | while IFS= read -r line || [ -n "$line" ]; do printf '%s\n' "$line" || exit done

echo success

Then the output will be processed as it comes. It's only after the pipeline exits that the shell will exit if the command failed thanks to the combination of errexit (-e) and pipefail. In that case, trailing empty lines are preserved.

Same using process substitution:

#! /bin/bash -
set -o nounset -o pipefail -o errexit

{ pid="$!" while IFS= read -r line || [ -n "$line" ]; do printf '%s\n' "$line" || exit done wait "$pid" } < <(cmd input.txt)

echo success

Or you could use a tempfile (here docs and here strings used to be implemented with temp files in bash).

#! /bin/bash -
set -o nounset -o pipefail -o errexit
tmp=$(mktemp)
{
  rm -f -- "$tmp"
  cat input.txt >&3
  exec 3>&-

while IFS= read -r line || [ -n "$line" ]; do printf '%s\n' "$line" || exit done wait "$pid" } 3> "$tmp" < "$tmp"

echo success

This way, the output is not processed if the command failed and you don't store the output (several times) in memory and the trailing empty lines are preserved.

In any case, see Why is using a shell loop to process text considered bad practice?

  • Yes cat input.txt is a placeholder for a long-running application that produces logs. So in my case the first option (temporary variable) does not work because I need to "stream" the output of the command, I can't wait for it to finish to collect the output. But the pipe approach combine with the lastpipe option does work! Thanks. One important thing to note for people reading this solution is that without the lastpipe option, any variable declared or changed inside the subshell is local and dissociated from the rest of the script. – Thomas Leplus Dec 16 '22 at 15:47
0

Like this:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

[[ -s input.txt ]] while read -r l; do echo "${l}" done <<< "$(cat input.txt)"

echo Success