Bagaimana cara menyimpan perintah dalam variabel di skrip shell?

113

Saya ingin menyimpan perintah untuk digunakan di lain waktu dalam variabel (bukan output dari perintah, tetapi perintah itu sendiri)

Saya memiliki skrip sederhana sebagai berikut:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

Namun, ketika saya mencoba sesuatu yang lebih rumit, gagal. Misalnya kalau saya bikin

command="ls | grep -c '^'";

Outputnya adalah:

Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

Tahu bagaimana saya bisa menyimpan perintah seperti itu (dengan pipa / beberapa perintah) dalam variabel untuk digunakan nanti?

Benjamin
sumber
10
Gunakan fungsi!
gniourf_gniourf

Jawaban:

146

Gunakan eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Erik
sumber
27
$ (...) sekarang direkomendasikan sebagai ganti backticks. y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead
14
evaladalah praktik yang dapat diterima hanya jika Anda mempercayai konten variabel Anda. Jika Anda menjalankan, katakanlah, x="ls $name | wc"(atau bahkan x="ls '$name' | wc"), kode ini adalah jalur cepat ke kerentanan injeksi atau eskalasi hak istimewa jika variabel itu dapat disetel oleh seseorang dengan hak istimewa yang lebih sedikit. (Mengulangi semua subdirektori di /tmp, misalnya? Sebaiknya Anda mempercayai setiap pengguna di sistem untuk tidak memanggilnya $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/').
Charles Duffy
9
evaladalah magnet bug besar yang tidak pernah direkomendasikan tanpa peringatan tentang risiko perilaku penguraian yang tidak terduga (bahkan tanpa string berbahaya, seperti dalam contoh @ CharlesDuffy). Misalnya, coba x='echo $(( 6 * 7 ))'lalu eval $x. Anda mungkin berharap untuk mencetak "42", tetapi mungkin tidak. Bisakah Anda menjelaskan mengapa itu tidak berhasil? Bisakah Anda menjelaskan mengapa saya berkata "mungkin"? Jika jawaban atas pertanyaan-pertanyaan itu tidak jelas bagi Anda, jangan pernah menyentuhnya eval.
Gordon Davisson
1
@Siswa, coba jalankan set -xsebelumnya untuk mencatat perintah yang dijalankan, yang akan membuatnya lebih mudah untuk melihat apa yang terjadi.
Charles Duffy
1
@Siswa Saya juga merekomendasikan shellcheck.net untuk menunjukkan kesalahan umum (dan kebiasaan buruk yang tidak boleh Anda ambil).
Gordon Davisson
41

Jangan tidak menggunakan eval! Ini memiliki risiko besar untuk memperkenalkan eksekusi kode arbitrer.

BashFAQ-50 - Saya mencoba untuk meletakkan perintah dalam variabel, tetapi kasus yang kompleks selalu gagal.

Memasukkannya ke dalam array dan memperluas semua kata dengan tanda kutip ganda "${arr[@]}"untuk tidak membiarkan IFSperpecahan kata-kata karena Firman Memisahkan .

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

dan melihat isi dari array di dalamnya. The declare -pmemungkinkan Anda melihat isi dalam array dengan masing-masing parameter perintah di indeks terpisah. Jika satu argumen seperti itu berisi spasi, mengutip di dalam sambil menambahkan ke array akan mencegahnya terpecah karena Pemisahan Kata.

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

dan jalankan perintah sebagai

"${cmdArgs[@]}"
23:15:18

(atau) semuanya menggunakan bashfungsi untuk menjalankan perintah,

cmd() {
   date '+%H:%M:%S'
}

dan menyebut fungsinya sebagai adil

cmd

POSIX shtidak memiliki larik, jadi yang paling dekat yang bisa Anda lakukan adalah membuat daftar elemen dalam parameter posisi. Berikut shcara POSIX untuk menjalankan program email

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

Perhatikan bahwa pendekatan ini hanya dapat menangani perintah sederhana tanpa pengalihan. Ia tidak dapat menangani pengalihan, pipeline, for / while loop, if statement, dll

Kasus penggunaan umum lainnya adalah saat menjalankan curlbeberapa kolom header dan payload. Anda selalu dapat menentukan args seperti di bawah ini dan memanggil curlkonten array yang diperluas

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

Contoh lain,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

Sekarang variabel telah ditentukan, gunakan array untuk menyimpan perintah Anda args

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

dan sekarang lakukan ekspansi yang dikutip dengan benar

curl "${curlCMD[@]}"
Inian
sumber
Ini tidak berhasil untuk saya, saya telah mencoba Command=('echo aaa | grep a')dan "${Command[@]}", berharap itu benar-benar menjalankan perintah echo aaa | grep a. Tidak. Saya ingin tahu apakah ada cara yang aman untuk mengganti eval, tetapi tampaknya setiap solusi yang memiliki kekuatan yang sama evalbisa berbahaya. Bukan?
Pelajar
Singkatnya, bagaimana ini bekerja jika string asli berisi pipa '|'?
Pelajar
@Siswa, jika string asli Anda berisi pipa, string tersebut harus melewati bagian yang tidak aman dari parser bash untuk dieksekusi sebagai kode. Jangan gunakan string dalam kasus itu; gunakan fungsi sebagai gantinya: Command() { echo aaa | grep a; }- setelah itu Anda bisa menjalankan Command, atau result=$(Command), atau sejenisnya.
Charles Duffy
1
@Siswa, benar; tetapi itu gagal dengan sengaja , karena apa yang Anda minta pada dasarnya tidak aman .
Charles Duffy
1
@Siswa: Saya telah menambahkan catatan pada akhirnya yang menyebutkan itu tidak berfungsi dalam kondisi tertentu
Inian
25
var=$(echo "asdf")
echo $var
# => asdf

Dengan menggunakan metode ini, perintah segera dievaluasi dan nilai kembaliannya disimpan.

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

Sama dengan backtick

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

Menggunakan eval di $(...)tidak akan membuatnya dievaluasi nanti

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

Menggunakan eval, ini dievaluasi saat evaldigunakan

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed

Dalam contoh di atas, jika Anda perlu menjalankan perintah dengan argumen, letakkan di string yang Anda simpan

stored_date="date -u"
# ...

Untuk skrip bash ini jarang relevan, tapi satu catatan terakhir. Berhati-hatilah eval. Evaluasi hanya string yang Anda kontrol, tidak pernah string yang berasal dari pengguna yang tidak tepercaya atau dibuat dari input pengguna yang tidak tepercaya.

  • Terima kasih kepada @CharlesDuffy karena mengingatkan saya untuk mengutip perintah!
Nate
sumber
Ini tidak menyelesaikan masalah asli di mana perintah berisi pipa '|'.
Pelajar
@Nate, perhatikan bahwa eval $stored_datemungkin cukup baik jika stored_datehanya berisi date, tetapi eval "$stored_date"jauh lebih dapat diandalkan. Jalankan str=$'printf \' * %s\\n\' *'; eval "$str"dengan dan tanpa tanda kutip di sekitar final "$str"sebagai contoh. :)
Charles Duffy
@CharlesDuffy Terima kasih, saya lupa mengutip. Saya berani bertaruh linter saya akan mengeluh jika saya repot-repot menjalankannya.
Nate
1

Untuk bash, simpan perintah Anda seperti ini:

command="ls | grep -c '^'"

Jalankan perintah Anda seperti ini:

echo $command | bash
Derek Hazell
sumber
1
Tidak yakin tetapi mungkin cara menjalankan perintah ini memiliki risiko yang sama dengan penggunaan 'eval'.
Derek Hazell
0

Saya mencoba berbagai metode berbeda:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

Keluaran:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

Seperti yang Anda lihat, hanya yang ketiga, yang "$@"memberikan hasil yang benar.

mpen
sumber
0

Berhati-hatilah saat mendaftarkan pesanan dengan: X=$(Command)

Yang ini masih dijalankan Bahkan sebelum dipanggil. Untuk memeriksa dan mengkonfirmasi ini, Anda dapat melakukan:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
Azerty
sumber
-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.
Silentexpert
sumber
-8

Tidak perlu menyimpan perintah dalam variabel meskipun Anda perlu menggunakannya nanti. jalankan saja seperti biasa. Jika Anda menyimpan dalam variabel, Anda akan memerlukan semacam evalpernyataan atau menjalankan beberapa proses shell yang tidak perlu untuk "mengeksekusi variabel Anda".

kurumi
sumber
1
Perintah yang akan saya simpan akan bergantung pada opsi yang saya kirimkan, jadi alih-alih memiliki banyak pernyataan bersyarat di sebagian besar program saya, jauh lebih mudah untuk menyimpan perintah yang saya perlukan untuk digunakan nanti.
Benjamin
1
@Benjamin, maka setidaknya simpan opsi sebagai variabel, dan bukan perintah. misalnyavar='*.txt'; find . -name "$var"
kurumi