1. Защо просто не отворя файловете в редактор?
Когато имам един файл — да, отварям го и го редактирам. Когато имам 200 файла с еднаква структура и трябва да вмъкна същия fragment във всеки от тях — вече не.
По-важното е, че не искам да рискувам да пропусна някой файл или да въведа грешка заради умора.
Един добре написан find + sed или perl команден ред работи
предсказуемо на всякакъв брой файлове.
Освен това, ако командата е записана в shell history или в скрипт, мога да я повторя или адаптирам за следващата подобна задача за секунди.
2. Основната схема: find + exec
Почти всичките ми команди за масова обработка следват една и съща схема:
find . -name "*.html" -type f -exec КОМАНДА {} \;
Това намира всички .html файлове в текущата директория и поддиректориите й
и изпълнява КОМАНДА за всеки от тях. Заместващият символ {} се разширява
до името на текущия файл, а \; терминира командата.
Когато командата е по-сложна и имам нужда от shell конструкции като if,
използвам bash -c:
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "ТЪРСЕН_ТЕКСТ" "$1"; then
sed -i "s|СТАРО|НОВО|g" "$1"
fi
' _ {} \;
_ след bash -c?
Това е конвенция. bash -c приема команден низ и след него — списък от аргументи,
които се достъпват като $0, $1 и т.н. Символът _ заема
мястото на $0 (името на shell-а), за да мога да достъпя името на файла чрез $1.
3. Проверка преди замяна — защо е задължителна
Преди да вмъкна нещо, винаги проверявам дали то вече не съществува. Ако не направя тази проверка и стартирам командата два пъти, ще получа дублиран фрагмент.
Използвам grep -q за тиха проверка:
if ! grep -q "<div id=\"related-posts\"></div>" "$1"; then
# Вмъквам само ако не съществува
fi
Флагът -q (quiet) потиска output-а — grep връща само exit code:
0 ако намери съвпадение, 1 ако не намери. С ! обръщам логиката:
„ако НЕ е намерен, тогава вмъкни".
4. Първото нещо, което правя — бекап на всички файлове
Преди да пусна каквато и да е inplace команда върху файлове, правя пълен бекап.
Не разчитам на -i.bak на sed или perl — това създава отделен .bak файл
до всеки оригинален файл, което е хаос при 200 файла.
Вместо това създавам една отделна папка с timestamp и копирам всички HTML файлове там:
# Създай папка за бекап в текущата директория
mkdir -p ./html_backup_$(date +%Y%m%d_%H%M%S)
# Копирай всички HTML файлове в бекъп папката
find . -name "*.html" -type f -exec cp {} ./html_backup_$(date +%Y%m%d_%H%M%S)/ \;
Резултатът е нещо подобно на ./html_backup_20250628_143052/ — една чиста папка
с всички HTML файлове в тяхното оригинално състояние. Ако нещо се обърка, възстановявам
с една команда:
find ./html_backup_20250628_143052/ -name "*.html" -type f -exec cp {} . \;
rsync:
rsync -av --delete ./html_backup_20250628_143052/ ./
Защо не ползвам само -i.bak
sed -i.bak и perl -i.bak създават file.html.bak
до всеки file.html. При 200 файла това означава 200 допълнителни файла,
разпръснати из цялата директория. След като се уверя, че всичко е наред, трябва
да изтрия 200 .bak файла — още една команда, още един риск.
С един бекъп папка имам: едно място за възстановяване, лесно изтриване на целия бекъп
с rm -rf ./html_backup_20250628_143052/ и ясен timestamp кога е направен.
5. Проблемът с новите редове в sed
Ето къде повечето уроци за sed спират или дават грешен съвет.
Когато искам да заменя нещо с текст, който съдържа нов ред, командата:
sed -i "s|старо|ново\nоще|g" file.html
...в много ситуации не работи. Причината е, че стандартният sed (POSIX sed, GNU sed без
определени флагове, BSD sed на macOS) третира \n в replacement низа литерално
като backslash и n, а не като нов ред.
Това е един от най-често срещаните източници на объркване при работа с sed.
Има поне три начина да се справя с това — всеки с свои предимства и недостатъци.
6. Вариант 1: sed с буквални нови редове
Най-простият и най-переносимият начин е да вложа буквален нов ред в replacement низа. В bash това означава просто да натисна Enter в средата на низа:
sed -i "s|<div id=\"guide-nav\"></div>|<div id=\"related-posts\"></div>
<div id=\"guide-nav\"></div>|g" file.html
Bash интерпретира новия ред вътре в двойни кавички като част от низа. sed получава
реален newline символ в replacement низа и го записва във файла.
Този начин работи навсякъде — GNU sed, BSD sed, BusyBox sed. Не зависи от версии или флагове.
Реален пример: вмъкване на div в 200 HTML файла
Ето командата, която използвах, за да вмъкна <div id="related-posts"></div>
преди <div id="guide-nav"></div> във всички HTML файлове:
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "<div id=\"related-posts\"></div>" "$1"; then
sed -i "s|<div id=\"guide-nav\"></div>|<div id=\"related-posts\"></div>
<div id=\"guide-nav\"></div>|g" "$1"
fi
' _ {} \;
7. Вариант 2: sed с $'\n'
Bash има синтаксис $'...', който интерпретира escape последователности вътре в
единични кавички. Това означава, че $'\n' се разширява до реален нов ред:
echo $'ред1\nред2\nред3'
Мога да комбинирам това с sed. Ключът е, че $'\n' се разширява от bash
преди да се подаде на sed. Така sed получава реален
newline символ, без да му трябва поддръжка на \n в replacement:
NEWLINE=$'\n'
sed -i "s|старо|ново${NEWLINE}още|g" file.html
Или директно в командата:
sed -i "s|старо|ново$'\n'още|g" file.html
Кога използвам $'\n'
Използвам този подход, когато командата е част от скрипт и искам да е четлива. В one-liner от терминала предпочитам или буквалния нов ред, или perl.
$'\n' е bash-специфичен синтаксис. Не работи в plain POSIX sh
или в други shell-ове като dash (което е /bin/sh на много Linux дистрибуции).
Ако скриптът ми започва с #!/bin/bash, няма проблем.
Реален пример: вмъкване на script tag с $'\n'
NL=$'\n'
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "<script src=\"/js/related.js\" defer></script>" "$1"; then
sed -i "s|<script src=\"/js/nav-config.js\" defer></script>|<script src=\"/js/related.js\" defer></script>${NL}<script src=\"/js/nav-config.js\" defer></script>|g" "$1"
fi
' _ {} \;
sh).
8. Вариант 3: perl — най-чистият вариант
Когато нещата стават сложни — много редове за вмъкване, специални символи, нужда от по-мощни
регулярни изрази — преминавам към perl.
perl -i -pe прави същото като sed -i: чете файл ред по ред,
прилага замяната и записва резултата обратно. Разликата е, че perl разбира
\n в replacement низа без допълнителни трикове:
perl -i -pe 's|старо|ново\nоще|g' file.html
Това просто работи. perl интерпретира \n като нов ред както в pattern,
така и в replacement. Няма нужда от $'\n', няма нужда от буквални нови редове.
Реален пример: вмъкване на script tag с perl
Същата задача — вмъкване на <script src="/js/related.js" defer></script>
преди <script src="/js/nav-config.js" defer></script> — но с perl:
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "<script src=\"/js/related.js\" defer></script>" "$1"; then
perl -i -pe "s|<script src=\"/js/nav-config.js\" defer></script>|<script src=\"/js/related.js\" defer></script>\n<script src=\"/js/nav-config.js\" defer></script>|g" "$1"
fi
' _ {} \;
Забелязвам, че тук използвам двойни кавички за perl командата, защото вътре има двойни кавички
за HTML атрибутите — те са екранирани с \". Алтернативно мога да използвам
единични кавички за perl и да сменя разделителя:
perl -i -pe 's{<script src="/js/nav-config.js" defer></script>}{<script src="/js/related.js" defer></script>\n<script src="/js/nav-config.js" defer></script>}g' file.html
\n работи естествено, по-мощни регулярни изрази,
по-чист синтаксис при сложни замени.
Недостатък: perl може да не е инсталиран на минимални системи (макар че на почти всеки
Linux е наличен).
9. sed срещу perl — кое избирам?
| Критерий | sed | perl |
|---|---|---|
| Наличност | Във всяка Unix система | Почти навсякъде, но не гарантирано |
| \n в replacement | Не работи стандартно; нужда от трикове | Работи директно |
| Производителност при много файлове | По-бърз за прости замени | Сравнимо бърз |
| Сложни регулярни изрази | Основни (BRE/ERE) | PCRE — пълен набор |
| Многоредова замяна | Възможна, но тромава | Естествена с \n |
| Четливост на командата | Зависи от подхода за \n | По-чиста при сложни случаи |
Моето правило е просто: за едноредова замяна без нови редове използвам sed.
Щом се появи нужда от \n в replacement, преминавам към perl.
10. Разделители — защо не винаги използвам /
Когато търся или замествам пътища до файлове, URL-та или HTML с атрибути,
стандартният разделител / създава нужда от екраниране:
sed -i "s/\/js\/nav-config\.js/\/js\/related\.js\n\/js\/nav-config\.js/g" file.html
Вместо това използвам | като разделител — и в sed, и в perl:
sed -i "s|/js/nav-config\.js|/js/related\.js\n/js/nav-config\.js|g" file.html
И sed, и perl позволяват произволен символ след s да бъде разделител.
Избирам символ, който не се среща в търсения или заместващия текст.
11. Флаг -i — inplace редакция
И sed -i, и perl -i редактират файла „на място" — прочитат го,
прилагат промените и записват резултата върху оригиналния файл.
Резервно копие с -i
Мога да направя резервно копие преди редакция:
sed -i.bak "s|старо|ново|g" file.html
Това създава file.html.bak с оригиналното съдържание. При perl е аналогично:
perl -i.bak -pe 's|старо|ново|g' file.html
-i.bak е излишен.
Използвам -i.bak само когато тествам на 1-2 файла без пълен бекъп.
За масова обработка разчитам на бекъп папката и използвам -i без суфикс.
12. Как тествам преди да пусна наистина
Моят процес е винаги един и същ:
цялата директория
без -i, виждам output
на всички файлове
Стъпка 1: Бекъп
Както описах в раздел 4 — създавам бекъп папка с timestamp.
Стъпка 2: Dry run
Премахвам -i и виждам какво би се променило, без да променям файла:
perl -pe "s|<script src=\"/js/nav-config.js\" defer></script>|<script src=\"/js/related.js\" defer></script>\n<script src=\"/js/nav-config.js\" defer></script>|g" file.html
Стъпка 3: Пускам на всички
Чакато съм сигурен, пускам пълната команда с find и -i.
Бекъп папката е моят застрахователен полис — ако нещо не е наред, възстановявам за секунди.
13. Специални символи — какво да екранирам
Когато търся HTML, често срещам символи, които имат специално значение в регулярни изрази:
| Символ | Значение в regex | Екраниране |
|---|---|---|
. | Всеки символ | \. |
* | Нулев или повече повторения | \* |
[ ] | Клас символи | \[ \] |
( ) | Група (засечане) | \( \) в sed; ( ) в perl |
& | Всичко, което е съвпаднало (sed) | \& |
/ | Разделител (ако е използван) | \/ или смяна на разделител |
| | Алтернатива (в perl) | \| |
& в sed: в replacement низа на sed,
символът & означава „цялото съвпадение". Ако заместващият текст съдържа
литерален &, трябва да го екранирам с \&.
В perl това не е проблем — там се използва $& за същата цел.
14. Замяна на цял ред, не само част от него
Понякога не искам да заменям част от ред, а целия ред. В sed мога да използвам
c\ (change):
sed -i '/<old-tag>/c\<new-tag>' file.html
В perl мога да заменя от началото до края на реда:
perl -i -pe 's/^.*<old-tag>.*$/*<new-tag>*/' file.html
Или още по-чисто — проверявам дали редът съдържа търсения текст и заменям целия ред:
perl -i -pe 's/^.*<old-tag>.*$/<new-tag>/ if /<old-tag>/' file.html
15. Вмъкване преди или след определен ред
Понякога не искам да заменям, а просто да вмъкна нов ред преди или след съществуващ:
sed -i '/<div id="guide-nav">/i\<div id="related-posts"></div>' file.html
sed -i '/<div id="guide-nav">/a\<div id="related-posts"></div>' file.html
i\ вмъква преди съвпадналия ред, a\ — след него.
Това е често по-чист подход от s|...|...|, когато не ми трябва да заменям част от реда.
В perl аналогичните команди са:
perl -i -pe 'print "<div id=\"related-posts\"></div>\n" if /<div id="guide-nav">/' file.html
print преди if гарантира, че новият ред се изважда преди съвпадналия.
За да вмъкна след, използвам:
perl -i -pe '$_.="<div id=\"related-posts\"></div>\n" if /<div id="guide-nav">/' file.html
16. Какво правя, когато имам специални символи навсякъде
Когато търсеният или заместващият текст е пълен с кавички, наклонени черти и точки, понякога е по-лесно да използвам fixed string вместо regex.
В perl използвам \Q...\E за литарален търсен текст:
perl -i -pe 's|\Q<script src="/js/nav-config.js" defer></script>\E|<script src="/js/related.js" defer></script>\n<script src="/js/nav-config.js" defer></script>|g' file.html
\Q...\E в perl цитира всичко между себе си — точките, звездичките и другите
специални символи се третират като литерални. Това премахва нуждата от екраниране
на всяка точка и наклонена черта в търсения текст.
\Q...\E около търсения текст
и нормален replacement. Така не се тревожа за специални символи в това, което търся.
17. Обработка на файлове с различни encoding
Ако някои файлове са в UTF-8, а други в друг encoding, мога да укажа encoding на perl:
perl -i -CS -Mutf8 -pe 's|старо|ново\nоще|g' file.html
-CS активира UTF-8 за stdin/stdout и -Mutf8 зарежда utf8 pragma.
За български текст в HTML файлове обикновено не ми се налага — повечето ми файлове са
вече в UTF-8 и perl работи коректно.
18. Комбиниране на множество замени
Когато трябва да направя няколко замени в един файл, мога да ги верижирам:
perl -i -pe '
s|старо1|ново1|g;
s|старо2|ново2\nново3|g;
s|старо4|ново4|g;
' file.html
Това е по-ефективно от три отделни perl извиквания, защото файлът се чете и записва само веднъж.
Същото може да се направи и с sed чрез множество -e аргументи:
sed -i -e 's|старо1|ново1|g' -e 's|старо2|ново2|g' file.html
19. Как избягвам бинарни файлове
Понякога в директорията има файлове, които изглеждат като .html, но са бинарни
(например генерирани грешно или смесени с binary data). sed -i върху такъв файл
може да го съсипе.
Добавям проверка с file:
find . -name "*.html" -type f -exec bash -c '
if file "$1" | grep -q "text"; then
if ! grep -q "ТЪРСЕН_ТЕКСТ" "$1"; then
perl -i -pe "s|СТАРО|НОВО\nСТАРО|g" "$1"
fi
fi
' _ {} \;
20. Производителност при много файлове
Когато имам хиляди файлове, find -exec с bash -c стартира
нов процес за всеки файл. Това е бавно. По-бързи алтернативи:
Опция 1: xargs
find . -name "*.html" -type f -print0 | xargs -0 perl -i -pe 's|старо|ново\nстаро|g'
-print0 и -0 използват null byte като разделител — безопасно за
имена на файлове с интервали и специални символи. xargs подава максималния брой
аргументи на едно perl извикване.
grep -q проверка!
При xargs не мога лесно да добавя условие „замени само ако не съществува".
Използвам xargs само когато съм сигурен, че искам да заменя във всички файлове,
или когато замяната е идемпотентна (повторното прилагане не променя нищо).
Опция 2: while loop
while IFS= read -r -d '' f; do
if ! grep -q "ТЪРСЕН_ТЕКСТ" "$f"; then
perl -i -pe "s|СТАРО|НОВО\nСТАРО|g" "$f"
fi
done < <(find . -name "*.html" -type f -print0)
Това стартира само един bash процес и извиква perl за всеки файл отделно — но без
допълнителния bash -c overhead на -exec.
21. Трите варианта за нов ред — обобщена таблица
| Подход | Синтаксис | Работи с | Четливост | Преносимост |
|---|---|---|---|---|
| Буквален нов ред | "s|a|b\nc|g" (Enter в низа) |
Всички sed и perl | ⭐⭐ | ⭐⭐⭐ |
$'\n' |
"s|a|b$'\n'c|g" |
Само bash + sed/perl | ⭐⭐⭐ | ⭐⭐ (bash only) |
perl \n |
perl -pe 's|a|b\nc|g' |
Само perl | ⭐⭐⭐ | ⭐⭐ (нужен perl) |
22. Моите реални команди — как изглеждат в практиката
Ето двете команди, които използвах при последната промяна в сайта си. Първата вмъква script tag с perl, втората — div с sed:
Вмъкване на related.js script tag (perl)
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "<script src=\"/js/related.js\" defer></script>" "$1"; then
perl -i -pe "s|<script src=\"/js/nav-config.js\" defer></script>|<script src=\"/js/related.js\" defer></script>\n<script src=\"/js/nav-config.js\" defer></script>|g" "$1"
fi
' _ {} \;
Вмъкване на related-posts div (sed с буквален нов ред)
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "<div id=\"related-posts\"></div>" "$1"; then
sed -i "s|<div id=\"guide-nav\"></div>|<div id=\"related-posts\"></div>
<div id=\"guide-nav\"></div>|g" "$1"
fi
' _ {} \;
И двете команди следват един и същ модел: find намира файловете, grep -q
проверява дали фрагментът вече съществува, и само при липса се извършва замяната.
Разликата е само в инструмента за самата замяна — perl за случая с script tag-а,
sed за случая с div-а.
23. Командите накратко
| Команда | За какво я използвам |
|---|---|
mkdir -p ./html_backup_$(date +%Y%m%d_%H%M%S) | Създавам бекъп папка с timestamp. |
find . -name "*.html" -exec cp {} БЕКАП/ \; | Копирам всички HTML в бекъп папката. |
find . -name "*.html" -type f | Намирам всички HTML файлове. |
grep -q "ТЕКСТ" файл | Тихо проверявам дали текст съществува. |
sed -i "s|старо|ново|g" файл | Заменям текст в файл (без нови редове). |
sed -i "s|a|b\nc|g" файл | Заменям с буквален нов ред в replacement. |
sed -i "s|a|b$'\n'c|g" файл | Заменям с $'\n' за нов ред (bash only). |
perl -i -pe 's|a|b\nc|g' файл | Заменям с \n — работи естествено в perl. |
sed -i '/ PATERN /i\НОВ_РЕД' файл | Вмъквам ред преди съвпадение. |
sed -i '/ PATERN /a\НОВ_РЕД' файл | Вмъквам ред след съвпадение. |
perl -i -pe 's|\Q...\E|...|g' | Литарален търсен текст без екраниране. |
rsync -av --delete БЕКАП/ ./ | Възстановявам от бекъп папка. |
find ... -print0 | xargs -0 perl ... | Бърза обработка на много файлове. |
24. Заключение
Търсенето и замяната в множество файлове не е сложна задача, ако знам правилните инструменти и — по-важното — правилните трикове за новите редове.
sed е достатъчен за повечето прости замени. Когато се появи нужда от
\n в replacement низа, имам три варианта: буквален нов ред (най-переносим),
$'\n' (най-четим в bash), или perl (най-чист синтаксис).
Но първото нещо, което правя, преди да напиша каквато и да е замяна, е бекъп. Една папка с timestamp, една команда за копиране и една команда за възстановяване. Тези 2 секунди ми дават спокойствието да експериментирам без страх.