ใช้“ ในขณะที่อ่าน…”, echo และ printf จะได้ผลลัพธ์ที่แตกต่างกัน


14

ตามคำถามนี้ " ใช้" ในขณะที่อ่าน ... "ในสคริปต์ linux "

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

ผล:

1, 2, 3 4 5 6

แต่เมื่อฉันแทนที่echoด้วยprintf

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

ผล

1, 2, 3
4, 5, 6

ใครช่วยบอกฉันทีว่าอะไรทำให้คำสั่งสองคำนี้แตกต่าง ขอบคุณ ~

คำตอบ:


24

มันไม่ใช่แค่ echo vs printf

ก่อนอื่นเรามาทำความเข้าใจว่าเกิดอะไรขึ้นกับread a b cส่วน readจะดำเนินการแยกคำตามค่าเริ่มต้นของIFSตัวแปรซึ่งเป็น space-tab-newline และพอดีทุกอย่างตามที่ หากมีอินพุตมากกว่าตัวแปรที่จะเก็บไว้มันจะใส่ส่วนที่แยกออกเป็นตัวแปรแรกและสิ่งที่ไม่สามารถติดตั้งได้ - จะเข้าสู่ช่วงสุดท้าย นี่คือสิ่งที่ฉันหมายถึง:

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

นี่เป็นวิธีที่อธิบายไว้ในbashคู่มือของ (ดูเครื่องหมายคำพูดท้ายคำตอบ)

ในกรณีของคุณสิ่งที่เกิดขึ้นคือการที่ 1 และ 2 พอดีและ b ตัวแปรและ c 3 4 5 6ใช้เวลาทุกอย่างอื่นซึ่งเป็น

สิ่งที่คุณจะเห็นบ่อยครั้งคือผู้คนใช้while IFS= read -r line; do ... ; done < input.txtอ่านไฟล์ข้อความทีละบรรทัด อีกครั้งIFS=อยู่ที่นี่เพื่อเหตุผลในการควบคุมการแยกคำหรือเฉพาะเจาะจงมากขึ้น - ปิดการใช้งานมันและอ่านข้อความบรรทัดเดียวในตัวแปร หากไม่มีอยู่readให้ลองปรับคำแต่ละคำให้เป็นlineตัวแปร แต่นั่นเป็นอีกเรื่องหนึ่งที่ฉันแนะนำให้คุณศึกษาในภายหลังเนื่องจากwhile IFS= read -r variableเป็นโครงสร้างที่ใช้บ่อยมาก

echo vs printf พฤติกรรม

echoทำในสิ่งที่คุณคาดหวังที่นี่ จะแสดงตัวแปรของคุณตรงตามที่readได้จัดไว้ สิ่งนี้แสดงให้เห็นแล้วในการสนทนาก่อนหน้านี้

printfมีความพิเศษมากเพราะมันจะเก็บตัวแปรที่เหมาะสมไว้ในรูปแบบของสตริงจนกว่าพวกมันจะหมด ดังนั้นเมื่อคุณพิมพ์printf "%d, %d, %d \n" $a $b $cprintf เห็นสตริงรูปแบบที่มี 3 ทศนิยม แต่มีข้อโต้แย้งมากกว่า 3 (เพราะตัวแปรของคุณขยายไปถึง 1,2,3,4,5,6 จริง ๆ ) สิ่งนี้อาจฟังดูสับสน แต่มีเหตุผลเนื่องจากพฤติกรรมที่ดีขึ้นจากสิ่งที่ฟังก์ชั่นจริง printf()ทำในภาษา C

สิ่งที่คุณทำที่นี่ที่มีผลต่อผลลัพธ์คือตัวแปรของคุณไม่ได้ถูกอ้างถึงซึ่งทำให้เชลล์ (ไม่printf) แบ่งตัวแปรออกเป็น 6 รายการแยกกัน เปรียบเทียบกับข้อความ:

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

เนื่องจาก$cตัวแปรถูกยกมาตอนนี้จะรับรู้เป็นสตริงทั้งหมด3 4และไม่พอดีกับ%dรูปแบบซึ่งเป็นเพียงจำนวนเต็มเดียว

ตอนนี้ทำเช่นเดียวกันโดยไม่ต้องอ้างถึง:

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

printf อีกครั้งบอกว่า: "ตกลงคุณมี 6 รายการที่นั่น แต่รูปแบบแสดงเพียง 3 ดังนั้นฉันจะเก็บสิ่งที่เหมาะสมและปล่อยให้ว่างเปล่าสิ่งที่ฉันไม่สามารถจับคู่กับการป้อนข้อมูลที่แท้จริงจากผู้ใช้"

และในกรณีเหล่านี้คุณไม่ต้องใช้คำพูดของฉัน เพียงแค่เรียกใช้strace -e trace=execveและดูด้วยตัวคุณเองว่าคำสั่งอะไรจริง "ดู":

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 +++

หมายเหตุเพิ่มเติม

ดังที่ Charles Duffy ชี้ให้เห็นอย่างถูกต้องในความคิดเห็นbashมีตัวของมันเองprintfซึ่งเป็นสิ่งที่คุณใช้ในคำสั่งของคุณstraceจะเรียก/usr/bin/printfเวอร์ชันจริง ๆไม่ใช่เวอร์ชันของเชลล์ นอกเหนือจากความแตกต่างเล็กน้อยสำหรับความสนใจของเราในคำถามนี้ตัวระบุรูปแบบมาตรฐานจะเหมือนกันและพฤติกรรมจะเหมือนกัน

สิ่งที่ควรคำนึงถึงก็คือprintfไวยากรณ์นั้นพกพาได้มากกว่า (และเป็นที่ต้องการมากกว่า) echoไม่ต้องพูดถึงว่าไวยากรณ์นั้นคุ้นเคยกับ C หรือภาษา C-like ใด ๆ ที่มีprintf()ฟังก์ชันอยู่มากกว่า เห็นนี้คำตอบที่ดีเยี่ยมโดย terdonในเรื่องของVSprintf echoในขณะที่คุณสามารถปรับแต่งเอาต์พุตให้เหมาะกับเชลล์เฉพาะของคุณบน Ubuntu เฉพาะรุ่นของคุณหากคุณกำลังจะย้ายสคริปต์ไปยังระบบต่าง ๆ คุณควรจะชอบprintfมากกว่าเสียงก้อง บางทีคุณอาจเป็นผู้ดูแลระบบมือใหม่ที่ทำงานกับเครื่อง Ubuntu และ CentOS หรืออาจเป็น FreeBSD ใครจะรู้ - ดังนั้นในกรณีเช่นนี้คุณจะต้องเลือก

อ้างอิงจากคู่มือทุบตีส่วนคำสั่งของเชลล์สร้าง

อ่าน [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-p หมดเวลา] [-t หมดเวลา] [-u fd] [ชื่อ ... ]

หนึ่งบรรทัดถูกอ่านจากอินพุตมาตรฐานหรือจากไฟล์ descriptor fd ที่ระบุเป็นอาร์กิวเมนต์สำหรับตัวเลือก -u และคำแรกถูกกำหนดให้กับชื่อแรกคำที่สองไปยังชื่อที่สองและอื่น ๆ ที่เหลือ คำและส่วนที่คั่นด้วยการแทรกแซงของพวกเขาได้รับมอบหมายให้ใช้นามสกุล หากมีคำที่อ่านจากอินพุตสตรีมน้อยกว่าชื่อชื่อที่เหลือจะถูกกำหนดค่าว่าง อักขระใน IFS ใช้เพื่อแยกบรรทัดเป็นคำโดยใช้กฎเดียวกับที่เชลล์ใช้สำหรับการขยาย (อธิบายไว้ข้างต้นภายใต้การแยกคำ)


2
ความแตกต่างที่สำคัญอย่างหนึ่งระหว่างstraceเคสกับอีกอย่างstrace printfคือใช้/usr/bin/printfในขณะที่printfโดยตรงใน bash คือการใช้เชลล์ builtin ในชื่อเดียวกัน พวกเขาจะไม่เหมือนกัน - ตัวอย่างเช่น bash มีตัวระบุรูปแบบ%qและในเวอร์ชันใหม่$()Tสำหรับการจัดรูปแบบเวลา
Charles Duffy

7

นี่เป็นเพียงข้อเสนอแนะและไม่ได้หมายถึงการแทนที่คำตอบของ Sergiy เลย ฉันคิดว่า Sergiy เขียนคำตอบที่ดีว่าทำไมพวกเขาถึงแตกต่างกันในการพิมพ์ วิธีตัวแปรในการอ่านที่ได้รับมอบหมายที่เหลือลงใน$cตัวแปร3 4 5 6หลัง1และ2ได้รับมอบหมายให้และ a จะไม่แยกตัวแปรสำหรับคุณที่จะกับsbechoprintf%d

อย่างไรก็ตามคุณสามารถทำให้พวกเขาให้คำตอบเดียวกันโดยการจัดการกับเสียงสะท้อนของตัวเลขที่จุดเริ่มต้นของคำสั่ง:

ใน/bin/bashคุณสามารถใช้:

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

ใน/bin/shคุณก็สามารถใช้:

echo "1 2 3 \n4 5 6"

Bash ใช้ -e เพื่อเปิดใช้งานอักขระ \ escape โดยที่ sh ไม่จำเป็นต้องใช้เนื่องจากเปิดใช้งานแล้ว \nทำให้เกิดการสร้างบรรทัดใหม่ดังนั้นตอนนี้บรรทัด echo จะแบ่งออกเป็นสองบรรทัดแยกกันซึ่งตอนนี้สามารถใช้สองครั้งสำหรับคำสั่งลูป echo ของคุณ:

:~$ 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

ซึ่งจะสร้างผลลัพธ์เดียวกันโดยใช้printfคำสั่ง:

:~$ 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 

ใน 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 

หวังว่านี่จะช่วยได้!


echo -eไม่ได้เป็นเพียงส่วนขยายของ POSIX - แต่ส่วนขยายใด ๆechoที่ไม่ได้ปล่อยออกมา-eเนื่องจากการป้อนข้อมูลเป็นการท้าทาย / ทำลายข้อกำหนดตัวอักษรสีดำ (ทุบตีไม่ปฏิบัติตามค่าเริ่มต้น แต่เป็นไปตามมาตรฐานเมื่อมีการตั้งค่าทั้งสองposixและxpg_echoค่าสถานะหลังสามารถทำเริ่มต้นที่รวบรวมเวลาหมายความว่าไม่ใช่ทุกรุ่นทุบตีจะทำงานด้วยecho -eตามที่คุณคาดหวังที่นี่)
Charles Duffy

ดูการใช้งานแอปพลิเคชั่นและเหตุผลของ POSIX สเป็คได้echoที่pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.htmlอธิบายechoพฤติกรรมของผู้ใช้อย่างชัดเจนในสถานะของ-nแบ็กสแลชหรือที่ใดก็ได้ในอินพุตprintfนำมาใช้แทน
ชาร์ลส์ดัฟฟี่

@CharlesDuffy ฉันเคยเขียนมาก่อนหรือเปล่าว่าฉันคาดหวังว่ามันจะใช้ได้กับ bash ทุกรุ่น? แล้วคุณมีปัญหาอะไรกับคำตอบของฉัน หากคุณรู้สึกว่าคุณมีทางออกที่ดีกว่าให้เขียนมันเป็นคำตอบแทนที่จะพยายามพิสูจน์คนผิด ฉันผ่านวิธีนี้มาแล้วหลายครั้งกับคนอย่างคุณดังนั้นโปรดหยุดความคิดเห็นเหล่านี้เช่นนี้ นั่นคือทั้งหมดที่ฉันต้องพูด
Terrance

3
@CharlesDuffy ใน Ask Ubuntu บางครั้งเราเขียนคำตอบที่ไม่สามารถเคลื่อนย้ายไปยัง Linux distros อื่น ๆ ได้ เนื่องจากตัวแทนของคุณที่นี่มีเพียง 103 คะแนนคุณอาจไม่ได้สังเกตว่า ตัวแทนของคุณใน Stack Overflow เป็น 123.3K จุดดังนั้นคุณจึงรู้ว่ามีระบบ * NIX มากมาย ฉันคิดว่าคุณเทอเรซและเซร์คใช้ได้ดี แต่กำลังมองดูด้ายจากมุมที่แตกต่างกัน ฉันสะท้อน (ไม่ตั้งใจเล่นสำนวน) ความเชื่อมั่นสำหรับคุณที่จะเขียนคำตอบที่ * พกพา NIX หรืออย่างน้อยพกพาข้าม distros Linux
WinEunuuchs2Unix

2
@CharlesDuffy ในขณะที่ฉันเห็นด้วยกับคำตอบความรู้สึกของคุณควรทำแบบพกพานี่คือไซต์ที่มุ่งเน้นในอูบุนตูดังนั้นผู้ใช้ควรใช้เครื่องมือเฉพาะของอูบุนตู ดังนั้นคำเตือนจึงมีความหมายโดยนัย อย่าไปพูดคุยกันนานเกินไป คุณคิดถูกว่าหอยอื่นควรพิจารณาและควรอ่านคำแนะนำใหม่ ๆ เพื่อเรียนรู้จากคำตอบด้วยเช่นกัน ความคิดเห็นสั้น ๆ อาจจะเพียงพอที่จะทำให้จุด
Sergiy Kolodyazhnyy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.