ควรอ้างอิงตัวแปรเมื่อดำเนินการหรือไม่


18

กฎทั่วไปในการเขียนสคริปต์เปลือกคือตัวแปรที่ควรจะยกมาเว้นแต่จะมีเหตุผลที่น่าสนใจที่จะไม่ สำหรับรายละเอียดมากกว่าที่คุณอาจต้องการทราบลองดูที่ Q&A ที่ยอดเยี่ยมนี้: ผลกระทบด้านความปลอดภัยของการลืมอ้างตัวแปรใน bash / POSIX shellsเปลือกหอย

พิจารณาอย่างไรก็ตามฟังก์ชั่นเช่นนี้:

run_this(){
    $@
}

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

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

การเรียกใช้สคริปต์ด้านบนส่งคืน:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

ฉันสามารถหลีกเลี่ยงสิ่งนั้นถ้าฉันใช้run_that $commแทนrun_that "$comm"แต่เนื่องจากrun_thisฟังก์ชั่น (ไม่พูดถึง) ทำงานกับทั้งคู่ดูเหมือนว่าการเดิมพันที่ปลอดภัยกว่า

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


6
run_thatพฤติกรรมของแน่นอนสิ่งที่ฉันคาดหวัง (ถ้ามีช่องว่างในเส้นทางไปยังคำสั่ง?) หากคุณต้องการพฤติกรรมอื่น ๆ คุณจะต้องถอนมันออกจากไซต์ที่คุณรู้ว่าข้อมูลคืออะไร? ฉันคาดว่าจะเรียกใช้ฟังก์ชั่นนี้run_that ls -lซึ่งใช้งานได้เหมือนกันในทั้งสองเวอร์ชัน มีกรณีที่ทำให้คุณคาดหวังแตกต่างกันหรือไม่?
Michael Homer

@MichaelHomer ฉันเดาว่าการแก้ไขของฉันที่นี่ทำให้เกิดสิ่งนี้: unix.stackexchange.com/a/250985/70524
muru

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

${mycmd[@]}ไม่มีเหตุผลว่าทำไมหอยยังคงสนับสนุนการทำงานแทนการบรรจุคำสั่งเป็นอาร์เรย์และดำเนินการด้วยคือ
chepner

คำตอบ:


20

ปัญหาอยู่ที่วิธีส่งคำสั่งไปยังฟังก์ชัน:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"ควรใช้ในกรณีทั่วไปที่run_thisฟังก์ชันของคุณถูกนำหน้าไปยังคำสั่งที่เขียนเป็นปกติ run_thisนำไปสู่การอ้างถึงนรก:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

ฉันไม่แน่ใจว่าฉันควรส่งชื่อไฟล์ด้วยช่องว่างrun_thisอย่างไร


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

@ การอ้างคำพูดของเทอร์ดอนกลายเป็นนิสัยที่ฉันคิดว่าคุณคง$@ไม่ได้พูดถึงโดยไม่ตั้งใจ ฉันควรจะทิ้งตัวอย่างไว้ : D
muru

2
ไม่จริงมันเป็นนิสัยมากที่ฉันทดสอบ (ผิด) และสรุปว่า "ฮะบางทีอันนี้อาจไม่ต้องการคำพูด" ขั้นตอนที่รู้จักกันทั่วไปว่าเป็น brainfart
terdon

1
run_thisคุณไม่สามารถส่งชื่อไฟล์ที่มีช่องว่างที่จะ นี้นั้นเป็นปัญหาเดียวกับที่คุณใช้เป็นที่มีการบรรจุคำสั่งที่ซับซ้อนเป็นสตริงที่กล่าวไว้ในทุบตีคำถามที่พบบ่อย 050
Etan Reisner

9

มันทั้ง:

interpret_this_shell_code() {
  eval "$1"
}

หรือ:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

หรือ:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

แต่:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

ไม่สมเหตุสมผลมากนัก

หากคุณต้องการรันls -lคำสั่ง (ไม่ใช่lsคำสั่งด้วยlsและ-lเป็นอาร์กิวเมนต์) คุณจะทำ:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

แต่ถ้า (เป็นไปได้มากกว่านั้น) ก็เป็นlsคำสั่งด้วยlsและ-lเป็นอาร์กิวเมนต์คุณจะเรียกใช้:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

ตอนนี้ถ้ามันเป็นมากกว่าคำสั่งง่ายๆที่คุณต้องการเรียกใช้ถ้าคุณต้องการทำการกำหนดตัวแปร, การเปลี่ยนเส้นทาง, ไพพ์ ... , interpret_this_shell_codeจะทำ:

interpret_this_shell_code 'ls -l 2> /dev/null'

แม้ว่าแน่นอนคุณสามารถทำ:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'

5

มองจากมุมมอง bash / ksh / zsh $*และ$@เป็นกรณีพิเศษของการขยายอาร์เรย์ทั่วไป การขยายแถวลำดับไม่เหมือนกับการขยายตัวแปรปกติ:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

ด้วย$*/ ${a[*]}การขยายคุณจะได้รับอาร์เรย์เข้าร่วมกับค่าแรกของIFS - ซึ่งเป็นช่องว่างโดยค่าเริ่มต้น - เป็นสตริงขนาดยักษ์ ถ้าคุณไม่พูดมันจะได้รับการแบ่งเหมือนสตริงปกติจะ

ด้วย$@/ ${a[@]}การขยายพฤติกรรมขึ้นอยู่กับว่า$@/ ${a[@]}ขยายจะถูกยกมาหรือไม่:

  1. ถ้ามันถูกยกมา ( "$@"หรือ"${a[@]}") คุณจะได้รับเทียบเท่า "$1" "$2" "$3" #... หรือ"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. หากไม่มีการเสนอราคา ( $@หรือ${a[@]}) คุณจะได้รับเทียบเท่า $1 $2 $3 #... หรือ${a[1]} ${a[2]} ${a[3]} # ...

สำหรับคำสั่งตัดคำคุณต้องการเครื่องหมาย@ ส่วนขยาย (1) อย่างแน่นอน


ข้อมูลที่ดีเพิ่มเติมเกี่ยวกับ bash (และ bash-like) arrays: https://lukeshu.com/blog/bash-arrays.html


1
เพิ่งรู้ว่าฉันหมายถึงลิงค์เริ่มต้นด้วยลุคในขณะที่สวมหน้ากากเวเดอร์ แรงมีความแข็งแกร่งกับโพสต์นี้
PSkocik

4

ตั้งแต่เมื่อคุณไม่ได้พูดสองครั้ง$@คุณออกจากปัญหาทั้งหมดในการเชื่อมโยงคุณมอบให้กับการทำงานของคุณ

คุณเรียกใช้คำสั่งที่มีชื่อว่าได้*อย่างไร? คุณไม่สามารถทำได้ด้วยrun_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

และคุณจะเห็นว่าแม้เกิดข้อผิดพลาด run_thatก็ให้ข้อความที่มีความหมายมากขึ้น

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

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

เป็นทางเลือกที่ดีกว่า หรือถ้าเชลล์ของคุณรองรับอาร์เรย์:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

แม้ในขณะที่เปลือกไม่สนับสนุนอาร์เรย์ที่ทุกท่านยังสามารถเล่นกับมันโดยใช้"$@"


3

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

  • ท่อ (เช่นls | grep filename)
  • การเปลี่ยนเส้นทางอินพุต / เอาต์พุต (เช่นls > /dev/null)
  • งบเชลล์เช่นif whileฯลฯ

หากสิ่งที่คุณต้องการทำคือหลีกเลี่ยงการทำซ้ำรหัสคุณจะดีกว่าด้วยการใช้ฟังก์ชั่น ตัวอย่างเช่นแทนที่จะเป็น:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

คุณควรเขียน

command() {
    ls -l
}
...
command

หากคำสั่งนั้นพร้อมใช้งานเฉพาะเวลารันไทม์คุณควรใช้evalซึ่งออกแบบมาโดยเฉพาะเพื่อจัดการกับนิสัยใจคอทั้งหมดที่จะทำให้run_thisล้มเหลว:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

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


1

ทางเลือกเป็นของคุณ หากคุณไม่ได้อ้างอิงค่า$@ใด ๆ ของมันจะได้รับการขยายและตีความเพิ่มเติม หากคุณอ้างถึงมันอาร์กิวเมนต์ทั้งหมดที่ส่งผ่านฟังก์ชันจะทำซ้ำในคำต่อคำแบบขยาย คุณจะไม่สามารถจัดการกับโทเค็นไวยากรณ์ของเชลล์เช่น&>|และอื่น ๆได้อย่างน่าเชื่อถือไม่ว่าจะด้วยวิธีใดโดยไม่ต้องแยกวิเคราะห์อาร์กิวเมนต์ด้วยตัวเอง - ดังนั้นคุณจึงเหลือทางเลือกที่เหมาะสมกว่าในการมอบฟังก์ชั่นของคุณ

  1. "$@"ว่าคำที่ใช้ในการดำเนินการของคำสั่งง่ายๆเดียวกับ

...หรือ...

  1. เวอร์ชันที่ขยายและตีความเพิ่มเติมของอาร์กิวเมนต์ของคุณซึ่งจะถูกนำไปใช้ร่วมกันเป็นคำสั่งง่ายๆด้วย$@เท่านั้น

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

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... มันไม่ไร้ประโยชน์เพียงแค่ใช้บ่อย และในbashเชลล์เนื่องจากbashไม่ได้กำหนดค่าเริ่มต้นไว้ที่ตัวแปรสภาพแวดล้อมแม้ว่าจะมีการกำหนดนิยามไว้ในบรรทัดคำสั่งของ builtin พิเศษหรือฟังก์ชันค่าโกลบอลสำหรับ$IFSไม่ได้รับผลกระทบและการประกาศเป็นแบบโลคัล เพียงเพื่อrun_this()โทร

ในทำนองเดียวกัน:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

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

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

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

สามารถอ้างถึงอะไรก็ได้ คำสั่งจะเรียกใช้ที่ยกมา มันทำงานได้เพราะเมื่อเวลาที่คำสั่งรันจริงคำอินพุตทั้งหมดได้ผ่านการกำจัดเครื่องหมายคำพูดแล้ว- ซึ่งเป็นขั้นตอนสุดท้ายของกระบวนการตีความอินพุตของเชลล์ ดังนั้นความแตกต่างระหว่าง'ls'และlsสามารถทำได้เฉพาะในขณะที่เชลล์กำลังตีความ - และนั่นเป็นสาเหตุที่การอ้างถึงlsทำให้มั่นใจได้ว่านามแฝงที่มีชื่อlsจะไม่ถูกแทนที่สำหรับlsคำคำสั่งที่ยกมาของฉัน นอกเหนือจากนั้นสิ่งเดียวที่ส่งผลกระทบต่อคำพูดคือการ delimiting ของคำ(ซึ่งเป็นอย่างไรและทำไม quoting ตัวแปร / input-whitespace ทำงาน)และการแปลความหมายของอักขระและคำที่สงวนไว้

ดังนั้น:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

คุณจะไม่สามารถทำอย่างใดอย่างหนึ่งrun_this()หรือrun_that()หรือ

แต่ชื่อฟังก์ชั่นหรือ$PATHคำสั่ง 'd หรือบิวด์อินจะทำงานได้ดีที่ยกมาหรือไม่ถูกยกมาและนั่นเป็นวิธีการrun_this()และการrun_that()ทำงานในสถานที่แรก คุณจะไม่สามารถทำอะไรที่เป็นประโยชน์กับ$<>|&(){}สิ่งเหล่านี้ สั้นของevalคือ

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

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

(run_that(){ "$@";}; echo hey | run_that cat)

hey

ฉันสามารถ<ป้อนข้อมูลได้อย่างง่ายดายหรือ>เอาต์พุตเช่นเดียวกับที่ฉันเปิดไพพ์

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

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