ฉันเพิ่งกำหนดตัวแปร แต่ตัวแปร echo $ แสดงอย่างอื่น


109

ต่อไปนี้เป็นชุดกรณีที่echo $varสามารถแสดงค่าที่แตกต่างจากที่เพิ่งกำหนดได้ สิ่งนี้จะเกิดขึ้นไม่ว่าค่าที่กำหนดจะเป็น "double quoted" "single quoted" หรือไม่มีเครื่องหมายคำพูด

ฉันจะทำให้เชลล์ตั้งค่าตัวแปรได้อย่างถูกต้องได้อย่างไร

ดอกจัน

ผลลัพธ์ที่คาดหวังคือ/* Foobar is free software */แต่ฉันได้รับรายชื่อไฟล์แทน:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

วงเล็บเหลี่ยม

ค่าที่คาดหวังคือ[a-z]แต่บางครั้งฉันก็ได้อักษรตัวเดียวแทน!

$ var=[a-z]
$ echo $var
c

ฟีดบรรทัด (บรรทัดใหม่)

ค่าที่คาดหวังคือรายการของบรรทัดที่แยกจากกัน แต่ค่าทั้งหมดจะอยู่ในบรรทัดเดียวแทน!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

หลายช่องว่าง

ฉันคาดหวังว่าส่วนหัวของตารางจะถูกจัดวางอย่างระมัดระวัง แต่ช่องว่างหลาย ๆ ช่องจะหายไปหรือถูกยุบลงไปแทน!

$ var="       title     |    count"
$ echo $var
title | count

แท็บ

ฉันคาดหวังค่าที่คั่นสองแท็บ แต่ฉันได้รับค่าที่คั่นด้วยช่องว่างสองค่าแทน!

$ var=$'key\tvalue'
$ echo $var
key value

3
ขอบคุณที่ทำสิ่งนี้ ฉันเจอไลน์ฟีดบ่อยครั้ง ดังนั้นvar=$(cat file)ดี แต่echo "$var"เป็นสิ่งจำเป็น
SND

3
BTW นี่คือ BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy


นอกจากนี้โปรดดูstackoverflow.com/questions/2414150/…
tripleee

คำตอบ:


148

ในทุกกรณีข้างต้นตัวแปรถูกตั้งค่าอย่างถูกต้อง แต่อ่านไม่ถูกต้อง! วิธีที่ถูกต้องคือใช้เครื่องหมายคำพูดคู่เมื่ออ้างถึง :

echo "$var"

สิ่งนี้ให้ค่าที่คาดหวังในตัวอย่างทั้งหมดที่ระบุ อ้างอิงตัวแปรเสมอ!


ทำไม?

เมื่อตัวแปรunquotedก็จะ:

  1. รับการแยกฟิลด์โดยที่ค่าถูกแบ่งออกเป็นหลายคำในช่องว่าง (โดยค่าเริ่มต้น):

    ก่อน: /* Foobar is free software */

    หลังจากที่: /*, Foobar, is, free, software,*/

  2. คำเหล่านี้แต่ละคำจะได้รับการขยายชื่อพา ธซึ่งรูปแบบจะถูกขยายไปยังไฟล์ที่ตรงกัน:

    ก่อน: /*

    หลังจากที่: /bin, /boot, /dev, /etc, /home...

  3. ในที่สุดอาร์กิวเมนต์ทั้งหมดจะถูกส่งผ่านไปยัง echo ซึ่งเขียนออกมาโดยคั่นด้วยช่องว่างเดียวโดยให้

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/
    

    แทนค่าของตัวแปร

เมื่อตัวแปรถูกยกมามันจะ:

  1. ถูกแทนที่ด้วยมูลค่า
  2. ไม่มีขั้นตอนที่ 2

นี่คือเหตุผลที่คุณควรอ้างอิงการอ้างอิงตัวแปรทั้งหมดยกเว้นว่าคุณต้องการการแยกคำและการขยายชื่อพา ธ โดยเฉพาะ มีเครื่องมือเช่นshellcheckเพื่อช่วยและจะเตือนเกี่ยวกับเครื่องหมายคำพูดที่ขาดหายไปในทุกกรณีข้างต้น


มันไม่ได้ผลเสมอไป ฉันสามารถยกตัวอย่าง: paste.ubuntu.com/p/8RjR6CS668
recolic

1
ใช่$(..)แถบต่อท้ายบรรทัดฟีด คุณสามารถใช้var=$(cat file; printf x); var="${var%x}"เพื่อหลีกเลี่ยงมัน
ผู้ชายคนนั้น

19

คุณอาจต้องการทราบว่าเหตุใดจึงเกิดขึ้น ร่วมกับคำอธิบายที่ยอดเยี่ยมของคนอื่น ๆค้นหาข้อมูลอ้างอิงว่าทำไมเชลล์สคริปต์ของฉันจึงหายใจไม่ออกในช่องว่างหรืออักขระพิเศษอื่น ๆ เขียนโดยGillesในUnix & Linux :

ทำไมต้องเขียน"$foo"? จะเกิดอะไรขึ้นหากไม่มีเครื่องหมายคำพูด

$fooไม่ได้หมายความว่า“ รับค่าของตัวแปรfoo” มันหมายถึงบางสิ่งที่ซับซ้อนมากขึ้น:

  • ขั้นแรกให้ใช้ค่าของตัวแปร
  • การแบ่งฟิลด์: ถือว่าค่านั้นเป็นรายการฟิลด์ที่คั่นด้วยช่องว่างและสร้างรายการผลลัพธ์ ตัวอย่างเช่นถ้าตัวแปรมีfoo * bar ​แล้วผลของขั้นตอนนี้คือรายการ 3 องค์ประกอบfoo, , *bar
  • การสร้างชื่อไฟล์: ถือว่าแต่ละฟิลด์เป็นลูกโลกเช่นเป็นรูปแบบสัญลักษณ์แทนและแทนที่ด้วยรายชื่อไฟล์ที่ตรงกับรูปแบบนี้ หากรูปแบบไม่ตรงกับไฟล์ใด ๆ ระบบจะไม่แก้ไขรูปแบบ ในตัวอย่างของเราผลนี้ในรายการที่มีดังต่อไปนี้ด้วยรายการของไฟล์ในไดเรกทอรีปัจจุบันและในที่สุดก็foo barหากไดเรกทอรีปัจจุบันเป็นที่ว่างเปล่า, ผลที่ได้คือfoo, ,* bar

โปรดทราบว่าผลลัพธ์คือรายการของสตริง มีสองบริบทในไวยากรณ์ของเชลล์: บริบทรายการและบริบทสตริง การแยกฟิลด์และการสร้างชื่อไฟล์เกิดขึ้นในบริบทรายการเท่านั้น แต่โดยส่วนใหญ่แล้ว เครื่องหมายคำพูดคู่คั่นบริบทสตริง: สตริงที่ยกมาคู่ทั้งหมดเป็นสตริงเดียวไม่ต้องแยก (ข้อยกเว้น: "$@"เพื่อขยายไปยังรายการพารามิเตอร์ตำแหน่งเช่น"$@"เทียบเท่ากับ"$1" "$2" "$3"ว่ามีพารามิเตอร์ตำแหน่งสามตัวดูอะไรคือความแตกต่างระหว่าง $ * และ $ @? )

เช่นเดียวกับที่เกิดขึ้นแทนคำสั่งด้วยหรือ$(foo) `foo`โปรดทราบว่าอย่าใช้`foo`: กฎการอ้างอิงนั้นแปลกและไม่พกพาได้และเชลล์สมัยใหม่ทั้งหมดรองรับ$(foo)ซึ่งเทียบเท่ากันอย่างแน่นอนยกเว้นมีกฎการอ้างอิงที่ใช้งานง่าย

ผลลัพธ์ของการแทนค่าเลขคณิตก็มีการขยายเหมือนกัน แต่โดยปกติแล้วไม่น่ากังวลเนื่องจากมีเฉพาะอักขระที่ไม่สามารถขยายได้ (สมมติว่าIFSไม่มีตัวเลขหรือ -)

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

เว้นแต่คุณจะหมายถึงให้ rigmarole ทั้งหมดนี้เกิดขึ้นอย่าลืมใช้เครื่องหมายคำพูดคู่รอบตัวแปรและการแทนที่คำสั่งเสมอ ทำดูแล: ออกจากคำพูดที่สามารถนำไปสู่ไม่เพียง แต่จะมีข้อผิดพลาด แต่เพื่อ รักษาความปลอดภัยหลุม


7

นอกเหนือจากปัญหาอื่น ๆ ที่เกิดจากการไม่สามารถอ้างถึง-nและ-eสามารถใช้echoเป็นอาร์กิวเมนต์ได้ (เฉพาะในอดีตเท่านั้นที่ถูกกฎหมายตามข้อกำหนด POSIX สำหรับechoแต่การใช้งานทั่วไปหลายอย่างละเมิดข้อกำหนดและใช้งาน-eเช่นกัน)

เพื่อหลีกเลี่ยงปัญหานี้ให้ใช้printfแทนechoเมื่อรายละเอียดมีความสำคัญ

ดังนั้น:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

อย่างไรก็ตามการอ้างอิงที่ถูกต้องจะไม่ช่วยคุณประหยัดเสมอไปเมื่อใช้echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... ในขณะที่มันจะช่วยคุณด้วยprintf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n

ใช่เราต้องการการหักเงินที่ดีสำหรับสิ่งนี้! ฉันยอมรับว่าสิ่งนี้เหมาะกับชื่อคำถาม แต่ฉันไม่คิดว่ามันจะได้รับการเปิดเผยที่สมควรได้รับที่นี่ คำถามใหม่à la "ทำไม-e/ -n/ แบ็กสแลชของฉันไม่แสดงขึ้นมา" เราสามารถเพิ่มลิงค์จากที่นี่ได้ตามความเหมาะสม
ผู้ชายคนนั้น

คุณหมายถึงบริโภค-nด้วยหรือไม่?
Pesa

1
@PesaThe -eไม่ฉันหมายถึง มาตรฐานสำหรับechoไม่ได้ระบุเอาต์พุตเมื่ออาร์กิวเมนต์แรกคือ-nทำให้เอาต์พุตใด ๆ / ทั้งหมดที่เป็นไปได้ถูกกฎหมายในกรณีนั้น -eไม่มีบทบัญญัติดังกล่าวสำหรับ
Charles Duffy

โอย ... อ่านไม่ออก โทษภาษาอังกฤษของฉันกันเถอะ ขอบคุณสำหรับคำอธิบาย
PesaThe

6

ผู้ใช้อ้างสองครั้งเพื่อให้ได้ค่าที่แน่นอน แบบนี้:

echo "${var}"

และจะอ่านค่าของคุณอย่างถูกต้อง


ผลงาน .. ขอบคุณ
Tshilidzi Mudau

สิ่งนี้ไม่ได้ผลสำหรับฉัน
KansaiRobot

2

echo $varผลลัพธ์ขึ้นอยู่กับค่าของIFSตัวแปร โดยค่าเริ่มต้นจะมีช่องว่างแท็บและอักขระขึ้นบรรทัดใหม่:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

ซึ่งหมายความว่าเมื่อเชลล์ทำการแยกฟิลด์ (หรือการแยกคำ) จะใช้อักขระเหล่านี้ทั้งหมดเป็นตัวคั่นคำ นี่คือสิ่งที่เกิดขึ้นเมื่ออ้างอิงตัวแปรโดยไม่มีเครื่องหมายอัญประกาศคู่เพื่อสะท้อน ( $var) ดังนั้นผลลัพธ์ที่คาดไว้จะถูกเปลี่ยนแปลง

วิธีหนึ่งในการป้องกันการแยกคำ (นอกเหนือจากการใช้เครื่องหมายคำพูดคู่) คือการตั้งค่าIFSเป็น null ดูhttp://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

ถ้าค่าของ IFS เป็นค่าว่างจะไม่มีการแยกฟิลด์

การตั้งค่าเป็น null หมายถึงการตั้งค่าเป็นค่าว่าง:

IFS=

ทดสอบ:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 

2
นอกจากนี้คุณยังต้องset -fป้องกันไม่ให้โก่ง
ผู้ชายคนนั้น

@thatotherguy จำเป็นจริงๆสำหรับตัวอย่าง 1-st ของคุณด้วยการขยายเส้นทางหรือไม่? เมื่อIFSตั้งค่าเป็น null echo $varจะถูกขยายecho '/* Foobar is free software */'และไม่ดำเนินการขยายเส้นทางภายในสตริงที่ยกมาเดี่ยว
ks1322

1
ใช่. หากคุณmkdir "/this thing called Foobar is free software etc/"จะเห็นว่ามันยังขยายอยู่ เห็นได้ชัดว่าเป็นประโยชน์มากกว่าสำหรับ[a-z]ตัวอย่าง
ผู้ชายคนนั้น

ฉันเห็นนี้ทำให้รู้สึกสำหรับ[a-z]ตัวอย่างเช่น
ks1322

2

คำตอบจาก ks1322ช่วยให้ฉันเพื่อระบุปัญหาในขณะที่ใช้docker-compose exec:

หากคุณละเว้น-Tแฟล็กdocker-compose execเพิ่มอักขระพิเศษที่แบ่งเอาต์พุตเราจะเห็นbแทนที่จะเป็น1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

ด้วยการ-Tตั้งค่าสถานะdocker-compose execทำงานได้ตามที่คาดไว้:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b

-2

นอกเหนือจากการใส่ตัวแปรในใบเสนอราคาแล้วยังสามารถแปลผลลัพธ์ของตัวแปรโดยใช้trและการแปลงช่องว่างเป็นบรรทัดใหม่

$ echo $var | tr " " "\n"
foo
bar
baz

แม้ว่าสิ่งนี้จะซับซ้อนกว่าเล็กน้อย แต่ก็เพิ่มความหลากหลายมากขึ้นด้วยเอาต์พุตเนื่องจากคุณสามารถแทนที่อักขระใดก็ได้เป็นตัวคั่นระหว่างตัวแปรอาร์เรย์


2
แต่สิ่งนี้จะแทนที่ช่องว่างทั้งหมดในการขึ้นบรรทัดใหม่ การอ้างอิงจะรักษาบรรทัดใหม่และช่องว่างที่มีอยู่
user000001

จริงใช่. ฉันคิดว่ามันขึ้นอยู่กับสิ่งที่อยู่ในตัวแปร ฉันใช้trวิธีอื่นในการสร้างอาร์เรย์จากไฟล์ข้อความ
Alek

3
การสร้างปัญหาโดยการไม่อ้างถึงตัวแปรอย่างถูกต้องแล้วจัดการกับมันด้วยกระบวนการเสริมที่อยู่ติดกันไม่ใช่การเขียนโปรแกรมที่ดี
tripleee

@Alek, ... เอ่ออะไรนะ? ไม่trจำเป็นต้องสร้างอาร์เรย์อย่างถูกต้อง / ถูกต้องจากไฟล์ข้อความคุณสามารถระบุตัวคั่นที่คุณต้องการได้โดยการตั้งค่า IFS ตัวอย่างเช่น: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')ทำงานย้อนกลับไปจนถึง bash 3.2 (เวอร์ชันที่เก่าที่สุดในการหมุนเวียนแบบกว้าง) และตั้งค่าสถานะการออกเป็นเท็จอย่างถูกต้องหากคุณcatล้มเหลว และถ้าคุณอยากบอกว่าแท็บแทนการขึ้นบรรทัดใหม่คุณต้องการเพียงแค่แทนที่ด้วย$'\n' $'\t'
Charles Duffy

1
@Alek ... ถ้าคุณทำอะไรแบบarrayname=( $( cat file | tr '\n' ' ' ) )นั้นมันจะแตกในหลาย ๆ เลเยอร์: มันเป็นการรวบรวมผลลัพธ์ของคุณ (ดังนั้น*จะเปลี่ยนเป็นรายการไฟล์ในไดเร็กทอรีปัจจุบัน) และมันจะทำงานได้ดีเช่นกันหากไม่มีtr ( หรือcatสำหรับเรื่องนั้นใคร ๆ ก็สามารถใช้arrayname=$( $(<file) )และมันจะเสียในรูปแบบเดียวกัน แต่ไม่มีประสิทธิภาพน้อยกว่า)
Charles Duffy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.