เมื่อใดจึงควรใส่เครื่องหมายคำพูดล้อมรอบตัวแปรเชลล์


184

มีใครบางคนบอกฉันว่าฉันควรจะใส่เครื่องหมายคำพูดล้อมรอบตัวแปรในเชลล์สคริปต์หรือไม่

ตัวอย่างเช่นแก้ไขต่อไปนี้:

xdg-open $URL 
[ $? -eq 2 ]

หรือ

xdg-open "$URL"
[ "$?" -eq "2" ]

และถ้าเป็นเช่นนั้นทำไม


2
ดูเพิ่มเติมที่unix.stackexchange.com/questions/171346/…
tripleee

คำถามนี้ได้รับการซ้ำซ้อนจำนวนมากซึ่งส่วนใหญ่ไม่เกี่ยวกับตัวแปรดังนั้นฉันจึงตั้งชื่อใหม่ว่า "value" แทนที่จะเป็น "variable" ฉันหวังว่านี่จะช่วยให้ผู้คนค้นพบหัวข้อนี้มากขึ้น
tripleee

1
@codeforester มีอะไรใหม่เกี่ยวกับการแก้ไขการคืนค่าหรือไม่
tripleee


คำตอบ:


131

กฎทั่วไป: อ้างว่ามันสามารถว่างเปล่าหรือมีช่องว่าง (หรือช่องว่างใด ๆ จริง ๆ ) หรืออักขระพิเศษ (สัญลักษณ์) การไม่อ้างสตริงที่มีช่องว่างมักจะนำไปสู่เชลล์แยกอาร์กิวเมนต์เดี่ยวออกเป็นหลาย ๆ

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

ฉันมักจะอ้างสายอักขระไม่ให้นิสัยเสมอเพราะมันปลอดภัยกว่านี้


2
โปรดทราบว่า "ช่องว่าง" หมายถึง "ช่องว่างใด ๆ " จริงๆ
William Pursell

4
@Cristian: ถ้าคุณไม่แน่ใจว่าสิ่งที่อาจจะอยู่ในตัวแปรก็จะปลอดภัยกว่าที่จะพูด ฉันมักจะทำตามหลักการเดียวกันกับ paxdiablo และทำนิสัยในการอ้างอิงทุกสิ่ง
Gordon Davisson

11
หากคุณไม่ทราบคุณค่าของ IFS ให้พูดมันไม่ว่าจะเกิดอะไรขึ้น ถ้าเป็นIFS=0เช่นนั้นก็echo $?น่าแปลกใจมาก
ชาร์ลส์ดัฟฟี่

3
อ้างอิงจากบริบทไม่ใช่สิ่งที่คุณคาดหวังว่าจะมีค่ามิฉะนั้นข้อบกพร่องของคุณจะแย่ลง ตัวอย่างเช่นคุณจะแน่ใจว่าไม่มีเส้นทางของคุณมีช่องว่างเพื่อให้คุณคิดว่าคุณสามารถเขียนcp $source1 $source2 $destแต่ถ้าด้วยเหตุผลบางอย่างที่ไม่คาดคิดdestไม่ได้รับการตั้งค่าอาร์กิวเมนต์ที่สามเพียงแค่หายไปและมันเงียบจะคัดลอกsource1มากกว่าsource2แทนที่จะให้คุณ ข้อผิดพลาดที่เหมาะสมสำหรับปลายทางที่ว่างเปล่า (อย่างที่มันจะมีถ้าคุณได้อ้างแต่ละข้อโต้แย้ง)
Derek Veit

3
quote it if...มีกระบวนการคิดย้อนหลัง - คำพูดไม่ใช่สิ่งที่คุณเพิ่มเมื่อคุณต้องการมันเป็นสิ่งที่คุณลบเมื่อคุณต้องการ ห่อสตริงและสคริปต์ในเครื่องหมายคำพูดเดี่ยวทุกครั้งเว้นแต่คุณจะต้องใช้เครื่องหมายคำพูดคู่ (เช่นเพื่อให้ตัวแปรขยายได้) หรือไม่จำเป็นต้องใช้เครื่องหมายคำพูด (เช่นเพื่อทำการขยายแบบวงกลมและการขยายชื่อไฟล์)
Ed Morton

92

ในระยะสั้นอ้างทุกอย่างที่คุณไม่ต้องการเปลือกเพื่อดำเนินการแยกโทเค็นและการขยายตัวสัญลักษณ์

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

$ echo 'Nothing \t in here $will change'
Nothing \t in here $will change

$ grep -F '@&$*!!' file /dev/null
file:I can't get this @&$*!! quoting right.

เครื่องหมายคำพูดคู่เหมาะสำหรับการแก้ไขตัวแปรที่จำเป็น ด้วยการดัดแปลงที่เหมาะสมจะเป็นการแก้ปัญหาที่ดีเมื่อคุณต้องการอัญประกาศเดี่ยวในสตริง (ไม่มีวิธีง่ายๆในการหลีกเลี่ยงการอ้างคำเดียวระหว่างคำพูดเดียวเพราะไม่มีกลไกการหลบหนีในเครื่องหมายคำพูดเดียว - ถ้ามีพวกเขาจะไม่พูดคำต่อคำอย่างสมบูรณ์)

$ echo "There is no place like '$HOME'"
There is no place like '/home/me'

ไม่มีเครื่องหมายอัญประกาศที่เหมาะสมเมื่อคุณต้องการให้เชลล์ทำการแยกโทเค็นและ / หรือการขยายสัญลักษณ์แทน

การแยกโทเค็น;

 $ words="foo bar baz"
 $ for word in $words; do
 >   echo "$word"
 > done
 foo
 bar
 baz

ตรงกันข้าม:

 $ for word in "$words"; do echo "$word"; done
 foo bar baz

(การวนซ้ำจะทำงานเพียงครั้งเดียวบนสตริงที่ยกมาเดี่ยว)

 $ for word in '$words'; do echo "$word"; done
 $words

(การวนซ้ำจะทำงานเพียงครั้งเดียวเหนือสตริงที่มีเครื่องหมายคำพูดเดียว)

การขยายตัวไวด์การ์ด:

$ pattern='file*.txt'
$ ls $pattern
file1.txt      file_other.txt

ตรงกันข้าม:

$ ls "$pattern"
ls: cannot access file*.txt: No such file or directory

(ไม่มีไฟล์ชื่อแท้จริงfile*.txt)

$ ls '$pattern'
ls: cannot access $pattern: No such file or directory

(ไม่มีไฟล์ชื่อ $patternเช่นกัน!)

ในแง่ที่เป็นรูปธรรมมากขึ้นสิ่งที่มีชื่อไฟล์ควรจะยกมา (เพราะชื่อไฟล์สามารถมีช่องว่างและ metacharacters เชลล์อื่น ๆ ) สิ่งใดก็ตามที่มี URL ควรจะเสนอราคา (เพราะ URL จำนวนมากมีอักขระเมตาของเชลล์เช่น?และ& ) สิ่งที่มี regex ควรจะยกมา (ditto ditto) สิ่งใดก็ตามที่มีช่องว่างที่มีนัยสำคัญนอกเหนือจากช่องว่างเดี่ยวระหว่างอักขระที่ไม่ใช่ช่องว่างจำเป็นต้องอ้างถึง (เพราะมิเช่นนั้นเชลล์จะรวมช่องว่างเข้าไว้ในช่องว่างได้อย่างมีประสิทธิภาพช่องว่างเดี่ยวและตัดช่องว่างนำหน้า

เมื่อคุณรู้ว่าตัวแปรสามารถมีได้เฉพาะค่าที่ไม่มีเชลล์อักขระเมตาอักขระการอ้างอิงเป็นตัวเลือก ดังนั้น unquote $?นั้นดีเพราะตัวแปรนี้สามารถมีจำนวนเดียวเท่านั้น อย่างไรก็ตาม"$?"ก็ถูกต้องและแนะนำสำหรับความสอดคล้องและความถูกต้องทั่วไป (แม้ว่านี่จะเป็นคำแนะนำส่วนตัวของฉันไม่ใช่นโยบายที่ได้รับการยอมรับอย่างกว้างขวาง)

ค่าที่ไม่ใช่ตัวแปรโดยทั่วไปจะเป็นไปตามกฎเดียวกันแม้ว่าคุณจะสามารถหลีกเลี่ยงเมทาเตอเรเตอร์ใด ๆ แทนที่จะอ้างถึง สำหรับตัวอย่างทั่วไป URL ที่มี&ในจะถูกแยกวิเคราะห์โดยเชลล์เป็นคำสั่งพื้นหลังยกเว้นเมตาอักขระจะหนีหรืออ้างอิง:

$ wget http://example.com/q&uack
[1] wget http://example.com/q
-bash: uack: command not found

(แน่นอนสิ่งนี้จะเกิดขึ้นหาก URL อยู่ในตัวแปรที่ไม่มีเครื่องหมายอัญประกาศ) สำหรับสตริงแบบคงที่เครื่องหมายอัญประกาศเดี่ยวมีความหมายมากที่สุดแม้ว่ารูปแบบของการอ้างอิงหรือการหลบหนีจะทำงานที่นี่

wget 'http://example.com/q&uack'  # Single quotes preferred for a static string
wget "http://example.com/q&uack"  # Double quotes work here, too (no $ or ` in the value)
wget http://example.com/q\&uack   # Backslash escape
wget http://example.com/q'&'uack  # Only the metacharacter really needs quoting

ตัวอย่างสุดท้ายยังเสนอแนวคิดที่มีประโยชน์อีกประการหนึ่งซึ่งฉันชอบเรียกว่า "กระดานหกอ้างอิง" หากคุณต้องการผสมคำพูดเดี่ยวและคู่คุณสามารถใช้คำพูดที่อยู่ติดกัน ตัวอย่างเช่นสตริงที่ยกมาต่อไปนี้

'$HOME '
"isn't"
' where `<3'
"' is."

สามารถวางรวมกันกลับไปกลับมาก่อตัวเป็นสตริงยาว ๆ เดียวหลังจาก tokenization และการลบเครื่องหมายคำพูด

$ echo '$HOME '"isn't"' where `<3'"' is."
$HOME isn't where `<3' is.

มันไม่ชัดเจนมากนัก แต่มันเป็นเทคนิคทั่วไปและน่ารู้

นอกเหนือจากนี้สคริปต์ไม่ควรใช้lsเพื่อสิ่งใด ในการขยายไวด์การ์ดให้ใช้ ... ใช้มัน

$ printf '%s\n' $pattern   # not ``ls -1 $pattern''
file1.txt
file_other.txt

$ for file in $pattern; do  # definitely, definitely not ``for file in $(ls $pattern)''
>  printf 'Found file: %s\n' "$file"
> done
Found file: file1.txt
Found file: file_other.txt

(ลูปมีความฟุ่มเฟือยอย่างสมบูรณ์ในตัวอย่างหลังprintfทำงานได้ดีกับอาร์กิวเมนต์หลายตัวโดยเฉพาะstatเช่นกัน แต่การวนซ้ำในการจับคู่ไวด์การ์ดนั้นเป็นปัญหาที่พบบ่อยและทำผิดพลาดบ่อยครั้ง)

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


1
นี้เป็นตัวแปรของ (ส่วนหนึ่งของ) คำตอบที่ผมโพสต์ไปยังคำถามที่เกี่ยวข้อง ฉันกำลังวางที่นี่เพราะมันสั้นและชัดเจนพอที่จะกลายเป็นคำถามที่ยอมรับได้สำหรับปัญหาเฉพาะนี้
tripleee

4
ฉันจะทราบว่านี่เป็นรายการ # 0 และธีมที่เกิดขึ้นประจำในคอลเลกชันmywiki.wooledge.org/BashPitfallsของข้อผิดพลาด Bash ทั่วไป หลายรายการหลายรายการในรายการนั้นโดยทั่วไปเกี่ยวกับปัญหานี้
tripleee

27

นี่คือสูตรสามจุดสำหรับคำพูดโดยทั่วไป:

เครื่องหมายคำพูดคู่

ในบริบทที่เราต้องการยับยั้งการแยกคำและการโค้งคำ นอกจากนี้ในบริบทที่เราต้องการให้ตัวอักษรเป็นสตริงไม่ใช่ regex

คำพูดเดียว

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

ไม่มีคำพูด

ในบริบทที่เรามั่นใจอย่างแน่นอนว่าไม่มีการแยกคำหรือการแยกประเด็นหรือเราต้องการการแยกคำและกลมglobbing


ตัวอย่าง

เครื่องหมายคำพูดคู่

  • สตริงตัวอักษรที่มีช่องว่าง ("StackOverflow rocks!" , "Steve's Apple")
  • การขยายตัวแปร ("$var" , "${arr[@]}")
  • การแทนที่คำสั่ง ("$(ls)" , "`ls`")
  • globs โดยที่ไดเรกทอรีพา ธ หรือส่วนชื่อไฟล์มีช่องว่าง ("/my dir/"* )
  • เพื่อปกป้องคำพูดเดียว ( "single'quote'delimited'string")
  • การขยายพารามิเตอร์ Bash ( "${filename##*/}")

คำพูดเดียว

  • ชื่อคำสั่งและอาร์กิวเมนต์ที่มีช่องว่างในนั้น
  • สตริงตัวอักษรที่ต้องการการแก้ไขที่จะถูกระงับ ( 'Really costs $$!', 'just a backslash followed by a t: \t')
  • เพื่อปกป้องเครื่องหมายคำพูดคู่ ( 'The "crux"')
  • ตัวอักษร regex ที่ต้องการการแก้ไขจะถูกระงับ
  • ใช้การอ้างอิงเชลล์สำหรับตัวอักษรที่เกี่ยวข้องกับอักขระพิเศษ ( $'\n\t')
  • ใช้การอ้างอิงเชลล์ที่เราจำเป็นต้องปกป้องเครื่องหมายคำพูดเดี่ยวและคู่ ( $'{"table": "users", "where": "first_name"=\'Steve\'}')

ไม่มีคำพูด

  • ตัวแปรที่เป็นตัวเลขรอบมาตรฐาน ( $$, $?, $#ฯลฯ )
  • ในบริบททางคณิตศาสตร์ชอบ((count++)), "${arr[idx]}","${string:start:length}"
  • [[ ]]การแสดงออกภายในซึ่งปราศจากปัญหาการแยกคำและการแยกเป็นวงกลม (นี่เป็นเรื่องของสไตล์และความคิดเห็นอาจแตกต่างกันอย่างกว้างขวาง)
  • ที่เราต้องการแยกคำ (for word in $words )
  • ที่เราต้องการ globbing ( for txtfile in *.txt; do ...)
  • ตำแหน่งที่เราต้องการ~ตีความว่าเป็น$HOME( ~/"some dir"แต่ไม่ใช่"~/some dir")

ดูสิ่งนี้ด้วย:


3
ตามแนวทางเหล่านี้จะได้รับรายชื่อของไฟล์ในไดเรกทอรีรากโดยการเขียน"ls" "/" วลี "บริบทสตริงทั้งหมด" จะต้องมีคุณสมบัติอย่างรอบคอบมากขึ้น
William Pursell

5
ในการ[[ ]]อ้างอิงมีความสำคัญทางด้านขวามือของ=/ ==และ=~: มันสร้างความแตกต่างระหว่างการตีความสตริงเป็นรูปแบบ / regex หรือแท้จริง
Benjamin W.

6
ภาพรวมที่ดี แต่ความเห็นของ @ BenjaminW. นั้นคุ้มค่าที่จะผสานรวมและสตริงที่ยกมา ANSI C ( $'...') ควรมีส่วนของตัวเองอย่างแน่นอน
mklement0

3
@ mklement0 แน่นอนพวกเขาเทียบเท่ากัน แนวทางเหล่านี้บ่งชี้ว่าคุณควรพิมพ์ทุกครั้ง"ls" "/"แทนที่จะเป็นเรื่องธรรมดามากขึ้นls /และฉันถือว่าเป็นข้อบกพร่องที่สำคัญในแนวทาง
วิลเลียม Pursell

4
หากไม่มีเครื่องหมายอัญประกาศคุณอาจเพิ่มการกำหนดตัวแปรหรือcase:)
PesaThe

4

ฉันมักจะใช้คำพูดที่อ้างถึง"$var"เพื่อความปลอดภัยเว้นแต่ฉันจะแน่ใจ$varไม่มีที่ว่าง

ฉันใช้$varเป็นวิธีง่ายๆในการเข้าร่วมรายการ:

lines="`cat multi-lines-text-file.txt`"
echo "$lines"                             ## multiple lines
echo $lines                               ## all spaces (including newlines) are zapped

ความคิดเห็นสุดท้ายค่อนข้างทำให้เข้าใจผิด; บรรทัดใหม่จะถูกแทนที่ด้วยช่องว่างอย่างมีประสิทธิภาพไม่ใช่เพียงแค่ลบออก
tripleee

-1

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

ตัวอย่าง:

echo "$ url name" - (สามารถใช้ได้ตลอดเวลา)

echo "$ url name" - (ไม่สามารถใช้ในสถานการณ์ดังกล่าวได้ดังนั้นควรระวังไว้ก่อนใช้)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.