Використання змінних оболонок для параметрів команд


19

У сценарії Bash я намагаюся зберігати параметри, якими я користуюся, rsyncв окрему змінну. Це добре працює для простих варіантів (наприклад --recursive), але у мене виникають проблеми з --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)

Як бачите, перехід --exclude='.*'на rsync"вручну" працює нормально ( .barне копіюється), він не працює, коли параметри спочатку зберігаються у змінній.

Я здогадуюсь, що це пов’язано або з цитатами, або з підстановкою (або з обома), але я не зміг зрозуміти, що саме не так.



1
Або подивіться цю відповідь на нашому власному сайті.
Скотт

Відповіді:


38

Взагалі, погано занижувати список окремих елементів в один рядок, незалежно від того, чи це список параметрів командного рядка чи список імен шляхів.

Використання масиву замість цього:

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

або

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

і пізніше ...

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

Таким чином зберігається цитування окремих варіантів (доки ви подвійно цитуєте розширення ${rsync_options[@]}). Це також дозволяє легко маніпулювати окремими записами масиву, чи потрібно це робити, перш ніж дзвонити rsync.

У будь-якій оболонці POSIX для цього можна використовувати список позиційних параметрів:

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

rsync "$@" source/ target

Знову ж таки, подвійне цитування розширення $@тут є критичним.

Тангенціально пов'язані:


Проблема полягає в тому, що коли ви ставите два набори опції в рядок, одинарні лапки --exclude параметра стають частиною цього значення. Отже,

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

працював би¹ ... але краще (як у безпечнішому) використовувати масив або позиційні параметри з окремо цитованими записами. Це також дозволить вам використовувати речі з пробілами в них, якщо вам потрібно, і уникатиме того, щоб оболонка виконувала генерацію імен файлів (глобулювання) у налаштуваннях.


¹ за умови, що $IFSце не модифіковане, і немає файлу, ім'я якого починається з --exclude=.поточного каталогу, і що параметри nullglobабо failglobоболонки не встановлені.


Використання масиву працює чудово, дякую за детальну відповідь!
Флоріан Брюкер

3

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

Цей матеріал мене довго плутав, тому корисне детальне пояснення, як це.
Джо

0

Коли ми пишемо функції та сценарії оболонки, в яких аргументи передаються для обробки, аргументи передаватимуться через числові іменні змінні, наприклад, $ 1, $ 2, $ 3

Наприклад :

bash my_script.sh Hello 42 World

Всередині my_script.shкоманди використовуватимуться $1для позначення Hello, $2to 42і $3forWorld

Посилання на змінну $0, буде розширюватися на ім'я поточного сценарію, наприкладmy_script.sh

Не грайте весь код з командами у вигляді змінних.

Мати на увазі :

1 Уникайте використання імен змінних з усіма великими літерами у скриптах.

2 Не використовуйте зворотні котирування, скоріше використовуйте $ (...), вони краще гніздяться.

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"
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.