1

I am unable to discern why timeout in a function call will cause a loop to stop. I have a "solution", but I am really very intrigued as to how / why this is happening! It seems to be something to do with cat being the command getting timed out?

TL;DR

while read -r line; do ... done < file gets terminated when a timeout occurs on cat, producing the wrong output and exit code. The loop does not loop through every line in the file.

If instead an array is created first of all lines in the file, and then ... is executed in a for line in "${all_lines[@]}"; do, all lines are processed and the output of timeout with respect to exit codes is correct.


Suppose the script grade.sh intends to read all of tests.txt and execute soln.sh, making sure that soln.sh terminates. To demonstrate a "working" example, soln.sh will first sleep.

tests.txt

first
second
third
fourth
fifth

grade.sh

#!/usr/bin/env bash

while read -r line; do
    echo "Test: $line"
    output="$(timeout 2 ./soln.sh "$line")"
    timed_exit=$?
    echo "  Soln Output: $output"
    echo "  Timed exit:  $timed_exit"
done < "tests.txt"

soln.sh

#!/usr/bin/env bash
if [[ "$1" == "third" ]]; then
    sleep 3
fi
echo "[soln running $1]"

expected output

Test: first
  Soln Output: [soln running first]
  Timed exit:  0
Test: second
  Soln Output: [soln running second]
  Timed exit:  0
Test: third
  Soln Output: 
  Timed exit:  124
Test: fourth
  Soln Output: [soln running fourth]
  Timed exit:  0
Test: fifth
  Soln Output: [soln running fifth]
  Timed exit:  0

If we change soln to do something that will continue forever (waiting for input), the loop ends instead

soln.sh

#!/usr/bin/env bash
if [[ "$1" == "third" ]]; then
    cat $(find . -name iamnothere.txt) | wc -l
fi
echo "[soln running $1]"

output terminates early, extra 2, wrong exit code

Test: first
  Soln Output: [soln running first]
  Timed exit:  0
Test: second
  Soln Output: [soln running second]
  Timed exit:  0
Test: third
  Soln Output: 2
[soln running third]
  Timed exit:  0

Hacky fix is to loop through every line first and use a for loop which will bypass this.

"fixed" grade.sh

#!/usr/bin/env bash

all_lines=()
idx=0
while read -r line; do
    all_lines[idx]="$line"
    (( idx++ ))
done < "tests.txt"

for line in "${all_lines[@]}"; do
    echo "Test: $line"
    output="$(timeout 2 ./soln.sh "$line")"
    timed_exit=$?
    echo "  Soln Output: $output"
    echo "  Timed exit:  $timed_exit"
done

expected output

Test: first
  Soln Output: [soln running first]
  Timed exit:  0
Test: second
  Soln Output: [soln running second]
  Timed exit:  0
Test: third
  Soln Output: 
  Timed exit:  124
Test: fourth
  Soln Output: [soln running fourth]
  Timed exit:  0
Test: fifth
  Soln Output: [soln running fifth]
  Timed exit:  0

is this a feature or a bug or am I missing something?

It seems to me like cat is somehow overriding timeout, since the rest of the script gets to execute.

1 Answer 1

2
 cat $(find . -name iamnothere.txt) | wc -l

assuming that iamnothere.txt does not exist becomes

cat | wc -l

which consumes standard input, the same standard input that the while loop is reading the lines from. for avoids this by not using standard input like while does. This can be observed by using a bare cat for the second line case, as this shows that the third line has been read by that cat:

$ cat lines 
first
secon
third
$ cat looper 
#!/bin/sh
while read line; do
    x=$(timeout 2 ./doer "$line")
    echo "$line out=$x code=$?"
done < lines

$ cat doer 
#!/bin/sh
if [ "$1" = secon ]; then
    cat
else
    echo "$1 pid$$"
fi

$ ./looper 
first out=first pid42079 code=0
secon out=third code=0
$ 
2
  • Wow thank you for explaining, I was aware that subshells are not completely new processes, but I had no idea they inherited stdin! If anybody else is reading this, this SO answer gives a good explanation of how it works in addition to what @thrig linked to in their answer.
    – svenevs
    Commented Jun 2, 2017 at 7:53
  • Not only is it inherited, the processes both point to the same underlying object. If stdin is seekable, a seek by one process will affect where the other subsequently reads from.
    – thrig
    Commented Jun 2, 2017 at 13:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .