Почему я не могу заключить в двойные кавычки переменную с несколькими параметрами?

Я пишу сценарий bash, который использует rsync для синхронизации каталогов. Согласно руководству по стилю оболочки Google:

  • Всегда заключайте в кавычки строки, содержащие переменные, подстановки команд, пробелы или метасимволы оболочки, если только не требуется осторожное расширение без кавычек.
  • Используйте "$@", если у вас нет особой причины использовать $*.

Я написал следующий тестовый сценарий:

#!/bin/bash

__test1(){
  echo stdbuf -i0 -o0 -e0 $@
  stdbuf -i0 -o0 -e0 $@
}

__test2(){
  echo stdbuf -i0 -o0 -e0 "$@"
  stdbuf -i0 -o0 -e0 "$@"
}


PARAM+=" --dry-run "
PARAM+=" mirror.leaseweb.net::archlinux/"
PARAM+=" /tmp/test"


echo "test A: ok"
__test1 nice -n 19 rsync $PARAM 

echo "test B: ok"
__test2 nice -n 19 rsync $PARAM

echo "test C: ok"
__test1 nice -n 19 rsync "$PARAM"

echo "test D: fails"
__test2 nice -n 19 rsync "$PARAM"

(Мне нужно stdbuf, чтобы немедленно наблюдать за выводом в моем более длинном сценарии, который я запускаю)

Итак, мой вопрос: почему тест D не работает с сообщением ниже?

rsync: getaddrinfo:  --dry-run  mirror.leaseweb.net 873: Name or service not known

echo в каждом тесте выглядит одинаково. Если я должен указать все переменные, почему в этом конкретном сценарии это не удается?


person C34654    schedule 10.04.2017    source источник
comment
Для справки: имена переменных, написанные заглавными буквами, указаны POSIX для переменных. со значением для ОС или системы, тогда как имена с символами нижнего регистра зарезервированы для использования приложением. Используя имена в нижнем регистре, вы избежите перезаписи переменных со значением для системы по ошибке (имейте в виду, что установка обычной переменной оболочки автоматически перезапишет любую переменную среды с таким же именем).   -  person Charles Duffy    schedule 10.04.2017
comment
BashFAQ #50 (я пытаюсь поместить команду в переменную, но сложные случаи всегда терпит неудачу!) тоже прямо в точку; он показывает некоторые случаи, когда версия без кавычек не будет работать (и вам должно использоваться либо массив, либо функция, либо другая альтернатива).   -  person Charles Duffy    schedule 10.04.2017
comment
Чарльз Даффи, спасибо. Я переименовал все переменные в нижний регистр!   -  person C34654    schedule 10.04.2017


Ответы (2)


Я согласен с @Fred, лучше всего использовать массивы. Вот небольшое объяснение и несколько советов по отладке.

Перед запуском тестов я добавил

echo "$PARAM"
set|grep '^PARAM='

чтобы на самом деле показать, что такое PARAM.** В вашем исходном тесте это:

PARAM=' --dry-run  mirror.leaseweb.net::archlinux/ /tmp/test'

То есть это одна строка, содержащая несколько частей, разделенных пробелами.

Как правило (с исключениями!*), bash разделяет слова, если вы не запретите это делать. В тестах A и C $@ без кавычек в __test1 дает bash возможность разделить $PARAM. В тесте B $PARAM без кавычек в вызове __test2has the same effect. Therefore,rsync` видит каждый элемент, разделенный пробелом, как отдельный параметр в тестах A-C.

В тесте D "$PARAM", переданный __test2, не разделяется при вызове __test2 из-за кавычек. Поэтому __test2 видит только один параметр в $@. Затем внутри __test2 указанный в кавычках "$@" сохраняет этот параметр вместе, поэтому он не разбивается на пробелы. В результате rsync думает, что все PARAM является именем хоста, поэтому терпит неудачу.

Если вы используете решение Фреда, вывод из sed|grep '^PARAM=' будет

PARAM=([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")

Это внутренняя нотация bash для массива: PARAM[0] равно "--dry-run" и т. д. Вы можете видеть каждое слово по отдельности. echo $PARAM не очень полезно для массива, так как выводит только первое слово (здесь --dry-run).

Правки

* Как указывает Фред, одно исключение состоит в том, что в назначении A=$B B не будет расширено. То есть A=$B и A="$B" совпадают.

** Как указывает Готи, вместо set|grep '^PARAM=' можно использовать declare -p PARAM. встроенная функция объявления с переключателем -p распечатает строку, которую вы можно вставить обратно в оболочку, чтобы воссоздать переменную. В этом случае этот вывод:

declare -a PARAM='([0]="--dry-run" [1]="mirror.leaseweb.net::archlinux/" [2]="/tmp/test")'

Это хороший вариант. Я лично предпочитаю подход set|grep, потому что declare -p дает вам дополнительный уровень цитирования, но оба варианта работают нормально. Изменить Как указывает @rici, используйте declare -p, если элемент вашего массива может включать новую строку.

В качестве примера дополнительных кавычек рассмотрим unset PARAM ; declare -a PARAM ; PARAM+=("Jim's") (новый массив с одним элементом). Тогда вы получите:

set|grep:   PARAM=([0]="Jim's")
      # just an apostrophe ^
declare -p: declare -a PARAM='([0]="Jim'\''s")'
      #    a bit uglier, in my opinion ^^^^ 
person cxw    schedule 10.04.2017
comment
@C34654 Рад это слышать и добро пожаловать на сайт! Пожалуйста, проголосуйте за ответы, которые вам полезны, и примите ответ, который, по вашему мнению, лучше всего решил вашу проблему, чтобы система (и будущие читатели) знали, что на ваш вопрос дан ответ. Удачного взлома! - person cxw; 10.04.2017
comment
Краткое примечание: вы пишете Как правило (с исключениями!), bash будет разделять слова, если вы не скажете ему не делать этого. Одно большое исключение - это присваивание переменной. Например, для Bash A=$B работает так же хорошо, как A="$B", даже если переменная B содержит пробелы. Точно так же A=$B$C не требует кавычек. Но если вы хотите добавить пробел между ними, вам понадобятся кавычки (например, A=$B $C). То же самое относится и к другим типам расширений, используемых в заданиях. Но в команде чаще всего нужны двойные кавычки. - person Fred; 10.04.2017
comment
@cxw, отличный ответ и объяснение отладки. Я добавлю одну вещь: вы можете получить лучшее представление о том, что находится внутри переменной, используя declare -p PARAM вместо строки set|grep. - person ghoti; 10.04.2017
comment
@cxw: если вы собираетесь использовать set|grep, включите =, чтобы не показывать PARAM1 и друзей. Но declare -p лучше, имхо, так как он не зависит от того, является ли значение одной строкой. (Попробуйте массив, содержащий значение, содержащее новую строку.) - person rici; 10.04.2017

Это не удается, потому что "$PARAM" расширяется как одна строка, и не выполняется разбиение слов, хотя оно содержит то, что должно интерпретироваться командой как несколько аргументов.

Один очень полезный метод — использовать массив вместо строки. Постройте массив следующим образом:

declare -a PARAM
PARAM+=(--dry-run)
PARAM+=(mirror.leaseweb.net::archlinux/)
PARAM+=(/tmp/test)

Затем используйте расширение массива для выполнения вашего вызова:

__test2 nice -n 19 rsync "${PARAM[@]}"

Расширение "${PARAM[@]}" имеет то же свойство, что и расширение "$@": оно расширяется до списка элементов (одно слово на элемент в списке массивов/аргументов), не происходит разделения слов, как если бы каждый элемент был заключен в кавычки.

person Fred    schedule 10.04.2017
comment
в этом случае не должен ли тест B также провалиться? - person C34654; 10.04.2017
comment
Нет, потому что в тесте B $PARAM не заключено в кавычки, а разбиение на слова происходит до вызова тестовой функции. - person Fred; 10.04.2017
comment
Большое спасибо за объяснение - person C34654; 10.04.2017
comment
Обратите внимание, что использование разбиения слов на раскрытие переменной без кавычек требует, чтобы вы были на 100 % уверены, что внутри нет лишних пробелов, из-за которых то, что следует понимать как один аргумент, на самом деле будет разделено на два. Если такое может случиться, то нужно ставить внутри кавычки, а то получается некрасиво. Вы избегаете всего этого с помощью решения на основе массива выше. - person Fred; 10.04.2017