Bagaimana kita bisa menjalankan perintah yang disimpan dalam variabel?

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

Saya bertanya-tanya mengapa cara-cara berikut untuk menjalankan perintah di atas gagal atau berhasil?

$ 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
Tim
sumber

Jawaban:

54

Ini telah dibahas dalam sejumlah pertanyaan di unix.SE, saya akan mencoba untuk mengumpulkan semua masalah yang bisa saya kemukakan di sini. Referensi di bagian akhir.


Kenapa gagal?

Alasan Anda menghadapi masalah tersebut adalah pemisahan kata dan fakta bahwa tanda kutip diperluas dari variabel tidak bertindak sebagai tanda kutip, tetapi hanya karakter biasa.

Kasus-kasus yang disajikan dalam pertanyaan:

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

Di sini, $abcterbelah, dan lsdapatkan dua argumen "/tmp/test/mydan dir"(dengan tanda kutip di depan yang pertama dan belakang yang kedua):

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Di sini, ekspansi dikutip, jadi disimpan sebagai satu kata. Shell mencoba untuk menemukan program yang disebut ls -l "/tmp/test/my dir", spasi dan kutipan disertakan.

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

Dan di sini, hanya kata pertama atau $abcdiambil sebagai argumen untuk -c, jadi Bash hanya berjalan lsdi direktori saat ini. Dengan kata lain argumen untuk bash, dan digunakan untuk mengisi $0, $1dll

$ bash -c $abc
'my dir'

Dengan bash -c "$abc", dan eval "$abc", ada langkah pemrosesan shell tambahan, yang membuat kutipan berhasil, tetapi juga menyebabkan semua ekspansi shell diproses lagi , jadi ada risiko tidak sengaja menjalankan ekspansi perintah dari data yang disediakan pengguna, kecuali jika Anda sangat hati-hati mengutip.


Cara yang lebih baik untuk melakukannya

Dua cara yang lebih baik untuk menyimpan perintah adalah a) menggunakan fungsi sebagai gantinya, b) menggunakan variabel array (atau parameter posisi).

Menggunakan fungsi:

Cukup mendeklarasikan fungsi dengan perintah di dalamnya, dan jalankan fungsi seolah-olah itu adalah perintah. Ekspansi dalam perintah di dalam fungsi hanya diproses ketika perintah dijalankan, bukan ketika itu didefinisikan, dan Anda tidak perlu mengutip perintah individu.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

Menggunakan array:

Array memungkinkan pembuatan variabel multi-kata di mana setiap kata berisi ruang putih. Di sini, kata-kata individual disimpan sebagai elemen array yang berbeda, dan "${array[@]}"ekspansi memperluas setiap elemen sebagai kata-kata shell yang terpisah:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

Sintaksnya sedikit mengerikan, tetapi array juga memungkinkan Anda untuk membangun baris perintah sepotong demi sepotong. Sebagai contoh:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

atau biarkan bagian-bagian dari baris perintah konstan dan gunakan array isi hanya sebagian saja, opsi atau nama file:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

Kelemahan dari array adalah bahwa mereka bukan fitur standar, jadi shell POSIX sederhana (seperti dash, default/bin/sh di Debian / Ubuntu) tidak mendukung mereka (tetapi lihat di bawah). Namun, bash, ksh dan zsh melakukannya, sehingga kemungkinan sistem Anda memiliki beberapa shell yang mendukung array.

Menggunakan "$@"

Dalam shells tanpa dukungan untuk array bernama, kita masih dapat menggunakan parameter posisi (pseudo-array "$@" ) untuk menyimpan argumen dari perintah.

Berikut ini harus bit script portabel yang melakukan setara dengan bit kode di bagian sebelumnya. Array diganti dengan "$@", daftar parameter posisi. Pengaturan "$@"dilakukan dengan set, dan tanda kutip ganda di sekitar "$@"adalah penting (ini menyebabkan elemen daftar dikutip secara individual).

Pertama, cukup menyimpan perintah dengan argumen di "$@"dan menjalankannya:

set -- ls -l "/tmp/test/my dir"
"$@"

Pengaturan bagian opsi baris perintah untuk suatu perintah:

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "$@" -l
fi
set -- "$@" "$targetdir"

"$@"

Hanya menggunakan "$@"untuk opsi dan operan:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(Tentu saja, "$@"biasanya diisi dengan argumen untuk skrip itu sendiri, jadi Anda harus menyimpannya di suatu tempat sebelum bertujuan ulang "$@".)


Hati-hati dengan eval!

Saat evalmemperkenalkan tingkat penawaran tambahan dan pemrosesan ekspansi, Anda harus berhati-hati dengan input pengguna. Misalnya, ini berfungsi selama pengguna tidak mengetikkan tanda kutip tunggal:

read -r filename
cmd="ls -l '$filename'"
eval "$cmd";

Tetapi jika mereka memberikan input '$(uname)'.txt, skrip Anda dengan senang hati menjalankan substitusi perintah.

Sebuah versi dengan array kebal terhadap hal itu karena kata-kata disimpan terpisah sepanjang waktu, tidak ada kutipan atau pemrosesan lain untuk konten filename.

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

Referensi

ilkkachu
sumber
2
Anda dapat menghindari eval mengutip hal dengan melakukan cmd="ls -l $(printf "%q" "$filename")". tidak cantik, tetapi jika pengguna mati untuk menggunakan eval, itu membantu. Ini juga sangat berguna untuk mengirimkan perintah meskipun hal serupa, seperti ssh foohost "ls -l $(printf "%q" "$filename")", atau dalam sprit pertanyaan ini: ssh foohost "$cmd".
Patrick
Tidak terkait langsung, tetapi apakah Anda sudah mengkodekan direktori? Dalam hal ini, Anda mungkin ingin melihat alias. Sesuatu seperti: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny
4

Cara teraman untuk menjalankan perintah (non-sepele) adalah eval. Kemudian Anda dapat menulis perintah seperti yang Anda lakukan pada baris perintah dan dieksekusi persis seolah-olah Anda baru saja memasukkannya. Tetapi Anda harus mengutip semuanya.

Kasus sederhana:

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

tidak begitu sederhana:

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

Tanda kutip kedua mematahkan perintah.

Ketika saya menjalankan:

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

Itu memberi saya kesalahan.

Tapi ketika saya lari

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

Tidak ada kesalahan sama sekali

Tidak ada cara untuk memperbaikinya pada saat itu (untuk saya) tetapi Anda dapat menghindari kesalahan dengan tidak memiliki ruang dalam nama direktori.

Jawaban ini mengatakan perintah eval dapat digunakan untuk memperbaikinya tetapi tidak berhasil untuk saya :(

Wattana Gaming
sumber
1
Ya, itu berfungsi asalkan tidak perlu misalnya nama file dengan spasi yang disematkan (atau yang berisi karakter gumpal).
ilkkachu
0

Jika ini tidak berhasil '' , maka Anda harus menggunakan ``:

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

Perbarui cara yang lebih baik:

abc=$(ls -l /tmp/test/my\ dir/)
balon
sumber
Ini menyimpan hasil perintah dalam variabel. OP (anehnya) ingin menyimpan perintah itu sendiri dalam sebuah variabel. Oh, dan Anda harus benar-benar mulai menggunakan $( command... )alih-alih backticks.
roaima
Terima kasih banyak atas klarifikasi dan tipsnya!
balon
0

Bagaimana dengan one-liner 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 ..
Marius Mitrofan
sumber