Làm thế nào để đọc từ một tập tin hoặc STDIN trong Bash?


244

Tập lệnh Perl sau đây ( my.pl) có thể đọc từ tệp trên dòng lệnh args hoặc từ STDIN:

while (<>) {
   print($_);
}

perl my.plsẽ đọc từ STDIN, trong khi perl my.pl a.txtsẽ đọc từ a.txt. Điều này rất thuận tiện.

Tự hỏi có một tương đương trong Bash?

Câu trả lời:


409

Giải pháp sau đây đọc từ một tệp nếu tập lệnh được gọi với tên tệp là tham số đầu tiên $1khác với đầu vào tiêu chuẩn.

while read line
do
  echo "$line"
done < "${1:-/dev/stdin}"

Việc thay thế ${1:-...}sẽ được sử dụng $1nếu được định nghĩa khác nếu tên tệp của đầu vào tiêu chuẩn của quy trình riêng được sử dụng.


1
Đẹp, nó hoạt động. Một câu hỏi khác là tại sao bạn thêm một trích dẫn cho nó? "$ {1: - / Proc / $ {$} / fd / 0}"
Dagang

15
Tên tệp bạn cung cấp trên dòng lệnh có thể có khoảng trống.
Fritz G. Mehner

3
Có sự khác biệt nào giữa việc sử dụng /proc/$$/fd/0/dev/stdinkhông? Tôi nhận thấy cái sau dường như phổ biến hơn và trông đơn giản hơn.
biết

19
Tốt hơn là thêm -rvào readlệnh của bạn , để nó không vô tình ăn \ chars; sử dụng while IFS= read -r lineđể bảo tồn khoảng trắng hàng đầu và dấu.
mkuity0

1
@NeDark: Thật tò mò; Tôi chỉ xác nhận rằng nó hoạt động trên nền tảng đó, ngay cả khi sử dụng /bin/sh- bạn đang sử dụng một lớp vỏ ngoài bashhay sh?
mkuity0

119

Có lẽ giải pháp đơn giản nhất là chuyển hướng stdin với toán tử chuyển hướng hợp nhất:

#!/bin/bash
less <&0

Stdin là mô tả tập tin bằng không. Ở trên sẽ gửi đầu vào đường ống đến tập lệnh bash của bạn vào stdin ít hơn.

Đọc thêm về chuyển hướng mô tả tập tin .


1
Tôi ước tôi có nhiều upvote để cung cấp cho bạn, tôi đã tìm kiếm điều này trong nhiều năm.
Marcus Downing

13
Không có lợi ích gì khi sử dụng <&0trong tình huống này - ví dụ của bạn sẽ hoạt động giống nhau hoặc không có nó - dường như, các công cụ bạn gọi từ trong tập lệnh bash theo mặc định sẽ xem stdin giống như chính tập lệnh (trừ khi tập lệnh sử dụng nó trước).
mkuity0

@ mkelement0 Vì vậy, nếu một công cụ đọc một nửa bộ đệm đầu vào, công cụ tiếp theo tôi gọi có nhận được phần còn lại không?
Asad Saeeduddin

"Thiếu tên tệp (" ít hơn - trợ giúp "để được giúp đỡ)" khi tôi làm điều này ... Ubuntu 16.04
OmarOthman

5
phần "hoặc từ tập tin" ở đâu trong câu trả lời này?
Sebastian

84

Đây là cách đơn giản nhất:

#!/bin/sh
cat -

Sử dụng:

$ echo test | sh my_script.sh
test

Để gán stdin cho biến, bạn có thể sử dụng: STDIN=$(cat -)hoặc chỉ đơn giản STDIN=$(cat)là toán tử là không cần thiết (theo nhận xét @ mkuity0 ).


Để phân tích từng dòng từ đầu vào tiêu chuẩn , hãy thử đoạn mã sau:

#!/bin/bash
while IFS= read -r line; do
  printf '%s\n' "$line"
done

Để đọc từ tệp hoặc stdin (nếu không có đối số), bạn có thể mở rộng nó thành:

#!/bin/bash
file=${1--} # POSIX-compliant; ${1:--} can be used either.
while IFS= read -r line; do
  printf '%s\n' "$line" # Or: env POSIXLY_CORRECT=1 echo "$line"
done < <(cat -- "$file")

Ghi chú:

- read -r- Không xử lý ký tự dấu gạch chéo ngược theo bất kỳ cách đặc biệt nào. Xem xét mỗi dấu gạch chéo ngược là một phần của dòng đầu vào.

- Không có cài đặt IFS, theo mặc định, các chuỗi SpaceTabở đầu và cuối của dòng bị bỏ qua (cắt xén).

- Sử dụng printfthay vì echođể tránh in các dòng trống khi dòng bao gồm một -e, -nhoặc -E. Tuy nhiên, có một cách giải quyết bằng cách sử dụng env POSIXLY_CORRECT=1 echo "$line"mà thực thi GNU bên ngoài của bạn echohỗ trợ nó. Xem: Làm thế nào để tôi lặp lại "-e"?

Xem: Làm thế nào để đọc stdin khi không có đối số được thông qua? tại stackoverflow SE


Bạn có thể đơn giản hóa [ "$1" ] && FILE=$1 || FILE="-"để FILE=${1:--}. (Quibble: tốt hơn để tránh các biến shell tất cả chữ hoa để tránh xung đột tên với các biến môi trường .)
mkuity0

Hân hạnh; trên thực tế, ${1:--} POSIX-compliant, vì vậy nó nên làm việc trong tất cả các POSIX giống như vỏ. Những gì sẽ không hoạt động trong tất cả các shell như vậy là quá trình thay thế ( <(...)); chẳng hạn, nó sẽ hoạt động trong bash, ksh, zsh, nhưng không phải trong dấu gạch ngang. Ngoài ra, tốt hơn để thêm -rvào readlệnh của bạn , để nó không vô tình ăn \ chars; chuẩn bị IFS= để bảo tồn khoảng trắng hàng đầu và dấu.
mkuity0

4
Trong thực tế, mã của bạn vẫn bị hỏng vì echo: nếu một dòng bao gồm -e, -nhoặc -E, nó sẽ không được hiển thị. Để khắc phục điều này, bạn phải sử dụng printf: printf '%s\n' "$line". Tôi đã không đưa nó vào bản chỉnh sửa trước đây của mình quá thường xuyên, các chỉnh sửa của tôi bị đảo ngược khi tôi sửa lỗi này :(.
gniourf_gniourf

1
Không, nó không thất bại. Và --sẽ vô ích nếu đối số đầu tiên là'%s\n'
gniourf_gniourf

1
Câu trả lời của bạn là tốt đối với tôi (ý tôi là không có lỗi hoặc các tính năng không mong muốn mà tôi biết nữa) Mặc dù nó không xử lý nhiều đối số như Perl. Trên thực tế, nếu bạn muốn xử lý nhiều đối số, cuối cùng bạn sẽ viết câu trả lời xuất sắc của Jonathan Leffler trên thực tế, câu trả lời của bạn sẽ tốt hơn vì bạn sử dụng IFS=cùng readprintfthay vì echo. :).
gniourf_gniourf

19

Tôi nghĩ rằng đây là cách đơn giản:

$ cat reader.sh
#!/bin/bash
while read line; do
  echo "reading: ${line}"
done < /dev/stdin

-

$ cat writer.sh
#!/bin/bash
for i in {0..5}; do
  echo "line ${i}"
done

-

$ ./writer.sh | ./reader.sh
reading: line 0
reading: line 1
reading: line 2
reading: line 3
reading: line 4
reading: line 5

4
Điều này không phù hợp với yêu cầu của người đăng để đọc từ stdin hoặc đối số tệp, điều này chỉ đọc từ stdin.
nash

2
Rời @ phản đối hợp lệ Nash sang một bên: readđọc từ stdin theo mặc định , do đó không cần phải cho < /dev/stdin.
mkuity0

13

Các echogiải pháp bổ sung thêm dòng mới bất cứ khi nào IFSphá vỡ các dòng đầu vào. Câu trả lời của @ fgm có thể được sửa đổi một chút:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"

Bạn có thể giải thích ý của bạn bằng cách "giải pháp echo thêm dòng mới bất cứ khi nào IFS phá vỡ luồng đầu vào" không? Trong trường hợp bạn đang đề cập đến readhành vi của: trong khi đó read khả năng phân chia thành nhiều mã thông báo bằng ký tự. chứa trong $IFSnó, nó chỉ trả về một mã thông báo duy nhất nếu bạn chỉ chỉ định một tên biến duy nhất (nhưng cắt và khoảng trắng theo dõi và theo dõi theo mặc định).
mkuity0

@ mkuity0 Tôi đồng ý 100% với bạn về hành vi của read$IFS- echochính nó thêm các dòng mới mà không cần -ncờ. "Tiện ích echo ghi bất kỳ toán hạng được chỉ định nào, được phân tách bằng các ký tự trống (` ') và theo sau là một ký tự dòng mới (`\ n'), cho đầu ra tiêu chuẩn."
David Souther

Hiểu rồi. Tuy nhiên, để mô phỏng vòng lặp Perl, bạn cần theo dõi \nđược thêm bởi echo: Perl's $_ bao gồm dòng kết thúc \ntừ dòng đọc, trong khi bash readthì không. (Tuy nhiên, như @gniourf_gniourf chỉ ra ở nơi khác, cách tiếp cận mạnh mẽ hơn là sử dụng printf '%s\n'thay cho echo).
mkuity0

8

Vòng lặp Perl trong câu hỏi đọc từ tất cả các đối số tên tệp trên dòng lệnh hoặc từ đầu vào tiêu chuẩn nếu không có tệp nào được chỉ định. Các câu trả lời tôi thấy dường như tất cả đều xử lý một tệp hoặc đầu vào tiêu chuẩn nếu không có tệp nào được chỉ định.

Mặc dù thường bị chế giễu chính xác là UUOC (Sử dụng vô dụng cat), đôi khi catcó công cụ tốt nhất cho công việc và người ta cho rằng đây là một trong số đó:

cat "$@" |
while read -r line
do
    echo "$line"
done

Nhược điểm duy nhất của việc này là nó tạo ra một đường ống chạy trong lớp vỏ phụ, do đó, những thứ như phép gán biến trong whilevòng lặp không thể truy cập được bên ngoài đường ống. Các bashcon đường xung quanh đó là Process Thay :

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

Điều này làm cho whilevòng lặp chạy trong vỏ chính, vì vậy các biến được đặt trong vòng lặp có thể truy cập bên ngoài vòng lặp.


1
Điểm tuyệt vời về nhiều tập tin. Tôi không biết ý nghĩa của tài nguyên và hiệu suất sẽ là gì, nhưng nếu bạn không sử dụng bash, ksh hoặc zsh và do đó không thể sử dụng thay thế quy trình, bạn có thể thử tài liệu thay thế lệnh này (trải rộng trên 3 dòng) >>EOF\n$(cat "$@")\nEOF. Cuối cùng, một ngụy biện: while IFS= read -r linelà một xấp xỉ tốt hơn những gì while (<>)làm trong Perl (duy trì khoảng trắng hàng đầu và dấu vết - mặc dù Perl cũng giữ dấu vết \n).
mkuity0

4

Hành vi của Perl, với mã được đưa ra trong OP có thể không có hoặc có một vài đối số và nếu một đối số là một dấu gạch nối đơn -thì điều này được hiểu là stdin. Hơn nữa, luôn luôn có thể có tên tệp với $ARGV. Không có câu trả lời nào được đưa ra cho đến nay thực sự bắt chước hành vi của Perl ở những khía cạnh này. Đây là một khả năng Bash thuần túy. Bí quyết là sử dụng exechợp lý.

#!/bin/bash

(($#)) || set -- -
while (($#)); do
   { [[ $1 = - ]] || exec < "$1"; } &&
   while read -r; do
      printf '%s\n' "$REPLY"
   done
   shift
done

Tên tệp có sẵn trong $1.

Nếu không có đối số nào được đưa ra, chúng ta đặt giả tạo -là tham số vị trí đầu tiên. Chúng tôi sau đó lặp trên các tham số. Nếu một tham số là không -, chúng tôi chuyển hướng đầu vào tiêu chuẩn từ tên tệp với exec. Nếu chuyển hướng này thành công, chúng ta lặp với một whilevòng lặp. Tôi đang sử dụng REPLYbiến tiêu chuẩn và trong trường hợp này bạn không cần đặt lại IFS. Nếu bạn muốn một tên khác, bạn phải đặt lại IFSnhư vậy (tất nhiên trừ khi bạn không muốn điều đó và biết bạn đang làm gì):

while IFS= read -r line; do
    printf '%s\n' "$line"
done

2

Chính xác hơn...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file

2
Tôi cho rằng đây thực chất là một nhận xét về stackoverflow.com/a/6980232/45375 , không phải là một câu trả lời. Để làm cho nhận xét rõ ràng: thêm IFS=-r vào readlệnh đảm bảo rằng mỗi dòng được đọc không thay đổi (bao gồm cả khoảng trắng đầu và cuối).
mkuity0

2

Vui lòng thử mã sau đây:

while IFS= read -r line; do
    echo "$line"
done < file

1
Lưu ý rằng ngay cả khi được sửa đổi, điều này sẽ không được đọc từ đầu vào tiêu chuẩn hoặc từ nhiều tệp, vì vậy nó không phải là một câu trả lời hoàn chỉnh cho câu hỏi. (Thật đáng ngạc nhiên khi thấy hai bản chỉnh sửa trong vài phút hơn 3 năm sau khi câu trả lời được gửi lần đầu tiên.)
Jonathan Leffler

@JonathanLeffler xin lỗi để chỉnh sửa một câu trả lời cũ (và không thực sự tốt) như vậy ... nhưng tôi không thể đứng nhìn thấy điều này nghèo readkhông IFS=-r, và người nghèo $linemà không có dấu ngoặc kép khỏe mạnh của nó.
gniourf_gniourf

1
@gniourf_gniourf: Tôi không thích read -rký hiệu này. IMO, POSIX đã sai; tùy chọn sẽ kích hoạt ý nghĩa đặc biệt cho dấu gạch chéo ngược, không vô hiệu hóa nó - để các tập lệnh hiện có (từ trước khi POSIX tồn tại) sẽ không bị hỏng vì -rđã bị bỏ qua. Tuy nhiên, tôi quan sát rằng đó là một phần của IEEE 1003.2 1992, đây là phiên bản đầu tiên của vỏ POSIX và tiêu chuẩn tiện ích, nhưng nó được đánh dấu là một bổ sung ngay cả khi đó, vì vậy điều này đang làm hỏng các cơ hội lâu dài. Tôi chưa bao giờ gặp rắc rối vì mã của tôi không sử dụng -r; Tôi phải may mắn. Mặc kệ tôi về điều này.
Jonathan Leffler

1
@JonathanLeffler Tôi thực sự đồng ý rằng đó -rphải là tiêu chuẩn. Tôi đồng ý rằng không có khả năng xảy ra trường hợp không sử dụng nó dẫn đến rắc rối. Mặc dù, mã bị hỏng là mã bị hỏng. Chỉnh sửa của tôi lần đầu tiên được kích hoạt bởi $linebiến nghèo nàn đó đã bỏ lỡ các trích dẫn của nó. Tôi đã sửa readtrong khi tôi đang ở đó. Tôi đã không sửa lỗi echovì đó là loại chỉnh sửa được khôi phục. :(.
gniourf_gniourf

1

${1:-/dev/stdin}sẽ chỉ hiểu đối số đầu tiên, vì vậy, làm thế nào về điều này.

ARGS='$*'
if [ -z "$*" ]; then
  ARGS='-'
fi
eval "cat -- $ARGS" | while read line
do
   echo "$line"
done

1

Tôi không tìm thấy bất kỳ câu trả lời nào trong số này. Cụ thể, câu trả lời được chấp nhận chỉ xử lý tham số dòng lệnh đầu tiên và bỏ qua phần còn lại. Chương trình Perl mà nó đang cố gắng mô phỏng xử lý tất cả các tham số dòng lệnh. Vì vậy, câu trả lời được chấp nhận thậm chí không trả lời câu hỏi. Các câu trả lời khác sử dụng tiện ích mở rộng bash, thêm các lệnh 'cat' không cần thiết, chỉ hoạt động trong trường hợp đơn giản là lặp lại đầu vào thành đầu ra hoặc chỉ phức tạp không cần thiết.

Tuy nhiên, tôi phải cung cấp cho họ một số tín dụng vì họ đã cho tôi một số ý tưởng. Đây là câu trả lời đầy đủ:

#!/bin/sh

if [ $# = 0 ]
then
        DEFAULT_INPUT_FILE=/dev/stdin
else
        DEFAULT_INPUT_FILE=
fi

# Iterates over all parameters or /dev/stdin
for FILE in "$@" $DEFAULT_INPUT_FILE
do
        while IFS= read -r LINE
        do
                # Do whatever you want with LINE here.
                echo $LINE
        done < "$FILE"
done

1

Tôi đã kết hợp tất cả các câu trả lời ở trên và tạo ra một hàm shell phù hợp với nhu cầu của tôi. Đây là từ một thiết bị đầu cuối cygwin của 2 máy Windows10 của tôi, nơi tôi có một thư mục dùng chung giữa chúng. Tôi cần có khả năng xử lý như sau:

  • cat file.cpp | tx
  • tx < file.cpp
  • tx file.cpp

Khi một tên tệp cụ thể được chỉ định, tôi cần sử dụng cùng tên tệp trong khi sao chép. Trường hợp luồng dữ liệu đầu vào đã được chuyển qua, thì tôi cần tạo một tên tệp tạm thời có giờ và giây. Mainfolder được chia sẻ có các thư mục con của các ngày trong tuần. Đây là cho mục đích tổ chức.

Kìa, kịch bản cuối cùng cho nhu cầu của tôi:

tx ()
{
  if [ $# -eq 0 ]; then
    local TMP=/tmp/tx.$(date +'%H%M%S')
    while IFS= read -r line; do
        echo "$line"
    done < /dev/stdin > $TMP
    cp $TMP //$OTHER/stargate/$(date +'%a')/
    rm -f $TMP
  else
    [ -r $1 ] && cp $1 //$OTHER/stargate/$(date +'%a')/ || echo "cannot read file"
  fi
}

Nếu có bất kỳ cách nào bạn có thể thấy để tiếp tục tối ưu hóa điều này, tôi muốn biết.


0

Phần sau hoạt động với tiêu chuẩn sh(Đã thử nghiệm dashtrên Debian) và khá dễ đọc, nhưng đó là vấn đề của hương vị:

if [ -n "$1" ]; then
    cat "$1"
else
    cat
fi | commands_and_transformations

Chi tiết: Nếu tham số đầu tiên không trống thì cattệp đó, catđầu vào tiêu chuẩn khác . Sau đó, đầu ra của toàn bộ ifcâu lệnh được xử lý bởi commands_and_transformations.


IMHO câu trả lời tốt nhất vì nó chỉ ra giải pháp thực sự : cat "${1:--}" | any_command. Đọc các biến shell và lặp lại chúng có thể hoạt động đối với các tệp nhỏ nhưng không mở rộng tốt như vậy.
Andreas Spindler

[ -n "$1" ]thể được đơn giản hóa để [ "$1" ].
agc

0

Điều này rất dễ sử dụng trên thiết bị đầu cuối:

$ echo '1\n2\n3\n' | while read -r; do echo $REPLY; done
1
2
3

-1

Làm thế nào về

for line in `cat`; do
    something($line);
done

Đầu ra của catsẽ được đặt vào dòng lệnh. Dòng lệnh có kích thước tối đa. Ngoài ra, điều này sẽ không đọc từng dòng, nhưng từng chữ.
Notinlist
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.