Użyj pliku konfiguracyjnego dla mojego skryptu powłoki

26

Muszę utworzyć plik konfiguracyjny dla własnego skryptu: tutaj przykład:

scenariusz:

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

Treść /home/myuser/test/config:

nam="Mark"
sur="Brown"

to działa!

Moje pytanie: czy to jest właściwy sposób, aby to zrobić, czy istnieją inne sposoby?

Pol Hallen
źródło
Zmienne powinny znajdować się na górze. Dziwię się, że to działa. W każdym razie, dlaczego potrzebujesz pliku konfiguracyjnego? Czy planujesz użyć tych zmiennych gdzieś indziej?
Faheem Mitha
Faheem, potrzebuję zmiennych, ponieważ mój skrypt ma wiele opcji: użycie pliku konfiguracyjnego spowoduje jego skrócenie. Dzięki
Pol Hallen,
5
IMHO jest w porządku. Zrobiłbym w ten sposób.
Tinti
abcderobi to również w ten sposób i jest to dość duży program (dla skryptu powłoki). Możesz to zobaczyć tutaj .
Lucas,

Odpowiedzi:

19

sourcenie jest bezpieczny, ponieważ wykona dowolny kod. Może to nie stanowić problemu, ale jeśli uprawnienia do plików są niepoprawne, osoba atakująca z dostępem do systemu plików może wykonać kod jako użytkownik uprzywilejowany przez wstrzyknięcie kodu do pliku konfiguracyjnego załadowanego w inny sposób zabezpieczonym skryptem, takim jak skrypt inicjujący.

Jak dotąd najlepszym rozwiązaniem, jakie udało mi się zidentyfikować, jest niezdarne rozwiązanie na nowo:

myscript.conf

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

Przy użyciu sourceuruchomiłoby się to echo rm -rf /dwukrotnie, a także zmieniłoby działającego użytkownika $PROMPT_COMMAND. Zamiast tego wykonaj następujące czynności:

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 (kompatybilny z 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

Proszę odpowiedzieć, jeśli znajdziesz w moim kodzie lukę w zabezpieczeniach.

Mikkel
źródło
1
Do Twojej wiadomości jest to rozwiązanie Bash w wersji 4.0, które niestety podlega
obłędnym
@Sukima Dobra uwaga. Dodałem wersję, która jest kompatybilna z Bash 3. Jego słabością jest to, że nie będzie *poprawnie obsługiwać danych wejściowych, ale co w Bash dobrze obsługuje tę postać?
Mikkel,
Pierwszy skrypt nie powiedzie się, jeśli hasło zawiera ukośnik odwrotny.
Kusalananda
@Kusalananda Co się stanie, jeśli ukośnik zostanie uniknięty? my\\password
Mikkel
10

Oto czysta i przenośna wersja, która jest kompatybilna z Bash 3 i nowszymi, zarówno na Macu, jak i Linuksie.

Określa wszystkie wartości domyślne w osobnym pliku, aby uniknąć potrzeby ogromnej, zaśmieconej, zduplikowanej funkcji konfiguracyjnej „domyślne” we wszystkich skryptach powłoki. I pozwala wybierać między czytaniem z domyślnymi awariami lub bez nich:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (jest to biblioteka, więc nie ma linii shebang):

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 (lub dowolne skrypty, w których chcesz odczytać wartości konfiguracji) :

#!/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

Objaśnienie skryptu testowego:

  • Zauważ, że wszystkie zastosowania config_get w test.sh są owinięte podwójnymi cudzysłowami. Zawijając każdy config_get w podwójne cudzysłowy, zapewniamy, że tekst w wartości zmiennej będzie nigdy nie będzie błędnie interpretowany jako flaga. Zapewnia to prawidłowe zachowanie białych znaków, takich jak wiele spacji w wierszu wartości konfiguracji.
  • A co to za printflinia? Cóż, jest to coś, o czym powinieneś wiedzieć: echoto złe polecenie do drukowania tekstu, nad którym nie masz kontroli. Nawet jeśli użyjesz podwójnego cudzysłowu, zinterpretuje flagi. Spróbuj ustawić myvar(in config.cfg) na, -ea zobaczysz pustą linię, ponieważ echopomyślisz, że to flaga. Ale printfnie ma tego problemu. printf --Mówi „drukuj, i niczego nie interpretować jako flagi”, a"%s\n" mówi „formatować dane wyjściowe jako ciąg z końcowym znakiem nowej linii, a wreszcie ostateczny parametr jest wartością dla printf do pliku PDF.
  • Jeśli nie zamierzasz wyświetlać wartości na ekranie, po prostu przypisujesz je normalnie, na przykład myvar="$(config_get myvar)";. Jeśli masz zamiar wydrukować je na ekranie, sugeruję użycie printf, aby całkowicie zabezpieczyć się przed ciągami niezgodnymi z echem, które mogą znajdować się w konfiguracji użytkownika. Ale echo jest w porządku, jeśli zmienna podana przez użytkownika nie jest pierwszym znakiem echa, ponieważ jest to jedyna sytuacja, w której „flagi” można interpretować, więc coś w tym rodzaju echo "foo: $(config_get myvar)";jest bezpieczne, ponieważ „foo” nie zacznij od myślnika i dlatego mówi echo, że reszta ciągu również nie jest dla niego oznaczona. :-)
gw0
źródło
@ user2993656 Dziękujemy za wykrycie, że mój oryginalny kod nadal zawierał moją prywatną nazwę pliku config (environment.cfg) zamiast poprawnego. Jeśli chodzi o edycję „echo -n”, to zależy to od użytej powłoki. W systemie Mac / Linux Bash „echo -n” oznacza „echo bez końcowego znaku nowej linii”, co zrobiłem, aby uniknąć końcowego znaku nowego wiersza. Ale wydaje się, że działa dokładnie tak samo bez niego, więc dziękuję za zmiany!
gw0
Właściwie właśnie przeszedłem i przepisałem go, aby użyć printf zamiast echa, co gwarantuje, że pozbędziemy się ryzyka błędnej interpretacji „flag” w wartościach konfiguracji.
gw0
Naprawdę podoba mi się ta wersja. Porzuciłem config.cfg.defaultszamiast definiowania ich w momencie dzwonienia $(config_get var_name "default_value"). tritarget.org/static/…
Sukima
7

Analizuj plik konfiguracyjny, nie wykonuj go.

Obecnie piszę w pracy aplikację, która używa niezwykle prostej konfiguracji XML:

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

W skrypcie powłoki („aplikacji”) robię to, aby uzyskać nazwę użytkownika (mniej więcej umieściłem ją w funkcji powłoki):

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

xmlKomenda jest XMLStarlet , który jest dostępny dla większości Uniksów.

Używam XML, ponieważ inne części aplikacji dotyczą również danych zakodowanych w plikach XML, więc było to najłatwiejsze.

Jeśli wolisz JSON, jest jq łatwy w użyciu parser JSON powłoki.

Mój plik konfiguracyjny wyglądałby mniej więcej w JSON:

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

A potem dostałbym nazwę użytkownika w skrypcie:

username="$( jq -r '.username' "$config_file" )"
Kusalananda
źródło
Wykonywanie skryptu ma wiele zalet i wad. Główną wadą jest bezpieczeństwo, jeśli ktoś może zmodyfikować plik konfiguracyjny, wówczas może wykonać kod, a trudniej jest uczynić go idiotycznym. Zaletą jest szybkość, w prostym teście jest ponad 10 000 razy szybsze źródło pliku konfiguracyjnego niż uruchomienie pq, i elastyczność, każdy, kto lubi pythonowe łatanie małp, doceni to.
icarus
@icarus Jak często spotykane są pliki konfiguracyjne i jak często trzeba je analizować w jednej sesji? Zauważ też, że za jednym razem można uzyskać kilka wartości z XML lub JSON.
Kusalananda
Zwykle tylko kilka (1 do 3) wartości. Jeśli używasz evaldo ustawiania wielu wartości, wówczas wykonujesz wybrane części pliku konfiguracyjnego :-).
icarus
1
@icarus Myślałem o tablicach ... Nie trzeba evalniczego. Wydajność wynikająca z używania standardowego formatu z istniejącym parserem (nawet jeśli jest to narzędzie zewnętrzne) jest nieznaczna w porównaniu z odpornością, ilością kodu, łatwością użycia i łatwością konserwacji.
Kusalananda
1
+1 za „parsowanie pliku konfiguracyjnego, nie uruchamiaj go”
Iiridayn
4

Najczęstszym, wydajnym i poprawnym sposobem jest użycie sourcelub .forma skrócona. Na przykład:

source /home/myuser/test/config

lub

. /home/myuser/test/config

Należy jednak wziąć pod uwagę problemy związane z bezpieczeństwem, które może spowodować użycie dodatkowego pliku konfiguracyjnego pochodzącego z zewnątrz, ponieważ można wstawić dodatkowy kod. Aby uzyskać więcej informacji, w tym na temat wykrywania i rozwiązywania tego problemu, polecam zajrzeć do sekcji „Zabezpiecz to” na http://wiki.bash-hackers.org/howto/conffile#secure_it

NFarrington
źródło
5
Miałem duże nadzieje na ten artykuł (pojawił się także w moich wynikach wyszukiwania), ale sugestia autora dotycząca próby użycia wyrażenia regularnego w celu odfiltrowania złośliwego kodu jest ćwiczeniem daremnym.
Mikkel
Procedura z kropką, wymaga bezwzględnej ścieżki? Z tym względnym nie działa
Davide
2

Używam tego w moich skryptach:

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
}

Powinien obsługiwać każdą kombinację znaków, z wyjątkiem kluczy, których nie można =w nich mieć , ponieważ jest to separator. Wszystko inne działa.

% 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

Jest to również całkowicie bezpieczne, ponieważ nie korzysta z żadnego rodzaju source/eval

tam pobrano
źródło
0

W moim scenariuszu sourcelub .było w porządku, ale chciałem wspierać lokalne zmienne środowiskowe (tj. FOO=bar myscript.sh), Które mają pierwszeństwo przed zmiennymi skonfigurowanymi. Chciałem też, aby plik konfiguracyjny był edytowalny przez użytkownika i wygodny dla kogoś, kto zwykł pozyskiwać pliki konfiguracyjne, i aby był tak mały / prosty, jak to możliwe, aby nie odwracał uwagi od głównego ciągu mojego bardzo małego skryptu.

Oto co wymyśliłem:

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)

Zasadniczo - sprawdza definicje zmiennych (bez zbytniej elastyczności co do białych znaków) i przepisuje te wiersze, aby wartość została przekonwertowana na wartość domyślną dla tej zmiennej, a zmienna nie jest modyfikowana, jeśli zostanie znaleziona, tak jak XDG_CONFIG_HOME zmienna powyżej. Źródła tej zmienionej wersji pliku konfiguracyjnego i kontynuuje.

Przyszłe prace mogą sprawić, że sedskrypt będzie bardziej niezawodny, odfiltrować wiersze, które wyglądają dziwnie lub nie są definicjami itp., Nie przerywają komentarzy na końcu wiersza - ale na razie jest to dla mnie wystarczające.

Iiridayn
źródło
0

Jest to zwięzłe i bezpieczne:

# 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`)

Te -i, zapewnia można dostać tylko od zmiennychcommon.vars

Marcin
źródło
-2

Możesz to zrobić:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Zatoka Perska
źródło