Кавычки в bash выглядят такой мелочью, что про них легко не думать вовсе. Обернул значение - и хорошо. Но именно эта мелочь стоит за доброй половиной странных багов в скриптах: то имя файла с пробелом разваливается на куски, то знак доллара внезапно подставляет чужое значение, то перенос строки ведёт себя совсем не так, как задумано. Разница между двумя видами кавычек не косметическая. Она определяет, проходит ли строка через машинерию подстановок оболочки или остаётся ровно тем набором символов, что был написан.

Чтобы перестать гадать, достаточно усвоить одно правило и несколько его следствий. Одинарные кавычки выключают любую обработку. Двойные оставляют часть обработки включённой. Всё остальное - подробности того, какая именно часть и где это аукнется.

Одинарные кавычки замораживают строку без единого исключения

Самый строгий способ закавычить текст - одинарные кавычки. Внутри них каждый символ сохраняет свой буквальный смысл. Никакой подстановки переменных, никакого выполнения команд, никакой особой роли у знака доллара, обратной кавычки или обратной косой черты. Что написано - то и получено, символ в символ.

имя="Борис"
echo 'Привет, $имя'      # Привет, $имя - подстановки нет
echo 'Он сказал "Привет"' # Он сказал "Привет" - двойные кавычки как текст

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

И тут притаилась единственная, но раздражающая особенность. Внутри одинарных кавычек невозможно поставить одинарную кавычку. Даже через обратную косую черту - она тоже воспринимается буквально и не экранирует. Строку приходится разрывать, выходить из кавычек, вставлять экранированную кавычку и заходить обратно.

echo 'Это конструкция don'\''t в действии'
# Это конструкция don't в действии

Выглядит как ребус, но логика проста: первая одинарная кавычка закрывает блок, \' вставляет литеральный апостроф, следующая одинарная кавычка открывает новый блок. Три части склеиваются в одну строку без пробелов между ними.

Двойные кавычки оставляют три канала подстановки открытыми

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

имя="Борис"
echo "Привет, $имя"           # Привет, Борис - переменная раскрылась
echo "Сегодня $(date +%A)"    # Сегодня и текущий день недели
echo "Сумма: $(( 2 + 2 ))"    # Сумма: 4

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

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

echo "Цена \$100"        # Цена $100 - доллар экранирован, слеш убран
echo "Путь C:\windows"   # Путь C:\windows - слеш сохранился как текст

Этот нюанс часто застаёт врасплох. Человек ставит обратную косую черту, ожидая, что она исчезнет, а она остаётся, потому что стоит перед символом, для которого экранирование не предусмотрено.

Восклицательный знак ломает интерактивный сеанс самым неожиданным образом

Особняком стоит история с восклицательным знаком. В интерактивном сеансе bash включено расширение истории команд, и восклицательный знак внутри двойных кавычек запускает поиск по истории - вовсе не то, чего ждёт автор строки.

echo "Привет!"
# в интерактивном сеансе может выдать: bash: !": event not found

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

Перенос строки внутри кавычек ведёт себя по-разному

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

Когда обратная косая черта стоит перед концом строки вне кавычек, пара слеш-перенос означает продолжение строки. Оболочка склеивает физические строки в одну логическую, а сам перенос и слеш исчезают. Это стандартный приём, чтобы разбить длинную команду на читаемые куски.

echo "первая часть" \
     "вторая часть"
# первая часть вторая часть

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

А вот настоящий перенос строки внутри кавычек - и одинарных, и двойных - сохраняется как есть. Многострочный текст между кавычками станет многострочным значением.

текст="строка один
строка два"
echo "$текст"
# строка один
# строка два - перенос сохранился

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

Когда нужен перенос строки именно как управляющий символ внутри короткой строки, на помощь приходит особый синтаксис ANSI-C - доллар перед одинарными кавычками. Внутри такой конструкции работают escape-последовательности вроде \n для переноса и \t для табуляции, чего обычные одинарные кавычки не умеют.

echo $'строка один\nстрока два'
# строка один
# строка два - \n превратился в реальный перенос

Кавычки исчезают до запуска команды, и это меняет всё

Есть концептуальная тонкость, без которой многие ошибки кажутся необъяснимыми. Кавычки в bash - это элемент синтаксиса, влияющий на то, как оболочка разбирает строку. Они не являются частью передаваемых команде данных. Оболочка убирает синтаксические кавычки до того, как вызовет команду.

Из этого следует коварное правило: кавычки, попавшие в переменную как часть её значения, не работают как синтаксические. Они остаются обычными символами текста.

АРГ='"мой составной аргумент"'
somecommand $АРГ
# передаст ТРИ слова, а кавычки внутри значения не спасут

Здесь кавычки внутри значения переменной - просто символы, и оболочка всё равно разобьёт значение по пробелам на три отдельных аргумента. Чтобы передать составной аргумент целиком, кавычить нужно саму подстановку переменной снаружи.

АРГ='мой составной аргумент'
somecommand "$АРГ"     # передаст ОДИН аргумент целиком

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

Практический свод, который стоит держать в голове

Картина упрощается до нескольких правил, которые покрывают почти все случаи. Двойные кавычки - выбор по умолчанию, когда внутри строки нужно подставить переменную или вывод команды, сохранив при этом пробелы. Одинарные кавычки - когда строка должна остаться буквальной до последнего символа: шаблоны, регулярные выражения, фрагменты кода. Синтаксис с долларом перед одинарными кавычками - когда нужны управляющие символы вроде переноса или табуляции в короткой строке.

Главная привычка, окупающаяся каждый день, - квотировать подстановки переменных двойными кавычками всегда, пока нет осознанной причины этого не делать. Голая $переменная без кавычек разваливается на пробелах и разворачивает шаблоны, а "$переменная" ведёт себя предсказуемо.

И последнее, что стоит унести: кавычки управляют разбором, а не передаются дальше как данные. Стоит запомнить, что синтаксические кавычки растворяются до вызова команды, и большинство загадочных случаев с разбиением аргументов перестают быть загадками. Оболочка обрабатывает строку слоями, и кавычки - это рычаг, которым выбирают, какие слои включить, а какие выключить.