сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Linux и bash

Bash: кавычки, переменные, разделение слов

Кавычки и разделение слов

Положил в переменную строку «отчёт за март» и передал её команде. Без кавычек команда получила ТРИ отдельных аргумента: «отчёт», «за», «март». В кавычках - один. Дальше хуже: положил в переменную звёздочку, а в каталоге лежали три файла - без кавычек команда получила четыре аргумента с именами этих файлов, потому что оболочка подставила их вместо звёздочки.

Стержень: развёрнутая без кавычек переменная сначала режется на слова по пробелам, а потом ещё раскрывается по маскам файлов - границы значения теряются.

// Формулировки: «зачем кавычки вокруг переменной?», «чем «$@» отличается от «$*»?», «как правильно читать файл построчно?»

Что оболочка делает со значением без кавычек

Порядок такой. Оболочка - программа, которая разбирает набранную команду и запускает её, - подставляет значение переменной, потом режет получившийся текст на слова по разделителям (по умолчанию это пробел, табуляция и перевод строки; их список лежит в переменной IFS), потом в каждом слове ищет маски файлов и раскрывает их. Кавычки отменяют оба последних шага.

Мои замеры буквальны. Значение «отчёт за март» без кавычек стало тремя аргументами, в кавычках - одним. Значение «*» без кавычек превратилось в список из четырёх имён, включая каталог с пробелами в названии; в кавычках осталось звёздочкой. И третий случай, самый вредный: пустая переменная в сравнении без кавычек вообще ломает синтаксис, оболочка говорит «unary operator expected», потому что от условия остаётся сравнение с пустым местом.

// Отсюда правило, которое не имеет исключений в скриптах: любое разворачивание переменной (то есть подстановку её значения в команду) берём в двойные кавычки. Одинарные кавычки не подойдут - внутри них переменная не подставляется вовсе и останется текстом со знаком доллара.

разделение на слова
разрезание подставленного текста по пробелам и переводам строк
раскрытие масок
замена звёздочки и вопросительного знака на подходящие имена файлов
двойные кавычки
подставляют переменную, но запрещают резать её на слова и раскрывать маски

Аргументы: собака, звёздочка и разница между ними

Внутри скрипта или функции есть два способа сослаться на все переданные аргументы, и они ведут себя по-разному. Проверил на трёх аргументах, у одного из которых пробел внутри: «второй с пробелом».

Запись «$@» в кавычках отдала ровно три аргумента, границы сохранились. Запись «$*» в кавычках отдала ОДИН аргумент - все три склеились в строку через пробел. Запись $@ без кавычек отдала ШЕСТЬ аргументов, потому что каждый прошёл разделение на слова, и «второй с пробелом» распался на три.

// Практический вывод простой: почти всегда нужен «$@» в кавычках - когда скрипт пробрасывает свои аргументы дальше, границы обязаны сохраниться. «$*» пригождается только там, где вы осознанно хотите одну строку, например для вывода в лог. А голый $@ не нужен никогда, и если он попался в чужом скрипте - это готовый баг, который сработает на первом же имени файла с пробелом.

«$@»
все аргументы по отдельности, границы сохраняются
«$*»
все аргументы, склеенные в одну строку

Чтение файла построчно

Классическая ошибка - обходить строки файла циклом по результату вывода файла. Проверил на файле из трёх строк, где во второй есть отступы, а в третьей обратный слэш. Такой цикл выдал ВОСЕМЬ элементов вместо трёх: он разрезал строки по пробелам, ровно как и положено разделению на слова.

Правильный способ - читать по одной строке специальной командой чтения, отключив разделение на слова и обработку обратного слэша. У меня это дало ровно три элемента: «первая строка», « вторая с отступом » с сохранёнными отступами и «третья\с обратным слэшем» с сохранённым символом.

// И ещё одна ловушка того же семейства. Конвейер - это цепочка команд, где вывод одной уходит на вход следующей. Если правую часть конвейера сделать циклом, который считает строки, то после конвейера счётчик останется нулевым: правая часть выполняется в отдельном процессе, и её переменные до основного скрипта не доезжают. Замерил: через конвейер результат 0, а через подстановку процесса (когда вывод команды подсовывается циклу как файл) - 3. Это причина доброй половины вопросов «почему переменная пустая после цикла».

чтение по строке
команда read с отключённым разделением слов и обработкой слэша
отдельный процесс
правая часть конвейера идёт в своей копии оболочки; её переменные не возвращаются

Как отвечать: «Зачем кавычки вокруг переменной, если значение и так подставится?»

Потому что подстановкой дело не заканчивается. После неё оболочка режет получившийся текст на слова по пробелам, а потом ищет в словах маски файлов и раскрывает их. Я это проверял: строка «отчёт за март» без кавычек приезжает в команду тремя отдельными аргументами, а переменная со звёздочкой превращается в список файлов текущего каталога. Плюс пустая переменная без кавычек ломает условие целиком, оболочка ругается на отсутствующий оператор. Поэтому правило без исключений: любое разворачивание в двойных кавычках. И там же рядом - «$@» в кавычках, когда пробрасываю аргументы дальше, иначе первое же имя файла с пробелом разъедет на два аргумента.

Ответ называет три шага обработки вместо отсылки к «так принято» и заканчивается практикой проброса аргументов. Это отличает человека, который ловил такой баг, от того, кто читал про кавычки.

На чём валятся

  • Разворачивают переменную без кавычек и получают разъехавшиеся аргументы на первом же пробеле.
  • Пишут $@ без кавычек при пробросе аргументов.
  • Обходят строки файла циклом по его выводу и режут строки по пробелам.
  • Считают что-то в правой части конвейера и удивляются нулевому счётчику после него.
  • Ставят одинарные кавычки там, где нужна подстановка, и получают текст со знаком доллара.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 8, остальные разбираются в тренажёре.

  1. #dvo_bash_basics1 / 5
    Обёртка прокидывает свои аргументы дальше. Чем «$@» отличается от «$*» в кавычках?
    A)«$@» — по слову на аргумент, «$*» — одна строка
    B)«$@» доступен в функции, «$*» — только на верхнем уровне скрипта
    C)«$@» сохраняет порядок аргументов, «$*» сортирует их по алфавиту
    D)Разницы нет: кавычки уравнивают обе формы
    показать ответ и разбор
    +A)«$@» — по слову на аргумент, «$*» — одна строка

    // разбор: В кавычках «$@» разворачивается в столько слов, сколько было аргументов, сохраняя границы: аргумент с пробелом останется одним. «$*» склеивает их через первый символ IFS в одну строку — вызванная команда получит один параметр вместо трёх. Потому в обёртках канон — exec real-cmd «$@», а «$*» годится разве что для лога.

  2. #dvo_bash_basics2 / 5
    Флаги для команды копятся в переменной: OPTS="--filter 'name=web' --format json". При вызове аргументы разъезжаются. Как правильно?
    A)Обернуть вызов в eval — он соберёт строку обратно
    B)Экранировать пробелы обратными слэшами внутри строки
    C)Развернуть переменную в кавычках: "$OPTS"
    D)Хранить флаги массивом и разворачивать как "${opts[@]}"
    показать ответ и разбор
    +D)Хранить флаги массивом и разворачивать как "${opts[@]}"

    // разбор: Строка не хранит границы аргументов: при развороте без кавычек bash режет её по IFS, не разбирая кавычки внутри, а в кавычках отдаёт одним куском. Массив хранит элементы отдельно, и "${opts[@]}" отдаёт ровно их. eval формально сработает, но исполнит содержимое строки как код — на данных из переменной это дыра.

  3. #dvo_bash_basics3 / 5
    Строка echo '$HOME/logs' печатает буквально $HOME/logs, а не путь. Почему?
    A)Одинарные кавычки отключают подстановку переменной
    B)Внутри кавычек переменную разворачивает только команда export
    C)Переменная $HOME не задана в окружении этого скрипта
    D)Нужна форма ${HOME} — короткая в кавычках не работает
    показать ответ и разбор
    +A)Одинарные кавычки отключают подстановку переменной

    // разбор: Одинарные кавычки в bash сохраняют содержимое дословно: никакой подстановки переменных, команд и глоба. Двойные кавычки разворачивают $переменные и $(команды), но защищают от разбиения по пробелам. Отсюда правило: пути и значения — в двойных кавычках, дословный текст — в одинарных.

  4. #dvo_bash_basics4 / 5
    Что делает конструкция "${DIR:?путь не задан}" в начале скрипта?
    A)Присваивает DIR значение по умолчанию «путь не задан»
    B)Проверяет, что каталог DIR действительно существует на диске
    C)Печатает содержимое DIR и продолжает, даже если он пуст
    D)Берёт DIR, а на пустом или незаданном — ошибка и выход
    показать ответ и разбор
    +D)Берёт DIR, а на пустом или незаданном — ошибка и выход

    // разбор: ${VAR:?word} — «обязательная» переменная: если пуста или не задана, шелл печатает VAR: word в stderr и завершает скрипт (в функции — возвращает управление). Рядом: ${VAR:-def} подставить значение, ${VAR:=def} подставить и присвоить, ${VAR:+alt} использовать alt, если VAR задана. Удобно проваливать скрипт на старте, а не на середине.

  5. #dvo_bash_basics5 / 5
    Скрипт считает строки: cat f | while read l; do ((n++)); done; echo "$n" — печатает 0, хотя строк много. Почему?
    A)read обнуляет переменную n на каждой итерации цикла
    B)Правая часть пайпа выполняется в субоболочке — её n снаружи не виден
    C)((n++)) под set -e рубит цикл ещё на первой строке
    D)cat отдаёт данные быстрее, чем while успевает их считать
    показать ответ и разбор
    +B)Правая часть пайпа выполняется в субоболочке — её n снаружи не виден

    // разбор: Каждое звено конвейера bash запускает в отдельной субоболочке. Тело while, стоящее справа от |, крутит свой n, но это копия в дочернем процессе — наружу она не возвращается, снаружи n остаётся 0. Лечится: while read l; do …; done < f (редирект вместо пайпа), process substitution < <(cmd), или shopt -s lastpipe.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.