Haruskah variabel dikutip ketika dieksekusi?

18

Aturan umum dalam skrip shell adalah bahwa variabel harus selalu dikutip kecuali ada alasan kuat untuk tidak melakukannya. Untuk perincian lebih dari yang mungkin ingin Anda ketahui, lihat T&J besar ini: Implikasi keamanan lupa mengutip variabel dalam bash / POSIX shells .

Namun pertimbangkan fungsi seperti berikut:

run_this(){
    $@
}

Haruskah $@dikutip di sana atau tidak? Saya bermain dengannya sebentar dan tidak dapat menemukan kasus di mana kurangnya kutipan menyebabkan masalah. Di sisi lain, menggunakan tanda kutip membuatnya rusak ketika melewati perintah yang berisi spasi sebagai variabel yang dikutip:

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

Menjalankan skrip di atas menghasilkan:

$ 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

Saya dapat menyiasatinya jika saya menggunakan run_that $commalih-alih run_that "$comm", tetapi karena fungsi run_this(yang tidak dikutip) berfungsi dengan keduanya, sepertinya ini adalah taruhan yang lebih aman.

Jadi, dalam kasus khusus menggunakan $@dalam fungsi yang tugasnya mengeksekusi $@sebagai perintah, harus $@dikutip? Tolong jelaskan mengapa itu harus / tidak boleh dikutip dan berikan contoh data yang dapat merusaknya.

terdon
sumber
6
run_thatPerilaku pasti apa yang saya harapkan (bagaimana jika ada ruang di jalan menuju perintah?). Jika Anda menginginkan perilaku lain, tentunya Anda akan berhenti tanda kutip di situs- panggilan tempat Anda mengetahui data apa itu? Saya berharap untuk memanggil fungsi ini sebagai run_that ls -l, yang berfungsi sama di kedua versi. Apakah ada kasus yang membuat Anda berharap berbeda?
Michael Homer
@MichaelHomer Saya kira edit saya di sini diminta ini: unix.stackexchange.com/a/250985/70524
muru
@MichaelHomer karena suatu alasan (mungkin karena saya masih belum minum kopi kedua saya) Saya belum mempertimbangkan spasi dalam argumen atau jalur perintah, tetapi hanya dalam perintah itu sendiri (opsi). Seperti yang sering terjadi, ini tampak sangat jelas dalam retrospeksi.
terdon
Ada alasan mengapa shells masih mendukung fungsi alih-alih hanya memasukkan perintah ke dalam array dan menjalankannya ${mycmd[@]}.
chepner

Jawaban:

20

Masalahnya terletak pada bagaimana perintah dilewatkan ke fungsi:

$ 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

"$@"harus digunakan dalam kasus umum di mana run_thisfungsi Anda diawali dengan perintah yang biasanya ditulis. run_thismengarah ke mengutip neraka:

$ 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

Saya tidak yakin bagaimana cara meloloskan nama file dengan spasi run_this.

muru
sumber
1
Memang hasil edit Anda yang mendorong ini. Untuk beberapa alasan tidak terpikir oleh saya untuk menguji dengan nama file dengan spasi. Saya sama sekali tidak tahu mengapa tidak, tapi begitulah. Anda tentu saja benar, saya juga tidak melihat cara untuk melakukan ini dengan benar run_this.
terdon
Kutipan @terdon sudah menjadi kebiasaan sehingga saya berasumsi Anda akan mengutip $@tanpa sengaja. Saya harus memberikan contoh. : D
muru
2
Nah, itu memang kebiasaan yang saya uji (salah) dan menyimpulkan bahwa "huh, mungkin yang ini tidak perlu tanda kutip". Suatu prosedur yang umumnya dikenal sebagai brainfart.
terdon
1
Anda tidak dapat meneruskan nama file dengan spasi run_this. Ini pada dasarnya adalah masalah yang sama Anda mengalami dengan memasukkan perintah kompleks ke dalam string seperti yang dibahas dalam Bash FAQ 050 .
Etan Reisner
9

Itu baik:

interpret_this_shell_code() {
  eval "$1"
}

Atau:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

atau:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Tapi:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Tidak masuk akal.

Jika Anda ingin menjalankan ls -lperintah (bukan lsperintah dengan lsdan -lsebagai argumen), Anda harus melakukan:

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

Tetapi jika (lebih mungkin), itu adalah lsperintah dengan lsdan -lsebagai argumen, Anda akan menjalankan:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Sekarang, jika lebih dari sekedar perintah sederhana yang ingin Anda jalankan, jika Anda ingin melakukan penugasan variabel, pengalihan, pipa ..., hanya interpret_this_shell_codeakan melakukan:

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

meskipun tentu saja Anda selalu dapat melakukannya:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Stéphane Chazelas
sumber
5

Melihatnya dari perspektif bash / ksh / zsh, $*dan $@merupakan kasus khusus ekspansi array umum. Ekspansi array tidak seperti ekspansi variabel normal:

$ 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

Dengan $*/ ${a[*]}ekspansi, Anda membuat array digabungkan dengan nilai pertama IFS—yang secara default adalah ruang — menjadi satu string raksasa. Jika Anda tidak mengutipnya, ia akan terpecah seperti string normal.

Dengan $@/ ${a[@]}ekspansi, perilaku tergantung pada apakah $@/ ${a[@]}ekspansi dikutip atau tidak:

  1. jika dikutip ( "$@"atau "${a[@]}"), Anda mendapatkan yang setara dengan "$1" "$2" "$3" #... atau"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. jika tidak dikutip ( $@atau ${a[@]}) Anda mendapatkan yang setara dengan $1 $2 $3 #... atau${a[1]} ${a[2]} ${a[3]} # ...

Untuk perintah pembungkus, Anda pasti menginginkan ekspansi @ dikutip (1.).


Lebih banyak info bagus tentang array bash (dan mirip bash): https://lukeshu.com/blog/bash-arrays.html

PSkocik
sumber
1
Baru sadar saya merujuk ke tautan yang dimulai dengan Luke, sambil mengenakan topeng Vader. Kekuatannya kuat dengan pos ini.
PSkocik
4

Karena ketika Anda tidak menggandakan penawaran $@, Anda meninggalkan semua masalah globbing di tautan yang Anda berikan ke fungsi Anda.

Bagaimana Anda bisa menjalankan perintah yang bernama *? Anda tidak dapat melakukannya dengan run_this:

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

Dan Anda lihat, bahkan ketika kesalahan terjadi, run_thatmemberi Anda pesan yang lebih bermakna.

Satu-satunya cara untuk memperluas $@ke kata-kata individual adalah kutip ganda. Jika Anda ingin menjalankannya sebagai perintah, Anda harus meneruskan perintah dan parameternya sebagai kata-kata yang terpisah. Bahwa hal yang Anda lakukan di sisi penelepon, bukan di dalam fungsi Anda.

$ 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

adalah pilihan yang lebih baik. Atau jika array dukungan shell Anda:

$ 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

Bahkan ketika shell sama sekali tidak mendukung array, Anda masih bisa memainkannya dengan menggunakan"$@" .

cuonglm
sumber
3

Mengeksekusi variabel dalam bashadalah teknik rawan kegagalan. Tidak mungkin untuk menulis run_thisfungsi yang menangani semua tepi case dengan benar, seperti:

  • jalur pipa (misalnya ls | grep filename)
  • pengalihan input / output (mis. ls > /dev/null)
  • pernyataan shell seperti if whiledll.

Jika semua yang ingin Anda lakukan adalah menghindari pengulangan kode, Anda lebih baik menggunakan fungsi. Misalnya, alih-alih:

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

Anda harus menulis

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

Jika perintah hanya tersedia pada saat run time, Anda harus menggunakan eval, yang secara khusus dirancang untuk menangani semua kebiasaan yang akan membuat run_thisgagal:

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

Perhatikan bahwa evalini dikenal untuk masalah keamanan, tetapi jika Anda melewatkan variabel dari sumber terpercaya untuk run_this, Anda akan menghadapi eksekusi kode sewenang-wenang sama dengan baik.

Dmitry Grigoryev
sumber
1

Pilihan ada padamu. Jika Anda tidak mengutip $@salah satu nilainya, perluas ekspansi dan interpretasi tambahan. Jika Anda mengutipnya semua argumen yang dilewati, fungsi tersebut direproduksi dalam perluasannya kata demi kata. Anda tidak akan pernah dapat menangani token sintaksis shell secara andal seperti &>|dan lain-lain dengan cara apa pun tanpa menguraikan argumen sendiri - dan karenanya Anda memiliki pilihan yang lebih masuk akal untuk menyerahkan fungsi Anda, salah satunya:

  1. Tepat kata-kata yang digunakan dalam pelaksanaan satu perintah sederhana dengan "$@".

...atau...

  1. Versi argumen Anda yang diperluas dan ditafsirkan lebih lanjut yang kemudian diterapkan bersama sebagai perintah sederhana $@.

Tidak ada cara yang salah jika disengaja dan jika efek dari apa yang Anda pilih dipahami dengan baik. Kedua cara memiliki keunggulan satu di atas yang lain, meskipun keunggulan yang kedua jarang bermanfaat. Masih...

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

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

... tidak sia - sia , hanya saja jarang berguna . Dan dalam sebuah bashshell, karena bashtidak secara default menempel definisi variabel ke lingkungannya bahkan ketika definisi tersebut ditambahkan ke baris perintah dari builtin khusus atau ke suatu fungsi, nilai global untuk $IFStidak terpengaruh, dan deklarasi adalah lokal hanya untuk run_this()panggilan.

Demikian pula:

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

ls: cannot access *: No such file or directory

... globbing juga dapat dikonfigurasi. Kutipan melayani tujuan - mereka bukan untuk apa-apa. Tanpa mereka ekspansi shell mengalami interpretasi ekstra - interpretasi yang dapat dikonfigurasi . Dulu - dengan beberapa sangat tua kerang - yang $IFStelah secara global diterapkan untuk semua masukan, dan bukan hanya ekspansi. Bahkan, kata shell berperilaku sangat seperti run_this()dalam hal mereka memecahkan semua kata input pada nilai $IFS. Jadi, jika yang Anda cari adalah perilaku shell yang sangat lama, maka Anda harus menggunakannya run_this().

Saya tidak mencarinya, dan saya agak kesulitan saat ini untuk memberikan contoh yang berguna. Saya biasanya lebih suka untuk perintah shell saya berjalan untuk menjadi yang saya ketik. Maka, diberi pilihan, saya hampir selalu run_that(). Kecuali itu...

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

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

Apa saja bisa dikutip. Perintah akan dijalankan dengan mengutip. Ini bekerja karena pada saat perintah itu benar-benar dijalankan, semua kata input telah mengalami penghapusan kutipan - yang merupakan tahap terakhir dari proses interpretasi input shell. Jadi perbedaan antara 'ls'dan lshanya bisa berarti ketika shell sedang menerjemahkan - dan itulah sebabnya mengutip lsmemastikan bahwa setiap alias yang bernama lstidak diganti dengan lskata perintah yang saya kutip . Selain itu, satu-satunya hal yang mempengaruhi kutipan adalah pembatasan kata-kata (yang adalah bagaimana dan mengapa variabel / input-spasi putih mengutip bekerja) , dan interpretasi dari metakarakter dan kata-kata yang dicadangkan.

Begitu:

'for' f in ...
 do   :
 done

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

Anda tidak akan pernah bisa melakukan itu dengan salah satu run_this()atau run_that().

Tetapi nama fungsi, atau $PATHperintah, atau builtin akan mengeksekusi dengan baik dikutip atau tidak dikutip, dan itulah bagaimana run_this()dan run_that()bekerja di tempat pertama. Anda tidak akan dapat melakukan sesuatu yang bermanfaat dengan $<>|&(){}semua itu. Singkatnya eval, adalah.

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

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

Tetapi tanpa itu, Anda dibatasi pada batas-batas perintah sederhana berdasarkan kutipan yang Anda gunakan (bahkan ketika Anda tidak melakukannya karena $@bertindak seperti kutipan pada awal proses ketika perintah diuraikan untuk karakter metakarakter) . Kendala yang sama berlaku untuk penugasan dan pengalihan baris perintah, yang terbatas pada baris perintah fungsi. Tapi itu bukan masalah besar:

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

hey

Saya dapat dengan mudah <mengarahkan ulang input atau >output di sana seperti saya membuka pipa.

Pokoknya, secara berputar-putar, tidak ada cara yang benar atau salah di sini - setiap cara memiliki kegunaannya. Hanya saja Anda harus menulisnya saat Anda bermaksud menggunakannya, dan Anda harus tahu apa yang ingin Anda lakukan. Menghilangkan tanda kutip dapat memiliki tujuan - jika tidak akan ada tanda kutip sama sekali - tetapi jika Anda menghilangkannya karena alasan yang tidak relevan dengan tujuan Anda, Anda hanya menulis kode yang buruk. Lakukan apa yang Anda maksudkan; Saya tetap mencoba.

mikeserv
sumber