Linux — практическо ръководство

Част 7: Търсене и замяна в Linux — sed и perl на практика

Имам 200 HTML файла и трябва да вмъкна един ред преди друг. Не го правя на ръка. В тази статия описвам точно как решавам този проблем — с find, sed и perl, включително досадният въпрос с новите редове.

ℹ Бележка: Тук говоря за реални команди, които използвам всеки ден. Не е теоретичен преглед на регулярни изрази. Всеки пример е тестван на реални HTML файлове и решава конкретен проблем, с който се сблъсквам при поддръжката на сайта си.

1. Защо просто не отворя файловете в редактор?

Когато имам един файл — да, отварям го и го редактирам. Когато имам 200 файла с еднаква структура и трябва да вмъкна същия fragment във всеки от тях — вече не.

По-важното е, че не искам да рискувам да пропусна някой файл или да въведа грешка заради умора. Един добре написан find + sed или perl команден ред работи предсказуемо на всякакъв брой файлове.

Освен това, ако командата е записана в shell history или в скрипт, мога да я повторя или адаптирам за следващата подобна задача за секунди.

2. Основната схема: find + exec

Почти всичките ми команди за масова обработка следват една и съща схема:

Основна схемаКопирай

find . -name "*.html" -type f -exec КОМАНДА {} \;

Това намира всички .html файлове в текущата директория и поддиректориите й и изпълнява КОМАНДА за всеки от тях. Заместващият символ {} се разширява до името на текущия файл, а \; терминира командата.

Когато командата е по-сложна и имам нужда от shell конструкции като if, използвам bash -c:

Схема с 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 за тиха проверка:

Проверка с grep -qКопирай

if ! grep -q "" "$1"; then
# Вмъквам само ако не съществува
fi

Флагът -q (quiet) потиска output-а — grep връща само exit code: 0 ако намери съвпадение, 1 ако не намери. С ! обръщам логиката: „ако НЕ е намерен, тогава вмъкни”.

⚠ Предупреждение: Не пропускам тази стъпка. Дублираните фрагменти в 200 файла са много по-лоши от липсващите — трудно се забелязват и могат да счупят layout или функционалност.

4. Първото нещо, което правя — бекап на всички файлове

Преди да пусна каквато и да е inplace команда върху файлове, правя пълен бекап. Не разчитам на -i.bak на sed или perl — това създава отделен .bak файл до всеки оригинален файл, което е хаос при 200 файла.

Вместо това създавам една отделна папка с timestamp и копирам всички HTML файлове там:

Бекап на всички 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 (запазва структура)Копирай

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 кога е направен.

✓: Моят навик: бекъп папката е първата команда, която изпълнявам. Чак след като видя, че файловете са там, започвам със замяната. Това ми струва 2 секунди и ми спестява часове при проблем.

5. Проблемът с новите редове в sed

Ето къде повечето уроци за 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 с буквален нов редКопирай

sed -i "s|||g" file.html

Bash интерпретира новия ред вътре в двойни кавички като част от низа. sed получава реален newline символ в replacement низа и го записва във файла.

Този начин работи навсякъде — GNU sed, BSD sed, BusyBox sed. Не зависи от версии или флагове.

Реален пример: вмъкване на div в 200 HTML файла

Ето командата, която използвах, за да вмъкна преди във всички HTML файлове:

find + sed с буквален нов ред — реален примерКопирай

find . -name "*.html" -type f -exec bash -c '
if ! grep -q "" "$1"; then
sed -i "s|||g" "$1"
fi
' _ {} \;

✓: Предимство: работи на всяка платформа без изключения. Недостатък: командата изглежда грозно в терминала и е трудна за копиране/поставяне, защото новият ред е част от самата команда.

7. Вариант 2: sed с $‘\n’

Bash има синтаксис $'...', който интерпретира escape последователности вътре в единични кавички. Това означава, че $'\n' се разширява до реален нов ред:

$‘\n’ в bashКопирай

echo $'ред1\nред2\nред3'

Мога да комбинирам това с sed. Ключът е, че $'\n' се разширява от bash преди да се подаде на sed. Така sed получава реален newline символ, без да му трябва поддръжка на \n в replacement:

sed с $‘\n’Копирай

NEWLINE=$'\n'
sed -i "s|старо|ново${NEWLINE}още|g" file.html

Или директно в командата:

sed с $‘\n’ директноКопирай

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’

find + sed с $‘\n’ — вмъкване на script tagКопирай

NL=$'\n'
find . -name "*.html" -type f -exec bash -c '
if ! grep -q "" "$1"; then
sed -i "s||${NL}|g" "$1"
fi
' _ {} \;

✓: Предимство: командата е на един ред и се копира лесно. Недостатък: изисква bash (не работи с sh ).

8. Вариант 3: perl — най-чистият вариант

Когато нещата стават сложни — много редове за вмъкване, специални символи, нужда от по-мощни регулярни изрази — преминавам към perl.

perl -i -pe прави същото като sed -i: чете файл ред по ред, прилага замяната и записва резултата обратно. Разликата е, че perl разбира \n в replacement низа без допълнителни трикове:

perl -i -pe с \nКопирай

perl -i -pe 's|старо|ново\nоще|g' file.html

Това просто работи. perl интерпретира \n като нов ред както в pattern, така и в replacement. Няма нужда от $'\n', няма нужда от буквални нови редове.

Реален пример: вмъкване на script tag с perl

Същата задача — вмъкване на преди — но с perl:

find + perl — реален пример от моя сайтКопирай

find . -name "*.html" -type f -exec bash -c '
if ! grep -q "" "$1"; then
perl -i -pe "s||\n|g" "$1"
fi
' _ {} \;

Забелязвам, че тук използвам двойни кавички за perl командата, защото вътре има двойни кавички за HTML атрибутите — те са екранирани с \". Алтернативно мога да използвам единични кавички за perl и да сменя разделителя:

perl с единични кавички и различен разделителКопирай

perl -i -pe 's{}{\n}g' file.html

✓: Предимство: \n работи естествено, по-мощни регулярни изрази, по-чист синтаксис при сложни замени. Недостатък: perl може да не е инсталиран на минимални системи (макар че на почти всеки Linux е наличен).

9. sed срещу perl — кое избирам?

Критерийsedperl
НаличностВъв всяка 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 с резервно копиеКопирай

sed -i.bak "s|старо|ново|g" file.html

Това създава file.html.bak с оригиналното съдържание. При perl е аналогично:

perl -i с резервно копиеКопирай

perl -i.bak -pe 's|старо|ново|g' file.html

⚠ Предупреждение: Ако вече имам бекъп папка от раздел 4, -i.bak е излишен. Използвам -i.bak само когато тествам на 1-2 файла без пълен бекъп. За масова обработка разчитам на бекъп папката и използвам -i без суфикс.

12. Как тествам преди да пусна наистина

Моят процес е винаги един и същ:

1. Бекъп

цялата директория

→

2. Dry run

без -i, виждам output

→

3. Пускам

на всички файлове

Стъпка 1: Бекъп

Както описах в раздел 4 — създавам бекъп папка с timestamp.

Стъпка 2: Dry run

Премахвам -i и виждам какво би се променило, без да променям файла:

Dry run — виждам резултата в терминалаКопирай

perl -pe "s||\n|g" file.html

Стъпка 3: Пускам на всички

Чакато съм сигурен, пускам пълната команда с find и -i. Бекъп папката е моят застрахователен полис — ако нещо не е наред, възстановявам за секунди.

13. Специални символи — какво да екранирам

Когато търся HTML, често срещам символи, които имат специално значение в регулярни изрази:

СимволЗначение в regexЕкраниране
.Всеки символ\.
*Нулев или повече повторения\*
[ ]Клас символи\[ \]
( )Група (засечане)\( \) в sed; ( ) в perl
&Всичко, което е съвпаднало (sed)\&
/Разделител (ако е използван)\/ или смяна на разделител
``Алтернатива (в perl)

ℹ Бележка: Важно за & в sed: в replacement низа на sed , символът & означава „цялото съвпадение”. Ако заместващият текст съдържа литерален & , трябва да го екранирам с & . В perl това не е проблем — там се използва $& за същата цел.

14. Замяна на цял ред, не само част от него

Понякога не искам да заменям част от ред, а целия ред. В sed мога да използвам c\ (change):

Замяна на цял ред с sedКопирай

sed -i '/<old-tag>/c\<new-tag>' file.html

В perl мога да заменя от началото до края на реда:

Замяна на цял ред с perlКопирай

perl -i -pe 's/^.*<old-tag>.*$/*<new-tag>*/' file.html

Или още по-чисто — проверявам дали редът съдържа търсения текст и заменям целия ред:

perl — условна замяна на цял редКопирай

perl -i -pe 's/^.*<old-tag>.*$/<new-tag>/ if /<old-tag>/' file.html

15. Вмъкване преди или след определен ред

Понякога не искам да заменям, а просто да вмъкна нов ред преди или след съществуващ:

Вмъкване преди ред с sedКопирай

sed -i '/<div id="guide-nav">/i\' file.html

Вмъкване след ред с sedКопирай

sed -i '/<div id="guide-nav">/a\' file.html

i\ вмъква преди съвпадналия ред, a\ — след него. Това е често по-чист подход от s|...|...|, когато не ми трябва да заменям част от реда.

В perl аналогичните команди са:

Вмъкване преди ред с perlКопирай

perl -i -pe 'print "\n" if /<div id="guide-nav">/' file.html

print преди if гарантира, че новият ред се изважда преди съвпадналия. За да вмъкна след, използвам:

perl — вмъкване след редКопирай

perl -i -pe '$_.="\n" if /<div id="guide-nav">/' file.html

16. Какво правя, когато имам специални символи навсякъде

Когато търсеният или заместващият текст е пълен с кавички, наклонени черти и точки, понякога е по-лесно да използвам fixed string вместо regex.

В perl използвам \Q...\E за литарален търсен текст:

perl с \Q — литерален търсен текстКопирай

perl -i -pe 's|\Q\E|\n|g' file.html

\Q...\E в perl цитира всичко между себе си — точките, звездичките и другите специални символи се третират като литерални. Това премахва нуждата от екраниране на всяка точка и наклонена черта в търсения текст.

✓: Трик, който използвам често: \Q…\E около търсения текст и нормален replacement. Така не се тревожа за специални символи в това, което търся.

17. Обработка на файлове с различни encoding

Ако някои файлове са в UTF-8, а други в друг encoding, мога да укажа encoding на perl:

perl с указан encodingКопирай

perl -i -CS -Mutf8 -pe 's|старо|ново\nоще|g' file.html

-CS активира UTF-8 за stdin/stdout и -Mutf8 зарежда utf8 pragma. За български текст в HTML файлове обикновено не ми се налага — повечето ми файлове са вече в UTF-8 и perl работи коректно.

18. Комбиниране на множество замени

Когато трябва да направя няколко замени в един файл, мога да ги верижирам:

perl с множество замениКопирай

perl -i -pe '
s|старо1|ново1|g;
s|старо2|ново2\nново3|g;
s|старо4|ново4|g;
' file.html

Това е по-ефективно от три отделни perl извиквания, защото файлът се чете и записва само веднъж. Същото може да се направи и с sed чрез множество -e аргументи:

sed с множество замениКопирай

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 + 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 loop — по-бърз от -exec bash -cКопирай

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. Трите варианта за нов ред — обобщена таблица

ПодходСинтаксисРаботи сЧетливостПреносимост
Буквален нов ред`“sab\ncg”` (Enter в низа)
$'\n'`“sab$‘\n’cg”`
perl \n`perl -pe ‘sab\ncg’`

22. Моите реални команди — как изглеждат в практиката

Ето двете команди, които използвах при последната промяна в сайта си. Първата вмъква script tag с perl, втората — div с sed:

Вмъкване на related.js script tag (perl)

Реална команда от fedia.eu — perl вариантКопирай

find . -name "*.html" -type f -exec bash -c '
if ! grep -q "" "$1"; then
perl -i -pe "s||\n|g" "$1"
fi
' _ {} \;

Реална команда от fedia.eu — sed вариантКопирай

find . -name "*.html" -type f -exec bash -c '
if ! grep -q "" "$1"; then
sed -i "s|||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старо
`sed -i “sa
`sed -i “sa
`perl -i -pe ‘sa
sed -i '/ PATERN /i\НОВ_РЕД' файлВмъквам ред преди съвпадение.
sed -i '/ PATERN /a\НОВ_РЕД' файлВмъквам ред след съвпадение.
`perl -i -pe ‘s\Q…\E
rsync -av --delete БЕКАП/ ./Възстановявам от бекъп папка.
`find … -print0xargs -0 perl …`

24. Заключение

Търсенето и замяната в множество файлове не е сложна задача, ако знам правилните инструменти и — по-важното — правилните трикове за новите редове.

sed е достатъчен за повечето прости замени. Когато се появи нужда от \n в replacement низа, имам три варианта: буквален нов ред (най-переносим), $'\n' (най-четим в bash), или perl (най-чист синтаксис).

Но първото нещо, което правя, преди да напиша каквато и да е замяна, е бекъп. Една папка с timestamp, една команда за копиране и една команда за възстановяване. Тези 2 секунди ми дават спокойствието да експериментирам без страх.

✓: Моят принцип: ако ще редактирам повече от 5 файла по един и същ начин, вече пиша команден ред, а не отварям файловете един по един. Но винаги — първо бекъп, после замяна. Спестеното време се натрупва, а рискът е минимален.