Làm thế nào tôi có thể đọc từng dòng từ một biến trong bash?


48

Tôi có một biến chứa đầu ra đa dòng của một lệnh. Cách hiệu quả nhất để đọc dòng đầu ra theo dòng từ biến là gì?

Ví dụ:

jobs="$(jobs)"
if [ "$jobs" ]; then
    # read lines from $jobs
fi

Câu trả lời:


64

Bạn có thể sử dụng vòng lặp while với quy trình thay thế:

while read -r line
do
    echo "$line"
done < <(jobs)

Một cách tối ưu để đọc một biến đa dòng là đặt một IFSbiến trống và printfbiến trong một dòng mới:

# Printf '%s\n' "$var" is necessary because printf '%s' "$var" on a
# variable that doesn't end with a newline then the while loop will
# completely miss the last line of the variable.
while IFS= read -r line
do
   echo "$line"
done < <(printf '%s\n' "$var")

Lưu ý: Theo shellcheck sc2031 , việc sử dụng trạm biến áp được ưu tiên hơn so với đường ống để tránh [tinh tế] tạo ra một lớp con.

Ngoài ra, xin vui lòng nhận ra rằng bằng cách đặt tên biến, jobsnó có thể gây nhầm lẫn vì đó cũng là tên của một lệnh shell thông thường.


3
Nếu bạn muốn giữ tất cả khoảng trắng của mình, thì hãy sử dụng while IFS= read.... Nếu bạn muốn ngăn \ phiên dịch, thì hãy sử dụngread -r
Peter.O

Tôi đã sửa các điểm fred.bear đã đề cập, cũng như thay đổi echothành printf %s, để tập lệnh của bạn sẽ hoạt động ngay cả với đầu vào không thuần hóa.
Gilles 'SO- ngừng trở thành ác quỷ'

Để đọc từ một biến đa dòng, một herestring thích hợp hơn là đường ống từ printf (xem câu trả lời của l0b0).
ata

1
@ata Mặc dù tôi đã nghe thấy "thích hợp" này thường xuyên, nhưng phải lưu ý rằng việc di truyền luôn đòi hỏi /tmpthư mục phải có thể ghi được, vì nó phụ thuộc vào việc có thể tạo một tệp công việc tạm thời. Nếu bạn thấy mình trên một hệ thống bị hạn chế ở chế độ /tmpchỉ đọc (và không thể thay đổi bởi bạn), bạn sẽ hài lòng về khả năng sử dụng giải pháp thay thế, ví dụ như với printfđường ống.
cú pháp

1
Trong ví dụ thứ hai, nếu biến nhiều dòng không chứa một dòng mới, bạn sẽ mất phần tử cuối cùng. Đổi nó thành:printf "%s\n" "$var" | while IFS= read -r line
David H. Bennett

26

Để xử lý đầu ra của một dòng lệnh theo dòng ( giải thích ):

jobs |
while IFS= read -r line; do
  process "$line"
done

Nếu bạn đã có dữ liệu trong một biến:

printf %s "$foo" | 

printf %s "$foo"gần như giống hệt echo "$foo", nhưng in theo $foonghĩa đen, trong khi echo "$foo"có thể hiểu $foolà một tùy chọn cho lệnh echo nếu nó bắt đầu bằng a -và có thể mở rộng các chuỗi dấu gạch chéo ngược $footrong một số shell.

Lưu ý rằng trong một số shell (ash, bash, pdksh, nhưng không phải ksh hoặc zsh), phía bên phải của một đường ống chạy trong một quy trình riêng biệt, do đó, bất kỳ biến nào bạn đặt trong vòng lặp đều bị mất. Ví dụ: tập lệnh đếm dòng sau đây in 0 trong các shell này:

n=0
printf %s "$foo" |
while IFS= read -r line; do
  n=$(($n + 1))
done
echo $n

Một cách giải quyết là đặt phần còn lại của tập lệnh (hoặc ít nhất là phần cần giá trị $ntừ vòng lặp) vào danh sách lệnh:

n=0
printf %s "$foo" | {
  while IFS= read -r line; do
    n=$(($n + 1))
  done
  echo $n
}

Nếu hành động trên các dòng không trống là đủ tốt và đầu vào không lớn, bạn có thể sử dụng phân tách từ:

IFS='
'
set -f
for line in $(jobs); do
  # process line
done
set +f
unset IFS

Giải thích: cài đặt IFSthành một dòng mới làm cho việc phân tách từ chỉ xảy ra ở dòng mới (trái ngược với bất kỳ ký tự khoảng trắng nào trong cài đặt mặc định). set -ftắt Globing (tức là mở rộng ký tự đại diện), điều này sẽ xảy ra đối với kết quả của việc thay thế lệnh $(jobs)hoặc thay thế biến $foo. Các forvòng lặp hoạt động trên tất cả các mảnh của $(jobs), đó là tất cả các dòng không trống trong đầu ra lệnh. Cuối cùng, khôi phục lại toàn cầu và IFScài đặt.


Tôi gặp vấn đề với việc thiết lập IFS và bỏ đặt IFS. Tôi nghĩ điều đúng đắn cần làm là lưu trữ giá trị cũ của IFS và đặt IFS trở lại giá trị cũ đó. Tôi không phải là một chuyên gia bash, nhưng theo kinh nghiệm của tôi, điều này đưa bạn trở lại với bahavior ban đầu.
Bjorn Roche

1
@BjornRoche: bên trong một chức năng, sử dụng local IFS=something. Nó sẽ không ảnh hưởng đến giá trị phạm vi toàn cầu. IIRC, unset IFSkhông đưa bạn trở lại mặc định (và chắc chắn không hoạt động nếu trước đó không phải là mặc định).
Peter Cordes

14

Vấn đề: nếu bạn sử dụng vòng lặp while, nó sẽ chạy trong subshell và tất cả các biến sẽ bị mất. Giải pháp: sử dụng cho vòng lặp

# change delimiter (IFS) to new line.
IFS_BAK=$IFS
IFS=$'\n'

for line in $variableWithSeveralLines; do
 echo "$line"

 # return IFS back if you need to split new line by spaces:
 IFS=$IFS_BAK
 IFS_BAK=
 lineConvertedToArraySplittedBySpaces=( $line )
 echo "{lineConvertedToArraySplittedBySpaces[0]}"
 # return IFS back to newline for "for" loop
 IFS_BAK=$IFS
 IFS=$'\n'

done 

# return delimiter to previous value
IFS=$IFS_BAK
IFS_BAK=

CẢM ƠN BẠN RẤT NHIỀU!! Tất cả các giải pháp trên đều thất bại đối với tôi.
hax0rettcode

Đường ống vào một while readvòng lặp trong bash có nghĩa là vòng lặp while nằm trong một khung con, vì vậy các biến không phải là toàn cục. while read;do ;done <<< "$var"làm cho cơ thể vòng lặp không phải là một subshell. (Bash gần đây có một tùy chọn để đặt phần thân của một cmd | whilevòng lặp không nằm trong một khung con, như ksh luôn có.)
Peter Cordes


10

Trong các phiên bản bash gần đây, sử dụng mapfilehoặc readarrayđể đọc hiệu quả đầu ra lệnh thành mảng

$ readarray test < <(ls -ltrR)
$ echo ${#test[@]}
6305

Disclaimer: ví dụ khủng khiếp, nhưng bạn có thể hoàn toàn đưa ra một lệnh tốt hơn để sử dụng hơn là chính mình


Đó là một cách hay, nhưng xả rác / var / tmp với các tệp tạm thời trên hệ thống của tôi. Dù sao +1
Eugene Yarmash

@eugene: thật buồn cười. Hệ thống (distro / OS) là gì?
sehe

Đó là FreeBSD 8. Cách tái tạo: đặt readarraymột hàm và gọi hàm đó một vài lần.
Eugene Yarmash

Đẹp đấy, @sehe. +1
Teresa e Junior

7
jobs="$(jobs)"
while IFS= read
do
    echo $REPLY
done <<< "$jobs"

Người giới thiệu:


3
-rcũng là một ý tưởng tốt; Nó ngăn \` interpretation... (it is in your links, but its probably worth mentioning, just to round out your IFS = `(điều cần thiết để tránh mất khoảng trắng)
Peter.O

-1

Bạn có thể sử dụng <<< để chỉ cần đọc từ biến chứa dữ liệu được phân tách bằng dòng mới:

while read -r line
do 
  echo "A line of input: $line"
done <<<"$lines"

1
Chào mừng bạn đến với Unix & Linux! Điều này về cơ bản trùng lặp một câu trả lời từ bốn năm trước. Vui lòng không đăng câu trả lời trừ khi bạn có điều gì đó mới để đóng góp.
G-Man nói 'Phục hồi Monica'
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.