Menggunakan "saat membaca ...", gema dan printf mendapatkan hasil yang berbeda

14

Menurut pertanyaan ini " Menggunakan" saat membaca ... "dalam skrip linux "

echo '1 2 3 4 5 6' | while read a b c;do echo "$a, $b, $c"; done

hasil:

1, 2, 3 4 5 6

tapi ketika saya ganti echodenganprintf

echo '1 2 3 4 5 6' | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done

hasil

1, 2, 3
4, 5, 6

Bisakah seseorang tolong beri tahu saya apa yang membuat kedua perintah ini berbeda? Terima kasih ~

pengguna3094631
sumber

Jawaban:

24

Ini bukan hanya gema vs printf

Pertama, mari kita pahami apa yang terjadi dengan read a b cbagian itu. readakan melakukan pemisahan kata berdasarkan nilai default IFSvariabel yaitu spasi-tab-baris baru, dan sesuai dengan segalanya berdasarkan itu. Jika ada lebih banyak input daripada variabel untuk menahannya, itu akan cocok bagian yang dibagi menjadi variabel pertama, dan apa yang tidak bisa dipasang - akan masuk ke yang terakhir. Inilah yang saya maksud:

bash-4.3$ read a b c <<< "one two three four"
bash-4.3$ echo $a
one
bash-4.3$ echo $b
two
bash-4.3$ echo $c
three four

Ini persis seperti yang dijelaskan dalam bashmanual (lihat kutipan di akhir jawaban).

Dalam kasus Anda yang terjadi adalah bahwa, 1 dan 2 masuk ke dalam variabel a dan b, dan c mengambil yang lainnya, yaitu 3 4 5 6.

Apa yang Anda juga akan sering lihat adalah yang digunakan orang while IFS= read -r line; do ... ; done < input.txtuntuk membaca file teks baris demi baris. Sekali lagi, IFS=ada di sini karena alasan untuk mengontrol pemisahan kata, atau lebih khusus - menonaktifkannya, dan membaca satu baris teks menjadi variabel. Jika tidak ada di sana, readakan mencoba mencocokkan setiap kata menjadi linevariabel. Tapi itu cerita lain, yang saya anjurkan Anda untuk pelajari nanti, karena while IFS= read -r variablemerupakan struktur yang sangat sering digunakan.

perilaku echo vs printf

echomelakukan apa yang Anda harapkan di sini. Ini menampilkan variabel Anda persis seperti yang readtelah mereka atur. Ini sudah ditunjukkan dalam diskusi sebelumnya.

printfsangat istimewa, karena akan terus memasukkan variabel ke dalam format string sampai semuanya habis. Jadi ketika Anda melakukan printf "%d, %d, %d \n" $a $b $cprintf melihat format string dengan 3 desimal, tetapi ada lebih banyak argumen dari 3 (karena variabel Anda benar-benar berkembang menjadi 1,2,3,4,5,6 individu). Ini mungkin terdengar membingungkan, tetapi ada karena alasan peningkatan perilaku dari apa fungsi sebenarnya printf() tidak dalam bahasa C.

Apa yang Anda lakukan di sini yang mempengaruhi output adalah bahwa variabel Anda tidak dikutip, yang memungkinkan shell (tidak printf) memecah variabel menjadi 6 item terpisah. Bandingkan ini dengan mengutip:

bash-4.3$ read a b c <<< "1 2 3 4"
bash-4.3$ printf "%d %d %d\n" "$a" "$b" "$c"
bash: printf: 3 4: invalid number
1 2 3

Persis karena $cvariabel dikutip, sekarang diakui sebagai satu string keseluruhan 3 4,, dan tidak cocok dengan %dformat, yang hanya merupakan integer tunggal

Sekarang lakukan hal yang sama tanpa mengutip:

bash-4.3$ printf "%d %d %d\n" $a $b $c
1 2 3
4 0 0

printf sekali lagi mengatakan: "OK, Anda memiliki 6 item di sana tetapi format hanya menunjukkan 3, jadi saya akan menyimpan barang dan membiarkan apa pun yang saya tidak cocok dengan input sebenarnya dari pengguna".

Dan dalam semua kasus ini Anda tidak harus mengambil kata saya untuk itu. Jalankan strace -e trace=execvedan lihat sendiri apa yang sebenarnya perintah "lihat":

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" $a $b $c
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3", "4"], [/* 80 vars */]) = 0
1 2 3
4 0 0
+++ exited with 0 +++

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" "$a" "$b" "$c"
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3 4"], [/* 80 vars */]) = 0
1 2 printf: 3 4’: value not completely converted
3
+++ exited with 1 +++

Catatan tambahan

Seperti yang ditunjukkan Charles Duffy dengan benar di komentar, bashmemiliki built-in sendiri printf, yang Anda gunakan dalam perintah Anda, stracesebenarnya akan memanggil /usr/bin/printfversi, bukan versi shell. Selain perbedaan kecil, untuk minat kami dalam pertanyaan khusus ini penentu format standar adalah sama dan perilaku adalah sama.

Yang juga harus diingat adalah bahwa printfsintaks jauh lebih portabel (dan karena itu lebih disukai) daripada echo, belum lagi bahwa sintaksis lebih akrab dengan C atau bahasa seperti C yang memiliki printf()fungsi di dalamnya. Lihat ini jawaban yang sangat baik dengan Terdon pada subjek printfvs echo. Meskipun Anda dapat membuat output yang disesuaikan dengan shell spesifik Anda pada versi spesifik Ubuntu Anda, jika Anda akan mem-porting skrip di berbagai sistem, Anda mungkin harus memilih printfdaripada gema. Mungkin Anda seorang administrator sistem pemula yang bekerja dengan mesin Ubuntu dan CentOS, atau mungkin bahkan FreeBSD - siapa tahu - jadi dalam kasus seperti itu Anda harus membuat pilihan.

Kutipan dari bash manual, bagian SHELL BUILTIN COMMANDS

baca [-ers] [-a aname] [-d delim] [-i teks] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [nama ... ]

Satu baris dibaca dari input standar, atau dari deskriptor file yang disediakan sebagai argumen ke opsi -u, dan kata pertama ditugaskan ke nama depan, kata kedua ke nama kedua, dan seterusnya, dengan sisa kata-kata dan pemisah intervensi mereka ditugaskan untuk nama belakang. Jika ada lebih sedikit kata yang dibaca dari aliran input dari nama, nama yang tersisa diberikan nilai kosong. Karakter dalam IFS digunakan untuk membagi baris menjadi kata-kata menggunakan aturan yang sama yang digunakan shell untuk ekspansi (dijelaskan di bawah di bawah Word Splitting).

Sergiy Kolodyazhnyy
sumber
2
Satu perbedaan penting antara stracekasing dan kasing lainnya - strace printfmenggunakan /usr/bin/printf, sedangkan printflangsung di bash menggunakan shell yang dibangun dengan nama yang sama. Mereka tidak akan selalu identik - misalnya, contoh bash memiliki penentu format %qdan, dalam versi baru, $()Tuntuk pemformatan waktu.
Charles Duffy
7

Ini hanya saran dan tidak dimaksudkan untuk menggantikan jawaban Sergiy sama sekali. Saya pikir Sergiy menulis jawaban yang bagus mengapa mereka berbeda dalam mencetak. Bagaimana variabel pada pembacaan ditugaskan dengan sisanya ke dalam $cvariabel seperti 3 4 5 6setelah 1dan 2ditugaskan ke adan b. echotidak akan membagi variabel untuk Anda printfdengan %ds.

Anda dapat, bagaimanapun, membuat mereka pada dasarnya memberi Anda jawaban yang sama dengan memanipulasi gema angka pada awal perintah:

Di dalam /bin/bashAnda dapat menggunakan:

echo -e "1 2 3 \n4 5 6"

Dalam /bin/shAnda hanya dapat menggunakan:

echo "1 2 3 \n4 5 6"

Bash menggunakan -e untuk mengaktifkan karakter \ escape di mana sh tidak membutuhkannya karena sudah diaktifkan. \nmenyebabkannya membuat garis baru, jadi sekarang garis gema dibagi menjadi dua baris terpisah yang sekarang dapat digunakan dua kali untuk pernyataan loop gema Anda:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6

Yang pada gilirannya menghasilkan output yang sama dengan menggunakan printfperintah:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

Di sh

$ echo "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6
$ echo "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

Semoga ini membantu!

Terrance
sumber
echo -ebukan hanya ekstensi untuk POSIX - tetapi segala sesuatu echoyang tidak memancarkan -eoutputnya mengingat input sebenarnya menentang / melanggar spesifikasi huruf hitam. (bash tidak patuh secara default, tetapi memenuhi standar ketika keduanya posixdan xpg_echoflag ditetapkan; yang terakhir dapat dibuat default pada waktu kompilasi, artinya tidak semua versi bash akan berfungsi echo -eseperti yang Anda harapkan di sini).
Charles Duffy
Lihat bagian APLIKASI PENGGUNAAN dan RATIONALE dari spesifikasi POSIX untuk echodi pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , secara eksplisit menggambarkan bahwa echoperilaku tidak ditentukan dalam kehadiran salah satu -natau garis miring di mana saja dalam masukan, dan menyarankan bahwa printfdigunakan sebagai gantinya.
Charles Duffy
@CharlesDuffy Apakah saya pernah menulis di saya bahwa saya berharap ini bekerja di semua versi bash? Dan masalah apa yang Anda miliki dengan jawaban saya? Jika Anda merasa memiliki solusi yang lebih baik, tulis sebagai jawaban alih-alih mencoba membuktikan kesalahan orang. Saya telah melalui cara ini terlalu sering dengan orang-orang seperti Anda, jadi tolong hentikan dengan komentar seperti ini. Itu yang harus saya katakan.
Terrance
3
@CharlesDuffy di Tanya Ubuntu kami terkadang menulis jawaban yang tidak dapat diangkut ke distro Linux lainnya. Karena perwakilan Anda di sini hanya 103 poin, Anda mungkin tidak memperhatikannya. Rep Anda pada Stack Overflow adalah 123,3K poin sehingga Anda jelas tahu banyak hal tentang sistem * NIX secara umum. Saya pikir Anda, Terrace dan Serg baik-baik saja tetapi melihat utas dari sudut yang berbeda. Saya menggemakan (no pun intended) sentimen bagi Anda untuk menulis jawaban yang * NIX portable, atau setidaknya portable di seluruh distro Linux.
WinEunuuchs2Unix
2
@CharlesDuffy Walaupun saya setuju dengan jawaban sentimen Anda yang harus dibuat portabel, ini adalah situs berorientasi Ubuntu, karenanya alat khusus Ubuntu harus diasumsikan oleh pengguna. Jadi peringatan itu agak tersirat. Jangan berdiskusi terlalu lama. Anda benar bahwa kerang lain harus dipertimbangkan, dan pemula yang membaca untuk belajar dari jawaban juga harus dipertimbangkan. Sebuah komentar singkat mungkin akan cukup untuk menjelaskan maksudnya.
Sergiy Kolodyazhnyy