Verwenden Sie die Konfigurationsdatei für mein Shell-Skript


26

Ich muss eine Konfigurationsdatei für mein eigenes Skript erstellen: hier ein Beispiel:

Skript:

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

Inhalt von /home/myuser/test/config:

nam="Mark"
sur="Brown"

das funktioniert!

Meine Frage: Ist dies der richtige Weg, oder gibt es andere Wege?


Die Variablen sollten oben stehen. Ich bin überrascht, dass es funktioniert. Wie auch immer, warum brauchst du eine Konfigurationsdatei? Planen Sie, diese Variablen woanders zu verwenden?
Faheem Mitha

Ich brauche die Variablen, weil mein Skript viele Optionen hat: Die Verwendung einer Konfigurationsdatei wird das Skript vereinfachen. Thanks
Pol Hallen

5
IMHO ist es in Ordnung. Ich würde das so machen.
Tinti

abcdetut es auch so und das ist ein ziemlich großes Programm (für ein Shell-Skript). Sie können es hier ansehen .
Lucas

Antworten:


19

sourceist nicht sicher, da es beliebigen Code ausführt. Dies ist möglicherweise kein Problem für Sie, aber wenn die Dateiberechtigungen nicht korrekt sind, kann ein Angreifer mit Dateisystemzugriff möglicherweise Code als privilegierter Benutzer ausführen, indem er Code in eine Konfigurationsdatei einfügt, die von einem ansonsten gesicherten Skript wie z Init-Skript.

Die bisher beste Lösung, die ich identifizieren konnte, ist die unbeholfene Neuerfindung des Rades:

myscript.conf

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

Bei Verwendung sourcewürde dies echo rm -rf /zweimal ausgeführt werden und auch den aktiven Benutzer ändern $PROMPT_COMMAND. Tun Sie stattdessen Folgendes:

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 (Mac / Bash 3-kompatibel)

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

Bitte antworten Sie, wenn Sie eine Sicherheitslücke in meinem Code finden.


1
Zu Ihrer Information, dies ist eine Bash-Version 4.0-Lösung, die leider verrückten Lizenzproblemen von Apple unterliegt und auf Macs
Sukima,

@Sukima Guter Punkt. Ich habe eine Version hinzugefügt, die mit Bash 3 kompatibel ist. Ihre Schwäche ist, dass sie *Eingaben nicht richtig verarbeitet, aber was in Bash behandelt dieses Zeichen dann gut?
Mikkel

Das erste Skript schlägt fehl, wenn das Kennwort einen Backslash enthält.
Kusalananda

@Kusalananda Was passiert, wenn der Backslash wegfällt? my\\password
Mikkel

10

Hier ist eine saubere und portable Version, die mit Bash 3 und höher auf Mac und Linux kompatibel ist.

Es gibt alle Standardeinstellungen in einer separaten Datei an, um zu vermeiden, dass in all Ihren Shell-Skripten eine riesige, überfüllte und duplizierte Konfigurationsfunktion für die Standardeinstellungen erforderlich ist. Und Sie können wählen, ob Sie mit oder ohne Standard-Fallbacks lesen möchten:

config.cfg :

myvar=Hello World

config.cfg.defaults :

myvar=Default Value
othervar=Another Variable

config.shlib (das ist eine Bibliothek, es gibt also keine Shebang-Linie):

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 (oder alle Skripte, in denen Sie Konfigurationswerte lesen möchten) :

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

Erklärung des Testskripts:

  • Beachten Sie, dass alle Verwendungen von config_get in test.sh in doppelte Anführungszeichen eingeschlossen sind. Indem wir jedes config_get in doppelte Anführungszeichen setzen, stellen wir sicher, dass der Text in der Variablen value niemals als Flags falsch interpretiert wird. Außerdem wird sichergestellt, dass Leerzeichen ordnungsgemäß beibehalten werden, z. B. mehrere Leerzeichen hintereinander im Konfigurationswert.
  • Und was ist das für eine printfZeile? Nun, es ist etwas, das Sie beachten sollten: Es echoist ein falscher Befehl zum Drucken von Text, über den Sie keine Kontrolle haben. Selbst wenn Sie doppelte Anführungszeichen verwenden, werden Flags interpretiert. Setzen Sie myvar(in config.cfg) auf -eund Sie werden eine leere Zeile sehen, weil Sie echodenken, dass es eine Flagge ist. Aber printfhat das Problem nicht. Der printf --Befehl "Drucken Sie dies und interpretieren Sie nichts als Flags" und der "%s\n"Befehl "Formatieren Sie die Ausgabe als Zeichenfolge mit einer nachgestellten Newline. Der letzte Parameter ist der Wert, den printf formatieren soll.
  • Wenn Sie keine Werte auf dem Bildschirm wiedergeben möchten, weisen Sie sie einfach wie gewohnt zu myvar="$(config_get myvar)";. Wenn Sie sie auf dem Bildschirm ausdrucken möchten, empfehle ich, printf zu verwenden, um die Sicherheit vor echounverträglichen Zeichenfolgen in der Benutzerkonfiguration zu gewährleisten. Aber Echo ist in Ordnung , wenn der Benutzer bereitgestellte Variable nicht ist das erste Zeichen der Zeichenfolge Sie Echo, denn das ist die einzige Situation ist , wo „Flags“ interpretiert werden könnte, so etwas wie echo "foo: $(config_get myvar)";sicher ist, da die „foo“ nicht beginne mit einem Bindestrich und sage daher echo, dass der Rest des Strings auch keine Flags dafür sind. :-)

@ user2993656 Vielen Dank, dass Sie festgestellt haben, dass mein ursprünglicher Code immer noch meinen privaten Konfigurationsdateinamen (environment.cfg) anstelle des richtigen enthält. Was die von Ihnen vorgenommene Bearbeitung von "echo -n" betrifft, hängt dies von der verwendeten Shell ab. Unter Mac / Linux Bash bedeutet "echo -n" "Echo ohne Zeilenumbruch", was ich getan habe, um Zeilenumbrüche zu vermeiden. Aber es scheint genauso zu funktionieren, also danke für die Änderungen!
gw0

Eigentlich habe ich es nur durchgearbeitet und neu geschrieben, um printf anstelle von echo zu verwenden, was sicherstellt, dass das Risiko von Echo-Fehlinterpretationen von "Flags" in den Konfigurationswerten beseitigt wird.
gw0

Ich mag diese Version wirklich. Ich habe die fallen lassenconfig.cfg.defaults anstelle der Definition zum Zeitpunkt des Anrufs$(config_get var_name "default_value") . tritarget.org/static/…
Sukima

7

Analysieren Sie die Konfigurationsdatei, führen Sie sie nicht aus.

Momentan schreibe ich eine Anwendung bei der Arbeit, die eine extrem einfache XML-Konfiguration verwendet:

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

Im Shell-Skript (der "Anwendung") ist dies das, was ich mache, um den Benutzernamen zu ermitteln (mehr oder weniger, ich habe es in eine Shell-Funktion eingefügt):

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

Der xmlBefehl lautet XMLStarlet und ist für die meisten Unices verfügbar.

Ich verwende XML, da andere Teile der Anwendung auch Daten verarbeiten, die in XML-Dateien codiert sind. Das war also am einfachsten.

Wenn Sie JSON bevorzugen, gibt jqes einen einfach zu verwendenden Shell-JSON-Parser.

Meine Konfigurationsdatei würde in JSON ungefähr so ​​aussehen:

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

Und dann würde ich den Benutzernamen im Skript bekommen:

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

Das Ausführen des Skripts hat eine Reihe von Vor- und Nachteilen. Die Hauptnachteile sind die Sicherheit. Wenn jemand die Konfigurationsdatei ändern kann, kann er Code ausführen, und es ist schwieriger, sie idiotensicher zu machen. Die Vorteile sind Geschwindigkeit, bei einem einfachen Test ist es mehr als 10.000-mal schneller, eine Konfigurationsdatei zu erstellen, als pq auszuführen, und Flexibilität, jeder, der Affen-Patching-Python mag, wird dies zu schätzen wissen.
Ikarus

@icarus Auf wie große Konfigurationsdateien stoßen Sie normalerweise und wie oft müssen Sie sie in einer Sitzung analysieren? Beachten Sie auch, dass möglicherweise mehrere Werte gleichzeitig aus XML oder JSON stammen.
Kusalananda

In der Regel nur wenige (1 bis 3) Werte. Wenn Sie evalmehrere Werte festlegen, führen Sie ausgewählte Teile der Konfigurationsdatei aus :-).
Ikarus

1
@icarus Ich dachte Arrays ... Keine Notwendigkeit für evalirgendetwas. Die Leistungseinbußen bei der Verwendung eines Standardformats mit einem vorhandenen Parser (auch wenn es sich um ein externes Dienstprogramm handelt) sind im Vergleich zu Robustheit, Codemenge, Benutzerfreundlichkeit und Wartbarkeit vernachlässigbar.
Kusalananda

1
+1 für "Analysiere die Konfigurationsdatei,
führe

4

Die gebräuchlichste, effizienteste und korrekteste Methode ist die Verwendung sourceoder .Kurzform. Beispielsweise:

source /home/myuser/test/config

oder

. /home/myuser/test/config

Zu berücksichtigen sind jedoch die Sicherheitsprobleme, die durch die Verwendung einer zusätzlichen externen Konfigurationsdatei auftreten können, da zusätzlicher Code eingefügt werden kann. Weitere Informationen, einschließlich Informationen zum Erkennen und Beheben dieses Problems, finden Sie im Abschnitt "Sichern" unter http://wiki.bash-hackers.org/howto/conffile#secure_it


5
Ich hatte große Hoffnungen auf diesen Artikel (der auch in meinen Suchergebnissen auftauchte), aber der Vorschlag des Autors, Regex zu verwenden, um bösartigen Code herauszufiltern, ist eine Übung der Sinnlosigkeit.
Mikkel

Die Prozedur mit Punkt erfordert einen absoluten Pfad? Mit dem Verwandten funktioniert es nicht
Davide

2

Ich benutze dies in meinen Skripten:

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
}

Sollte jede Zeichenkombination unterstützen, mit Ausnahme der Tasten, die nicht enthalten sind =, da dies der Trenner ist. Alles andere funktioniert.

% 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

Auch das ist völlig sicher, da es keine Art von source/ verwendeteval


0

Für mein Szenario sourceoder .war in Ordnung, aber ich wollte lokale Umgebungsvariablen unterstützen (dh FOO=bar myscript.sh), die Vorrang vor konfigurierten Variablen haben. Ich wollte auch, dass die Konfigurationsdatei vom Benutzer bearbeitet werden kann und für jemanden komfortabel ist, der Konfigurationsdateien verwendet und sie so klein / einfach wie möglich hält, um nicht vom Hauptschwerpunkt meines sehr kleinen Skripts abzulenken.

Das habe ich mir ausgedacht:

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)

Im Wesentlichen - es prüft auf Variablendefinitionen (ohne sehr flexibel in Bezug auf Leerzeichen zu sein) und schreibt diese Zeilen neu, sodass der Wert in einen Standardwert für diese Variable konvertiert wird und die Variable unverändert bleibt, wenn sie wie die gefunden wird XDG_CONFIG_HOME obige Variable. Es bezieht diese geänderte Version der Konfigurationsdatei und fährt fort.

Zukünftige Arbeiten könnten das sedSkript robuster machen, Zeilen herausfiltern, die seltsam aussehen oder keine Definitionen usw. sind, und keine Zeilenende-Kommentare unterbrechen - aber das ist für mich im Moment gut genug.


0

Das ist kurz und bündig:

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

Das -istellt sicher, dass Sie nur die Variablen von erhaltencommon.vars


-2

Du kannst es schaffen:

#!/bin/bash
name="mohsen"
age=35
cat > /home/myuser/test/config << EOF
Name=$name
Age=$age
EOF
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.