В bash-скрипте, как захватывать стандартный вывод построчно

26

В bash-скрипте я хотел бы захватить стандартный вывод длинной командной строки за строкой, чтобы их можно было анализировать и сообщать, пока начальная команда еще выполняется. Это сложный способ сделать это:

# Start long command in a separated process and redirect stdout to temp file
longcommand > /tmp/tmp$$.out &

#loop until process completes
ps cax | grep longcommand > /dev/null
while [ $? -eq 0 ]
do
    #capture the last lines in temp file and determine if there is new content to analyse
    tail /tmp/tmp$$.out

    # ...

    sleep 1 s  # sleep in order not to clog cpu

    ps cax | grep longcommand > /dev/null
done

Я хотел бы знать, есть ли более простой способ сделать это.

РЕДАКТИРОВАТЬ:

Чтобы уточнить мой вопрос, я добавлю это. longcommandОтображает его статус построчно один раз в секунду. Я хотел бы поймать вывод до longcommandзавершения.

Таким образом, я могу потенциально убить, longcommandесли он не даст ожидаемых результатов.

Я пытался:

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Но whatever(например echo) выполняется только после longcommandзавершения.

gfrigon
источник
askubuntu.com/questions/344407/…
Тревор Бойд Смит

Ответы:

32

Просто передайте команду в whileцикл. Здесь есть ряд нюансов, но в основном (в bashлюбой оболочке POSIX):

longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Другой основной недостаток с этим (кроме IFSматериала ниже) - это когда вы пытаетесь использовать переменные внутри цикла после его завершения. Это потому, что цикл на самом деле выполняется в под-оболочке (просто в другом процессе оболочки), к которой у вас нет доступа к переменным (также он завершается, когда цикл выполняется, и в этот момент переменные полностью исчезают. Чтобы обойти это, ты можешь сделать:

longcommand | {
  while IFS= read -r line
  do
    whatever "$line"
    lastline="$line"
  done

  # This won't work without the braces.
  echo "The last line was: $lastline"
}

Пример Hauke по настройке lastpipeв bashдругое решение.

Обновить

Чтобы убедиться, что вы обрабатываете вывод команды «как это происходит», вы можете использовать stdbufдля установки процесса « stdoutбуферизацию строки.

stdbuf -oL longcommand |
  while IFS= read -r line
  do
    whatever "$line"
  done

Это настроит процесс для записи одной строки за раз в канал вместо внутренней буферизации его вывода в блоки. Помните, что программа может самостоятельно изменить эту настройку. Аналогичный эффект может быть достигнут с unbuffer(часть expect) или script.

stdbufдоступно в системах GNU и FreeBSD, оно влияет только на stdioбуферизацию и работает только для приложений без setuid и setgid, которые динамически связаны (так как использует трюк LD_PRELOAD).

Graeme
источник
@ Stephane The IFS=не требуется bash, я проверил это после последнего раза.
Грэм
2
Да, это. Это не нужно, если вы пропустите line(в этом случае результат вставляется $REPLYбез обрезки начальных и конечных пробелов). Попробуйте: echo ' x ' | bash -c 'read line; echo "[$line]"'и сравните с echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'илиecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Стефан Шазелас
@ Стефан, ладно, никогда не понимал, что есть разница между именованной и переменной. Спасибо.
Грэм,
@Graeme Я, возможно, не был ясно в своем вопросе, но я хотел бы обработать вывод построчно до того, как longcommand завершится (чтобы быстро реагировать, если longcommands отображает сообщение об ошибке). Я отредактирую свой вопрос, чтобы прояснить
gfrigon
@gfrigon, обновлено.
Грэм,
2
#! /bin/bash
set +m # disable job control in order to allow lastpipe
shopt -s lastpipe
longcommand |
  while IFS= read -r line; do lines[i]="$line"; ((i++)); done
echo "${lines[1]}" # second line
Хауке Лагинг
источник