Dalam skrip bash, cara menangkap stdout baris demi baris

26

Dalam skrip bash, saya ingin menangkap keluaran standar dari baris perintah panjang demi baris, sehingga dapat dianalisis dan dilaporkan saat perintah awal masih berjalan. Ini adalah cara rumit yang bisa saya bayangkan melakukannya:

# 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

Saya ingin tahu apakah ada cara yang lebih sederhana untuk melakukannya.

EDIT:

Untuk memperjelas pertanyaan saya, saya akan menambahkan ini. The longcommandmenampilkan baris status demi baris sekali per detik. Saya ingin menangkap output sebelum longcommandselesai.

Dengan cara ini, saya berpotensi membunuh longcommandjika tidak memberikan hasil yang saya harapkan.

Saya telah mencoba:

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

Tetapi whatever(misalnya echo) hanya dieksekusi setelah longcommandselesai.

gfrigon
sumber

Jawaban:

32

Cukup kirimkan perintah ke whileloop. Ada sejumlah nuansa untuk ini, tetapi pada dasarnya (dalam bashatau shell POSIX):

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

Gotcha utama lainnya dengan ini (selain dari IFShal - hal di bawah ini) adalah ketika Anda mencoba menggunakan variabel dari dalam loop setelah selesai. Ini karena loop sebenarnya dieksekusi dalam sub-shell (hanya proses shell lain) yang Anda tidak dapat mengakses variabel dari (juga selesai ketika loop tidak, di mana titik variabel benar-benar hilang. Untuk mengatasi ini, Anda dapat melakukan:

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"
}

Misalnya Hauke tentang pengaturan lastpipedi bashsolusi lain.

Memperbarui

Untuk memastikan Anda memproses output dari perintah 'seperti yang terjadi', Anda dapat menggunakan stdbufuntuk mengatur proses ' stdoutmenjadi buffer line.

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

Ini akan mengonfigurasi proses untuk menulis satu baris pada suatu waktu ke dalam pipa alih-alih secara internal menyangga outputnya ke dalam blok. Berhati-hatilah karena program dapat mengubah pengaturan ini secara internal. Efek serupa dapat dicapai dengan unbuffer(bagian dari expect) atau script.

stdbuftersedia pada sistem GNU dan FreeBSD, hanya memengaruhi stdiobuffering dan hanya berfungsi untuk aplikasi non-setuid, non-setgid yang terhubung secara dinamis (karena menggunakan trik LD_PRELOAD).

Graeme
sumber
@Stephane Tidak IFS=diperlukan di bash, saya memeriksa ini setelah terakhir kali.
Graeme
2
ya itu. Ini tidak diperlukan jika Anda menghilangkan line(dalam hal ini hasilnya dimasukkan $REPLYtanpa spasi terkemuka dan tertinggal). Coba: echo ' x ' | bash -c 'read line; echo "[$line]"'dan bandingkan dengan echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'atauecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas
@Stephane, ok, tidak pernah menyadari ada perbedaan antara itu dan variabel bernama. Terima kasih.
Graeme
@ Grema Saya mungkin tidak jelas dalam pertanyaan saya, tetapi saya ingin memproses output baris demi baris sebelum perintah panjang selesai (untuk bereaksi cepat jika perintah panjang menampilkan pesan kesalahan). Saya akan mengedit pertanyaan saya untuk memperjelas
gfrigon
@ grigrigon, diperbarui.
Graeme
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
Hauke ​​Laging
sumber