тайм-аут приводит к завершению цикла чтения while, когда `cat` истекает

тайм-аут приводит к завершению цикла чтения while, когда `cat` истекает

Я не могу понять, почему timeoutвызов функции приведет к остановке цикла. У меня есть "решение", но я действительно очень заинтригован тем, как / почему это происходит! Кажется, это как-то связано с тем, catчто команда истекает по тайм-ауту?

TL;DR

while read -r line; do ... done < fileзавершается, когда timeoutпроисходит на cat, производя неправильный вывод и код выхода. Цикл делаетнетпройтись по каждой строке файла.

Если вместо этого сначала создать массив всех строк в файле, а затем ...выполнить его в for line in "${all_lines[@]}"; do, все строки будут обработаны, и вывод timeoutс точки зрения кодов завершения будет правильным.


Предположим, что скрипт grade.shнамеревается прочитать все tests.txtи выполнить soln.sh, убедившись, что soln.shзавершается. Чтобы продемонстрировать "рабочий" пример, soln.shсначала 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]"

ожидаемый результат

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

Если мы изменим solnдействие, чтобы оно продолжалось вечно (ожидание ввода), цикл завершится.

soln.sh

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

вывод завершается преждевременно, лишний 2, неправильный exitкод

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

Хакерское решение — сначала пройтись по каждой строке и использовать forцикл, который обойдет это.

"зафиксированный"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

ожидаемый результат

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

Это фича, баг или я что-то упускаю?

Мне кажется, что catэто как-то переопределяет timeout, поскольку остальная часть скрипта может быть выполнена.

решение1

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

предполагая, что iamnothere.txtне существует, становится

cat | wc -l

который потребляет стандартный ввод,тот же стандартный ввод, whileиз которого цикл считывает строки. forизбегает этого, не используя стандартный ввод, как whileделает. Это можно наблюдать, используя bare catдля случая второй строки, поскольку это показывает, что третья строка была прочитана этим 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
$ 

Связанный контент