Я просто призначив змінну, але змінна echo $ показує щось інше


104

Ось низка випадків, коли 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

2
Дякую за це. Я стикаюсь, що рядкові канали часто зустрічаються. Так var=$(cat file)це добре, але echo "$var"потрібно.
snd

3
До речі, це також BashPitfalls №14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Чарльз Даффі


Крім того , дивіться також stackoverflow.com/questions/2414150 / ...
tripleee

Відповіді:


139

У всіх вищезазначених випадках змінна правильно встановлена, але неправильно прочитана! Правильний спосіб - використовувати подвійні лапки при посиланні :

echo "$var"

Це дає очікуване значення у всіх наведених прикладах. Завжди цитуйте змінні посилання!


Чому?

Коли змінна не цитується , вона:

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

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

    Після того, як : /*, Foobar, is, free, software,*/

  2. Кожне з цих слів зазнає розширення імені шляху , де шаблони будуть розширені на відповідні файли:

    Перед: /*

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

  3. Нарешті, всі аргументи передаються наголос, що виписує їх розділеними єдиними пробілами , даючи

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

    замість значення змінної.

Коли ця змінна котирується, вона буде:

  1. Замініть його значення.
  2. Ні кроку 2 немає.

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


це не завжди працює. Я можу навести приклад: paste.ubuntu.com/p/8RjR6CS668
спогад

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

17

Можливо, ви захочете знати, чому це відбувається. Разом із чудовим поясненням цього іншого хлопця знайдіть посилання на те, чому мій скрипт оболонки задихається у пробілі чи інших спеціальних символах? написав Жиль в 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

Так, для цього нам потрібен гарний депутат! Я погоджуюсь, що це відповідає заголовку питання, але я не думаю, що він отримає видимість, яку вона заслуговує тут. Як щодо нового запитання à la "Чому мій -e/ -n/ косої риски не відображається?" Ми можемо додати посилання звідси, як це доречно.
той інший хлопець

Можливо , ви мали в виду споживати -nв колодязі ?
Песа

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

Ой, я не можу читати. Давайте звинувачуватимемо мою англійську в цьому. Дякую за пояснення.
Песа

6

подвійну пропозицію користувача, щоб отримати точне значення. подобається це:

echo "${var}"

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


Роботи .. Дякую
Цилідзі Мудау

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, чи справді це потрібно для вашого першого прикладу з розширенням шляху? Якщо IFSвстановлено значення zero, 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
Створення проблеми, якщо не цитувати змінну належним чином, а потім працювати над нею з додатковим процесом, не є хорошим програмуванням.
трійка

@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' ' ' ) ), то це розбито на кілька шарів: це глобалізує ваші результати (тому *перетворюється на список файлів у поточному каталозі), і воно буде працювати так само добре безtr ( або cat, з цього приводу, можна просто використовувати, arrayname=$( $(<file) )і це було б порушено однаково, але менш ефективно).
Чарльз Даффі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.