Usa il file di configurazione per il mio script di shell


26

Devo creare un file di configurazione per il mio script: qui un esempio:

script:

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

Contenuto di /home/myuser/test/config:

nam="Mark"
sur="Brown"

funziona!

La mia domanda: è questo il modo corretto di farlo o ci sono altri modi?


Le variabili dovrebbero essere in cima. Sono sorpreso che funzioni. Comunque, perché hai bisogno di un file di configurazione? Stai pensando di usare queste variabili da qualche altra parte?
Faheem Mitha,

Faheem, ho bisogno delle variabili perché il mio script ha molte opzioni: l'uso di un file di configurazione semplifica lo script. Grazie
Pol Hallen il

5
IMHO va bene. Vorrei fare così.
Tinti,

abcdelo fa anche in questo modo ed è un programma abbastanza grande (per uno script di shell). Puoi dare un'occhiata qui .
Lucas,

Risposte:


19

sourcenon è sicuro in quanto eseguirà codice arbitrario. Questo potrebbe non essere un problema per te, ma se le autorizzazioni dei file non sono corrette, potrebbe essere possibile per un utente malintenzionato con accesso al filesystem eseguire codice come utente privilegiato iniettando il codice in un file di configurazione caricato da uno script altrimenti protetto come un script init.

Finora, la migliore soluzione che sono stato in grado di identificare è la soluzione goffa che reinventa la ruota:

myscript.conf

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

Utilizzando source, questo verrebbe eseguito echo rm -rf /due volte, oltre a cambiare l'utente in esecuzione $PROMPT_COMMAND. Invece, fai questo:

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 (compatibile con 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

Ti preghiamo di rispondere se trovi un exploit di sicurezza nel mio codice.


1
Cordiali saluti, questa è una soluzione di Bash versione 4.0 che purtroppo è soggetta a folli problemi di licenza imposti da Apple e non è disponibile per impostazione predefinita su Mac
Sukima

@Sukima Un buon punto. Ho aggiunto una versione compatibile con Bash 3. Il suo punto debole è che non gestirà *correttamente gli input, ma allora cosa in Bash gestisce bene quel personaggio?
Mikkel,

Il primo script ha esito negativo se la password contiene una barra rovesciata.
Kusalananda

@Kusalananda Cosa succede se la barra rovesciata viene salvata? my\\password
Mikkel,

10

Ecco una versione pulita e portatile che è compatibile con Bash 3 e versioni successive, sia su Mac che Linux.

Specifica tutte le impostazioni predefinite in un file separato, per evitare la necessità di una funzione di configurazione "impostazioni predefinite" ingombra, ingombra, duplicata in tutti gli script della shell. E ti consente di scegliere tra leggere con o senza fallback predefiniti:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (questa è una libreria, quindi non esiste una riga 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 (o qualsiasi script in cui desideri leggere i valori di configurazione) :

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

Spiegazione dello script di test:

  • Si noti che tutti gli utilizzi di config_get in test.sh sono racchiusi tra virgolette doppie. Avvolgendo ogni config_get tra virgolette doppie, ci assicuriamo che il testo nel valore della variabile non venga mai interpretato erroneamente come flag. E assicura che conserviamo correttamente gli spazi bianchi, come più spazi di fila nel valore di configurazione.
  • E cos'è quella printflinea? Bene, è qualcosa di cui dovresti essere consapevole: echoè un cattivo comando per la stampa di testo su cui non hai alcun controllo. Anche se usi virgolette doppie, interpreterà le bandiere. Prova a impostare myvar(in config.cfg) su -ee vedrai una linea vuota, perché echopenserai che sia una bandiera. Ma printfnon ha questo problema. La printf --dice "Stampa questo, e non interpretare nulla come bandiere", e la "%s\n"dice "formattare l'output come una stringa con un ritorno a capo finale, e infine il parametro finale è il valore per printf formato.
  • Se non stai facendo eco ai valori sullo schermo, li assegneresti semplicemente normalmente, come myvar="$(config_get myvar)";. Se hai intenzione di stamparli sullo schermo, ti suggerisco di usare printf per essere totalmente al sicuro da eventuali stringhe incompatibili con l'eco che potrebbero trovarsi nella configurazione dell'utente. Ma l'eco va bene se la variabile fornita dall'utente non è il primo carattere della stringa che stai echeggiando, poiché questa è l'unica situazione in cui i "flag" potrebbero essere interpretati, quindi qualcosa di simile echo "foo: $(config_get myvar)";è sicuro, dal momento che il "foo" non lo fa inizia con un trattino e quindi dice a echo che il resto della stringa non è neanche flag per esso. :-)

@ user2993656 Grazie per aver notato che il mio codice originale conteneva ancora il mio nome file di configurazione privato (environment.cfg) invece di quello corretto. Per quanto riguarda la modifica "echo -n" che hai fatto, dipende dalla shell utilizzata. Su Mac / Linux Bash, "echo -n" significa "echo senza trascinare newline", cosa che ho fatto per evitare di trascinare newline. Ma sembra funzionare esattamente lo stesso senza di essa, quindi grazie per le modifiche!
gw0,

In realtà, l'ho appena passato e l'ho riscritto per usare printf invece di echo, il che assicura che ci libereremo del rischio che l'eco interpreti erroneamente le "bandiere" nei valori di configurazione.
gw0,

Mi piace molto questa versione. Ho lasciato perdere config.cfg.defaultsinvece di definirli al momento della chiamata $(config_get var_name "default_value"). tritarget.org/static/…
Sukima,

7

Analizza il file di configurazione, non eseguirlo.

Attualmente sto scrivendo un'applicazione al lavoro che utilizza una configurazione XML estremamente semplice:

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

Nello script della shell (l '"applicazione"), questo è ciò che faccio per ottenere il nome utente (più o meno, l'ho messo in una funzione shell):

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

Il xmlcomando è XMLStarlet , disponibile per la maggior parte degli Unices.

Sto usando XML poiché altre parti dell'applicazione si occupano anche di dati codificati in file XML, quindi è stato più semplice.

Se preferisci JSON, c'è jqun parser JSON di shell facile da usare.

Il mio file di configurazione sarebbe simile a questo in JSON:

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

E poi otterrei il nome utente nello script:

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

L'esecuzione dello script presenta numerosi vantaggi e svantaggi. Gli svantaggi principali sono la sicurezza, se qualcuno può modificare il file di configurazione, può eseguire il codice ed è più difficile renderlo idiota. I vantaggi sono la velocità, in un semplice test è più di 10.000 volte più veloce per generare un file di configurazione che per eseguire pq, e la flessibilità, a tutti coloro che amano il pitone di patch di scimmie lo apprezzeranno.
icarus

@icarus Quanti file di configurazione di grandi dimensioni di solito incontri e con quale frequenza devi analizzarli in una sessione? Nota anche che diversi valori possono essere estratti da XML o JSON in una volta sola.
Kusalananda

Di solito solo pochi (da 1 a 3) valori. Se si utilizza evalper impostare più valori, si eseguono parti selezionate del file di configurazione :-).
Icaro

1
@icarus Pensavo alle matrici ... Non c'è bisogno di evalniente. Il successo prestazionale dell'utilizzo di un formato standard con un parser esistente (anche se si tratta di un'utilità esterna) è trascurabile rispetto alla robustezza, alla quantità di codice, alla facilità d'uso e alla manutenibilità.
Kusalananda

1
+1 per "analizzare il file di configurazione, non eseguirlo"
Iiridayn

4

Il modo più comune, efficiente e corretto è quello di utilizzare sourceo .come forma abbreviata. Per esempio:

source /home/myuser/test/config

o

. /home/myuser/test/config

Qualcosa da considerare, tuttavia, sono i problemi di sicurezza che possono essere generati dall'utilizzo di un file di configurazione aggiuntivo proveniente da fonti esterne, dato che è possibile inserire codice aggiuntivo. Per ulteriori informazioni, incluso su come rilevare e risolvere questo problema, consiglierei di dare un'occhiata alla sezione "Proteggilo" di http://wiki.bash-hackers.org/howto/conffile#secure_it


5
Avevo grandi speranze per quell'articolo (apparso anche nei miei risultati di ricerca), ma il suggerimento dell'autore di tentare di usare regex per filtrare il codice maligno è un esercizio di futilità.
Mikkel,

La procedura con punto richiede un percorso assoluto? Con quello relativo non funziona
Davide

2

Lo uso nei miei script:

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
}

Dovrebbe supportare tutte le combinazioni di caratteri, tranne le chiavi che non possono avere =in esse, dato che è il separatore. Qualsiasi altra cosa funziona.

% 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

Inoltre, questo è completamente sicuro poiché non utilizza alcun tipo di source/eval


0

Per il mio scenario, sourceo .andava bene, ma volevo supportare le variabili di ambiente locale (ovvero, FOO=bar myscript.sh) che hanno la precedenza sulle variabili configurate. Volevo anche che il file di configurazione fosse modificabile dall'utente e comodo per qualcuno abituato a reperire i file di configurazione, e per mantenerlo il più piccolo / semplice possibile, per non distrarre dalla spinta principale del mio piccolo script.

Questo è quello che mi è venuto in mente:

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)

In sostanza: controlla le definizioni delle variabili (senza essere molto flessibile riguardo agli spazi bianchi) e riscrive tali righe in modo che il valore venga convertito in un valore predefinito per quella variabile e, se presente, la XDG_CONFIG_HOMEvariabile non viene modificata, come quella sopra. Proviene da questa versione alterata del file di configurazione e continua.

Il lavoro futuro potrebbe rendere lo sedscript più robusto, filtrare le righe che sembrano strane o che non sono definizioni, ecc., Non interrompere i commenti di fine riga, ma per ora è abbastanza buono per me.


0

Questo è sintetico e sicuro:

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

I -ivi permette di ricevere solo le variabili dacommon.vars


-2

Puoi farlo:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.