Вопрос или проблема
Я заметил, что если я добавляю \n
в шаблон для замены с помощью sed
, он не срабатывает. Пример:
$ cat > alpha.txt
Это
тест
Пожалуйста, не
пугайтесь
$ sed -i'.original' 's/a test\nPlease do not/not a test\nBe/' alpha.txt
$ diff alpha.txt{,.original}
$ # Разницы не обнаружено
Как мне это заставить работать?
В самом простом вызове sed в нем содержится одна строка текста в пространстве шаблона, т.е. 1 строка текста, разделенная \n
, из входных данных. Единичная строка в пространстве шаблона не содержит \n
. Вот почему ваша регулярная выражение ничего не находит.
Вы можете читать несколько строк в пространство шаблона и манипулировать ими, удивительно хорошо, но это требует больше нормальных усилий. Sed имеет набор команд, которые позволяют выполнять этого типа операции. Вот ссылка на Конспект команд для sed. Это лучший ресурс, который я нашел, и он помог мне начать.
Однако забудьте о идее “однострочника”, как только вы начнете использовать микро-команды sed. Полезно представлять это как структурированную программу, пока вы не привыкнете к этому. Это удивительно просто и одновременно необычно. Вы можете представить это как “язык ассемблера” для редактирования текста.
Резюме: используйте sed для простых задач, и может быть чуть больше, но в общем, когда дело доходит до работы с более чем одной строкой, большинство людей предпочитают что-то другое.
Я позволю кому-то другому предложить что-то еще, так как я действительно не уверен, что будет лучшим выбором (я бы использовал sed, но это потому что я не знаю perl достаточно хорошо.)
sed '/^a test$/{
$!{ N # добавить следующую строку, когда не на последней строке
s/^a test\nPlease do not$/not a test\nBe/
# теперь проверить на успешную замену, иначе
#+ несоответствующие "a test" строки будут неправильно обработаны
t sub-yes # переход по замене (переход к метке :sub-yes)
:sub-not # метка (не обязательно; здесь для самодокументации)
# если замены нет, напечатать только первую строку
P # печать первой строки шаблона
D # усечению верхней части шаблона (строка+nl)
:sub-yes # метка (цель перехода 't')
# перейти к последней автоматической печати шаблона (2 строки)
}
}' alpha.txt
Вот тот же скрипт, сжатый в то, что, очевидно, труднее читать и с чем работать, но некоторые могли бы сомнительно назвать однострочником
sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;ty;P;D;:y}}' alpha.txt
Вот моя “шпаргалка” команд
: # метка
= # номер_строки
a # добавить_текст_в_stdout_после_очистки
b # безусловный_переход
c # изменение_диапазона
d # удаление_шаблона_верх/цикл
D # усечению верхней части шаблона (строка+nl)
g # шаблон=удерживать
G # шаблон+=nl+удерживать
h # удерживать=шаблон
H # удерживать+=nl+шаблон
i # вставить_текст_в_stdout_прямо_сейчас
l # список_шаблона
n # сброс_шаблона=след_строка_продолжить
N # шаблон+=nl+след_строка
p # печать_шаблона
P # печать_первой_строки_шаблона
q # сброс_выход
r # добавить_файл_в_stdout_после_очистки
s # замена
t # переход_по_замене
w # добавить_шаблон_в_файл_прямо_сейчас
x # обменять_шаблон_и_удерживать
y # преобразовать_символы
Используйте perl
вместо sed
:
$ perl -0777 -i.original -pe 's/a test\nPlease do not/not a test\nBe/igs' alpha.txt
$ diff alpha.txt{,.original}
2,3c2,3
< not a test
< Be
---
> a test
> Please do not
-pi -e
это ваша стандартная команда “заменить на месте” в командной строке, а -0777 заставляет perl загружать файлы целиком. Смотрите перлодок perlrun, чтобы узнать больше об этом.
Я думаю, что лучше заменить символ \n
на какой-то другой символ, а затем действовать как обычно:
например, неработающий исходный код:
cat alpha.txt | sed -e 's/a test\nPlease do not/not a test\nBe/'
можно изменить на:
cat alpha.txt | tr '\n' '\r' | sed -e 's/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
Если кто-то не знает, \n
это окончание строки UNIX, \r\n
– Windows, \r
– классический Mac OS. Обычный текст UNIX не использует символ \r
, поэтому его безопасно использовать для этого случая.
Вы также можете использовать какой-то экзотический символ, чтобы временно заменить \n. Например – \f (символ перевода страницы). Вы можете найти больше символов здесь.
cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/' | tr '\f' '\n'
GNU sed
имеет опцию -z
, которая позволяет использовать синтаксис, который пытался применить OP. (страница man)
Пример:
$ cat alpha.txt
Это
тест
Пожалуйста, не
пугайтесь
$ sed -z 's/a test\nPlease do not\nbe/not a test\nBe/' -i alpha.txt
$ cat alpha.txt
Это
не тест
Будьте настороже
Обратите внимание: Если вы используете ^
и $
, они теперь соответствуют началу и концу строк, разделенных символом NUL (а не \n
). И чтобы убедиться, что совпадения на всех ваших (\n
-разделенных) строках заменяются, не забудьте использовать флаг g
для глобальных замен (например, s/.../.../g
).
Кредиты: @stéphane-chazelas впервые упомянул -z в комментарии выше.
Если все учитывать, чистка всего файла может быть самым быстрым вариантом.
Основной синтаксис таков:
sed -e '1h;2,$H;$!d;g' -e 's/__YOUR_REGEX_GOES_HERE__...'
Имейте в виду, что очистка всего файла может быть невозможна, если файл очень большой. В таких случаях другие ответы, предоставленные здесь, предлагают индивидуальные решения, которые гарантированно работают с небольшим объемом памяти.
Для всех остальных ситуаций, просто добавление -e '1h;2,$H;$!d;g'
, за которым следует ваш оригинальный аргумент регулярного выражения sed
, в целом выполняет работу.
например:
$ echo -e "Собака\nЛиса\nКот\nЗмея\n" | sed -e '1h;2,$H;$!d;g' -re 's/([^\n]*)\n([^\n]*)\n/Быстрая \2\nЛенивая \1\n/g'
Быстрая Лиса
Ленивая Собака
Быстрая Змея
Ленивая Кошка
Что делает -e '1h;2,$H;$!d;g'
?
Части 1
, 2,$
, $!
являются спецификаторами строк, которые ограничивают, на каких строках выполняется следующая команда.
1
: только первая строка2,$
: все строки, начиная со второй$!
: каждая строка, кроме последней
Следовательно, развернуто, это происходит с каждой строкой N-строчного ввода.
1: h, d
2: H, d
3: H, d
.
.
N-2: H, d
N-1: H, d
N: H, g
Команда g
не имеет спецификатора строки, но предшествующая команда d
имеет специальное условие “Начать следующий цикл.“, и это предотвращает выполнение g
на всех строках, кроме последней.
Что касается значения каждой команды:
- h, за которой следуют
H
на каждой строке, копирует указанные строки ввода вsed
‘s хранилище. (Думайте об этом как о произвольном текстовом буфере.) - Затем
d
отбрасывает каждую строку, чтобы предотвратить запись этих строк в вывод. Однако хранилище сохраняется. - Наконец, на последней строке
g
восстанавливает накопление каждой строки из хранилища, чтобыsed
мог запускать свое регулярное выражение на всем вводе (вместо по одной строке), и, следовательно, мог соответствовать\n
s.
sed
имеет три команды для управления многострочными операциями: N
, D
и P
(сравните их с обычными n
, d
и p
).
В этом случае вы можете сопоставить первую строку вашего шаблона, использовать N
, чтобы добавить вторую строку к пространству шаблона, а затем использовать s
, чтобы выполнить вашу замену.
Что-то вроде:
/a test$/{
N
s/a test\nPlease do not/not a test\nBe/
}
Вы можете, но это тяжело. Я рекомендую переключиться на другой инструмент. Если есть регулярное выражение, которое никогда не совпадает ни с одной частью текста, который вы хотите заменить, вы можете использовать его в качестве разделителя записей в GNU awk.
awk -v RS='a' '{gsub(/hello/, "world"); print}'
Если в вашей строке поиска никогда нет двух последовательных переводов строк, вы можете использовать “параграфный режим” awk (одна или несколько пустых строк разделяют записи).
awk -v RS='' '{gsub(/hello/, "world"); print}'
Легкое решение – использовать Perl и загрузить файл полностью в память.
perl -0777 -pe 's/hello/world/g'
Я думаю, что это решение sed для сопоставления 2 строк.
sed -n '$!N;s@a test\nPlease do not@not a test\nBe@;P;D' alpha.txt
Если вы хотите сопоставить 3 строки, тогда …
sed -n '1{$!N};$!N;s@aaa\nbbb\nccc@xxx\nyyy\nzzz@;P;D'
Если вы хотите сопоставить 4 строки, тогда …
sed -n '1{$!N;$!N};$!N;s@ ... @ ... @;P;D'
Если замена в команде “s” вызывает сжатие строк, тогда это немного сложнее, как это:
# aaa\nbbb\nccc сжимаются в одну строку "xxx"
sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@xxx@;$!N;$!N};P;D'
Если часть замены увеличивает количество строк, тогда это немного сложнее, как это:
# aaa\nbbb\nccc увеличиваются до пяти строк vvv\nwww\nxxx\nyyy\nzzz
sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@vvv\nwww\nxxx\nyyy\nzzz@;P;s/.*\n//M;P;s/.*\n//M};P;D'
Этот второй метод – простая замена текста, строчка за строчкой, для обычных небольших текстовых файлов (нужен файл оболочки)
#!/bin/bash
# копировать и вставить содержимое, которое вы хотите заменить
AA=$( cat <<\EOF | sed -z -e 's#\([][^$*\.#]\)#\\\1#g' -e 's#\n#\\n#g'
a test
Please do not
EOF
)
BB=$( cat <<\EOF | sed -z -e 's#\([&\#]\)#\\\1#g' -e 's#\n#\\n#g'
not a test
Be
EOF
)
sed -z -i 's#'"${AA}"'#'"${BB}"'#g' *.txt # применить ко всем *.txt файлам
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt
Здесь /a test/,/Please do not/
рассматривается как блок (многострочный) текста, c
является командой замены, за которой следует новый текст not a test \nBe
В случае, если текст для замены очень длинный, я бы предложил синтаксис ex.
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out
Просто немного увеличьте ваше окно ввода.
Это довольно просто. Кроме стандартной замены; вам нужно только $!N
, P
, и D
здесь.
Кроме Perl, общий и удобный подход для многострочного редактирования для потоков (а также для файлов) следующий:
Сначала создайте уникальный разделитель строки, какой вам нравится, например
$ S=__ABC__ # просто
$ S=__$RANDOM$RANDOM$RANDOM__ # лучше
$ S=$(openssl rand -hex 16) # идеальный
Затем в вашей команде sed (или любом другом инструменте) вы заменяете \n на ${S}, например:
$ cat file.txt | awk 1 ORS=$S | sed -e "s/a test${S}Please do not/not a test\nBe/" | awk 1 RS=$S > file_new.txt
( awk заменяет ASCII-разделитель строки на ваш и наоборот. )
Это небольшое изменение умного ответа xara, чтобы оно работало на OS X (я использую 10.10):
cat alpha.txt | tr '\n' '\r' | sed -e 's/a test$(printf '\r')Please do not/not a test$(printf '\r')Be/' | tr '\r' '\n'
Вместо явного использования \r
, вы должны использовать $(printf '\r')
.
Расширяя блестящий принятый ответ Peter.O, если вы, как и я, нуждаетесь в решении для замены более чем 2 строк за раз, попробуйте это:
#!/bin/bash
pattern_1="<Directory \"\/var\/www\/cgi-bin\">"
pattern_2="[ ]*AllowOverride None\n"
pattern_3="[ ]*Options +ExecCGI\n"
pattern_4="[ ]*AddHandler cgi-script \.cgi \.pl\n"
pattern_5="[ ]*Require all granted\n"
pattern_6="<\/Directory>"
complete_pattern="$pattern_1\n$pattern_2$pattern_3$pattern_4$pattern_5$pattern_6"
replacement_1="#<Directory \"\/var\/www\/cgi-bin\">\n"
replacement_2=" #AllowOverride None\n"
replacement_3=" #Options +ExecCGI\n"
replacement_4=" #AddHandler cgi-script \.cgi \.pl\n"
replacement_5=" #Require all granted\n"
replacement_6="#<\/Directory>"
complete_replacement="$replacement_1$replacement_2$replacement_3$replacement_4$replacement_5$replacement_6"
filename="test.txt"
echo ""
echo "SEDding"
sed -i "/$pattern_1/{
N;N;N;N;N
s/$complete_pattern/$complete_replacement/
}" $filename
Пусть ваш входной файл будет:
#
#Это некоторые тестовые комментарии
# Пропустите это
#
<Directory "/var/www/cgi-bin">
AllowOverride None
Options +ExecCGI
AddHandler cgi-script .cgi .pl
Require all granted
</Directory>
После выполнения скрипта sed, файл будет заменен на:
#
#Это некоторые тестовые комментарии
# Пропустите это
#
#<Directory "/var/www/cgi-bin">
#AllowOverride None
#Options +ExecCGI
#AddHandler cgi-script .cgi .pl
#Require all granted
#</Directory>
Объяснение
-
pattern_1="<Directory \"\/var\/www\/cgi-bin\">"
— Специальные символы должны быть экранированы обратным слэшем\
. -
[ ]*
— Это будет соответствовать 0 или многим пробелам. Стандартная нотация RegEx -
sed -i "/$pattern_1/{
— Это будет искать файл, построчно, для pattern_1 [<Directory "/var/www/cgi-bin">
]. Обратите внимание, что шаблон поиска НЕ ДОЛЖЕН СОДЕРЖАТЬ НОВУЮ СТРОКУЕсли и только если sed находит
$pattern_1
в файле, он будет продолжать выполнять под-код внутри фигурных скобок{}
. Он начнет с линии, совпадающей с шаблоном файла. -
N;N;N;N;N
—N
говорит sed читать следующую строку после шаблона и прикреплять ее к текущей строке. Важно понимать, что sed предназначен для замены только 1 строки за раз, поэтомуN
по сути заставляет sed читать 2 строки и рассматривать их как одну строку с одним переводом строки\n
между ними. Перевод строки во второй строке будет проигнорирован. Связывая 5N
, мы инструктируем sed читать 6 строк файла, начиная с строки, совпадающей с шаблоном. -
s/$complete_pattern/$complete_replacement/
— Заменить$complete_pattern
на$complete_replacement
. Обратите внимание на наличие переносов строк в переменных. Понимание этой части займет некоторое время и эксперименты.
Я хотел добавить несколько строк HTML в файл с помощью sed, (и в итоге попал сюда). Обычно я просто использую perl, но я был на компьютере, где был только sed, bash и не много другого. Я обнаружил, что если я изменю строку на одну строку и позволю bash/sed интерполировать \t\n
, все получится:
HTML_FILE='a.html' #содержит якорь в форме <a name="nchor" />
BASH_STRING_A='яблоки'
BASH_STRING_B='бананы'
INSERT="\t<li>$BASH_STRING_A<\/li>\n\t<li>$BASH_STRING_B<\/li>\n<a name=\"nchor\"\/>"
sed -i "s/<a name=\"nchor"\/>/$INSERT/" $HTML_FILE
Было бы чище иметь функцию для экранирования двойных кавычек и косых слэшей, но иногда абстракция отвлекает от времени.
Sed делит ввод по переносам строк. Он оставляет только одну строку на цикл.
По этой причине нет способа сопоставить \n
(перенос строки), если пространство шаблона не содержит его.
Тем не менее, есть способ, вы можете заставить sed сохранить две последовательные строки в пространстве шаблона, используя цикл:
sed 'N;l;P;D' alpha.txt
Добавьте любую обработку, необходимую между N и P (заменив l
).
В этом случае (2 строки):
$ sed 'N;s/a test\nPlease do not/not a test\nBe/;P;D' alpha.txt
Это
не тест
Будьте настороже
be alarmed
Или, для трех строк:
$ sed -n '1{$!N};$!N;s@a test\nPlease do not\nbe@not a test\nDo\nBe@;P;D' alpha.txt
Это
не тест
Do
Будьте настороже
Предполагая, что заменяется такое же количество строк.
Хотя ripgrep
конкретно не поддерживает встроенные замены, я обнаружил, что его текущая функциональность --replace
уже полезна для этой задачи и предпочтительнее использования sed
, например:
rg --replace $'not a test\nBe' --passthru --no-line-number \
--multiline 'a test\nPlease do not' alpha.txt > output.txt
Объяснение:
--replace 'строка'
включает режим замены и устанавливает строку замены. Может включать захваченные группы regex, используя$1
и т.д.$'строка'
является расширением Bash, чтобы\n
стал переносом строки для многострочной строки.--passthru
необходим, посколькуripgrep
обычно показывает только строки, соответствующие шаблону regex. С этой опцией он также показывает все строки из файла, которые не совпадают.--no-line-number / -N
из-за того, что по умолчаниюripgrep
включает номера строк в выводе (полезно, когда показаны только совпадающие строки).--multiline / -U
включает многострочную обработку, которая по умолчанию отключена.> output.txt
, с--passthrough
иno-line-number
опциями стандартный вывод совпадает с желаемым новым файлом с заменами и может быть сохранен так же, как обычно.--multiline-dotall
можно опционально добавить, если вы хотите, чтобы точка (‘.’) паттерна regex соответствовала переносам строк (\n
).
Тем не менее, эта команда не так полезна для обработки нескольких файлов, так как ее необходимо запускать отдельно для каждого файла.
Поскольку это уже объясняет большинство операций sed, я добавлю, как вы можете выполнить поиск в блоке.
Предположим, что вы хотите изменить x
в padding
, но не offset
:
{
padding: {
x: 2,
y: 0
},
offset: {
x: 0,
y: 1
}
}
Сначала выберите блок от padding: {
до }
sed -r '/padding: \{/,/\}/ {
# и внутри блока вы заменяете значение x:
s/^( +x:).*/\1 1,/
}'
Это также работает, чтобы ответить на вопрос, хотя это не так элегантно, как пример JSON:
echo -e 'Это\na тест\nПожалуйста, не\nпугайтесь' | sed -r '
/a тест/,/Пожалуйста, не/ {
s/a тест/не тест/
s/Пожалуйста, не/Будьте настороже/
}'
Другой вариант – диапазоны sed. Я не уверен, что это применимо к этому конкретному вопросу, но поскольку я попал сюда и нашел диапазоны sed полезными в своем случае, я нахожу ценным поделиться следующим ответом здесь: Использование sed для замены многострочного текста
Ответ или решение
Использование sed
для замены многострочных строк может показаться сложной задачей, но, при должном понимании его команд и структуры, это вполне возможно. В этой статье мы подробно рассмотрим, как выполнить замену многострочной строки с помощью sed
, приведем практические примеры и различные подходы для решения этой задачи.
Проблема с sed
и многострочными строками
По умолчанию sed
обрабатывает текст построчно. Это значит, что при попытке выполнения замены с использованием новостных символов (\n
) в регулярном выражении вы не получите ожидаемого результата, поскольку sed
по умолчанию не включает переносы строк в область поиска.
Пример проблемы
Рассмотрим следующий пример:
cat > alpha.txt << EOF
Это тест
Пожалуйста, не волнуйтесь
Не пугайтесь
EOF
sed -i'.original' 's/тест\nПожалуйста, не волнуйтесь/не тест\nБудьте внимательны/' alpha.txt
В этом случае sed
не выполнит замену, так как \n
не воспринимается как символ переноса строки в области поиска.
Решения для работы с многострочными строками
Есть несколько способов обойти эту проблему.
1. Использование команды N
Один из наиболее прямолинейных способов – использовать команду N
, которая объединяет текущую строку с следующей в одну область поиска:
sed '/тест$/ {
N
s/тест\nПожалуйста, не волнуйтесь/не тест\nБудьте внимательны/
}' alpha.txt
Эта команда сначала ищет строку, заканчивающуюся на "тест", затем объединяет ее с следующей строкой и выполняет замену. После выполнения замены вы можете использовать команду P
для вывода результата.
2. Использование флага -z
Если вы используете GNU sed
, командный интерфейс поддерживает флаг -z
, который позволяет обрабатывать весь входной файл как одну строку, разделенную символами NUL вместо переносов строк, что позволяет использовать как \n
, так и другие символы. Пример:
sed -z 's/тест\nПожалуйста, не волнуйтесь/не тест\nБудьте внимательны/' -i alpha.txt
Этот способ наиболее удобен, так как он позволяет просто указать новую строку, как и в стандартных выражениях замены.
3. Использование awk
Если sed
не подходит под ваши нужды, вы можете рассмотреть использование awk
, который также позволяет более гибко работать с текстом. С помощью awk
вы можете использовать разделитель записи (например, пустую строку) для замены:
awk -v RS= -v ORS='\n' '{
gsub(/тест\nПожалуйста, не волнуйтесь/, "не тест\nБудьте внимательны");
print;
}' alpha.txt > output.txt
Такой подход позволяет работать с текстовыми блоками, облегчая замену многострочных строк.
Заключение
Использование sed
для замены многострочных строк может требовать немного больше усилий, чем работа с однострочными строками, однако, зная подходы и команды, можно добиваться желаемых результатов. Команды N
и -z
, а также альтернативные методы, такие как awk
, дают вам мощные инструменты для манипуляции текстом на уровне строк и объединения их в соответствии с вашими потребностями.
Это знание будет полезно не только в повседневной работе с текстовыми файлами, но и в ситуациях, когда необходимо выполнять сложные текстовые манипуляции.