เรื่องนี้มีการพูดคุยกันในหลายคำถามเกี่ยวกับยูนิกซ์ฉันจะพยายามรวบรวมปัญหาทั้งหมดที่ฉันสามารถทำได้ที่นี่ การอ้างอิงในตอนท้าย
ทำไมมันล้มเหลว
เหตุผลที่คุณประสบปัญหาเหล่านี้คือการแยกคำและข้อเท็จจริงที่ว่าคำพูดที่ขยายจากตัวแปรไม่ได้ทำหน้าที่เป็นคำพูด แต่เป็นเพียงตัวอักษรสามัญ
กรณีที่นำเสนอในคำถาม:
$ abc='ls -l "/tmp/test/my dir"'
ที่นี่$abc
จะถูกแบ่งและls
รับอาร์กิวเมนต์ทั้งสอง"/tmp/test/my
และdir"
(พร้อมเครื่องหมายคำพูดที่ด้านหน้าของส่วนแรกและส่วนหลังของสอง):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
ที่นี่การขยายตัวถูกยกมาดังนั้นจึงถูกเก็บไว้เป็นคำเดียว เชลล์พยายามหาโปรแกรมที่เรียกว่าls -l "/tmp/test/my dir"
ช่องว่างและเครื่องหมายคำพูดรวมอยู่ด้วย
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
และที่นี่มีเพียงคำแรกหรือ$abc
ถูกใช้เป็นอาร์กิวเมนต์-c
ดังนั้น Bash จึงทำงานls
ในไดเรกทอรีปัจจุบัน คำอื่น ๆ ที่มีข้อโต้แย้งที่จะทุบตีและถูกนำมาใช้ในการกรอกข้อมูล$0
, $1
ฯลฯ
$ bash -c $abc
'my dir'
ด้วยbash -c "$abc"
และeval "$abc"
มีขั้นตอนการประมวลผลเชลล์เพิ่มเติมซึ่งทำให้การทำงานของอัญประกาศ แต่ยังทำให้การขยายเชลล์ทั้งหมดถูกประมวลผลอีกครั้งดังนั้นจึงมีความเสี่ยงในการรันการขยายคำสั่งจากข้อมูลที่ผู้ใช้ให้โดยไม่ตั้งใจ ระมัดระวังเกี่ยวกับการอ้างอิง
วิธีที่ดีกว่าที่จะทำ
สองวิธีที่ดีกว่าในการเก็บคำสั่งคือ a) ใช้ฟังก์ชั่นแทน, b) ใช้ตัวแปรอาร์เรย์ (หรือพารามิเตอร์ตำแหน่ง)
ใช้ฟังก์ชั่น:
เพียงประกาศฟังก์ชั่นที่มีคำสั่งอยู่ภายในและเรียกใช้ฟังก์ชันราวกับว่ามันเป็นคำสั่ง การขยายในคำสั่งภายในฟังก์ชั่นนั้นจะได้รับการประมวลผลเฉพาะเมื่อคำสั่งรันไม่ใช่เมื่อถูกกำหนดไว้และคุณไม่จำเป็นต้องอ้างอิงแต่ละคำสั่ง
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
ใช้อาร์เรย์:
อาร์เรย์อนุญาตให้สร้างตัวแปรหลายคำโดยที่แต่ละคำมีช่องว่าง ที่นี่แต่ละคำจะถูกเก็บไว้เป็นองค์ประกอบอาเรย์ที่แตกต่างกันและการ"${array[@]}"
ขยายตัวขยายแต่ละองค์ประกอบเป็นคำเปลือกแยก:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
ไวยากรณ์น่ากลัวเล็กน้อย แต่อาร์เรย์ยังอนุญาตให้คุณสร้างบรรทัดคำสั่งทีละชิ้น ตัวอย่างเช่น:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
หรือทำให้บางส่วนของค่าคงที่บรรทัดคำสั่งและใช้อาร์เรย์เติมเพียงบางส่วนของมันตัวเลือกหรือชื่อไฟล์:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
ข้อเสียของอาร์เรย์คือพวกมันไม่ใช่คุณสมบัติมาตรฐานดังนั้นเชลล์ POSIX ธรรมดา (เช่นdash
ค่าเริ่มต้น/bin/sh
ใน Debian / Ubuntu) ไม่รองรับพวกมัน (แต่ดูด้านล่าง) อย่างไรก็ตาม Bash, ksh และ zsh ทำดังนั้นอาจเป็นไปได้ว่าระบบของคุณมีเชลล์บางตัวที่รองรับอาร์เรย์
การใช้ "$@"
ในเชลล์ที่ไม่มีการสนับสนุนสำหรับอาร์เรย์ที่มีชื่อหนึ่งยังคงสามารถใช้พารามิเตอร์ตำแหน่ง (อาร์เรย์เทียม"$@"
) เพื่อเก็บอาร์กิวเมนต์ของคำสั่ง
ต่อไปนี้ควรเป็นบิตสคริปต์แบบพกพาที่ทำหน้าที่เทียบเท่ากับบิตของรหัสในส่วนก่อนหน้า อาร์เรย์จะถูกแทนที่ด้วย"$@"
รายการพารามิเตอร์ตำแหน่ง การตั้งค่า"$@"
เสร็จสิ้นset
และเครื่องหมายอัญประกาศคู่"$@"
มีความสำคัญ (สิ่งเหล่านี้ทำให้องค์ประกอบของรายการถูกยกมาทีละรายการ)
ก่อนอื่นเพียงแค่เก็บคำสั่งที่มีอาร์กิวเมนต์ใน"$@"
และเรียกใช้:
set -- ls -l "/tmp/test/my dir"
"$@"
การตั้งค่าส่วนต่างๆของตัวเลือกบรรทัดคำสั่งตามเงื่อนไขสำหรับคำสั่ง:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
ใช้"$@"
สำหรับตัวเลือกและตัวถูกดำเนินการเท่านั้น:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(แน่นอนว่า"$@"
มักจะเต็มไปด้วยข้อโต้แย้งของสคริปต์ดังนั้นคุณจะต้องบันทึกไว้ที่ใดที่หนึ่งก่อนที่จะกำหนดใหม่"$@"
)
ระวังด้วยeval
!
เนื่องจากeval
เป็นการเพิ่มระดับการเสนอราคาและการขยายตัวเพิ่มเติมคุณจะต้องระมัดระวังกับการป้อนข้อมูลของผู้ใช้ ตัวอย่างเช่นใช้งานได้ตราบใดที่ผู้ใช้ไม่พิมพ์คำพูดใด ๆ :
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
แต่ถ้าพวกเขาให้อินพุต'$(uname)'.txt
สคริปต์ของคุณจะรันการทดแทนคำสั่งอย่างมีความสุข
รุ่นที่มีอาร์เรย์จะมีภูมิคุ้มกันว่าตั้งแต่คำจะถูกเก็บไว้ที่แยกต่างหากสำหรับตลอดเวลาไม่มีการพูดหรือการประมวลผลอื่น ๆ filename
สำหรับเนื้อหาของ
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
อ้างอิง