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


34
$ ls -l /tmp/test/my\ dir/
total 0

ฉันสงสัยว่าทำไมวิธีต่อไปนี้ในการเรียกใช้คำสั่งด้านบนจึงล้มเหลวหรือสำเร็จ

$ abc='ls -l "/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

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0


คำตอบ:


54

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


ทำไมมันล้มเหลว

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

กรณีที่นำเสนอในคำถาม:

$ 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[@]}"

อ้างอิง


2
คุณจะได้รับรอบ EVAL quoting cmd="ls -l $(printf "%q" "$filename")"สิ่งโดยการทำ ไม่ใช่เรื่องสวย แต่ถ้าผู้ใช้ถูกกำหนดให้ใช้evalงานมันจะช่วยได้ นอกจากนี้ยังเป็นประโยชน์อย่างมากสำหรับการส่งคำสั่ง แต่สิ่งที่คล้ายกันเช่นหรือในเสากระโดงเรือขวางของคำถามนี้:ssh foohost "ls -l $(printf "%q" "$filename")" ssh foohost "$cmd"
Patrick

ไม่เกี่ยวข้องโดยตรง แต่คุณเข้ารหัสไดเรกทอรีอย่างหนักหรือไม่ ในกรณีนี้คุณอาจต้องการดูนามแฝง สิ่งที่ต้องการ: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny

4

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

กรณีง่าย ๆ :

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

กรณีที่ไม่ง่าย:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

3

เครื่องหมายคำพูดที่สองแบ่งคำสั่ง

เมื่อฉันวิ่ง:

abc="ls -l '/home/wattana/Desktop'"
$abc

มันทำให้ฉันมีข้อผิดพลาด

แต่เมื่อฉันวิ่ง

abc="ls -l /home/wattana/Desktop"
$abc

ไม่มีข้อผิดพลาดเลย

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

คำตอบนี้ บอกว่าสามารถใช้คำสั่ง eval เพื่อแก้ไขปัญหานี้ แต่มันไม่ทำงานสำหรับฉัน :(


1
ใช่มันใช้งานได้ตราบเท่าที่ไม่จำเป็นต้องมีเช่นชื่อไฟล์ที่มีช่องว่างในตัว (หรือที่มีอักขระ glob)
ilkkachu

0

หากวิธีนี้ใช้ไม่ได้ผล''คุณควรใช้``:

abc=`ls -l /tmp/test/my\ dir/`

อัปเดตวิธีที่ดีกว่า:

abc=$(ls -l /tmp/test/my\ dir/)

สิ่งนี้จะเก็บผลลัพธ์ของคำสั่งไว้ในตัวแปร OP คือ (แปลก) ที่ต้องการบันทึกคำสั่งในตัวแปร โอ้และคุณควรเริ่มใช้$( command... )แทน backticks จริงๆ
roaima

ขอบคุณมากสำหรับคำอธิบายและเคล็ดลับ!
balon

0

วิธีการเกี่ยวกับ python3 หนึ่งซับ?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.