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

Поточная обработка строк и базовый синтаксис замен в Sed

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

Основная команда скрипта - замена s. Формат записи: s/шаблон/замена/флаги. Пример с конфигом OpenSSH:

sed 's/^#Port 22/Port 2200/' /etc/ssh/sshd_config

Закомментированная строка с портом превратится в активную, и весь конфиг с применённой заменой уйдёт на экран. Без флага g замена затронет только первое совпадение в строке, а флаг g указывает менять все вхождения:

sed 's/app-server-1/app-server-2/g' /etc/hosts

Несколько выражений объединяются ключом -e для каждого правила либо точкой с запятой внутри одного скрипта:

sed -e 's/^#Port .*/Port 2200/' -e 's/^#AddressFamily.*/AddressFamily inet/' /etc/ssh/sshd_config

Адресация по номерам строк и шаблонам для точечных правок

Команда может предваряться адресом: он указывает, к каким строкам применять правило. Адрес задаётся номером, диапазон - парой чисел, символ $ обозначает последнюю строку.

sed '5s/yes/no/' settings.conf
sed '10,20s/oldvalue/newvalue/g' settings.conf

Адресация по содержимому пишется шаблоном в слэшах, и правило сработает только в строках с совпадением:

sed '/^PermitRootLogin/s/yes/no/' /etc/ssh/sshd_config
sed '/^\[server\]/,/^$/s/enable="no"/enable="yes"/' app.conf

Вторая команда задаёт диапазон от шаблона до шаблона: замена идёт от строки секции server до первой пустой строки, внутри блока. Инверсия через восклицательный знак отсекает лишнее: конструкция '/^#/!s/old/new/' меняет все строки, кроме начинающихся с решётки.

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

Ключ -i с суффиксом резервной копии и порядок записи изменений на диск

Ключ -i с суффиксом превращает потоковую правку в запись на диск. Суффикс приклеивается к ключу напрямую и становится расширением резервной копии:

sed -i.bak 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config

После выполнения рядом с файлом окажется копия sshd_config.bak, а сам файл получит изменённое содержимое. У копии время изменения останется прежним: переименование не трогает метаданные времени. Суффикс может быть любым: .orig, .old, .copy. В GNU Sed допустим и голый -i без суффикса, но тогда копии не будет вовсе, и для живого сервера это худший из вариантов.

Наивная попытка записать вывод обратно в источник ломается сразу:

sed 's/oldvalue/newvalue/g' settings.conf > settings.conf

Оболочка усекает файл в момент открытия, до того как Sed успеет его прочитать, и на выходе получается пустой файл. Под капотом у GNU Sed порядок такой: создаётся временный файл в том же каталоге, в него записывается обработанный текст, исходный файл переименовывается в имя с суффиксом, затем временный файл получает исходное имя. По прежнему пути лежит новый файл, рядом осталась копия до правки. В вариантах утилиты из мира BSD ключ -i требует аргумент суффикса даже пустого, поэтому запись вида -i.bak переносима между семействами, а голый -i - нет.

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

Побочные эффекты замены через новый файл и переименование

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

Первое - метаданные. Новый файл получает новый индексный дескриптор, и права доступа с отдельными атрибутами могут не пережить замену: утилита пытается их сохранить, но гарантий нет, а владелец с группой нередко сменяются на того, кто запустил команду, особенно при правке от имени root. Списки контроля доступа, контексты SELinux и прочие расширенные атрибуты на новый файл автоматически не переезжают, поэтому в системах с SELinux после серии правок полезно прогнать restorecon. Файл с неизменяемым флагом через chattr +i не даст себя переименовать, и утилита честно сообщит об ошибке.

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

Третье - символические ссылки. Поведение зависит от варианта утилиты: в одних случаях ссылка превращается в обычный файл, в других правка следует по ссылке к цели. В GNU Sed для второго поведения есть ключ --follow-symlinks, в остальных случаях результат проверяют на копии.

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

Дисциплина правки конфига с проверкой результата и сверкой различий

Хорошая привычка для правки живого конфигурационного файла складывается из четырёх шагов:

  1. прогон замены без ключа на месте и просмотр результата на экране;
  2. проверка, что под шаблон попали только нужные строки и ни одной лишней;
  3. запись изменений ключом -i.bak с созданием резервной копии;
  4. сверка различий между новым файлом и копией плюс проверка синтаксиса штатной командой службы.

Просмотр делается той же командой без ключа записи:

sed 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config | less

Затем запись и сверка различий:

sed -i.bak 's/^#PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config
diff -u /etc/ssh/sshd_config.bak /etc/ssh/sshd_config

diff -u покажет контекст: убранные строки помечаются знаком минус, добавленные - знаком плюс. Если различий нет, а правка обязана была сработать, шаблон не совпал, и причина обычно в экранировании или лишнем пробеле. Дальше идёт проверка синтаксиса: у sshd это ключ -t, у nginx тоже -t, файл sudoers проверяется visudo -c, и ошибка шаблона поймается до перезапуска службы.

Откат сводится к одному движению:

mv /etc/ssh/sshd_config.bak /etc/ssh/sshd_config

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

Экранирование спецсимволов и выбор разделителя для читаемых команд

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

sed 's/192\.168\.1\.10/192.168.1.20/' hosts.conf

В левой части точки экранированы, иначе шаблон совпал бы с 192x168y1z10. В правой части экранировать не нужно: заменяющий текст регулярным выражением не является. Амперсанд в правой части раскрывается в найденный текст, поэтому литеральный амперсанд записывается как \&.

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

sed 's#/var/log/app#/var/log/service#g' app.conf

Читаемость растёт, а вероятность ошибки падает. Годится почти любой символ, которого нет в шаблонах: вертикальная линия, решётка, двоеточие.

По умолчанию утилита работает с базовыми регулярными выражениями, где +, ? и | обычные, а специальное значение получают с обратным слэшем. Ключ -E включает расширенный режим, где всё наоборот. Группы запоминают части совпадения, ссылки на них в замене пишутся как \1, \2:

sed -E 's/^([a-z]+)-([a-z]+)$/\2-\1/' pairs.txt

Для переносимости между вариантами утилиты надёжнее классы вроде [[:space:]] и [[:digit:]], а границы слов через \b есть не во всех вариантах.

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

NEWPORT=2200
sed -i.bak "s/^Port .*/Port $NEWPORT/" /etc/ssh/sshd_config

Значение переменной попадает в шаблон как есть.

И последнее - осторожность с флагом g. Глобальная замена проходит по каждой строке, и короткий шаблон легко совпадает в комментариях, похожих параметрах и путях. Перед заменой полезно посмотреть совпадения: grep -c выведет количество строк, grep -n покажет их с номерами. Расхождение с ожиданием - сигнал сузить шаблон или добавить адрес.

Практические сценарии от точечной правки до обработки списка файлов

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

#!/bin/sh
set -e
CONF=/etc/ssh/sshd_config
if grep -q '^PermitRootLogin no' "$CONF"; then
  echo "параметр уже установлен"
  exit 0
fi
sed -i.bak 's/^#*PermitRootLogin.*/PermitRootLogin no/' "$CONF"
sshd -t

Шаблон с #* совпадает и с закомментированной строкой, и с активной, а замена всей строки даёт одинаковый результат. Проверка синтаксиса в конце ловит огрехи шаблона, а set -e останавливает скрипт на первой неудаче.

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

grep -rl 'app-server-1' /etc/app
sed -i.bak 's/app-server-1/app-server-2/g' /etc/app/*.conf

Ключ -l у grep перечисляет имена файлов. Каждому файлу Sed создаст собственную копию с суффиксом, откат возможен по каждому в отдельности.

Третий сценарий - правка по списку файлов из обхода каталога. find подбирает файлы по имени, xargs собирает их в вызовы утилиты, а пара -print0 и -0 страхует от имён с пробелами:

find /etc/app -type f -name "*.conf" -print0 | xargs -0 sed -i.bak 's/oldvalue/newvalue/g'

После проверки конфигураций копии убираются одной командой:

find /etc/app -type f -name "*.bak" -delete

а откат всей серии выглядит так:

for f in /etc/app/*.conf.bak; do mv -f "$f" "${f%.bak}"; done

Цикл перебирает копии и возвращает каждой её исходное имя. Перед серией на живом каталоге полезна репетиция: cp -a скопирует его вместе с режимами доступа, владельцем и жёсткими ссылками, и по копии видно и объём изменений, и поведение команды в окружении сервера. Час репетиции дешевле ночи разбирательств.

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