Чому параметри в змінній, що котирується, виходять з ладу, але працюють, коли вони не мають котирування?


18

Я читав про це, я повинен навести змінні в bash, наприклад "$ foo" замість $ foo. Однак під час написання сценарію я знайшов випадок, коли він працює без лапок, але не з ними:

wget_options='--mirror --no-host-directories'
local_root="$1" # ./testdir recieved from command line
remote_root="$2" # ftp://XXX recieved from command line 
relative_path="$3" # /XXX received from command line

Цей працює:

wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

Цього немає (зверніть увагу на подвійні лапки навколо $ wget_options):

wget "$wget_options" --directory_prefix="$local_root" "$remote_root$relative_path"
  • У чому причина цього?

  • Чи є перший рядок гарною версією; чи варто підозрювати, що десь є прихована помилка, яка спричиняє таку поведінку?

  • Загалом, де я можу знайти гарну документацію, щоб зрозуміти, як працює bash та його цитування? Під час написання цього сценарію я відчуваю, що почав працювати над базою проб і помилок замість розуміння правил.


3
На ваше запитання тут відповіли: mywiki.wooledge.org/BashFAQ/050
glenn jackman

3
Перейдіть до джерела для правил: посібник з bash . Зверніть пильну увагу на розділ 3.5 "Розширення оболонки", особливо розбиття слів та розширення імені файлів - ці два фактори - це те, що ви використовуєте лапки для контролю.
glenn jackman


4
Я думаю, це допомагає зрозуміти, як працюють аргументи командного рядка на низькому рівні. Коли програма виконується, вона отримує аргументи у вигляді списку списків символів (достатньо близько). Кожен внутрішній список - це те, що ми називаємо «аргументом». Більшість програм залежить від логічного поділу між аргами. Тут ви бачите, що wgetне знає, що --mirror --no-host-directoriesозначає (як один аргумент), але він обробляє це, коли він розділений на два аргументи. Дуже мало програм спеціально обробляє пробіли та цитати, коли вони знаходяться всередині аргументу. Проблема полягає в тому bash, що і інші снаряди мають на увазі бути>
HTNW

2
> використовується людиною. Буде прикро визначати межі між аргументами вручну, тому оболонки розбиваються на пробіл, щоб перетворити рядок (список символів) у вектор аргументу (список списків символів). Змінне розширення - це одне з перших розширень bash, тому ви можете уявити, що $aце рівнозначно безпосередньо написанню його вмісту. Тепер питання очевидно: a="-a -b"; cmd "$a"розширюється до cmd "-a -b", але, cmdймовірно, не знає, що це означає. cmd $aрозширюється до cmd -a -b, що, ймовірно , працює.
HTNW

Відповіді:


28

В основному, ви повинні подвоїти цитати змінних розширень, щоб захистити їх від розщеплення слів (і генерації імен файлів). Однак у вашому прикладі,

wget_options='--mirror --no-host-directories'
wget $wget_options --directory_prefix="$local_root" "$remote_root$relative_path"

розділення слів - це саме те, що ви хочете .

З "$wget_options"(цитується), wgetне знає, що робити з одним аргументом --mirror --no-host-directoriesі скаржиться

wget: unknown option -- mirror --no-host-directories

Для того, wgetщоб побачити два варіанти --mirrorі --no-host-directoriesяк окремий, має відбутися розщеплення слів.

Є більш надійні способи зробити це. Якщо ви використовуєте bashабо будь-яку іншу оболонку, яка використовує масиви, як bashdo, дивіться відповідь Глена Джекмана . Відповідь Жиля додатково описує альтернативне рішення для більш простих снарядів, таких як стандарт /bin/sh. Обидва фактично зберігають кожен варіант як окремий елемент у масиві.

Питання, пов’язане з хорошими відповідями: Чому мій скрипт оболонки задихається у пробілі чи інших спеціальних символах?


Подвійне цитування змінних розширень є хорошим правилом. Зробіть це . Тоді пам’ятайте про дуже небагато випадків, коли цього не слід робити. Вони представлять вас за допомогою діагностичних повідомлень, таких як вищезгадане повідомлення про помилку.

Також є кілька випадків, коли вам не потрібно цитувати розширення. Але все одно простіше продовжувати використовувати подвійні лапки, оскільки це не має великого значення. Один з таких випадків є

variable=$other_variable

Ще один -

case $variable in
    ...) ... ;;
esac

2
Перш ніж використовувати цей оператор split + glob, можливо, потрібно переконатися, що він $IFSмістить правильне значення. Тут вам потрібно розділити пробіл, і текст не містить жодної вкладки чи нового рядка, тому значення за замовчуванням $IFSбуде, але якщо цей код буде використовуватися у функції, яку можна викликати в контексті, де $IFSможна було змінити , ви хочете встановити його $IFSзаздалегідь (і, можливо, відновити його згодом або використовувати для нього локальну область, якщо решта коду передбачає немодифіковану $IFS)
Stéphane Chazelas

32

Найбільш надійний спосіб кодування - використання масиву:

wget_options=(
    --mirror 
    --no-host-directories
    --directory_prefix="$1"
)
wget "${wget_options[@]}" "$2/$3"

Це правильна відповідь. Довідка
l0b0

2
Це хороша відповідь, тому я підтримав це, але Кусаланда допоміг мені зрозуміти, чому мій код був неправильним, і я можу прийняти лише один.
z32a7ul

Я зіткнувся зі світом неприємностей, поки хтось із списку rsync не показав мені цю конструкцію. Особливо корисно, якщо деякі елементи можуть бути порожніми рядками. Це змушує порожні рядки зникати. Деякі команди люблять cpі rsyncбудуть робити несподівані речі, якщо ваша команда розширюється на щось подібне rsync '' rest of parameters. Це чудово підходить для умовного побудови командного фрагмента, а потім просто запуску його в одному місці.
Джо

17

Ви намагаєтесь зберегти список рядків у рядковій змінній. Це не підходить. Незалежно від того, як ви отримуєте доступ до змінної, щось порушено.

wget_options='--mirror --no-host-directories'встановлює змінну wget_optionsна рядок, який містить пробіл. На даний момент не можна дізнатися, чи повинен простір бути частиною опції чи роздільником між параметрами.

Коли ви отримуєте доступ до змінної з цитованою заміною wget "$wget_options", значення змінної використовується як рядок. Це означає, що він передається як єдиний параметр wget, тому це єдиний варіант. Це порушується у вашому випадку, оскільки ви задумали, що це означає декілька варіантів.

Коли ви використовуєте підстановку без котирування wget $wget_options, значення змінної рядка проходить процес розширення під назвою "split + glob":

  1. Візьміть значення змінної та розділіть її на частини, розділені пробілом (якщо ви не змінили $IFSзмінну). Це призводить до проміжного списку рядків.
  2. Для кожного елемента проміжного списку, якщо це шаблон підстановки, який відповідає одному або більше файлів, замініть цей елемент списком відповідних файлів.

Це трапляється у вашому прикладі, тому що процес розбиття перетворює простір у роздільник, але взагалі не працює, оскільки опція може містити пробіли та символи підстановки.

У ksh, bash, yash та zsh можна використовувати змінну масиву. Масив термінології оболонки - це список рядків, тому втрати інформації немає. Щоб зробити змінну масиву, поставте дужки навколо елементів масиву, призначаючи значення змінній. Для доступу до всіх елементів масиву використовуйте - це узагальнення , яке формує список з елементів масиву. Зауважте, що тут вам потрібні також подвійні лапки, інакше кожен елемент зазнає розділення + glob."${VARIABLE[@]}""$@"

wget_options=(--mirror --no-host-directories --user-agent="I can haz spaces")
wget "${wget_options[@]}" 

У звичайному sh немає змінних масивів. Якщо ви не проти втратити позиційні аргументи, ви можете використовувати їх для зберігання одного списку рядків.

set -- --mirror --no-host-directories --user-agent="I can haz spaces"
wget "$@" 

Для отримання додаткової інформації див. Чому мій скрипт оболонки задавлюється пробілом або іншими спеціальними символами?


Для простих ш подоболочкі збереже позиційні аргументи: (set -- ...; exec wget "$@" ...).
Джон Кугельман підтримує Моніку
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.