Почему параметры в кавычках не работают, но работают без кавычек?

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 и его цитирование? Во время написания этого сценария я чувствую, что начал работать методом проб и ошибок вместо понимания правил.

z32a7ul
источник
3
Ответ на ваш вопрос здесь: mywiki.wooledge.org/BashFAQ/050
Гленн Джекман
3
Перейти к источнику для правил: руководство по bash . Обратите особое внимание на раздел 3.5 «Расширения оболочки», особенно на разделение слов и расширение имени файла - эти два фактора используются для управления кавычками.
Гленн Джекман
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
Kusalananda
источник
2
Перед использованием этого оператора split + glob может потребоваться убедиться, что он $IFSсодержит правильное значение. Здесь вам нужно разделить пробел, и текст не будет содержать табуляции или новой строки, поэтому будет использоваться значение по умолчанию $IFS, но если этот код будет использоваться в функции, которая может быть вызвана в контексте, где ее $IFSможно было изменить , вы хотели бы установить $IFSзаранее (и, возможно, восстановить его позже или использовать локальную область видимости для него, если остальная часть кода предполагает неизмененный $IFS)
Стефан
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 вы можете использовать переменную массива. Массив в терминологии оболочки представляет собой список строк, поэтому нет потери информации. Чтобы создать переменную массива, поместите скобки вокруг элементов массива при назначении значения переменной. Чтобы получить доступ ко всем элементам массива, используйте - это обобщение , которое формирует список из элементов массива. Обратите внимание, что здесь вам также нужны двойные кавычки, в противном случае каждый элемент подвергается split + 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 "$@" ...).
Джон Кугельман поддерживает Монику