Używanie zmiennych powłoki dla opcji poleceń

19

W skrypcie Bash próbuję zapisać opcje, których używam, rsyncw osobnej zmiennej. Działa to dobrze w przypadku prostych opcji (np. --recursive), Ale mam problemy z --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)

Jak widać, przekazywanie --exclude='.*'do rsync„ręcznie” działa dobrze ( .barnie jest kopiowane), nie działa, gdy opcje są najpierw przechowywane w zmiennej.

Zgaduję, że jest to albo związane z cudzysłowami, albo ze znakiem wieloznacznym (albo z obydwoma), ale nie byłem w stanie dowiedzieć się, co dokładnie jest nie tak.

Florian Brucker
źródło

Odpowiedzi:

38

Ogólnie rzecz biorąc, złym pomysłem jest obniżenie listy oddzielnych elementów w jednym ciągu, bez względu na to, czy jest to lista opcji wiersza poleceń, czy lista ścieżek.

Zamiast tego użyj tablicy:

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

lub

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

i później...

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

W ten sposób cytowanie poszczególnych opcji jest zachowane (pod warunkiem dwukrotnego zacytowania rozwinięcia ${rsync_options[@]}). Umożliwia także łatwe manipulowanie poszczególnymi wpisami tablicy, gdybyś musiał to zrobić przed wywołaniem rsync.

W dowolnej powłoce POSIX można użyć do tego listy parametrów pozycyjnych:

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

rsync "$@" source/ target

Ponownie, podwójnie cytując rozszerzenie $@ ma tutaj kluczowe znaczenie.

Stycznie powiązane:


Problem polega na tym, że gdy umieścisz dwa zestawy opcji w ciągu, pojedyncze cudzysłowy wartości --excludeopcji stają się częścią tej wartości. W związku z tym,

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

zadziałałoby¹ ... ale lepiej (jak bezpieczniej) użyć tablicy lub parametrów pozycyjnych z indywidualnie cytowanymi wpisami. Takie postępowanie pozwoliłoby również na użycie elementów ze spacjami, jeśli zajdzie taka potrzeba, i pozwala uniknąć generowania przez powłokę nazw plików (globbing) w opcjach.


¹ pod warunkiem, że $IFSnie jest zmodyfikowany i że --exclude=.w bieżącym katalogu nie ma pliku, którego nazwa zaczyna się od, oraz że opcje nullgloblub failglobpowłoki nie są ustawione.

Kusalananda
źródło
Korzystanie z tablicy działa dobrze, dziękuję za szczegółową odpowiedź!
Florian Brucker
3

@Kusalananda wyjaśnił już podstawowy problem i jak go rozwiązać, a także wpis FAQ Bash do którego link @glenn jackmann, również zawiera wiele przydatnych informacji. Oto szczegółowe wyjaśnienie tego, co dzieje się w moim problemie na podstawie tych zasobów.

Użyjemy małego skryptu, który drukuje każdy z argumentów w osobnym wierszu, aby zilustrować różne rzeczy ( argtest.bash):

#!/bin/bash

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

Przekazywanie opcji „ręcznie”:

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

Zgodnie z oczekiwaniami części -rnvi --exclude='.*'są podzielone na dwa argumenty, ponieważ są one oddzielone niecytowanymi białymi spacjami (nazywa się to dzieleniem słów ).

Zauważ również, że cytaty wokół .*zostały usunięte: pojedyncze cytaty każą powłoce przekazać treść bez specjalnej interpretacji , ale same cytaty nie są przekazywane do polecenia .

Jeśli teraz przechowujemy opcje w zmiennej jako ciąg (w przeciwieństwie do używania tablicy), wówczas cudzysłowy nie są usuwane :

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

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

Wynika to z dwóch powodów: podwójne cudzysłowy użyte podczas definiowania $OPTSzapobiegają specjalnemu traktowaniu pojedynczych cudzysłowów, więc te ostatnie są częścią wartości:

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

Kiedy teraz używamy $OPTSjako argumentu dla polecenia, wówczas cudzysłowy są przetwarzane przed rozwinięciem parametru , więc cudzysłowy w$OPTS pojawiają się „za późno”.

Oznacza to, że (w moim pierwotnym problemie ) zamiast wzorca rsyncużywa wzorca wykluczenia '.*'(z cudzysłowami!).* - wyklucza pliki, których nazwa zaczyna się od pojedynczego cudzysłowu, po którym następuje kropka i kończy się pojedynczym cudzysłowiem. Oczywiście nie to było zamierzone.

Obejściem tego problemu byłoby pominięcie podwójnych cudzysłowów podczas definiowania $OPTS:

$ OPTS2=--exclude='.*'

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

Jednak dobrą praktyką jest zawsze cytowanie przypisań zmiennych ze względu na subtelne różnice w bardziej złożonych przypadkach.

Jak zauważył @Kusalananda, nie cytowanie .*też by zadziałało. Dodałem cudzysłowy, aby zapobiec rozszerzaniu wzorców , ale w tym szczególnym przypadku nie było to absolutnie konieczne :

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

Okazuje się, że Bash ma wykonać ekspansję wzoru, ale wzór --exclude=.*nie pasuje do żadnego pliku, więc wzór jest przekazywana do polecenia. Porównać:

$ touch some_file

$ ./argtest.bash some_*
some_file

$ ./argtest.bash does_not_exit_*
does_not_exit_*

Nie cytowanie wzorca jest jednak niebezpieczne, ponieważ jeśli (z jakiegokolwiek powodu) istnieje plik pasujący, --exclude=.*wzorzec zostanie rozwinięty:

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

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

Na koniec zobaczmy, dlaczego użycie tablicy zapobiega mojemu problemowi cytowania (oprócz innych zalet używania tablic do przechowywania argumentów poleceń).

Podczas definiowania tablicy podział słów i obsługa cytatów przebiega zgodnie z oczekiwaniami:

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

Podczas przekazywania opcji do polecenia używamy składni "${ARRAY[@]}", która rozwija każdy element tablicy w osobne słowo:

$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*
Florian Brucker
źródło
Te rzeczy długo mnie zdezorientowały, więc takie szczegółowe wyjaśnienie jest pomocne.
Joe
0

Kiedy piszemy funkcje i skrypty powłoki, w których przekazywane są argumenty do przetworzenia, argumenty będą przekazywane w zmiennych o nazwach numerycznych, np. 1 $, 2 $, 3 $

Na przykład :

bash my_script.sh Hello 42 World

Wewnątrz my_script.shpolecenia będą $1się odnosić do Witaj, $2do 42i $3dlaWorld

Odwołanie do zmiennej $0rozwinie się do nazwy bieżącego skryptu, npmy_script.sh

Nie odtwarzaj całego kodu za pomocą poleceń jako zmiennych.

Pamiętaj :

1 Unikaj używania w skryptach nazw zmiennych pisanych wielkimi literami.

2 Nie używaj cudzysłowów, zamiast tego używaj $ (...), lepiej zagnieżdża się.

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"
mistrz-biegacz
źródło