Bagaimana cara membaca dari file atau STDIN di Bash?

245

Script Perl berikut ( my.pl) dapat membaca dari file pada argumen baris perintah atau dari STDIN:

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

perl my.plakan membaca dari STDIN, sementara perl my.pl a.txtakan membaca dari a.txt. Ini sangat nyaman.

Ingin tahu apakah ada yang setara di Bash?

Dagang
sumber

Jawaban:

410

Solusi berikut dibaca dari file jika skrip dipanggil dengan nama file sebagai parameter pertama $1dari input standar.

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

Substitusi ${1:-...}mengambil $1jika didefinisikan sebaliknya nama file dari input standar dari proses sendiri digunakan.

Fritz G. Mehner
sumber
1
Bagus, itu berhasil. Pertanyaan lain adalah mengapa Anda menambahkan penawaran untuk itu? "$ {1: - / proc / $ {$} / fd / 0}"
Dagang
15
Nama file yang Anda berikan pada baris perintah bisa kosong.
Fritz G. Mehner
3
Apakah ada perbedaan antara menggunakan /proc/$$/fd/0dan /dev/stdin? Saya perhatikan yang terakhir tampaknya lebih umum dan terlihat lebih mudah.
knowah
19
Lebih baik menambahkan perintah -rAnda read, sehingga tidak sengaja memakan \ karakter; gunakan while IFS= read -r lineuntuk melestarikan spasi putih terkemuka dan tertinggal.
mklement0
1
@NeDark: Sangat ingin tahu; Saya baru saja memverifikasi bahwa itu berfungsi pada platform itu, bahkan ketika menggunakan /bin/sh- apakah Anda menggunakan shell selain bashatau sh?
mklement0
119

Mungkin solusi paling sederhana adalah mengarahkan ulang stdin dengan operator pengalihan penggabungan:

#!/bin/bash
less <&0

Stdin adalah deskriptor file nol. Di atas mengirimkan input yang disalurkan ke skrip bash Anda ke less's stdin.

Baca selengkapnya tentang pengalihan deskriptor file .

Ryan Ballantyne
sumber
1
Saya berharap saya memiliki lebih banyak suara untuk diberikan kepada Anda, saya sudah mencari ini selama bertahun-tahun.
Marcus Downing
13
Tidak ada manfaat untuk menggunakan <&0dalam situasi ini - contoh Anda akan bekerja sama dengan atau tanpa itu - tampaknya, alat yang Anda panggil dari dalam skrip bash secara default melihat stdin yang sama dengan skrip itu sendiri (kecuali skrip yang menggunakannya terlebih dahulu).
mklement0
@ mkelement0 Jadi jika alat membaca setengah dari buffer input, akankah alat berikutnya saya meminta sisanya?
Asad Saeeduddin
"Missen filename (" less --help "for help)" ketika saya melakukan ini ... Ubuntu 16.04
OmarOthman
5
di mana bagian "atau dari file" dalam jawaban ini?
Sebastian
85

Inilah cara paling sederhana:

#!/bin/sh
cat -

Pemakaian:

$ echo test | sh my_script.sh
test

Untuk menetapkan stdin ke variabel, Anda dapat menggunakan: STDIN=$(cat -)atau hanya STDIN=$(cat)karena operator tidak diperlukan (sesuai komentar @ mklement0 ).


Untuk mengurai setiap baris dari input standar , coba skrip berikut:

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

Untuk membaca dari file atau stdin (jika argumen tidak ada), Anda dapat memperluas ke:

#!/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")

Catatan:

- read -r- Jangan perlakukan karakter backslash dengan cara khusus apa pun. Pertimbangkan setiap garis miring terbalik sebagai bagian dari jalur input.

- Tanpa pengaturan IFS, secara default urutan Spacedan Tabdi awal dan akhir garis diabaikan (dipangkas).

- Gunakan printfalih-alih echountuk menghindari mencetak garis kosong ketika garis terdiri dari satu -e, -natau -E. Namun ada solusi dengan menggunakan env POSIXLY_CORRECT=1 echo "$line"yang mengeksekusi GNU eksternal Anda echoyang mendukungnya. Lihat: Bagaimana saya menggemakan "-e"?

Lihat: Bagaimana membaca stdin ketika tidak ada argumen yang disampaikan? di stackoverflow SE

kenorb
sumber
Anda bisa menyederhanakan [ "$1" ] && FILE=$1 || FILE="-"untuk FILE=${1:--}. (Quibble: lebih baik untuk menghindari variabel shell huruf besar semua untuk menghindari tabrakan nama dengan variabel lingkungan .)
mklement0
Dengan senang hati; sebenarnya, ${1:--} adalah POSIX-compliant, jadi ia harus bekerja di semua kerang mirip POSIX. Apa yang tidak akan bekerja di semua shell tersebut adalah substitusi proses ( <(...)); itu akan bekerja di bash, ksh, zsh, tetapi tidak di dash, misalnya. Juga, lebih baik menambahkan perintah -rAnda read, sehingga tidak sengaja memakan \ karakter; bertanggung jawab IFS= untuk melestarikan spasi putih terkemuka dan tertinggal.
mklement0
4
Bahkan kode Anda masih rusak karena echo: jika suatu baris terdiri dari -e, -natau -E, itu tidak akan ditampilkan. Untuk mengatasinya, Anda harus menggunakan printf: printf '%s\n' "$line". Saya tidak memasukkannya dalam edit saya sebelumnya ... terlalu sering suntingan saya dibatalkan ketika saya memperbaiki kesalahan ini :(.
gniourf_gniourf
1
Tidak, itu tidak gagal. Dan --tidak ada gunanya jika argumen pertama adalah'%s\n'
gniourf_gniourf
1
Jawaban saya baik-baik saja oleh saya (maksud saya tidak ada bug atau fitur yang tidak diinginkan yang saya sadari lagi) - meskipun tidak memperlakukan beberapa argumen seperti Perl. Bahkan, jika Anda ingin menangani beberapa argumen, Anda akan berakhir menulis Jonathan Leffler baik jawaban-pada kenyataannya Anda akan lebih baik karena Anda akan menggunakan IFS=dengan readdan printfbukan echo. :).
gniourf_gniourf
19

Saya pikir ini adalah jalan lurus ke depan:

$ 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
Amir Mehler
sumber
4
Ini tidak sesuai dengan persyaratan oleh poster untuk membaca dari stdin atau argumen file, ini hanya dibaca dari stdin.
nash
3
Meninggalkan @ keberatan berlaku nash ini selain: readmembaca dari stdin secara default , jadi ada tidak perlu untuk < /dev/stdin.
mklement0
13

The echosolusi menambahkan baris baru setiap kali IFSistirahat input stream. Jawaban @ fgm dapat dimodifikasi sedikit:

cat "${1:-/dev/stdin}" > "${2:-/dev/stdout}"
David Souther
sumber
Bisakah Anda jelaskan apa yang Anda maksud dengan "solusi gema menambah baris baru setiap kali IFS merusak aliran input"? Dalam kasus Anda mengacu pada read's perilaku: sementara read tidak berpotensi dibagi menjadi beberapa token oleh karakter. terkandung dalam $IFS, itu hanya mengembalikan token tunggal jika Anda hanya menentukan nama variabel tunggal (tetapi trims dan memimpin dan mengikuti spasi putih secara default).
mklement0
@ mklement0 Saya setuju 100% dengan Anda tentang perilaku readdan $IFS- echoitu sendiri menambahkan baris baru tanpa -nbendera. "Utilitas gema menulis operan tertentu, dipisahkan oleh karakter tunggal kosong (` ') dan diikuti oleh karakter baris baru (`\ n'), ke output standar."
David Souther
Mengerti. Namun, untuk meniru loop Perl Anda membutuhkan trailing yang \nditambahkan oleh echo: Perl $_ termasuk baris yang berakhir \ndari baris yang dibaca, sedangkan bash readtidak. (Namun, seperti yang ditunjukkan @gniourf_gniourf di tempat lain, pendekatan yang lebih kuat adalah untuk digunakan printf '%s\n'sebagai pengganti echo).
mklement0
8

Loop Perl dalam pertanyaan membaca dari semua argumen nama file pada baris perintah, atau dari input standar jika tidak ada file yang ditentukan. Jawaban yang saya lihat sepertinya memproses satu file atau input standar jika tidak ada file yang ditentukan.

Meskipun sering diejek secara akurat sebagai UUOC (Penggunaan yang Tidak Berguna cat), ada kalanya catalat terbaik untuk pekerjaan itu, dan dapat diperdebatkan bahwa ini adalah salah satunya:

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

Satu-satunya downside ke ini adalah bahwa ia menciptakan sebuah pipa berjalan di sub-shell, sehingga hal-hal seperti penugasan variabel dalam whileloop tidak dapat diakses di luar pipa. The bashcara mengatasinya adalah Proses Pergantian :

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

Ini meninggalkan whileloop berjalan di shell utama, sehingga variabel yang diatur dalam loop dapat diakses di luar loop.

Jonathan Leffler
sumber
1
Poin bagus tentang banyak file. Saya tidak tahu apa implikasi sumber daya dan kinerja yang akan terjadi, tetapi jika Anda tidak menggunakan bash, ksh, atau zsh dan karena itu tidak dapat menggunakan substitusi proses, Anda dapat mencoba dokumen di sini dengan substitusi perintah (tersebar di 3 baris) >>EOF\n$(cat "$@")\nEOF. Akhirnya, quibble: while IFS= read -r lineadalah perkiraan yang lebih baik dari apa yang while (<>)dilakukan di Perl (mempertahankan spasi putih terdepan dan tertinggal - meskipun Perl juga menjaga trailing \n).
mklement0
4

Perilaku Perl, dengan kode yang diberikan dalam OP dapat mengambil tidak ada atau beberapa argumen, dan jika argumen adalah tanda hubung tunggal -ini dipahami sebagai stdin. Selain itu, selalu memungkinkan untuk memiliki nama file $ARGV. Tidak ada jawaban yang diberikan sejauh ini yang benar-benar meniru perilaku Perl dalam hal ini. Inilah kemungkinan Bash murni. Caranya adalah menggunakan dengan exectepat.

#!/bin/bash

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

Nama file tersedia di $1.

Jika tidak ada argumen yang diberikan, kami secara artifisial menetapkan -sebagai parameter posisi pertama. Kami kemudian mengulangi parameter. Jika parameter tidak -, kami mengarahkan input standar dari nama file dengan exec. Jika pengalihan ini berhasil, kami mengulang dengan whileloop. Saya menggunakan REPLYvariabel standar , dan dalam hal ini Anda tidak perlu mengatur ulang IFS. Jika Anda ingin nama lain, Anda harus mengatur ulang IFSseperti itu (kecuali, tentu saja, Anda tidak menginginkannya dan tahu apa yang Anda lakukan):

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

Lebih akurat...

while IFS= read -r line ; do
    printf "%s\n" "$line"
done < file
sorpigal
sumber
2
Saya berasumsi ini pada dasarnya adalah komentar di stackoverflow.com/a/6980232/45375 , bukan jawaban. Untuk membuat komentar eksplisit: menambahkan IFS=dan -r ke readperintah memastikan bahwa setiap baris dibaca tidak dimodifikasi (termasuk spasi spasi awal dan akhir).
mklement0
2

Silakan coba kode berikut:

while IFS= read -r line; do
    echo "$line"
done < file
Pencuri web
sumber
1
Perhatikan bahwa meskipun telah diubah, ini tidak akan membaca dari input standar, atau dari banyak file, jadi itu bukan jawaban yang lengkap untuk pertanyaan tersebut. (Juga mengejutkan melihat dua suntingan dalam hitungan menit lebih dari 3 tahun setelah jawaban pertama kali disampaikan.)
Jonathan Leffler
@JonathanLeffler maaf karena mengedit jawaban yang lama (dan tidak terlalu bagus) ... tapi saya tidak tahan melihat orang miskin ini readtanpa IFS=dan -r, dan orang miskin $linetanpa tanda kutip yang sehat.
gniourf_gniourf
1
@ gniourf_gniourf: Saya tidak suka read -rnotasi. IMO, POSIX salah; opsi harus mengaktifkan makna khusus untuk trailing backslash, bukan menonaktifkannya - sehingga skrip yang ada (dari sebelum POSIX ada) tidak akan rusak karena -rdihilangkan. Saya mengamati, bagaimanapun, bahwa itu adalah bagian dari IEEE 1003.2 1992, yang merupakan versi paling awal dari shell POSIX dan standar utilitas, tetapi itu ditandai sebagai tambahan bahkan kemudian, jadi ini menggerutu tentang peluang yang sudah lama hilang. Saya tidak pernah mengalami masalah karena kode saya tidak digunakan -r; Saya pasti beruntung. Abaikan aku dalam hal ini.
Jonathan Leffler
1
@ JonathanLeffler Saya sangat setuju itu -rharus menjadi standar. Saya setuju bahwa tidak mungkin dalam kasus di mana tidak menggunakannya menyebabkan masalah. Padahal, kode rusak adalah kode rusak. Suntingan saya pertama-tama dipicu oleh $linevariabel yang buruk itu yang terlewatkan kutipnya. Saya memperbaiki readsementara saya berada di itu. Saya tidak memperbaiki echokarena itulah jenis pengeditan yang dibatalkan. :(.
gniourf_gniourf
1

Kode ${1:-/dev/stdin}hanya akan mengerti argumen pertama, jadi, bagaimana dengan ini.

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

Saya tidak menemukan jawaban ini yang dapat diterima. Secara khusus, jawaban yang diterima hanya menangani parameter baris perintah pertama dan mengabaikan sisanya. Program Perl yang ia coba tiru menangani semua parameter baris perintah. Jadi jawaban yang diterima bahkan tidak menjawab pertanyaan. Jawaban lain menggunakan ekstensi bash, tambahkan perintah 'cat' yang tidak perlu, hanya berfungsi untuk kasus sederhana dari gema input ke output, atau hanya rumit yang tidak perlu.

Namun, saya harus memberi mereka kredit karena mereka memberi saya beberapa ide. Inilah jawaban lengkapnya:

#!/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
Gungwald
sumber
1

Saya menggabungkan semua jawaban di atas dan membuat fungsi shell yang sesuai dengan kebutuhan saya. Ini dari terminal cygwin dari 2 mesin Windows10 saya di mana saya memiliki folder bersama di antara mereka. Saya harus bisa menangani yang berikut ini:

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

Di mana nama file tertentu ditentukan, saya harus menggunakan nama file yang sama saat menyalin. Di mana aliran data input telah disalurkan melalui, maka saya perlu membuat nama file sementara yang memiliki jam menit dan detik. Mainfolder bersama memiliki subfolder dari hari-hari dalam seminggu. Ini untuk tujuan organisasi.

Lihatlah, naskah akhir untuk kebutuhan saya:

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
}

Jika ada cara yang dapat Anda lihat untuk lebih mengoptimalkan ini, saya ingin tahu.

kebenaranadjustr
sumber
0

Berikut ini berfungsi dengan standar sh(Diuji dengan dashpada Debian) dan cukup mudah dibaca, tapi itu masalah selera:

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

Detail: Jika parameter pertama tidak kosong maka catfile itu, atau catinput standar. Kemudian output dari seluruh ifpernyataan diproses oleh commands_and_transformations.

Tidak dalam daftar
sumber
IMHO jawaban terbaik sehingga karena menunjuk ke solusi yang benar: cat "${1:--}" | any_command. Membaca ke variabel shell dan menggema mereka mungkin bekerja untuk file kecil tetapi tidak skala dengan baik.
Andreas Spindler
The [ -n "$1" ]dapat disederhanakan [ "$1" ].
agc
0

Yang ini mudah digunakan di terminal:

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

Bagaimana tentang

for line in `cat`; do
    something($line);
done
Charles Cooper
sumber
Output catakan ditempatkan ke dalam baris perintah. Baris perintah memiliki ukuran maksimum. Juga ini tidak akan membaca baris demi baris, tetapi kata demi kata.
Notinlist