@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 -rnv
i --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 $OPTS
zapobiegają specjalnemu traktowaniu pojedynczych cudzysłowów, więc te ostatnie są częścią wartości:
$ echo $OPTS
--exclude='.*'
Kiedy teraz używamy $OPTS
jako 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 rsync
uż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=.*