Membaca karakter demi karakter dengan bash read

8

Saya sudah mencoba menggunakan bash untuk membaca file karakter demi karakter.

Setelah banyak percobaan dan kesalahan, saya menemukan bahwa ini berfungsi:

exec 4<file.txt 
declare -i n
while read -r ch <&4; 
     n=0
     while [ ! $n -eq ${#ch} ]
           do  echo -n "${ch:$n:1}"
               (( n++ ))
          done
     echo "" 
     done

Yaitu, saya bisa membacanya baris demi baris dan kemudian mengulangi setiap baris char dengan char.

Sebelum melakukan ini, saya telah mencoba: exec 4<file.txt && while read -r -n1 ch <&4; do; echo -n "$ch"; done tetapi itu akan melewatkan semua spasi putih di file .

Bisakah Anda jelaskan mengapa? Apakah ada cara untuk membuat strategi kedua (yaitu membaca char by char dengan bash's read) berfungsi?

PSkocik
sumber
4
Tetapkan IFSapa-apa agar spasi putih bertahan dari pemisahan kata.
manatwork
Sudah mencoba itu dengan IFS = '', tapi saya kira itu hanya IFS =. Terima kasih!
PSkocik

Jawaban:

12

Anda perlu menghapus karakter spasi dari $IFSparameter untuk readberhenti melompati karakter yang memimpin dan mengekor (dengan -n1, karakter spasi jika ada yang akan memimpin dan mengekor, jadi dilewati):

while IFS= read -rn1 a; do printf %s "$a"; done

Tetapi meskipun demikian, bash's readakan melewati karakter baris baru, yang dapat Anda atasi:

while IFS= read -rn1 a; do printf %s "${a:-$'\n'}"; done

Meskipun Anda dapat menggunakan IFS= read -d '' -rn1sebagai gantinya atau bahkan lebih baik IFS= read -N1(ditambahkan pada 4.1, disalin dari ksh93(ditambahkan dalam o)) yang merupakan perintah untuk membaca satu karakter.

Perhatikan bahwa bash readtidak dapat mengatasi karakter NUL. Dan ksh93 memiliki masalah yang sama dengan bash.

Dengan zsh:

while read -ku0 a; do print -rn -- "$a"; done

(zsh dapat mengatasi karakter NUL).

Perhatikan bahwa mereka read -k/n/Nmembaca sejumlah karakter , bukan byte . Jadi untuk karakter multibyte, mereka mungkin harus membaca beberapa byte hingga karakter penuh dibaca. Jika input berisi karakter yang tidak valid, Anda mungkin berakhir dengan variabel yang berisi urutan byte yang tidak membentuk karakter yang valid dan yang shell akhirnya menghitung sebagai beberapa karakter . Misalnya di lokal UTF-8:

$ printf '\375\200\200\200\200ABC' | bash -c '
    IFS= read  -rN1 a; echo "${#a}"'
6

Itu \375akan memperkenalkan karakter UTF-8 6-byte. Namun, yang ke-6 ( A) di atas tidak valid untuk karakter UTF-8. Anda masih berakhir dengan \375\200\200\200\200Adi $a, yang bashdihitung sebagai 6 karakter meskipun 5 yang pertama tidak benar-benar karakter, hanya 5 byte yang tidak membentuk bagian dari karakter apa pun.

Stéphane Chazelas
sumber
Terima kasih. Sederhana dan indah. Saya benar-benar mencoba sesuatu untuk tujuan ini (memodifikasi variabel IFS), tapi itu agak tidak berhasil bagi saya jadi saya berakhir dengan ramuan itu (tidak perlu bermain dengan deskriptor file, dll.).
PSkocik
1
Menariknya, sepertinya menggunakan read -rN1bukan memecahkan masalah baris baru dan dengan demikian menghilangkan perlu memberikan baris baru sebagai default saat mencetak $a.
krb686
Hanya FTR saya sedang membaca file 4118 baris 20 MB. Menggunakan read -n1(char by char) membutuhkan waktu 4 menit 51 detik dan memanaskan laptop hingga 90 derajat. Menggunakan read -r(baris demi baris) membutuhkan 1,3 detik dan laptop tetap pada 54 derajat dengan kipas ganda diam.
WinEunuuchs2Unix
2

Ini adalah contoh sederhana menggunakan cut, forlingkaran & wc:

bytes=$(wc -c < /etc/passwd)
file=$(</etc/passwd)

for ((i=0; i<bytes; i++)); do
    echo $file | cut -c $i
done

CIUMAN bukan?

Gilles Quenot
sumber
Jika itu kiss, lalu apa adalah murni bashsolusi: file="$(</etc/passwd)"; bytes="${#file}"; for ((i=0;i<bytes;i++)); do echo "${file:i:1}"; done?
manatwork
Terima kasih untuk keduanya. Ya, jika saya harus mengambil karakter-karakter itu dari baris, saya mungkin juga mendapatkannya dari keseluruhan file. Saya menemukan solusi sch yang paling KISS.
PSkocik
@manatwork Itu solusi yang bagus dan sederhana. Meski begitu, menurut saya jawaban di atas menggunakan loop baca sedikit lebih cepat untuk beberapa alasan. Mungkin substring dalam bash cukup lambat?
krb686
@ krb686, sebenarnya keseluruhan bash“Ini terlalu besar dan terlalu lambat.” menurut bagian BUGS halaman manualnya. Namun demikian, masih lebih cepat untuk mengiris string dalam memori daripada membaca file lagi dan lagi untuk setiap karakter. Setidaknya di mesin saya: pastebin.com/zH5trQQs
manatwork