Gunakan file konfigurasi untuk skrip shell saya


26

Saya perlu membuat file konfigurasi untuk skrip saya sendiri: di sini sebuah contoh:

naskah:

#!/bin/bash
source /home/myuser/test/config
echo "Name=$nam" >&2
echo "Surname=$sur" >&2

Isi dari /home/myuser/test/config:

nam="Mark"
sur="Brown"

itu berhasil!

Pertanyaan saya: apakah ini cara yang benar untuk melakukan ini atau ada cara lain?


Variabel harus di atas. Saya terkejut itu berhasil. Lagi pula, mengapa Anda memerlukan file konfigurasi? Apakah Anda berencana menggunakan variabel-variabel ini di tempat lain?
Faheem Mitha

Faheem, saya perlu variabel karena skrip saya memiliki banyak pilihan: menggunakan file konfigurasi akan semplify skrip. Terima kasih
Pol Hallen

5
IMHO tidak apa-apa. Saya akan melakukan ini.
Tinti

abcdejuga melakukannya dengan cara ini dan itu adalah program yang cukup besar (untuk skrip shell). Anda dapat melihatnya di sini .
Lucas

Jawaban:


19

sourcetidak aman karena akan mengeksekusi kode arbitrer. Ini mungkin bukan masalah bagi Anda, tetapi jika izin file tidak benar, mungkin bagi penyerang dengan akses sistem file untuk mengeksekusi kode sebagai pengguna istimewa dengan menyuntikkan kode ke file konfigurasi yang dimuat oleh skrip yang diamankan seperti skrip yang diamankan seperti skrip skrip init.

Sejauh ini, solusi terbaik yang dapat saya identifikasi adalah solusi reinventing-the-wheel yang canggung:

myscript.conf

password=bar
echo rm -rf /
PROMPT_COMMAND='echo "Sending your last command $(history 1) to my email"'
hostname=localhost; echo rm -rf /

Menggunakan source, ini akan berjalan echo rm -rf /dua kali, serta mengubah pengguna yang berjalan $PROMPT_COMMAND. Sebaliknya, lakukan ini:

myscript.sh (Bash 4)

#!/bin/bash
typeset -A config # init array
config=( # set default values in config array
    [username]="root"
    [password]=""
    [hostname]="localhost"
)

while read line
do
    if echo $line | grep -F = &>/dev/null
    then
        varname=$(echo "$line" | cut -d '=' -f 1)
        config[$varname]=$(echo "$line" | cut -d '=' -f 2-)
    fi
done < myscript.conf

echo ${config[username]} # should be loaded from defaults
echo ${config[password]} # should be loaded from config file
echo ${config[hostname]} # includes the "injected" code, but it's fine here
echo ${config[PROMPT_COMMAND]} # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

myscript.sh (kompatibel Mac / Bash 3)

#!/bin/bash
config() {
    val=$(grep -E "^$1=" myscript.conf 2>/dev/null || echo "$1=__DEFAULT__" | head -n 1 | cut -d '=' -f 2-)

    if [[ $val == __DEFAULT__ ]]
    then
        case $1 in
            username)
                echo -n "root"
                ;;
            password)
                echo -n ""
                ;;
            hostname)
                echo -n "localhost"
                ;;
        esac
    else
        echo -n $val
    fi
}

echo $(config username) # should be loaded from defaults
echo $(config password) # should be loaded from config file
echo $(config hostname) # includes the "injected" code, but it's fine here
echo $(config PROMPT_COMMAND) # also respects variables that you may not have
               # been looking for, but they're sandboxed inside the $config array

Harap balas jika Anda menemukan exploit keamanan dalam kode saya.


1
FYI ini adalah solusi Bash versi 4.0 yang sayangnya tunduk pada masalah lisensi gila yang dipaksakan oleh Apple dan tidak tersedia secara default pada Mac
Sukima

@Sukima Poin bagus. Saya telah menambahkan versi yang kompatibel dengan Bash 3. Kelemahannya adalah ia tidak akan menangani *input dengan benar, tapi lalu apa yang di Bash menangani karakter itu dengan baik?
Mikkel

Skrip pertama gagal jika kata sandi berisi backslash.
Kusalananda

@ Kusalananda Bagaimana jika backslash lolos? my\\password
Mikkel

10

Ini adalah versi yang bersih dan portabel yang kompatibel dengan Bash 3 dan yang lebih baru, di Mac dan Linux.

Ini menentukan semua default di file terpisah, untuk menghindari kebutuhan akan fungsi konfigurasi "default" yang sangat besar, berantakan, duplikat di semua skrip shell Anda. Dan itu memungkinkan Anda memilih antara membaca dengan atau tanpa fallback default:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (ini adalah perpustakaan, jadi tidak ada shebang-line):

config_read_file() {
    (grep -E "^${2}=" -m 1 "${1}" 2>/dev/null || echo "VAR=__UNDEFINED__") | head -n 1 | cut -d '=' -f 2-;
}

config_get() {
    val="$(config_read_file config.cfg "${1}")";
    if [ "${val}" = "__UNDEFINED__" ]; then
        val="$(config_read_file config.cfg.defaults "${1}")";
    fi
    printf -- "%s" "${val}";
}

test.sh (atau skrip mana pun yang ingin Anda baca nilai konfigurasi) :

#!/usr/bin/env bash
source config.shlib; # load the config library functions
echo "$(config_get myvar)"; # will be found in user-cfg
printf -- "%s\n" "$(config_get myvar)"; # safer way of echoing!
myvar="$(config_get myvar)"; # how to just read a value without echoing
echo "$(config_get othervar)"; # will fall back to defaults
echo "$(config_get bleh)"; # "__UNDEFINED__" since it isn't set anywhere

Penjelasan naskah tes:

  • Perhatikan bahwa semua penggunaan config_get di test.sh dibungkus dengan tanda kutip ganda. Dengan membungkus setiap config_get dalam tanda kutip ganda, kami memastikan bahwa teks dalam nilai variabel tidak akan pernah disalahartikan sebagai flag. Dan itu memastikan bahwa kita menjaga spasi dengan benar, seperti beberapa spasi dalam satu baris dalam nilai konfigurasi.
  • Dan apa printfgaris itu? Ya, itu sesuatu yang harus Anda waspadai: echoadalah perintah buruk untuk mencetak teks yang tidak dapat Anda kendalikan. Bahkan jika Anda menggunakan tanda kutip ganda, itu akan menafsirkan bendera. Coba atur myvar(dalam config.cfg) ke -edan Anda akan melihat baris kosong, karena echoakan berpikir bahwa itu adalah bendera. Tetapi printftidak memiliki masalah itu. The printf --mengatakan "mencetak ini, dan tidak menafsirkan sesuatu sebagai bendera", dan "%s\n"mengatakan "memformat output sebagai string dengan baris baru Trailing, dan terakhir parameter akhir adalah nilai untuk printf ke format.
  • Jika Anda tidak akan menggema nilai ke layar, maka Anda cukup menetapkannya secara normal, seperti myvar="$(config_get myvar)";. Jika Anda akan mencetaknya di layar, saya sarankan menggunakan printf agar benar-benar aman terhadap string yang tidak kompatibel dengan gema yang mungkin ada di konfigurasi pengguna. Tapi gema baik-baik saja jika variabel yang disediakan pengguna bukan karakter pertama dari string yang Anda gema, karena itulah satu-satunya situasi di mana "bendera" dapat ditafsirkan, jadi sesuatu seperti echo "foo: $(config_get myvar)";aman, karena "foo" tidak mulai dengan tanda hubung dan oleh karena itu memberi tahu gema bahwa sisa string juga bukan flag untuk itu. :-)

@ user2993656 Terima kasih telah melihat bahwa kode asli saya masih memiliki nama file config pribadi saya (environment.cfg) di dalamnya alih-alih yang benar. Adapun edit "echo -n" yang Anda lakukan, itu tergantung pada shell yang digunakan. Di Mac / Linux Bash, "echo -n" berarti "echo tanpa trailing newline", yang saya lakukan untuk menghindari trailing newlines. Tetapi tampaknya bekerja persis sama tanpa itu, jadi terima kasih atas suntingannya!
gw0

Sebenarnya, saya baru saja melalui dan menulis ulang untuk menggunakan printf bukan echo, yang memastikan bahwa kita akan menghilangkan risiko echo salah mengartikan "bendera" dalam nilai konfigurasi.
gw0

Saya sangat suka versi ini. Saya menjatuhkan config.cfg.defaultssebagai pengganti mendefinisikan mereka pada saat menelepon $(config_get var_name "default_value"). tritarget.org/static/…
Sukima

7

Parsing file konfigurasi, jangan jalankan.

Saat ini saya sedang menulis aplikasi di tempat kerja yang menggunakan konfigurasi XML yang sangat sederhana:

<config>
    <username>username-or-email</username>
    <password>the-password</password>
</config>

Dalam skrip shell ("aplikasi"), inilah yang saya lakukan untuk mendapatkan nama pengguna (kurang lebih, saya telah memasukkannya ke dalam fungsi shell):

username="$( xml sel -t -v '/config/username' "$config_file" )"

The xmlperintah XMLStarlet , yang tersedia untuk sebagian besar mesin-mesin Unix.

Saya menggunakan XML karena bagian lain dari aplikasi juga berkaitan dengan data yang dikodekan dalam file XML, jadi itu paling mudah.

Jika Anda lebih suka JSON, ada jqyang mudah digunakan shell JSON parser.

File konfigurasi saya akan terlihat seperti ini di JSON:

{                                 
  "username": "username-or-email",
  "password": "the-password"      
}                

Dan kemudian saya akan mendapatkan nama pengguna dalam skrip:

username="$( jq -r '.username' "$config_file" )"

Mengeksekusi skrip memiliki sejumlah kelebihan dan kekurangan. Kerugian utama adalah keamanan, jika seseorang dapat mengubah file konfigurasi maka mereka dapat mengeksekusi kode, dan lebih sulit untuk membuatnya menjadi bukti idiot. Keuntungannya adalah kecepatan, pada tes sederhana lebih dari 10.000 kali lebih cepat untuk sumber file konfigurasi daripada menjalankan pq, dan fleksibilitas, siapa pun yang suka monyet patching python akan menghargai ini.
icarus

@icarus Seberapa besar file konfigurasi yang biasa Anda temui, dan seberapa sering Anda perlu menguraikannya dalam satu sesi? Perhatikan juga bahwa beberapa nilai mungkin didapat dari XML atau JSON dalam sekali jalan.
Kusalananda

Biasanya hanya beberapa (1 hingga 3) nilai. Jika Anda menggunakan evaluntuk menetapkan beberapa nilai maka Anda mengeksekusi bagian yang dipilih dari file konfigurasi :-).
icarus

1
@icarus saya sedang berpikir array ... Tidak perlu evalapa - apa. Hit kinerja menggunakan format standar dengan parser yang ada (meskipun itu utilitas eksternal) dapat diabaikan dibandingkan dengan kekokohan, jumlah kode, kemudahan penggunaan, dan pemeliharaan.
Kusalananda

1
+1 untuk "parsing file config, jangan jalankan"
Iiridayn

4

Cara yang paling umum, efisien dan benar adalah dengan menggunakan source, atau .sebagai bentuk singkatan. Sebagai contoh:

source /home/myuser/test/config

atau

. /home/myuser/test/config

Namun, yang perlu dipertimbangkan adalah masalah keamanan yang dapat ditimbulkan oleh file konfigurasi eksternal yang bersumber eksternal, mengingat kode tambahan dapat dimasukkan. Untuk informasi lebih lanjut, termasuk tentang cara mendeteksi dan menyelesaikan masalah ini, saya akan merekomendasikan untuk melihat bagian 'Amankan' di http://wiki.bash-hackers.org/howto/conffile#secure_it


5
Saya memiliki harapan besar untuk artikel itu (muncul dalam hasil pencarian saya juga), tetapi saran penulis untuk mencoba menggunakan regex untuk menyaring kode berbahaya adalah latihan yang sia-sia.
Mikkel

Prosedur dengan titik, membutuhkan jalur absolut? Dengan yang relatif tidak bekerja
Davide

2

Saya menggunakan ini dalam skrip saya:

sed_escape() {
  sed -e 's/[]\/$*.^[]/\\&/g'
}

cfg_write() { # path, key, value
  cfg_delete "$1" "$2"
  echo "$2=$3" >> "$1"
}

cfg_read() { # path, key -> value
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" | sed "s/^$(echo "$2" | sed_escape)=//" | tail -1
}

cfg_delete() { # path, key
  test -f "$1" && sed -i "/^$(echo $2 | sed_escape).*$/d" "$1"
}

cfg_haskey() { # path, key
  test -f "$1" && grep "^$(echo "$2" | sed_escape)=" "$1" > /dev/null
}

Harus mendukung setiap kombinasi karakter, kecuali kunci yang tidak boleh ada =di dalamnya, karena itulah pemisahnya. Ada lagi yang berhasil.

% cfg_write test.conf mykey myvalue
% cfg_read test.conf mykey
myvalue
% cfg_delete test.conf mykey
% cfg_haskey test.conf mykey || echo "It's not here anymore"
It's not here anymore

Juga, ini sepenuhnya aman karena tidak menggunakan source/eval


0

Untuk skenario saya, sourceatau .baik-baik saja, tetapi saya ingin mendukung variabel lingkungan lokal (yaitu, FOO=bar myscript.sh) diutamakan daripada variabel yang dikonfigurasi. Saya juga ingin agar file konfigurasi dapat diedit pengguna dan nyaman bagi seseorang yang menggunakan sumber file konfigurasi, dan menjaganya agar tetap sekecil / sesederhana mungkin, agar tidak mengalihkan perhatian dari tekanan utama skrip saya yang sangat kecil.

Inilah yang saya pikirkan:

CONF=${XDG_CONFIG_HOME:-~/config}/myscript.sh
if [ ! -f $CONF ]; then
    cat > $CONF << CONF
VAR1="default value"
CONF
fi
. <(sed 's/^\([^=]\+\) *= *\(.*\)$/\1=${\1:-\2}/' < $CONF)

Pada dasarnya - ini memeriksa definisi variabel (tanpa menjadi sangat fleksibel tentang spasi putih) dan menulis ulang garis-garis sehingga nilai dikonversi ke default untuk variabel itu, dan variabel tidak dimodifikasi jika ditemukan, seperti XDG_CONFIG_HOMEvariabel di atas. Sumber ini mengubah versi file konfigurasi ini dan melanjutkan.

Pekerjaan di masa depan dapat membuat sedskrip lebih kuat, memfilter baris yang terlihat aneh atau tidak definisi, dll, tidak merusak komentar akhir baris - tetapi ini cukup baik untuk saya saat ini.


0

Ini singkat dan aman:

# Read common vars from common.vars
# the incantation here ensures (by env) that only key=value pairs are present
# then declare-ing the result puts those vars in our environment 
declare $(env -i `cat common.vars`)

The -iMemastikan Anda hanya mendapatkan variabel daricommon.vars


-2

Kamu bisa melakukannya:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.