@Kusalananda вже пояснив основну проблему та способи її вирішення, і запис Bash FAQ, пов’язаний з @glenn jackmann, також надає багато корисної інформації. Ось детальне пояснення того, що відбувається в моїй проблемі на основі цих ресурсів.
Ми будемо використовувати невеликий сценарій, який друкує кожен його аргумент в окремому рядку, щоб проілюструвати речі ( argtest.bash
):
#!/bin/bash
for var in "$@"
do
echo "$var"
done
Параметри передачі "вручну":
$ ./argtest.bash -rnv --exclude='.*'
-rnv
--exclude=.*
Як і очікувалося, частини -rnv
і --exclude='.*'
розбиваються на два аргументи, оскільки вони розділені процитованими пробілами (це називається розділенням слів ).
Також зауважте, що цитати навколо .*
були видалені: одиничні цитати сповіщають оболонку передавати їхній вміст без спеціальної інтерпретації , але самі лапки не передаються команді .
Якщо ми зараз зберігаємо параметри у змінній у вигляді рядка (на відміну від використання масиву), то лапки не видаляються :
$ OPTS="--exclude='.*'"
$ ./argtest.bash $OPTS
--exclude='.*'
Це пояснюється двома причинами: подвійні лапки, що використовуються при визначенні $OPTS
перешкоджають спеціальному трактуванню одиничних лапок, тому останні є частиною значення:
$ echo $OPTS
--exclude='.*'
Коли ми зараз використовуємо $OPTS
як аргумент для команди, то лапки обробляються перед розширенням параметра , тому цитати $OPTS
виникають "занадто пізно".
Це означає, що (в моїй початковій проблемі) rsync
використовується схема виключення'.*'
замість шаблону (з лапок!) .*
- він виключає файли, ім'я яких починається з одинарної цитати, а потім крапки і закінчується однією цитатою. Очевидно, що це не те, що було призначено.
Обхідним шляхом було б пропустити подвійні лапки при визначенні $OPTS
:
$ OPTS2=--exclude='.*'
$ ./argtest.bash $OPTS2
--exclude=.*
Однак це хороша практика завжди цитувати призначення змінних через тонкі відмінності в більш складних випадках.
Як зазначив @Kusalananda, не цитування .*
також спрацювало б. Я додав лапки, щоб запобігти розширенню шаблону , але це не було вкрай необхідним у цьому спеціальному випадку :
$ ./argtest.bash --exclude=.*
--exclude=.*
Виявляється, Bash робить розширення шаблону, але шаблон--exclude=.*
не відповідає будь-якому файлу, тому шаблон передається команді. Порівняйте:
$ touch some_file
$ ./argtest.bash some_*
some_file
$ ./argtest.bash does_not_exit_*
does_not_exit_*
Однак не цитувати шаблон небезпечно, тому що якщо (з будь-якої причини) відбулося збіг файлів --exclude=.*
то візерунок розширюється:
$ touch -- --exclude=.special-filenames-happen
$ ./argtest.bash --exclude=.*
--exclude=.special-filenames-happen
Нарешті, давайте розберемося, чому використання масиву запобігає моїй проблемі цитування (на додаток до інших переваг використання масивів для зберігання аргументів команди).
При визначенні масиву розбиття слів і обробка цитат відбувається так, як очікувалося:
$ 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=.*
При передачі параметрів команді ми використовуємо синтаксис "${ARRAY[@]}"
, який розширює кожен елемент масиву в окреме слово:
$ ./argtest.bash "${ARRAY_OPTS[@]}"
-rnv
--exclude=.*