Bash: кавычки, переменные, разделение слов
Положил в переменную строку «отчёт за март» и передал её команде. Без кавычек команда получила ТРИ отдельных аргумента: «отчёт», «за», «март». В кавычках - один. Дальше хуже: положил в переменную звёздочку, а в каталоге лежали три файла - без кавычек команда получила четыре аргумента с именами этих файлов, потому что оболочка подставила их вместо звёздочки.
Стержень: развёрнутая без кавычек переменная сначала режется на слова по пробелам, а потом ещё раскрывается по маскам файлов - границы значения теряются.
// Формулировки: «зачем кавычки вокруг переменной?», «чем «$@» отличается от «$*»?», «как правильно читать файл построчно?»
Что оболочка делает со значением без кавычек
Порядок такой. Оболочка - программа, которая разбирает набранную команду и запускает её, - подставляет значение переменной, потом режет получившийся текст на слова по разделителям (по умолчанию это пробел, табуляция и перевод строки; их список лежит в переменной IFS), потом в каждом слове ищет маски файлов и раскрывает их. Кавычки отменяют оба последних шага.
Мои замеры буквальны. Значение «отчёт за март» без кавычек стало тремя аргументами, в кавычках - одним. Значение «*» без кавычек превратилось в список из четырёх имён, включая каталог с пробелами в названии; в кавычках осталось звёздочкой. И третий случай, самый вредный: пустая переменная в сравнении без кавычек вообще ломает синтаксис, оболочка говорит «unary operator expected», потому что от условия остаётся сравнение с пустым местом.
// Отсюда правило, которое не имеет исключений в скриптах: любое разворачивание переменной (то есть подстановку её значения в команду) берём в двойные кавычки. Одинарные кавычки не подойдут - внутри них переменная не подставляется вовсе и останется текстом со знаком доллара.
- разделение на слова
- разрезание подставленного текста по пробелам и переводам строк
- раскрытие масок
- замена звёздочки и вопросительного знака на подходящие имена файлов
- двойные кавычки
- подставляют переменную, но запрещают резать её на слова и раскрывать маски
Аргументы: собака, звёздочка и разница между ними
Внутри скрипта или функции есть два способа сослаться на все переданные аргументы, и они ведут себя по-разному. Проверил на трёх аргументах, у одного из которых пробел внутри: «второй с пробелом».
Запись «$@» в кавычках отдала ровно три аргумента, границы сохранились. Запись «$*» в кавычках отдала ОДИН аргумент - все три склеились в строку через пробел. Запись $@ без кавычек отдала ШЕСТЬ аргументов, потому что каждый прошёл разделение на слова, и «второй с пробелом» распался на три.
// Практический вывод простой: почти всегда нужен «$@» в кавычках - когда скрипт пробрасывает свои аргументы дальше, границы обязаны сохраниться. «$*» пригождается только там, где вы осознанно хотите одну строку, например для вывода в лог. А голый $@ не нужен никогда, и если он попался в чужом скрипте - это готовый баг, который сработает на первом же имени файла с пробелом.
- «$@»
- все аргументы по отдельности, границы сохраняются
- «$*»
- все аргументы, склеенные в одну строку
Чтение файла построчно
Классическая ошибка - обходить строки файла циклом по результату вывода файла. Проверил на файле из трёх строк, где во второй есть отступы, а в третьей обратный слэш. Такой цикл выдал ВОСЕМЬ элементов вместо трёх: он разрезал строки по пробелам, ровно как и положено разделению на слова.
Правильный способ - читать по одной строке специальной командой чтения, отключив разделение на слова и обработку обратного слэша. У меня это дало ровно три элемента: «первая строка», « вторая с отступом » с сохранёнными отступами и «третья\с обратным слэшем» с сохранённым символом.
// И ещё одна ловушка того же семейства. Конвейер - это цепочка команд, где вывод одной уходит на вход следующей. Если правую часть конвейера сделать циклом, который считает строки, то после конвейера счётчик останется нулевым: правая часть выполняется в отдельном процессе, и её переменные до основного скрипта не доезжают. Замерил: через конвейер результат 0, а через подстановку процесса (когда вывод команды подсовывается циклу как файл) - 3. Это причина доброй половины вопросов «почему переменная пустая после цикла».
- чтение по строке
- команда read с отключённым разделением слов и обработкой слэша
- отдельный процесс
- правая часть конвейера идёт в своей копии оболочки; её переменные не возвращаются
Как отвечать: «Зачем кавычки вокруг переменной, если значение и так подставится?»
Потому что подстановкой дело не заканчивается. После неё оболочка режет получившийся текст на слова по пробелам, а потом ищет в словах маски файлов и раскрывает их. Я это проверял: строка «отчёт за март» без кавычек приезжает в команду тремя отдельными аргументами, а переменная со звёздочкой превращается в список файлов текущего каталога. Плюс пустая переменная без кавычек ломает условие целиком, оболочка ругается на отсутствующий оператор. Поэтому правило без исключений: любое разворачивание в двойных кавычках. И там же рядом - «$@» в кавычках, когда пробрасываю аргументы дальше, иначе первое же имя файла с пробелом разъедет на два аргумента.
Ответ называет три шага обработки вместо отсылки к «так принято» и заканчивается практикой проброса аргументов. Это отличает человека, который ловил такой баг, от того, кто читал про кавычки.
На чём валятся
- −Разворачивают переменную без кавычек и получают разъехавшиеся аргументы на первом же пробеле.
- −Пишут $@ без кавычек при пробросе аргументов.
- −Обходят строки файла циклом по его выводу и режут строки по пробелам.
- −Считают что-то в правой части конвейера и удивляются нулевому счётчику после него.
- −Ставят одинарные кавычки там, где нужна подстановка, и получают текст со знаком доллара.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 8, остальные разбираются в тренажёре.
- Обёртка прокидывает свои аргументы дальше. Чем «$@» отличается от «$*» в кавычках?A)«$@» — по слову на аргумент, «$*» — одна строкаB)«$@» доступен в функции, «$*» — только на верхнем уровне скриптаC)«$@» сохраняет порядок аргументов, «$*» сортирует их по алфавитуD)Разницы нет: кавычки уравнивают обе формы
показать ответ и разбор
+A)«$@» — по слову на аргумент, «$*» — одна строка// разбор: В кавычках «$@» разворачивается в столько слов, сколько было аргументов, сохраняя границы: аргумент с пробелом останется одним. «$*» склеивает их через первый символ IFS в одну строку — вызванная команда получит один параметр вместо трёх. Потому в обёртках канон — exec real-cmd «$@», а «$*» годится разве что для лога.
- Флаги для команды копятся в переменной: OPTS="--filter 'name=web' --format json". При вызове аргументы разъезжаются. Как правильно?A)Обернуть вызов в eval — он соберёт строку обратноB)Экранировать пробелы обратными слэшами внутри строкиC)Развернуть переменную в кавычках: "$OPTS"D)Хранить флаги массивом и разворачивать как "${opts[@]}"
показать ответ и разбор
+D)Хранить флаги массивом и разворачивать как "${opts[@]}"// разбор: Строка не хранит границы аргументов: при развороте без кавычек bash режет её по IFS, не разбирая кавычки внутри, а в кавычках отдаёт одним куском. Массив хранит элементы отдельно, и "${opts[@]}" отдаёт ровно их. eval формально сработает, но исполнит содержимое строки как код — на данных из переменной это дыра.
- Строка echo '$HOME/logs' печатает буквально $HOME/logs, а не путь. Почему?A)Одинарные кавычки отключают подстановку переменнойB)Внутри кавычек переменную разворачивает только команда exportC)Переменная $HOME не задана в окружении этого скриптаD)Нужна форма ${HOME} — короткая в кавычках не работает
показать ответ и разбор
+A)Одинарные кавычки отключают подстановку переменной// разбор: Одинарные кавычки в bash сохраняют содержимое дословно: никакой подстановки переменных, команд и глоба. Двойные кавычки разворачивают $переменные и $(команды), но защищают от разбиения по пробелам. Отсюда правило: пути и значения — в двойных кавычках, дословный текст — в одинарных.
- Что делает конструкция "${DIR:?путь не задан}" в начале скрипта?A)Присваивает DIR значение по умолчанию «путь не задан»B)Проверяет, что каталог DIR действительно существует на дискеC)Печатает содержимое DIR и продолжает, даже если он пустD)Берёт DIR, а на пустом или незаданном — ошибка и выход
показать ответ и разбор
+D)Берёт DIR, а на пустом или незаданном — ошибка и выход// разбор: ${VAR:?word} — «обязательная» переменная: если пуста или не задана, шелл печатает VAR: word в stderr и завершает скрипт (в функции — возвращает управление). Рядом: ${VAR:-def} подставить значение, ${VAR:=def} подставить и присвоить, ${VAR:+alt} использовать alt, если VAR задана. Удобно проваливать скрипт на старте, а не на середине.
- Скрипт считает строки: 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.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.