Я только что назначил переменную, но echo $ variable показывает что-то еще


105

Вот серия случаев, когда echo $varможет отображаться значение, отличное от того, что было только что присвоено. Это происходит независимо от того, было ли присвоенное значение «двойными кавычками», «одинарными кавычками» или без кавычек.

Как мне заставить оболочку правильно установить мою переменную?

Звездочки

Ожидаемый результат /* Foobar is free software */, но вместо этого я получаю список имен файлов:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Квадратные скобки

Ожидаемое значение равно [a-z], но иногда вместо этого я получаю одну букву!

$ var=[a-z]
$ echo $var
c

Перевод строки (новые строки)

Ожидаемое значение - это список отдельных строк, но вместо этого все значения находятся в одной строке!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Несколько пространств

Я ожидал, что заголовок таблицы будет тщательно выровнен, но вместо этого несколько пробелов либо исчезнут, либо объединятся в один!

$ var="       title     |    count"
$ echo $var
title | count

Вкладки

Я ожидал двух значений, разделенных табуляцией, но вместо этого получаю два значения, разделенных пробелами!

$ var=$'key\tvalue'
$ echo $var
key value

3
Спасибо за это. Я часто сталкиваюсь с переводом строки. Так var=$(cat file)что нормально, но echo "$var"нужно.
сб

3
Кстати, это тоже BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Чарльз Даффи,


Кроме того , смотрите также stackoverflow.com/questions/2414150/...
tripleee

Ответы:


142

Во всех вышеперечисленных случаях переменная установлена ​​правильно, но неправильно прочитана! Правильный способ - использовать двойные кавычки при ссылке :

echo "$var"

Это дает ожидаемое значение во всех приведенных примерах. Всегда указывайте ссылки на переменные!


Зачем?

Когда переменная не заключена в кавычки , она:

  1. Пройдите разделение поля, где значение разбивается на несколько слов по пробелу (по умолчанию):

    Перед: /* Foobar is free software */

    После того, как : /*, Foobar, is, free, software,*/

  2. Каждое из этих слов подвергнется расширению имени пути , где шаблоны будут преобразованы в соответствующие файлы:

    Перед: /*

    После того, как : /bin, /boot, /dev, /etc, /home, ...

  3. Наконец, все аргументы передаются в echo, которое записывает их через одиночные пробелы , давая

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    вместо значения переменной.

Когда переменная цитируется, она:

  1. Подмените его ценность.
  2. Шага 2 нет.

Вот почему вы всегда должны заключать в кавычки все ссылки на переменные , если только вам не требуется специально разбиение слов и расширение имени пути. Такие инструменты, как shellcheck, могут помочь и предупредить об отсутствующих кавычках во всех вышеупомянутых случаях.


это не всегда работает. Могу привести пример: paste.ubuntu.com/p/8RjR6CS668
recolic

1
Ага, $(..)обрезает завершающие переводы строки. Вы можете использовать, var=$(cat file; printf x); var="${var%x}"чтобы обойти это.
тот другой парень

18

Возможно, вы захотите узнать, почему это происходит. Вместе с прекрасным объяснением того другого парня найдите ссылку на то, почему мой сценарий оболочки задыхается от пробелов или других специальных символов? написано Жилем в Unix и Linux :

Зачем мне писать "$foo" ? Что происходит без кавычек?

$fooне означает «взять значение переменной foo». Это означает нечто гораздо более сложное:

  • Сначала возьмите значение переменной.
  • Разделение полей: обработайте это значение как список полей, разделенных пробелами, и создайте результирующий список. Например, если переменная содержит foo * bar ​то результатом этого шага является список 3-элемент foo, *, bar.
  • Генерация имени файла: обрабатывайте каждое поле как глобус, т. Е. Как шаблон подстановки, и заменяйте его списком имен файлов, которые соответствуют этому шаблону. Если шаблон не соответствует ни одному файлу, он остается неизменным. В нашем примере это приводит список , содержащие foo, подчиняющийся список файлов в текущем каталоге, и , наконец bar. Если текущий каталог пуст, результат foo, *, bar.

Обратите внимание, что результатом является список строк. В синтаксисе оболочки есть два контекста: контекст списка и контекст строки. Разделение полей и генерация имени файла происходит только в контексте списка, но в большинстве случаев. Двойные кавычки ограничивают контекст строки: вся строка, заключенная в двойные кавычки, представляет собой одну строку, которую нельзя разделять. (Исключение: "$@"расширение до списка позиционных параметров, например "$@", эквивалентно "$1" "$2" "$3"наличию трех позиционных параметров. См. В чем разница между $ * и $ @? )

То же самое происходит с подстановкой команд с помощью $(foo)или с `foo`. Кстати, не используйте `foo`: его правила цитирования странные и непереносимые, а все современные оболочки поддерживают$(foo) что абсолютно эквивалентно, за исключением интуитивных правил цитирования.

Результат арифметической подстановки также подвергается таким же расширениям, но обычно это не вызывает беспокойства, поскольку он содержит только нерасширяемые символы (при условии, IFSчто не содержит цифр или -).

См. « Когда необходимы двойные кавычки?» подробнее о случаях, когда цитаты можно не указывать.

Если вы не имеете в виду, что вся эта чушь произошла, просто не забывайте всегда заключать в двойные кавычки замену переменных и команд. Будьте осторожны: отсутствие кавычек может привести не только к ошибкам, но и к дырам в безопасности .


7

В дополнение к другим вопросам , из- за невозможности процитировать, -nи -eможет потребляться в echoкачестве аргументов. (Только первое является законным в соответствии со спецификацией POSIX echo, но несколько распространенных реализаций нарушают спецификацию и также потребляют -e).

Чтобы этого избежать, используйте printfвместо, echoкогда важны детали.

Таким образом:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

Однако правильное цитирование не всегда спасает вас при использовании echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... в то время как он будет спасать вас printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n

Ура, для этого нужна хорошая дедупликация! Я согласен, что это соответствует названию вопроса, но не думаю, что здесь он получит ту заметность, которой заслуживает. Как насчет нового вопроса а-ля "Почему моя обратная косая черта -e/ -n/ не отображается?" Мы можем добавлять ссылки отсюда по мере необходимости.
тот другой парень

Вы тоже имели -nв виду потреблять ?
PesaThe

1
@PesaThe, нет, я имел в виду -e. Стандарт для echoне определяет вывод, когда его первый аргумент имеет значение -n, что делает любой / все возможные выходные данные допустимыми в этом случае; такого положения нет -e.
Чарльз Даффи

О ... я не умею читать. Будем винить в этом мой английский. Спасибо за объяснение.
PesaThe

6

двойные кавычки пользователя, чтобы получить точное значение. как это:

echo "${var}"

и он правильно прочитает ваше значение.


Работает .. Спасибо
Tshilidzi Mudau

2

echo $varвывод сильно зависит от значения IFSпеременной. По умолчанию он содержит символы пробела, табуляции и новой строки:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Это означает, что когда оболочка выполняет разделение полей (или разделение слов), она использует все эти символы в качестве разделителей слов. Вот что происходит при ссылке на переменную без двойных кавычек, чтобы повторить ее ($var ), и, таким образом, ожидаемый результат изменяется.

Один из способов предотвратить разделение слов (помимо использования двойных кавычек) - установить IFSзначение null. См. Http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Если значение IFS равно нулю, разделение полей выполняться не будет.

Установка на null означает установку на пустое значение:

IFS=

Тест:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 

2
Тебе также придется set -fпредотвратить появление глобусов
этот другой парень

@thatotherguy, действительно ли это необходимо для вашего 1-го примера с расширением пути? Если IFSустановлено значение null, echo $varбудет расширяться до, echo '/* Foobar is free software */'а расширение пути не выполняется внутри строк, заключенных в одинарные кавычки.
ks1322

1
Да. Если вы mkdir "/this thing called Foobar is free software etc/"увидите, что он все еще расширяется. Для [a-z]примера это очевидно более практично .
тот другой парень

Я понимаю, [a-z]например , это имеет смысл .
ks1322

2

Ответ от ks1322 помог мне определить проблему при использовании docker-compose exec:

Если опустить -Tфлаг, docker-compose execдобавить специальный символ, который прерывает вывод, мы видим bвместо 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

С -Tфлагом docker-compose execработает как положено:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b

-2

Помимо помещения переменной в кавычки, можно также перевести вывод переменной, используя tr и преобразовывая пробелы в символы новой строки.

$ echo $var | tr " " "\n"
foo
bar
baz

Хотя это немного более запутанно, это добавляет больше разнообразия в вывод, поскольку вы можете заменить любой символ в качестве разделителя между переменными массива.


2
Но это заменяет все пробелы на новые строки. Цитирование сохраняет существующие символы новой строки и пробелы.
user000001

Верно, да. Я полагаю, это зависит от того, что находится в переменной. На самом деле я использую trдругой способ создания массивов из текстовых файлов.
Алек

3
Создание проблемы из-за неправильного заключения переменной в кавычки и последующего обхода ее с помощью неуклюжего дополнительного процесса - плохое программирование.
Tripleee 01

@Alek, ... эээ, что? Нет trнеобходимости правильно / правильно создавать массив из текстового файла - вы можете указать любой разделитель, который хотите, установив IFS. Например: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')работает вплоть до bash 3.2 (самая старая широко распространенная версия) и правильно устанавливает статус выхода в false, если вы catпотерпели неудачу. И если вы хотите, скажем, вкладки вместо перевода строки, вы бы просто заменить $'\n'с $'\t'.
Чарльз Даффи

1
@Alek, ... если вы делаете что-то подобное arrayname=( $( cat file | tr '\n' ' ' ) ), то это разбито на нескольких уровнях: оно объединяет ваши результаты (так что a *превращается в список файлов в текущем каталоге), и он будет работать так же хорошо безtr ( или cat, если на то пошло; можно было просто использовать, arrayname=$( $(<file) )и он был бы сломан таким же образом, но менее неэффективно).
Чарльз Даффи
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.