Что такое sed
sed (stream editor) — потоковый редактор: читает input построчно, применяет команды редактирования, выводит результат. Не открывает файл целиком (как vim), не требует pattern + action как awk — просто transformation pipeline для текста.
Создан в 1973 году Lee McMahon в Bell Labs как inline-обёртка над ed. Основная сила — substitution (s command) с regex.
$ echo "hello world" | sed 's/world/sed/'
hello sed
$ echo "user=alice id=42" | sed 's/alice/bob/'
user=bob id=42
Базовая команда s/PATTERN/REPLACEMENT/FLAGS — старейший и самый используемый паттерн в sed.
Базовый s-command
sed 's/OLD/NEW/' file # заменить первое вхождение OLD на NEW в каждой строке
sed 's/OLD/NEW/g' file # заменить все вхождения (g = global)
sed 's/OLD/NEW/2' file # заменить только 2-е вхождение в каждой строке
sed 's/OLD/NEW/I' file # case-insensitive
sed 's/OLD/NEW/gi' file # global + case-insensitive
Без g sed заменяет только первое вхождение OLD в каждой строке. Это часто удивляет: для replace-all всегда нужен g.
$ echo "foo bar foo bar foo" | sed 's/foo/XXX/'
XXX bar foo bar foo # только первое
$ echo "foo bar foo bar foo" | sed 's/foo/XXX/g'
XXX bar XXX bar XXX # все
Флаги после третьего слэша
После /NEW/ может идти комбинация модификаторов.
Альтернативные разделители
Стандартный разделитель s/.../.../ — это /. Но если pattern или replacement содержат /, экранирование становится adского:
# [X] Сложно читать:
$ sed 's/\/usr\/local\/bin/\/usr\/bin/'
# [x] Любой printable-символ может быть разделителем:
$ sed 's|/usr/local/bin|/usr/bin|'
$ sed 's#/usr/local/bin#/usr/bin#'
$ sed 's@/usr/local/bin@/usr/bin@'
Общая идиома — | или # для путей с /. Идея sed: после s следует любой символ, который дальше используется как разделитель — никаких других требований.
$ echo "PATH=/usr/local/bin" | sed 's|/usr/local|/opt|'
PATH=/opt/bin
Captured groups: \1, \2, …
В BRE (basic regex) sed используются escaped группы \(...\) и back-references \1, \2:
$ echo "alice 42" | sed 's/\([a-z]*\) \([0-9]*\)/\2 \1/'
42 alice
\(...\) захватывают groups, \1 подставляет первую group в replacement. С -E (extended regex) — обычные (...) и \1:
$ echo "alice 42" | sed -E 's/([a-z]+) ([0-9]+)/\2 \1/'
42 alice
& — весь match
$ echo "amount: 150" | sed 's/[0-9]\+/<\&>/'
amount: <150>
& в replacement = вся совпавшая часть. Удобно для wrapping: добавить prefix/suffix к каждому совпадению.
# Обернуть все email в скобки
$ echo "Contact [email protected] or [email protected]" \
| sed -E 's/[a-z]+@[a-z.]+/<&>/g'
Contact <[email protected]m> or <[email protected]m>
In-place edit: sed -i (CRITICAL difference GNU vs BSD)
sed -i модифицирует файл на месте. Это огромное удобство, но синтаксис различается на Linux vs macOS:
# GNU sed (Linux):
$ sed -i 's/foo/bar/g' file
# BSD sed (macOS):
$ sed -i '' 's/foo/bar/g' file
# ^^^^ ОБЯЗАТЕЛЬНЫЙ пустой аргумент (suffix for backup)
На BSD -i требует аргумент: что использовать как suffix для backup. '' (пустая строка) — «не делать backup». Если опустить — sed создаст fileE (где E — первый символ команды).
Кросс-платформенный pattern:
# Backup-safe (создаёт file.bak)
$ sed -i.bak 's/foo/bar/g' file
# Работает на обеих платформах!
Это формально правильный POSIX: -i.bak создаёт file.bak с оригиналом перед редактированием.
В скриптах для CI/Linux-only можно sed -i ... без аргумента, но в shared dotfiles и portable-скриптах используй -i.bak или detect platform:
# detect platform:
if [[ "$(uname)" == "Darwin" ]]; then
SED_INPLACE=(sed -i '')
else
SED_INPLACE=(sed -i)
fi
"${SED_INPLACE[@]}" 's/foo/bar/g' file
-i под капотом
sed -i не пишет в файл «на лету» — это было бы небезопасно при сбое:
- Создаёт новый временный файл в той же директории
- Читает input, пишет модифицированный output в temp
- После завершения делает atomic
rename()temp на target
Это значит: (1) права/owner файла могут измениться (sed создаёт temp от своего UID); (2) hardlinks теряются — старый inode остаётся, новый файл с тем же именем — другой inode. Если кто-то держит open() FD к старому файлу — он не увидит изменений.
$ ls -li file
12345 -rw-r--r-- 1 levo levo file
$ sed -i 's/x/y/' file
$ ls -li file
67890 -rw-r--r-- 1 levo levo file
# inode сменился!
DE-сценарии sed substitution
1. Замена пути конфигурации
# Поменять URL базы данных в конфигах:
$ sed -i 's|postgresql://old-db|postgresql://new-db|g' /etc/airflow/*.cfg
2. Удалить trailing whitespace
$ sed -i 's/[[:space:]]*$//' file.py
# Удаляет все trailing whitespace в конце строк
# Полезно перед git commit
pre-commit хуки — автоматическая очистка перед git commit
3. Анонимизация данных
# Заменить все email на <REDACTED>
$ sed -E 's/[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+/<REDACTED>/g' users.csv
4. Apply patches к множеству файлов
# Поменять deprecated parameter во всех Python:
$ find . -name '*.py' -exec sed -i 's/use_legacy_sql=True/use_legacy_sql=False/g' {} +
5. Удаление BOM (Byte Order Mark) из UTF-8
# BOM = три байта \xef\xbb\xbf в начале файла, иногда добавляется Windows-редакторами
$ sed -i '1s/^\xef\xbb\xbf//' file.csv
1s/.../.../ — apply substitution только к первой строке.
sed vs perl -pe
Для сложных substitutions perl часто удобнее:
$ perl -pe 's/foo/bar/g' file # эквивалент sed 's/foo/bar/g'
$ perl -i -pe 's/foo/bar/g' file # in-place
Perl всегда использует PCRE (\d, \w, \s, lookahead/lookbehind), не зависит от GNU/BSD. На macOS perl -i -pe идентичен Linux — нет cursed-разницы -i '' vs -i.
Многие DE-команды переходят на perl -pe для cross-platform pipelines:
# Удалить лишние пробелы в начале/конце:
$ perl -pe 's/^\s+|\s+$//g' file
Sed с stdin
$ cat data.csv | sed 's/,/|/g'
# Изменить delimiter CSV на pipe
$ curl -s api.example.com/users | sed 's/"//g'
# Убрать все кавычки из JSON (грубо!)
ВНИМАНИЕ: Для JSON используй jq, не sed. Для XML/HTML — не используй sed («cthulhu fhtagn»). Sed хорош для построчных текстовых трансформаций.
Попробуй сам
- Простая замена:
echo "hello world" | sed 's/hello/hi/' - Global vs single:
echo "aaa bbb aaa" | sed 's/aaa/X/' echo "aaa bbb aaa" | sed 's/aaa/X/g' - Captured groups:
echo "John Doe" | sed -E 's/(\w+) (\w+)/\2, \1/' # -> "Doe, John" - In-place edit с backup:
echo "foo bar foo" > /tmp/test.txt sed -i.bak 's/foo/REPLACED/g' /tmp/test.txt cat /tmp/test.txt cat /tmp/test.txt.bak # original - Альтернативный delimiter:
echo "/usr/local/bin" | sed 's|/usr|/opt|'
macOS-различия
| Опция | GNU (Linux) | BSD (macOS) | Кросс-платформ |
|---|---|---|---|
| -i (in-place) | sed -i 's/x/y/' | sed -i '' 's/x/y/' | sed -i.bak 's/x/y/' |
| -E (extended regex) | поддержан | поддержан | -E |
\b word boundary | работает | работает | [[:space:]] или [[:<:]]/[[:>:]] |
\d, \w, \s | НЕТ (только PCRE/perl) | НЕТ | [0-9], [[:alnum:]_], [[:space:]] |
-i NUM (line address) | работает | работает | OK |
Для action production-скриптов где нужна portable sed — лучше использовать perl -pe или awk.
Главное
sed 's/OLD/NEW/'— заменить первое вхождение в каждой строке.s/OLD/NEW/g— все.- Альтернативные разделители:
s|...|...|,s#...#...#— для путей с/. - Captured groups:
\(...\)в BRE,(...)в-E. Back-references\1,\2.&= весь match. sed -iразличается на GNU vs BSD. Portable:sed -i.bak ....- Под капотом
-iсоздаёт temp-файл + atomic rename — inode меняется, FD-watchers могут не увидеть изменения. - Для cross-platform сложного
sed— переключайся наperl -pe. - НЕ парси JSON/XML/HTML через sed.