Trong bash script, làm thế nào để chụp dòng stdout theo dòng


26

Trong một tập lệnh bash, tôi muốn nắm bắt đầu ra tiêu chuẩn của một dòng lệnh dài theo từng dòng để chúng có thể được phân tích và báo cáo trong khi lệnh ban đầu vẫn đang chạy. Đây là cách phức tạp mà tôi có thể tưởng tượng khi làm điều đó:

# 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

Tôi muốn biết nếu có một cách đơn giản hơn để làm như vậy.

CHỈNH SỬA:

Để làm rõ câu hỏi của tôi, tôi sẽ thêm điều này. Các longcommandhiển thị dòng trạng thái của mình bằng dòng một lần mỗi giây. Tôi muốn bắt đầu ra trước khi longcommandhoàn thành.

Bằng cách này, tôi có khả năng có thể giết chết longcommandnếu nó không cung cấp kết quả mà tôi mong đợi.

Tôi đã thử:

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

Nhưng whatever(ví dụ echo) chỉ thực hiện sau khi longcommandhoàn thành.


Câu trả lời:


32

Chỉ cần đặt lệnh vào một whilevòng lặp. Có một số sắc thái cho điều này, nhưng về cơ bản (trong bashhoặc bất kỳ vỏ POSIX nào):

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

Gotcha chính khác với điều này (trừ những IFSthứ bên dưới) là khi bạn cố gắng sử dụng các biến từ bên trong vòng lặp sau khi nó kết thúc. Điều này là do vòng lặp thực sự được thực thi trong lớp vỏ phụ (chỉ là một quá trình shell khác) mà bạn không thể truy cập các biến từ đó (cũng là kết thúc khi vòng lặp thực hiện, tại đó các biến đã hoàn toàn biến mất. bạn có thể làm:

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

Ví dụ về cách thiết lập Hauke của lastpipetrong bashlà một giải pháp khác.

Cập nhật

Để đảm bảo bạn đang xử lý đầu ra của lệnh 'như nó xảy ra', bạn có thể sử dụng stdbufđể đặt quy trình ' stdoutthành bộ đệm dòng.

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

Điều này sẽ cấu hình quá trình để viết một dòng tại một thời điểm vào đường ống thay vì nội bộ đệm đầu ra của nó thành các khối. Coi chừng chương trình có thể tự thay đổi cài đặt này. Một hiệu ứng tương tự có thể đạt được với unbuffer(một phần của expect) hoặc script.

stdbufcó sẵn trên các hệ thống GNU và FreeBSD, nó chỉ ảnh hưởng đến stdiobộ đệm và chỉ hoạt động đối với các ứng dụng không setuid, không setgid được liên kết động (vì nó sử dụng thủ thuật LD_PRELOAD).


@Stephane IFS=Không cần thiết bash, tôi đã kiểm tra cái này sau lần trước.
Graeme

2
đúng vậy Không cần thiết nếu bạn bỏ qua line(trong trường hợp đó, kết quả được đưa vào $REPLYmà không có khoảng trắng ở đầu và cuối được cắt bớt). Hãy thử: echo ' x ' | bash -c 'read line; echo "[$line]"'và so sánh với echo ' x ' | bash -c 'IFS= read line; echo "[$line]"'hoặcecho ' x ' | bash -c 'read; echo "[$REPLY]"'
Stéphane Chazelas

@Stephane, ok, không bao giờ nhận ra có một sự khác biệt giữa điều đó và một biến được đặt tên. Cảm ơn.
Graeme

@Graeme Tôi có thể không rõ ràng trong câu hỏi của mình nhưng tôi muốn xử lý dòng đầu ra theo từng dòng trước khi longcommand hoàn thành (để phản ứng nhanh nếu longcommands hiển thị thông báo lỗi). Tôi sẽ chỉnh sửa câu hỏi của mình để làm rõ hơn
gfrigon

@gfrigon, cập nhật.
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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.