Menggunakan variabel shell untuk opsi perintah

19

Dalam skrip Bash, saya mencoba untuk menyimpan opsi yang saya gunakan rsyncdalam variabel yang terpisah. Ini berfungsi dengan baik untuk opsi sederhana (seperti --recursive), tapi saya mengalami masalah dengan --exclude='.*':

$ find source
source
source/.bar
source/foo

$ rsync -rnv --exclude='.*' source/ dest
sending incremental file list
foo

sent 57 bytes  received 19 bytes  152.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

$ RSYNC_OPTIONS="-rnv --exclude='.*'"

$ rsync $RSYNC_OPTIONS source/ dest
sending incremental file list
.bar
foo

sent 78 bytes  received 22 bytes  200.00 bytes/sec
total size is 0  speedup is 0.00 (DRY RUN)

Seperti yang Anda lihat, meneruskan --exclude='.*'ke rsync"secara manual" berfungsi dengan baik ( .bartidak disalin), itu tidak berfungsi ketika opsi disimpan dalam variabel terlebih dahulu.

Saya menduga ini terkait dengan tanda kutip atau wildcard (atau keduanya), tetapi saya belum dapat menemukan apa yang sebenarnya salah.

Florian Brucker
sumber

Jawaban:

38

Secara umum, itu ide yang buruk untuk menurunkan daftar item yang terpisah menjadi satu string, tidak peduli apakah itu daftar opsi baris perintah atau daftar nama path.

Alih-alih menggunakan array:

rsync_options=( -rnv --exclude='.*' )

atau

rsync_options=( -r -n -v --exclude='.*' )

dan kemudian ...

rsync "${rsync_options[@]}" source/ target

Dengan cara ini, kutipan dari opsi-opsi individual dipertahankan (selama Anda menggandakan penawaran ekspansi ${rsync_options[@]}). Ini juga memungkinkan Anda untuk dengan mudah memanipulasi entri individual dari array, apakah Anda perlu melakukannya, sebelum memanggil rsync.

Dalam setiap shell POSIX, seseorang dapat menggunakan daftar parameter posisi untuk ini:

set -- -rnv --exclude='.*'

rsync "$@" source/ target

Sekali lagi, mengutip ganda ekspansi $@sangat penting di sini.

Terkait secara tangensial:


Masalahnya adalah ketika Anda meletakkan dua set opsi ke dalam string, tanda kutip tunggal dari nilai --excludeopsi menjadi bagian dari nilai itu. Karenanya,

RSYNC_OPTIONS='-rnv --exclude=.*'

akan bekerja¹ ... tetapi lebih baik (seperti dalam lebih aman) untuk menggunakan array atau parameter posisi dengan entri yang dikutip secara individual. Melakukan hal itu juga akan memungkinkan Anda untuk menggunakan hal-hal dengan spasi di dalamnya, jika Anda perlu, dan menghindari memiliki shell melakukan pembuatan nama file (globbing) pada opsi.


¹ asalkan $IFStidak diubah dan bahwa tidak ada berkas yang dimulai nama dengan --exclude=.di direktori saat ini, dan bahwa nullglobatau failglobshell pilihan tidak diatur.

Kusalananda
sumber
Menggunakan array berfungsi dengan baik, terima kasih atas jawaban terperinci Anda!
Florian Brucker
3

@ Kusalananda telah menjelaskan masalah dasar dan bagaimana menyelesaikannya, dan entri Bash FAQ yang ditautkan oleh @glenn jackmann juga menyediakan banyak informasi berguna. Berikut adalah penjelasan terperinci tentang apa yang terjadi dalam masalah saya berdasarkan sumber daya ini.

Kami akan menggunakan skrip kecil yang mencetak setiap argumennya pada baris terpisah untuk menggambarkan hal-hal ( argtest.bash):

#!/bin/bash

for var in "$@"
do
    echo "$var"
done

Melewati opsi "secara manual":

$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*

Seperti yang diharapkan, bagian-bagian -rnvdan --exclude='.*'dibagi menjadi dua argumen, karena mereka dipisahkan oleh spasi kosong (ini disebut pemisahan kata ).

Juga perhatikan bahwa tanda kutip di sekitar .*telah dihapus: tanda kutip tunggal memberitahu shell untuk meneruskan konten mereka tanpa interpretasi khusus , tetapi tanda kutip itu sendiri tidak diteruskan ke perintah .

Jika sekarang kita menyimpan opsi dalam variabel sebagai string (sebagai lawan menggunakan array), maka tanda kutip tidak dihapus :

$ OPTS="--exclude='.*'"

$ ./argtest.bash $OPTS
--exclude='.*'

Ini karena dua alasan: tanda kutip ganda yang digunakan ketika mendefinisikan $OPTSmencegah perlakuan khusus dari tanda kutip tunggal, sehingga tanda kutip adalah bagian dari nilai:

$ echo $OPTS
--exclude='.*'

Ketika kita sekarang menggunakan $OPTSargumen pada perintah, maka kutipan diproses sebelum ekspansi parameter , sehingga tanda kutip yang $OPTSterjadi "terlambat".

Ini berarti bahwa (dalam masalah awal saya) rsyncmenggunakan pola kecualikan '.*'(dengan tanda kutip!) Alih-alih pola .*- itu tidak termasuk file yang namanya dimulai dengan tanda kutip tunggal diikuti oleh titik dan berakhir dengan tanda kutip tunggal. Jelas bukan itu yang dimaksudkan.

Solusinya adalah dengan menghilangkan tanda kutip ganda ketika mendefinisikan $OPTS:

$ OPTS2=--exclude='.*'

$ ./argtest.bash $OPTS2
--exclude=.*

Namun, itu praktik yang baik untuk selalu mengutip penugasan variabel karena perbedaan halus dalam kasus yang lebih kompleks.

Seperti @Kusalananda catat, tidak mengutip .*juga akan berhasil. Saya telah menambahkan tanda kutip untuk mencegah perluasan pola , tetapi itu tidak terlalu diperlukan dalam kasus khusus ini :

$ ./argtest.bash --exclude=.*
--exclude=.*

Ternyata Bash tidak melakukan ekspansi pola, tapi pola --exclude=.*tidak cocok file apapun, sehingga pola diteruskan kepada perintah. Membandingkan:

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*

Namun, tidak mengutip polanya berbahaya, karena jika (karena alasan apa pun) ada file yang cocok --exclude=.*maka polanya akan diperluas:

$ touch -- --exclude=.special-filenames-happen

$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen

Akhirnya, mari kita lihat mengapa menggunakan array mencegah masalah mengutip saya (di samping keuntungan lain menggunakan array untuk menyimpan argumen perintah).

Saat mendefinisikan array, pemisahan kata dan penanganan penawaran terjadi seperti yang diharapkan:

$ ARRAY_OPTS=( -rnv --exclude='.*' )

$ echo length of the array: "${#ARRAY_OPTS[@]}"
length of the array: 2

$ echo first element: "${ARRAY_OPTS[0]}"
first element: -rnv

$ echo second element: "${ARRAY_OPTS[1]}"
second element: --exclude=.*

Saat meneruskan opsi ke perintah, kami menggunakan sintaks "${ARRAY[@]}", yang memperluas setiap elemen array menjadi kata yang terpisah:

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*
Florian Brucker
sumber
Hal ini membuat saya bingung untuk waktu yang lama, jadi penjelasan terperinci seperti ini sangat membantu.
Joe
0

Ketika kita menulis fungsi dan skrip shell, di mana argumen diteruskan untuk diproses, argumen akan dikirimkan ke variabel yang dinamai secara numerik, misalnya $ 1, $ 2, $ 3

Sebagai contoh :

bash my_script.sh Hello 42 World

Di dalam my_script.sh, perintah akan digunakan $1untuk merujuk ke Halo, $2untuk 42, dan $3untukWorld

Referensi variabel $0,, akan diperluas ke nama skrip saat ini, misalnyamy_script.sh

Jangan mainkan seluruh kode dengan perintah sebagai variabel.

Ingat :

1 Hindari menggunakan nama variabel huruf besar semua dalam skrip.

2 Jangan gunakan backquotes, gunakan $ (...) sebagai gantinya, itu bersarang lebih baik.

if [ $# -ne 2 ]
then
    echo "Usage: $(basename $0) DIRECTORY BACKUP_DIRECTORY"
    exit 1
fi

directory=$1
backup_directory=$2
current_date=$(date +%Y-%m-%dT%H-%M-%S)
backup_file="${backup_directory}/${current_date}.backup"

tar cv "$directory" | openssl des3 -salt | split -b 1024m - "$backup_file"
juara-pelari
sumber