Shell-Variablen für Befehlsoptionen verwenden

18

In einem Bash-Skript versuche ich, die von mir verwendeten Optionen rsyncin einer separaten Variablen zu speichern . Dies funktioniert gut für einfache Optionen (wie --recursive), aber ich habe Probleme mit --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)

Wie Sie sehen, funktioniert die Übergabe --exclude='.*'an rsync"manuell" einwandfrei ( .barwird nicht kopiert), wenn die Optionen zuerst in einer Variablen gespeichert werden.

Ich vermute, dass dies entweder mit den Anführungszeichen oder dem Platzhalter (oder beiden) zusammenhängt, aber ich konnte nicht herausfinden, was genau falsch ist.

Florian Brucker
quelle

Antworten:

37

Im Allgemeinen ist es eine schlechte Idee, eine Liste von separaten Elementen in eine einzelne Zeichenfolge herabzusetzen, unabhängig davon, ob es sich um eine Liste von Befehlszeilenoptionen oder um eine Liste von Pfadnamen handelt.

Verwenden Sie stattdessen ein Array:

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

oder

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

und später...

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

Auf diese Weise bleibt die Notierung der einzelnen Optionen erhalten (solange Sie die Erweiterung von doppelt zitieren ${rsync_options[@]}). Darüber hinaus können Sie die einzelnen Einträge des Arrays vor dem Aufruf auf einfache Weise bearbeiten rsync.

In jeder POSIX-Shell kann man dazu die Liste der Positionsparameter verwenden:

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

rsync "$@" source/ target

Auch hier ist die doppelte Angabe der Erweiterung von von $@entscheidender Bedeutung.

Tangential verbunden:


Das Problem ist, dass die einfachen Anführungszeichen des --excludeOptionswerts Teil dieses Werts werden , wenn Sie die beiden Optionssätze in eine Zeichenfolge einfügen. Daher,

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

hätte funktioniert¹ ... aber es ist besser (wie in sicherer), ein Array oder die Positionsparameter mit einzeln zitierten Einträgen zu verwenden. Auf diese Weise können Sie bei Bedarf auch Dinge mit Leerzeichen verwenden und vermeiden, dass die Shell Dateinamen für die Optionen generiert (globbing).


¹ vorausgesetzt , dass $IFSnicht geändert wird und dass es keine Datei , deren Name beginnt mit --exclude=.im aktuellen Verzeichnis, und dass die nullgloboder failglobShell - Optionen sind nicht festgelegt.

Kusalananda
quelle
Die Verwendung eines Arrays funktioniert einwandfrei. Vielen Dank für Ihre ausführliche Antwort!
Florian Brucker
3

@Kusalananda hat bereits das Grundproblem und seine Lösung erläutert , und auch der von @glenn jackmann verlinkte Bash-FAQ-Eintrag enthält viele nützliche Informationen. Hier ist eine detaillierte Erklärung, was in meinem Problem passiert, basierend auf diesen Ressourcen.

Wir werden ein kleines Skript verwenden, das jedes seiner Argumente in einer separaten Zeile ausgibt, um die Dinge zu veranschaulichen ( argtest.bash):

#!/bin/bash

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

Übergabeoptionen "manuell":

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

Wie erwartet werden die Teile -rnvund --exclude='.*'in zwei Argumente aufgeteilt, da sie durch ein Leerzeichen ohne Anführungszeichen voneinander getrennt sind (dies wird als Wortteilung bezeichnet ).

Beachten Sie auch, dass die Anführungszeichen .*entfernt wurden: Die einzelnen Anführungszeichen weisen die Shell an, ihren Inhalt ohne spezielle Interpretation weiterzugeben , die Anführungszeichen selbst werden jedoch nicht an den Befehl weitergegeben .

Wenn wir jetzt die Optionen in einer Variablen als Zeichenfolge speichern (im Gegensatz zur Verwendung eines Arrays), werden die Anführungszeichen nicht entfernt :

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

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

Dies liegt an zwei Gründen: Die bei der Definition verwendeten doppelten Anführungszeichen $OPTSverhindern eine Sonderbehandlung der einfachen Anführungszeichen, sodass letztere Teil des Werts sind:

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

Wenn wir jetzt $OPTSals Argument für einen Befehl verwenden , werden die Anführungszeichen vor der Parametererweiterung verarbeitet , sodass die Anführungszeichen $OPTS"zu spät" auftreten.

Dies bedeutet, dass (in meinem ursprünglichen Problem) anstelle des Musters rsyncdas Ausschlussmuster '.*'(mit Anführungszeichen! ) Verwendet wird. .*Dateien, deren Name mit einem einfachen Anführungszeichen gefolgt von einem Punkt beginnt und mit einem einfachen Anführungszeichen endet, werden ausgeschlossen. Offensichtlich war das nicht beabsichtigt.

Eine Problemumgehung wäre gewesen, die doppelten Anführungszeichen bei der Definition wegzulassen $OPTS:

$ OPTS2=--exclude='.*'

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

Es ist jedoch empfehlenswert, Variablenzuweisungen immer anzugeben, da in komplexeren Fällen geringfügige Unterschiede bestehen.

Wie @Kusalananda feststellte, .*hätte auch ein Verzicht auf Zitate funktioniert. Ich hatte die Anführungszeichen hinzugefügt, um eine Mustererweiterung zu verhindern , aber das war in diesem speziellen Fall nicht unbedingt erforderlich :

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

Es stellt sich heraus, dass Bash zwar eine Mustererweiterung durchführt, das Muster --exclude=.*jedoch keiner Datei entspricht, sodass das Muster an den Befehl weitergeleitet wird. Vergleichen Sie:

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*

Es ist jedoch gefährlich, das Muster nicht in Anführungszeichen zu setzen, da das Muster erweitert wird, wenn (aus welchem ​​Grund auch immer) eine Datei gefunden wurde, die übereinstimmt --exclude=.*:

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

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

Lassen Sie uns abschließend sehen, warum die Verwendung eines Arrays mein Anführungszeichenproblem verhindert (zusätzlich zu den anderen Vorteilen der Verwendung von Arrays zum Speichern von Befehlsargumenten).

Bei der Definition des Arrays werden die Wörter wie erwartet aufgeteilt und die Anführungszeichen behandelt:

$ 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=.*

Bei der Übergabe der Optionen an den Befehl verwenden wir die Syntax "${ARRAY[@]}", die jedes Element des Arrays zu einem separaten Wort erweitert:

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*
Florian Brucker
quelle
Dieses Zeug hat mich lange verwirrt, daher ist eine detaillierte Erklärung wie diese hilfreich.
Joe
0

Wenn wir Funktionen und Shell-Skripten schreiben, in denen Argumente zur Verarbeitung übergeben werden, werden die Argumente in numerisch benannten Variablen übergeben, z. B. $ 1, $ 2, $ 3

Zum Beispiel :

bash my_script.sh Hello 42 World

Im Inneren my_script.shwerden Befehle verwendet $1, um auf Hello, $2to 42und $3for zu verweisenWorld

Die Variablenreferenz $0wird auf den Namen des aktuellen Skripts erweitert, zmy_script.sh

Spielen Sie nicht den gesamten Code mit Befehlen als Variablen ab.

Denken Sie daran :

1 Vermeiden Sie die Verwendung von Variablennamen mit Großbuchstaben in Skripten.

2 Verwenden Sie keine Anführungszeichen, verwenden Sie stattdessen $ (...), da dies die bessere Verschachtelung darstellt.

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"
Champion-Läufer
quelle