ทำไมเชลล์สคริปต์ของฉันถึงสำลักในช่องว่างหรืออักขระพิเศษอื่น ๆ


284

หรือคู่มือแนะนำการจัดการชื่อไฟล์ที่มีประสิทธิภาพและการส่งผ่านสตริงอื่น ๆ ในเชลล์สคริปต์

ฉันเขียนเชลล์สคริปต์ซึ่งทำงานได้ดีเกือบตลอดเวลา แต่มันฉายาบางอินพุต (เช่นชื่อไฟล์บางชื่อ)

ฉันพบปัญหาเช่นต่อไปนี้:

  • ฉันมีชื่อไฟล์ที่มีพื้นที่hello worldและมันก็ถือว่าเป็นสองไฟล์แยกต่างหากและhelloworld
  • ฉันมีสายอินพุตพร้อมช่องว่างสองช่องติดต่อกัน
  • ช่องว่างนำหน้าและต่อท้ายหายไปจากบรรทัดอินพุต
  • บางครั้งเมื่ออินพุตมีอักขระหนึ่งตัวอักขระ\[*?เหล่านั้นจะถูกแทนที่ด้วยข้อความบางส่วนซึ่งเป็นชื่อไฟล์จริง
  • มีเครื่องหมายอะโพสโทรฟี'(หรือเครื่องหมายคำพูดคู่") ในอินพุตและสิ่งต่าง ๆ แปลกประหลาดหลังจากจุดนั้น
  • มีแบ็กสแลชในอินพุต (หรือ: ฉันใช้ Cygwin และชื่อไฟล์บางส่วนของฉันมี\ตัวคั่นสไตล์ Windows )

เกิดอะไรขึ้นและฉันจะแก้ไขได้อย่างไร


16
shellcheckช่วยคุณปรับปรุงคุณภาพโปรแกรมของคุณ
aurelien

3
นอกเหนือจากเทคนิคการป้องกันที่อธิบายไว้ในคำตอบและถึงแม้ว่ามันจะเป็นที่ชัดเจนสำหรับผู้อ่านส่วนใหญ่ฉันคิดว่ามันอาจจะคุ้มค่าที่จะแสดงความคิดเห็นว่าเมื่อไฟล์ตั้งใจจะถูกประมวลผลโดยใช้เครื่องมือบรรทัดคำสั่ง ชื่อในสถานที่แรกถ้าเป็นไปได้
bli


1
@bli ไม่นั่นเป็นเพียงการสร้างบั๊กที่ใช้เวลานานกว่า วันนี้มันมีการซ่อนข้อบกพร่อง และตอนนี้คุณไม่รู้ชื่อไฟล์ทั้งหมดที่ใช้กับรหัสของคุณในภายหลัง
Volker Siegel

ก่อนอื่นหากพารามิเตอร์ของคุณมีช่องว่างพวกเขาจำเป็นต้องยกมาใน (ในบรรทัดคำสั่ง) อย่างไรก็ตามคุณสามารถคว้าบรรทัดคำสั่งทั้งหมดแล้วแยกวิเคราะห์ด้วยตัวคุณเอง สองช่องว่างไม่เปลี่ยนเป็นหนึ่งช่องว่าง จำนวนช่องว่างใด ๆ บอกสคริปต์ของคุณว่าเป็นตัวแปรถัดไปดังนั้นหากคุณทำเช่น "echo $ 1 $ 2" สคริปต์ของคุณจะใส่ช่องว่างหนึ่งช่องเข้าไป นอกจากนี้ยังใช้ "find (-exec)" เพื่อวนซ้ำไฟล์ที่มีช่องว่างแทนการวนซ้ำ คุณสามารถจัดการกับช่องว่างได้ง่ายขึ้น
Patrick Taylor

คำตอบ:


352

ใช้เครื่องหมายคำพูดคู่รอบ ๆ การแทนที่ตัวแปรและการแทนที่คำสั่งเสมอ: "$foo","$(foo)"

ถ้าคุณใช้$foounquoted สคริปต์ของคุณจะสำลักกับการป้อนข้อมูลหรือพารามิเตอร์ (หรือคำสั่งออกด้วย$(foo)) \[*?ที่มีช่องว่างหรือ

ที่นั่นคุณสามารถหยุดอ่าน ดีตกลงนี่คืออีกไม่กี่:

  • read- หากต้องการอ่านอินพุตบรรทัดต่อบรรทัดโดยใช้readบิวด์อินให้ใช้while IFS= read -r line; do …
    Plain readถือแบ็กสแลชและช่องว่างพิเศษ
  • xargs- หลีกเลี่ยงการ xargsหากคุณต้องใช้xargsให้ทำสิ่งxargs -0นั้น แทนที่จะfind … | xargs, ต้องการ find … -exec …
    xargsถือว่าช่องว่างและตัวละคร\"'เป็นพิเศษ

คำตอบนี้นำไปใช้กับเปลือกหอยบอร์น / POSIX สไตล์ ( sh, ash, dash, bash, ksh, mksh, yash... ) ผู้ใช้ Zsh ควรข้ามและอ่านจุดสิ้นสุดของการอ้างอิงสองครั้งเมื่อใด แทน. หากคุณต้องการ nitty-gritty ทั้งหมดให้อ่านมาตรฐานหรือคู่มือของเชลล์


โปรดทราบว่าคำอธิบายด้านล่างมีการประมาณสองสามประการ (ข้อความที่เป็นจริงในเงื่อนไขส่วนใหญ่ แต่อาจได้รับผลกระทบจากบริบทแวดล้อมหรือการกำหนดค่า)

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

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

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

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

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

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

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

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

ฉันจะประมวลผลรายการชื่อไฟล์ได้อย่างไร

หากคุณเขียนmyfiles="file1 file2"ด้วยช่องว่างเพื่อแยกไฟล์สิ่งนี้จะไม่สามารถใช้งานได้กับชื่อไฟล์ที่มีช่องว่าง ชื่อไฟล์ Unix สามารถมีอักขระอื่น ๆ นอกเหนือจาก/(ซึ่งเป็นตัวคั่นไดเรกทอรีเสมอ) และ null ไบต์ (ซึ่งคุณไม่สามารถใช้ในเชลล์สคริปต์กับเชลล์ส่วนใหญ่)

myfiles=*.txt; … process $myfilesปัญหาเดียวกันกับ เมื่อคุณทำเช่นนี้ตัวแปรจะmyfilesมีสตริง 5 ตัวอักษร*.txtและเมื่อคุณเขียน$myfilesว่าอักขระตัวแทนจะถูกขยาย myfiles="$someprefix*.txt"; … process $myfilesตัวอย่างเช่นนี้จริงจะทำงานจนกว่าคุณจะเปลี่ยนสคริปต์ของคุณจะ หากsomeprefixตั้งค่าเป็นfinal reportสิ่งนี้จะไม่ทำงาน

ในการประมวลผลรายการใด ๆ (เช่นชื่อไฟล์) ให้ใส่ไว้ในอาร์เรย์ สิ่งนี้ต้องใช้ mksh, ksh93, yash หรือ bash (หรือ zsh ซึ่งไม่มีปัญหาการอ้างอิงทั้งหมด) POSIX เชลล์ธรรมดา (เช่นเถ้าหรือเส้นประ) ไม่มีตัวแปรอาเรย์

myfiles=("$someprefix"*.txt)
process "${myfiles[@]}"

Ksh88 มีตัวแปรอาร์เรย์ที่มีไวยากรณ์การกำหนดที่แตกต่างกันset -A myfiles "someprefix"*.txt(ดูตัวแปรการกำหนดภายใต้สภาพแวดล้อม ksh ที่ต่างกันหากคุณต้องการความสะดวกในการพกพา ksh88 / bash) Bourne / POSIX-style shells มีหนึ่งอาเรย์เดียว, อาเรย์ของพารามิเตอร์ตำแหน่ง"$@"ที่คุณตั้งค่าด้วยsetและที่อยู่ในท้องถิ่นของฟังก์ชั่น:

set -- "$someprefix"*.txt
process -- "$@"

แล้วชื่อไฟล์ที่ขึ้นต้นด้วย-ล่ะ

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

-หรือคุณสามารถตรวจสอบให้แน่ใจว่าชื่อไฟล์ของคุณเริ่มต้นด้วยตัวอักษรอื่นที่ไม่ใช่ ชื่อไฟล์ที่แน่นอนเริ่มต้นด้วย/และคุณสามารถเพิ่ม./ที่จุดเริ่มต้นของชื่อญาติ ตัวอย่างต่อไปนี้จะเปลี่ยนเนื้อหาของตัวแปรfเป็นวิธีที่“ปลอดภัย” -ของหมายถึงไฟล์เดียวกันที่รับประกันไม่ได้เริ่มต้นด้วย

case "$f" in -*) "f=./$f";; esac

ในบันทึกสุดท้ายในหัวข้อนี้ระวังว่าคำสั่งบางคนตีความเป็นความหมายเข้ามาตรฐานหรือมาตรฐานการส่งออกแม้หลังจากที่- --หากคุณต้องการอ้างถึงไฟล์จริงชื่อ-หรือถ้าคุณกำลังเรียกโปรแกรมดังกล่าวและคุณไม่ต้องการให้มันอ่านจาก stdin หรือเขียนไปยัง stdout ตรวจสอบให้แน่ใจว่าได้เขียนใหม่-ดังกล่าวข้างต้น ดูความแตกต่างระหว่าง "du -sh *" และ "du -sh ./*" คืออะไร สำหรับการสนทนาต่อไป

ฉันจะเก็บคำสั่งในตัวแปรได้อย่างไร

“ Command” อาจหมายถึงสามสิ่ง: ชื่อคำสั่ง (ชื่อเป็นไฟล์เรียกทำงานที่มีหรือไม่มีพา ธ เต็มหรือชื่อของฟังก์ชันบิวด์อินหรือนามแฝง) ชื่อคำสั่งที่มีอาร์กิวเมนต์หรือชิ้นส่วนของรหัสเชลล์ มีวิธีที่แตกต่างกันในการจัดเก็บไว้ในตัวแปร

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

command_path="$1"

"$command_path" --option --message="hello world"

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

cmd=(/path/to/executable --option --message="hello world" --)
cmd=("${cmd[@]}" "$file1" "$file2")
"${cmd[@]}"

ถ้าคุณใช้เชลล์ที่ไม่มีอาร์เรย์ คุณยังคงสามารถใช้พารามิเตอร์ตำแหน่งได้หากคุณไม่ต้องการแก้ไขค่าเหล่านั้น

set -- /path/to/executable --option --message="hello world" --
set -- "$@" "$file1" "$file2"
"$@"

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

code='/path/to/executable --option --message="hello world" -- /path/to/file1 | grep "interesting stuff"'
eval "$code"

หมายเหตุ: คำพูดที่ซ้อนกันในความหมายของcode: ราคาเดียว'…'คั่นสตริงตัวอักษรเพื่อให้ค่าของตัวแปรเป็นสตริงcode builtin บอกเปลือกจะแยกสตริงผ่านเป็นอาร์กิวเมนต์เป็นถ้ามันปรากฏในสคริปต์เพื่อให้จุดที่คำพูดและท่อมีการแยกวิเคราะห์ ฯลฯ/path/to/executable --option --message="hello world" -- /path/to/file1eval

การใช้evalเป็นเรื่องยุ่งยาก คิดให้รอบคอบเกี่ยวกับสิ่งที่ถูกแยกวิเคราะห์เมื่อ โดยเฉพาะอย่างยิ่งคุณไม่สามารถใส่ชื่อไฟล์ลงในรหัสได้: คุณจำเป็นต้องอ้างอิงมันเช่นเดียวกับที่คุณทำถ้ามันอยู่ในไฟล์ซอร์สโค้ด ไม่มีวิธีโดยตรงที่จะทำเช่นนั้น สิ่งที่ต้องการcode="$code $filename"แบ่งถ้าชื่อไฟล์มีเปลือกอักขระพิเศษใด ๆ (พื้นที่, $, ;, |, <, >ฯลฯ ) ยังคงแบ่งบนcode="$code \"$filename\"" "$\`แม้จะหยุดพักหากชื่อไฟล์ที่มีcode="$code '$filename'" 'มีสองวิธีแก้ไข

  • เพิ่มเลเยอร์ของเครื่องหมายคำพูดรอบ ๆ ชื่อไฟล์ วิธีที่ง่ายที่สุดที่จะทำคือการเพิ่มราคาเดียวรอบ ๆ '\''มันและแทนที่คำพูดเดียวโดย

    quoted_filename=$(printf %s. "$filename" | sed "s/'/'\\\\''/g")
    code="$code '${quoted_filename%.}'"
  • คงการขยายตัวของตัวแปรไว้ในโค้ดเพื่อให้มันค้นหาเมื่อมีการประเมินโค้ดไม่ใช่เมื่อมีการสร้างแฟรกเมนต์โค้ด สิ่งนี้ง่ายกว่า แต่จะใช้ได้ก็ต่อเมื่อตัวแปรยังคงมีค่าเท่าเดิมในขณะที่มีการเรียกใช้งานรหัสไม่ใช่เช่นถ้ามีการสร้างรหัสในลูป

    code="$code \"\$filename\""

สุดท้ายคุณต้องการตัวแปรที่มีรหัสหรือไม่? วิธีที่เป็นธรรมชาติที่สุดในการตั้งชื่อให้กับ code block คือการกำหนดฟังก์ชั่น

อะไรขึ้นกับread?

โดยไม่ต้อง-r, readช่วยให้เส้นต่อเนื่อง - นี้เป็นสายเชิงตรรกะเดียวของการป้อนข้อมูล:

hello \
world

readแยกบรรทัดอินพุตออกเป็นฟิลด์ที่คั่นด้วยอักขระใน$IFS(โดยไม่ใช้-rแบ็กสแลชก็จะหลีกเลี่ยงสิ่งเหล่านั้น) ตัวอย่างเช่นหากอินพุตเป็นบรรทัดที่มีสามคำให้read first second thirdตั้งค่าfirstเป็นคำแรกของอินพุตsecondเป็นคำที่สองและthirdเป็นคำที่สาม หากมีคำมากกว่านี้ตัวแปรสุดท้ายจะมีทุกอย่างที่เหลือหลังจากตั้งค่าก่อนหน้า ช่องว่างนำหน้าและต่อท้ายถูกตัดแต่ง

การตั้งค่าIFSเป็นสตริงว่างหลีกเลี่ยงการตัดแต่งใด ๆ ดูว่าเหตุใด `ในขณะที่ IFS = read` ใช้บ่อยๆแทนที่จะเป็น` IFS =; ในขณะที่อ่าน .. สำหรับคำอธิบายที่ยาวขึ้น

มีอะไรผิดปกติกับxargs?

รูปแบบการป้อนข้อมูลของxargsสตริงที่คั่นด้วยช่องว่างซึ่งอาจเป็นทางเลือกเดียวหรือสองครั้งที่ยกมา ไม่มีเครื่องมือมาตรฐานส่งออกรูปแบบนี้

อินพุตxargs -L1หรือxargs -lเกือบเป็นรายการของเส้น แต่ไม่มาก - หากมีช่องว่างที่ท้ายบรรทัดบรรทัดต่อไปนี้เป็นบรรทัดต่อเนื่อง

คุณสามารถใช้ในxargs -0กรณีที่สามารถใช้งานได้(และหากมี: GNU (Linux, Cygwin), BusyBox, BSD, OSX แต่ไม่ได้อยู่ใน POSIX) ปลอดภัยเนื่องจากไบต์ null ไม่สามารถปรากฏในข้อมูลส่วนใหญ่โดยเฉพาะในชื่อไฟล์ ในการสร้างรายการชื่อไฟล์ที่คั่นด้วย null ให้ใช้find … -print0(หรือคุณสามารถใช้find … -exec …ตามที่อธิบายไว้ด้านล่าง)

ฉันจะประมวลผลไฟล์ที่พบได้findอย่างไร

find  -exec some_command a_parameter another_parameter {} +

some_commandจำเป็นต้องเป็นคำสั่งภายนอกมันไม่สามารถเป็นฟังก์ชันของเชลล์หรือนามแฝงได้ หากคุณต้องการเรียกใช้เชลล์เพื่อประมวลผลไฟล์ให้เรียกshอย่างชัดเจน

find  -exec sh -c '
  for x do
    … # process the file "$x"
  done
' find-sh {} +

ฉันมีคำถามอื่น

เรียกดูแท็กในเว็บไซต์นี้หรือหรือเปลือกสคริปต์(คลิกที่ปุ่ม“เรียนรู้เพิ่มเติม ...” เพื่อดูเคล็ดลับทั่วไปและรายการมือเลือกคำถามที่พบบ่อย.) หากคุณได้ค้นหาและคุณไม่สามารถหาคำตอบถามออกไป


6
@ John1024 เป็นคุณลักษณะของ GNU เท่านั้นดังนั้นฉันจะยึดติดกับ "ไม่มีเครื่องมือมาตรฐาน"
Gilles

2
นอกจากนี้คุณยังต้องคำพูดรอบ$(( ... ))(ยัง$[...]อยู่ในเปลือกหอยบางส่วน) ยกเว้นในzsh(แม้ในการดวลจุดโทษจำลอง) mkshและ
Stéphane Chazelas

3
โปรดทราบว่าxargs -0ไม่ใช่ POSIX ยกเว้นกับ FreeBSD xargsคุณโดยทั่วไปต้องการแทนxargs -r0 xargs -0
Stéphane Chazelas

2
@ John1024 ไม่มีไม่เข้ากันกับls --quoting-style=shell-always xargsลองtouch $'a\nb'; ls --quoting-style=shell-always | xargs
Stéphane Chazelas

3
คุณสมบัติอื่นที่ดี (GNU เท่านั้น) เป็นxargs -d "\n"เพื่อให้คุณสามารถทำงานได้เช่นlocate PATTERN1 |xargs -d "\n" grep PATTERN2การค้นหาชื่อไฟล์ที่ตรงกับpattern1ที่มีเนื้อหาตรงกับpattern2 หากไม่มี GNU คุณสามารถทำเช่นนั้นได้locate PATTERN1 |perl -pne 's/\n/\0/' |xargs -0 grep PATTERN1
Adam Katz

26

ในขณะที่คำตอบของ Gilles นั้นยอดเยี่ยม แต่ฉันก็มีประเด็นสำคัญ

ใช้เครื่องหมายคำพูดคู่ล้อมรอบการแทนที่ตัวแปรและการแทนที่คำสั่งเสมอ: "$ foo", "$ (foo)"

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

§การแยกคำ

คำสั่งเหล่านี้สามารถทำงานได้โดยไม่มีข้อผิดพลาด

foo=$bar
bar=$(a command)
logfile=$logdir/foo-$(date +%Y%m%d)
PATH=/usr/local/bin:$PATH ./myscript
case $foo in bar) echo bar ;; baz) echo baz ;; esac

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


19
ตามที่ฉันพูดถึงในคำตอบของฉันดูunix.stackexchange.com/questions/68694/…สำหรับรายละเอียด สังเกตคำถาม -“ ทำไมเชลล์สคริปต์ของฉันถึงหายใจไม่ออก” ปัญหาที่พบบ่อยที่สุด (จากประสบการณ์หลายปีในไซต์นี้และที่อื่น ๆ ) ไม่มีเครื่องหมายคำพูดคู่ “ ใช้เครื่องหมายคำพูดคู่เสมอ” ง่ายต่อการจดจำได้ง่ายกว่า“ ใช้เครื่องหมายคำพูดคู่เสมอยกเว้นกรณีเหล่านี้ที่ไม่จำเป็นต้องใช้”
Gilles

14
กฎเป็นเรื่องยากที่จะเข้าใจสำหรับผู้เริ่มต้น ตัวอย่างเช่นfoo=$barตกลง แต่มีexport foo=$barหรือenv foo=$varไม่มี (อย่างน้อยในเชลล์บางตัว) ให้คำแนะนำสำหรับการเริ่มต้น: เสมอพูดตัวแปรของคุณจนกว่าคุณจะรู้ว่าสิ่งที่คุณทำและมีเหตุผลที่ดีที่จะไม่
Stéphane Chazelas

5
@StevenPenny ถูกต้องกว่านี้จริงหรือ มีกรณีที่สมเหตุสมผลที่ราคาจะทำลายสคริปต์หรือไม่ ในสถานการณ์ที่ต้องใช้เครื่องหมายคำพูดในครึ่งกรณีและในอีกครึ่งคำพูดอาจใช้เป็นทางเลือก - คำแนะนำ "ใช้คำพูดเสมอในกรณี" เป็นคำที่ควรพิจารณาเนื่องจากเป็นจริงง่ายและมีความเสี่ยงน้อย การสอนรายการข้อยกเว้นดังกล่าวให้กับผู้เริ่มต้นนั้นเป็นที่ทราบกันดีว่าไม่มีประสิทธิภาพ (ขาดบริบทพวกเขาจะไม่จดจำพวกเขา) และต่อต้านเนื่องจากพวกเขาจะสร้างความสับสนให้กับคำพูดที่ต้องการ / คำพูดที่ไม่ต้องการ
Peteris

6
$ 0.02 ของฉันจะเป็นที่แนะนำให้พูดทุกอย่างเป็นคำแนะนำที่ดี การอ้างถึงสิ่งที่ไม่ต้องการโดยไม่ได้ตั้งใจนั้นเป็นความผิดพลาดโดยไม่ได้ตั้งใจไม่สามารถอ้างถึงบางสิ่งที่ไม่จำเป็นต้องเป็นอันตราย ดังนั้นสำหรับผู้เขียนเชลล์สคริปส่วนใหญ่ที่จะไม่เข้าใจความซับซ้อนของการแยกคำเกิดขึ้นการอ้างอิงทุกสิ่งนั้นปลอดภัยกว่าการพยายามอ้างเฉพาะที่จำเป็นเท่านั้น
godlygeek

5
@Peteris และ godlygeek: "มีกรณีที่เหมาะสมที่ราคาจะทำลายสคริปต์หรือไม่" ขึ้นอยู่กับคำจำกัดความของคุณของ "สมเหตุสมผล" หากสคริปต์ตั้งค่าcriteria="-type f"ก็ใช้find . $criteriaงานได้ แต่ใช้find . "$criteria"ไม่ได้
G-Man

22

เท่าที่ฉันรู้มีเพียงสองกรณีที่จำเป็นในการขยายเครื่องหมายคำพูดสองเท่าและกรณีเหล่านั้นเกี่ยวข้องกับพารามิเตอร์เชลล์พิเศษสองรายการ"$@"และ"$*"- ซึ่งระบุไว้เพื่อขยายแตกต่างกันเมื่ออยู่ในเครื่องหมายคำพูดคู่ ในกรณีอื่น ๆ ทั้งหมด(ไม่รวมบางทีการใช้งานอาเรย์เฉพาะเชลล์)พฤติกรรมของการขยายตัวเป็นสิ่งที่กำหนดค่าได้ - มีตัวเลือกสำหรับสิ่งนั้น

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

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

  1. ยอมรับอินพุต

  2. แปลและแยกให้ถูกต้องเป็นคำที่ป้อนเข้าโทเค็น

    • คำที่ป้อนเข้าเป็นรายการไวยากรณ์ของเชลล์เช่น$wordหรือecho $words 3 4* 5

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

  3. ขยายสิ่งเหล่านั้นหากจำเป็นในหลาย ๆฟิลด์

    • เขตข้อมูลเป็นผลมาจากการขยายคำ - พวกเขาทำขึ้นคำสั่งปฏิบัติการสุดท้าย

    • ยกเว้น"$@", $IFS ข้อมูลการแยกและการขยายตัวของพา ธใส่คำมักจะต้องประเมินเดียวฟิลด์

  4. จากนั้นเพื่อดำเนินการคำสั่งที่เกิดขึ้น

    • ในกรณีส่วนใหญ่เกี่ยวข้องกับการส่งผ่านผลลัพธ์ของการตีความในบางรูปแบบหรืออื่น ๆ

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

และนั่นคือสิ่งที่$IFSมาใน. $IFSเป็นปัจจุบันเสมอ - และตัวอย่างไฟล์เช่นเดียวกัน - พารามิเตอร์เปลือกที่กำหนดวิธีเปลือกควรแยกขยายเปลือกจากคำไปสนาม - เฉพาะในสิ่งที่ค่าเหล่านั้นเขตควรคั่น $IFSแยกการขยายเชลล์บนตัวคั่นอื่นที่ไม่ใช่NUL- หรือกล่าวอีกนัยหนึ่งเชลล์จะแทนที่ไบต์ซึ่งเป็นผลมาจากการขยายตัวที่ตรงกับค่าที่$IFSมีNULในอาร์เรย์ข้อมูลภายใน เมื่อคุณดูอย่างนั้นคุณอาจเริ่มเห็นว่าการขยายเชลล์การแบ่งฟิลด์ทุกครั้งเป็น$IFSอาเรย์ข้อมูลที่ถูก จำกัด

สิ่งสำคัญคือต้องเข้าใจว่า$IFSมีเพียงการขยายขอบเขตที่ยังไม่ได้คั่นด้วย - ซึ่งคุณสามารถทำได้ด้วย"เครื่องหมายคำพูดคู่ เมื่อคุณอ้างถึงการขยายคุณกำหนดขอบเขตที่ส่วนหัวและอย่างน้อยก็จนถึงส่วนท้ายของค่า ในกรณี$IFSดังกล่าวใช้ไม่ได้เนื่องจากไม่มีฟิลด์ที่จะแยก ในความเป็นจริงการขยายตัวที่ยกมาสองครั้งเหมือนการจัดแสดงนิทรรศการฟิลด์แยกพฤติกรรมการขยายตัว unquoted เมื่อIFS=ถูกตั้งค่าเป็นค่าว่าง

เว้นแต่จะยกมา$IFSมันเป็น$IFSตัวขยายเชลล์ที่คั่นด้วย เป็นค่าเริ่มต้นให้เป็นค่าที่ระบุ<space><tab><newline>- $IFSทั้งหมดสามที่แสดงคุณสมบัติพิเศษเมื่อบรรจุอยู่ภายใน ในขณะที่ค่าอื่น ๆ$IFSที่ระบุไว้ในการประเมินที่เดียวฟิลด์ต่อการขยายตัวของการเกิดขึ้น , $IFS ช่องว่าง - ใด ๆ ของบรรดาสาม - มีการระบุที่จะมองข้ามกับสนามเดียวต่อการขยายตัวตามลำดับและนำ / ลำดับต่อท้ายจะ elided ทั้งหมด นี่อาจเป็นตัวอย่างที่เข้าใจง่ายที่สุด

slashes=///// spaces='     '
IFS=/; printf '<%s>' $slashes$spaces
<><><><><><     >
IFS=' '; printf '<%s>' $slashes$spaces
</////>
IFS=; printf '<%s>' $slashes$spaces
</////     >
unset IFS; printf '<%s>' "$slashes$spaces"
</////     >

แต่นั่นเป็นเพียง$IFS- แยกคำหรือช่องว่างตามที่ถามดังนั้นสิ่งที่ตัวละครพิเศษ ?

เชลล์ - โดยค่าเริ่มต้น - จะขยายโทเค็นที่ไม่มีเครื่องหมายอัญประกาศ(เช่น?*[ที่บันทึกไว้ที่นี่ที่อื่น)ไปยังหลาย ๆฟิลด์เมื่อเกิดขึ้นในรายการ นี้เรียกว่าการขยายตัวของพา ธหรือglobbing มันเป็นเครื่องมือที่มีประโยชน์อย่างไม่น่าเชื่อและเมื่อมันเกิดขึ้นหลังจากการแยกฟิลด์ในการแยกวิเคราะห์คำสั่งของเชลล์มันไม่ได้รับผลกระทบจาก$ IFS - ฟิลด์ที่สร้างโดยการขยายชื่อพา ธ จะคั่นด้วยหัว / หางของชื่อไฟล์เอง เนื้อหาของพวกเขามีตัวอักษรใด ๆ $IFSในขณะนี้ ลักษณะการทำงานนี้ถูกตั้งค่าเป็นเปิดโดยค่าเริ่มต้น - แต่มันถูกกำหนดค่าไว้เป็นอย่างอื่นอย่างง่ายดาย

set -f

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

set +f

... ถูกส่งไปยังเชลล์ การเสนอราคาสองครั้ง - เช่นเดียวกับ$IFS การแยกฟิลด์ - ทำให้การตั้งค่าส่วนกลางนี้ไม่จำเป็นต่อการขยายตัว ดังนั้น:

echo "*" *

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

set -f; echo "*" *

... ผลลัพธ์ของอาร์กิวเมนต์ทั้งสองนั้นเหมือนกัน - *จะไม่ขยายในกรณีนั้น


ฉันเห็นด้วยกับ @ StéphaneChazelasจริง ๆ แล้วว่า (ส่วนใหญ่) ทำให้เกิดความสับสนมากกว่าการช่วย ... แต่ฉันคิดว่ามันมีประโยชน์ส่วนตัวเป็นการส่วนตัวฉันเลย upvoted ตอนนี้ฉันมีความคิดที่ดีขึ้น (และตัวอย่าง) ของวิธีการIFSใช้งานจริง สิ่งที่ฉันไม่ได้รับคือทำไมมันจะเคยเป็นความคิดที่ดีในการตั้งIFSอย่างอื่นที่ไม่ใช่ค่าเริ่มต้น
สัญลักษณ์แทน

1
@ Wildcard - เป็นตัวคั่นฟิลด์ $IFSถ้าคุณมีความคุ้มค่าในตัวแปรที่คุณต้องการที่จะขยายไปหลายเขตข้อมูลให้คุณสามารถแยกบน cd /usr/bin; set -f; IFS=/; for path_component in $PWD; do echo $path_component; doneพิมพ์\nแล้วนั้นusr\n bin\nอันแรกechoว่างเปล่าเพราะ/เป็นฟิลด์ว่าง path_components สามารถขึ้นบรรทัดใหม่หรือเว้นวรรคหรืออะไรก็ได้ - ไม่สำคัญเพราะส่วนประกอบถูกแยก/และไม่ใช่ค่าเริ่มต้น ผู้คนทำมันด้วยawkตลอดเวลา เปลือกของคุณก็ทำเช่นกัน
mikeserv

3

ฉันมีโครงการวิดีโอขนาดใหญ่ที่มีช่องว่างในชื่อไฟล์และช่องว่างในชื่อไดเรกทอรี ในขณะที่ใช้find -type f -print0 | xargs -0งานได้หลายวัตถุประสงค์และข้ามเชลล์ที่แตกต่างกันฉันพบว่าการใช้ IFS แบบกำหนดเอง (ตัวคั่นฟิลด์อินพุต) ให้ความยืดหยุ่นแก่คุณมากขึ้นหากคุณกำลังใช้ bash ตัวอย่างด้านล่างใช้ bash และตั้งค่า IFS ให้เป็นแค่บรรทัดใหม่ หากไม่มีบรรทัดใหม่ในชื่อไฟล์ของคุณ:

(IFS=$'\n'; for i in $(find -type f -print) ; do
    echo ">>>$i<<<"
done)

สังเกตการใช้ parens เพื่อแยกนิยามใหม่ของ IFS ฉันได้อ่านโพสต์อื่น ๆ เกี่ยวกับวิธีการกู้คืน IFS แต่นี่เป็นเรื่องง่าย

ยิ่งไปกว่านั้นการตั้งค่า IFS เป็นบรรทัดใหม่ให้คุณตั้งค่าตัวแปรเชลล์ล่วงหน้าและพิมพ์ออกมาได้อย่างง่ายดาย ตัวอย่างเช่นฉันสามารถสร้างตัวแปร V เพิ่มขึ้นโดยใช้บรรทัดใหม่เป็นตัวคั่น:

V=""
V="./Ralphie's Camcorder/STREAM/00123.MTS,04:58,05:52,-vf yadif"
V="$V"$'\n'"./Ralphie's Camcorder/STREAM/00111.MTS,00:00,59:59,-vf yadif"
V="$V"$'\n'"next item goes here..."

และตามลําดับ:

(IFS=$'\n'; for v in $V ; do
    echo ">>>$v<<<"
done)

ตอนนี้ฉันสามารถ "แสดงรายการ" การตั้งค่าของ V ด้วยการecho "$V"ใช้เครื่องหมายคำพูดคู่เพื่อแสดงบรรทัดใหม่ (ให้เครดิตกับกระทู้นี้สำหรับ$'\n'คำอธิบาย)


3
แต่คุณจะยังคงมีปัญหากับชื่อไฟล์ที่มีอักขระขึ้นบรรทัดใหม่หรือตัวกลม ดูเพิ่มเติมที่: ทำไมการวนซ้ำของผลลัพธ์ที่ไม่เหมาะสมของ find find? . หากใช้zshคุณสามารถใช้IFS=$'\0'และใช้งานได้-print0( zshไม่ทำให้กลมกลืนไปกับการขยายดังนั้นตัวอักษรแบบกลมก็ไม่มีปัญหา)
Stéphane Chazelas

1
ใช้งานได้กับชื่อไฟล์ที่มีช่องว่าง แต่ไม่สามารถใช้งานได้กับชื่อไฟล์ที่อาจเป็นศัตรูหรือชื่อไฟล์ "ไม่เป็น" ที่ไม่ได้ตั้งใจ set -fคุณสามารถแก้ไขปัญหาของชื่อไฟล์ที่มีอักขระตัวแทนโดยการเพิ่ม ในทางกลับกันวิธีการของคุณล้มเหลวโดยพื้นฐานด้วยชื่อไฟล์ที่มีการขึ้นบรรทัดใหม่ เมื่อจัดการกับข้อมูลอื่นที่ไม่ใช่ชื่อไฟล์มันก็ล้มเหลวด้วยรายการที่ว่างเปล่า
Gilles

ใช่ข้อแม้ของฉันคือมันจะไม่ทำงานกับบรรทัดใหม่ในชื่อไฟล์ แต่ผมเชื่อว่าเราจะต้องวาดเส้นเพียงอายของความบ้า ;-)
รัส

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

0

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

$ FILES='"a b" c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
$ FILES='a\ b c'
$ eval ls $FILES
ls: a b: No such file or directory
ls: c: No such file or directory
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.