เหตุใดตัวเลือกในตัวแปรที่ยกมาจึงล้มเหลว แต่ทำงานเมื่อไม่ได้อ้างอิง


18

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

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

อันนี้ใช้ได้ผล:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

อันนี้ไม่ (บันทึกคำพูดคู่ aroung $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • อะไรคือสาเหตุของสิ่งนี้?

  • บรรทัดแรกเป็นเวอร์ชั่นที่ดี; หรือฉันควรสงสัยว่ามีข้อผิดพลาดที่ซ่อนอยู่ที่ไหนสักแห่งที่ทำให้เกิดพฤติกรรมนี้?

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


3
คำถามของคุณจะได้รับคำตอบที่นี่: mywiki.wooledge.org/BashFAQ/050
glenn jackman

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


4
ฉันคิดว่ามันช่วยให้เข้าใจว่าอาร์กิวเมนต์บรรทัดคำสั่งทำงานในระดับต่ำได้อย่างไร เมื่อโปรแกรมถูกเรียกใช้งานโปรแกรมจะรับอาร์กิวเมนต์เป็นรายการของอักขระ (ใกล้พอ) แต่ละรายการภายในคือสิ่งที่เราเรียกว่า "อาร์กิวเมนต์" โปรแกรมส่วนใหญ่ขึ้นอยู่กับการแยกเชิงตรรกะระหว่าง args ที่นี่คุณจะเห็นว่าwgetไม่รู้ว่าอะไร--mirror --no-host-directories(เป็นหนึ่งอาร์กิวเมนต์) แต่มันจัดการเมื่อแบ่งออกเป็นสองอาร์กิวเมนต์ โปรแกรมน้อยมากที่รักษาช่องว่างและคำพูดเป็นพิเศษเมื่อพวกเขาอยู่ในเวกเตอร์อาร์กิวเมนต์ ปัญหาคือว่าbashและกระสุนอื่น ๆ มีความหมายเป็น>
HTNW

2
> ใช้โดยมนุษย์ มันน่ารำคาญที่จะกำหนดขอบเขตระหว่างอาร์กิวเมนต์ด้วยตนเองดังนั้นเชลล์จึงแยกบน whitespace เพื่อเปลี่ยนบรรทัด (รายการของอักขระ) ไปเป็นเวกเตอร์อาร์กิวเมนต์ (รายการของรายการอักขระ) การขยายตัวแปรเป็นสิ่งที่การขยายครั้งแรกbashทำได้ดังนั้นคุณสามารถจินตนาการ$aได้ว่าเทียบเท่ากับการเขียนเนื้อหาโดยตรง ตอนนี้ปัญหาชัดเจน: a="-a -b"; cmd "$a"ขยายไปcmd "-a -b"แต่cmdอาจไม่รู้ว่าแปลว่าอะไร cmd $aขยายไปcmd -a -bซึ่งอาจไม่ทำงาน
HTNW

คำตอบ:


28

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

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

คำแยกเป็นสิ่งที่คุณต้องการ

ด้วย"$wget_options"(ที่ยกมา) wgetไม่ทราบว่าจะทำอย่างไรกับอาร์กิวเมนต์เดียว--mirror --no-host-directoriesและบ่น

wget: unknown option -- mirror --no-host-directories

เพื่อที่wgetจะเห็นทั้งสองตัวเลือก--mirrorและ--no-host-directoriesแยกกันการแยกคำจะต้องเกิดขึ้น

มีวิธีที่แข็งแกร่งกว่านี้ในการทำเช่นนี้ หากคุณกำลังใช้bashหรือเปลือกอื่น ๆ ที่ใช้อาร์เรย์ชอบbashทำดูคำตอบที่เกล็นแจ๊กแมน คำตอบที่กิลส์/bin/shนอกจากนี้ยังอธิบายถึงวิธีการแก้ปัญหาทางเลือกสำหรับเปลือกหอยชัดเช่นมาตรฐาน ทั้งสองจัดเก็บแต่ละตัวเลือกเป็นองค์ประกอบแยกต่างหากในอาร์เรย์

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


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

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

variable=$other_variable

อีกอันหนึ่งก็คือ

case $variable in
    ...) ... ;;
esac

2
ก่อนที่จะใช้โอเปอเรเตอร์แยก + glob ผู้ใช้อาจต้องตรวจสอบให้แน่ใจว่า$IFSมีค่าที่ถูกต้อง ที่นี่คุณต้องแยกพื้นที่และข้อความที่เกิดขึ้นไม่ได้มีแท็บหรือขึ้นบรรทัดใหม่ดังนั้นค่าเริ่มต้นของ$IFSจะทำ แต่ถ้ารหัสที่จะต้องใช้ในฟังก์ชั่นที่อาจจะเรียกว่าในบริบทที่$IFSอาจมีการแก้ไข คุณต้องการตั้งค่า$IFSไว้ล่วงหน้า (และอาจกู้คืนได้ในภายหลังหรือใช้ขอบเขตในท้องถิ่นหากรหัสที่เหลือถือว่าไม่มีการแก้ไข$IFS)
Stéphane Chazelas

32

วิธีที่แข็งแกร่งที่สุดในการใช้รหัสคือใช้อาร์เรย์:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"

นี่คือคำตอบที่ถูกต้อง การอ้างอิง
l0b0

2
เป็นคำตอบที่ดีดังนั้นฉัน upvote แต่ Kusalanda ช่วยให้ฉันเข้าใจมากกว่าว่าทำไมรหัสของฉันผิดและฉันสามารถรับได้เพียงหนึ่งเดียว
z32a7ul

ฉันวิ่งเข้าไปในโลกของปัญหาจนกระทั่งมีคนในรายการ rsync แสดงโครงสร้างนี้ให้ฉัน เป็นประโยชน์อย่างยิ่งหากองค์ประกอบบางอย่างอาจเป็นสตริงว่าง สิ่งนี้ทำให้สตริงว่างหายไป คำสั่งบางอย่างเช่นcpและrsyncจะทำสิ่งที่ไม่คาดคิดหากคำสั่งของคุณขยายออกไปเป็นอย่างrsync '' rest of parametersอื่น นี่เป็นสิ่งที่ยอดเยี่ยมสำหรับการสร้างคำสั่งทีละชิ้นอย่างมีเงื่อนไขและจากนั้นเรียกใช้เพียงครั้งเดียวในที่เดียว
Joe

17

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

wget_options='--mirror --no-host-directories'ตั้งค่าตัวแปรwget_optionsเป็นสตริงที่มีช่องว่าง ณ จุดนี้ไม่มีวิธีที่จะรู้ว่าพื้นที่ควรจะเป็นส่วนหนึ่งของตัวเลือกหรือตัวคั่นระหว่างตัวเลือก

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

เมื่อคุณใช้การทดแทนที่ไม่ได้wget $wget_optionsกล่าวถึงค่าของตัวแปรสตริงจะผ่านกระบวนการขยายชื่อเล่นว่า "split + glob":

  1. รับค่าของตัวแปรแล้วแบ่งออกเป็นส่วนที่คั่นด้วยช่องว่าง (สมมติว่าคุณยังไม่ได้แก้ไข$IFSตัวแปร) ผลลัพธ์นี้อยู่ในรายการระดับกลางของสตริง
  2. สำหรับแต่ละองค์ประกอบของรายการระดับกลางหากเป็นรูปแบบไวด์การ์ดที่ตรงกับไฟล์หนึ่งไฟล์ขึ้นไปให้แทนที่องค์ประกอบนั้นด้วยรายการไฟล์ที่ตรงกัน

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

ใน ksh, bash, yash และ zsh คุณสามารถใช้ตัวแปรอาร์เรย์ได้ อาร์เรย์ในเชลล์คำศัพท์คือรายการของสตริงดังนั้นจึงไม่มีการสูญเสียข้อมูล ในการสร้างตัวแปรอาร์เรย์ให้ใส่วงเล็บไว้รอบองค์ประกอบอาร์เรย์เมื่อกำหนดค่าให้กับตัวแปร ในการเข้าถึงองค์ประกอบทั้งหมดของอาร์เรย์ให้ใช้- นี่คือลักษณะทั่วไปของซึ่งเป็นรูปแบบรายการจากองค์ประกอบของอาร์เรย์ โปรดทราบว่าคุณต้องการเครื่องหมายคำพูดคู่ที่นี่ด้วยมิฉะนั้นองค์ประกอบแต่ละอย่างจะผ่านการแยก + glob"${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

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

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

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


สำหรับการดวลจุดโทษธรรมดา subshell (set -- ...; exec wget "$@" ...)จะรักษาข้อโต้แย้งตำแหน่ง:
John Kugelman สนับสนุน Monica
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.