Как использовать sed для замены только первого вхождения в файле?
-
02-07-2019 - |
Вопрос
Я хотел бы обновить большое количество исходных файлов C++, добавив дополнительную директиву include перед любыми существующими директивами #include.Для задач такого рода я обычно использую небольшой скрипт bash с sed для перезаписи файла.
Как я могу получить sed
заменить только первое вхождение строки в файле, а не заменять каждое вхождение?
Если я использую
sed s/#include/#include "newfile.h"\n#include/
он заменяет все #includes.
Альтернативные предложения для достижения той же цели также приветствуются.
Решение
# sed script to change "foo" to "bar" only on the first occurrence
1{x;s/^/first/;x;}
1,/foo/{x;/first/s///;x;s/foo/bar/;}
#---end of script---
или, если хотите: Примечание редактора:работает с ГНУ sed
только.
sed '0,/RE/s//to_that/' file
Другие советы
Напишите сценарий sed, который будет заменять только первое появление «Apple» на «Banana».
Пример ввода:Выход:
Apple Banana
Orange Orange
Apple Apple
Это простой скрипт: Примечание редактора:работает с ГНУ sed
только.
sed '0,/Apple/{s/Apple/Banana/}' filename
sed '0,/pattern/s/pattern/replacement/' filename
это сработало для меня.
пример
sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt
Примечание редактора:оба работают с ГНУ sed
только.
Ан обзор из многих полезных существующие ответы, дополненный объяснения:
В приведенных здесь примерах используется упрощенный вариант использования:замените слово «foo» на «bar» только в первой совпадающей строке.
Из-за использования Строки в кавычках ANSI C ($'...'
) чтобы предоставить образцы входных строк, bash
, ksh
, или zsh
предполагается как оболочка.
ГНУ sed
только:
Ответ Бена Хоффштейна показывает нам, что GNU предоставляет расширение к Спецификация POSIX для sed
что позволяет использовать следующую двухадресную форму: 0,/re/
(re
здесь представляет собой произвольное регулярное выражение).
0,/re/
позволяет регулярному выражению соответствовать на самой первой линии тоже.Другими словами:такой адрес создаст диапазон от 1-й строки до строки, которая соответствует re
- ли re
встречается в 1-й строке или в любой последующей строке.
- Сравните это с POSIX-совместимой формой.
1,/re/
, который создает диапазон, соответствующий от первой строки до соответствующей строки включительноre
на последующий линии;другими словами:этот не обнаружит первое появлениеre
совпадение, если оно происходит на 1-й линия а также предотвращает использование сокращений//
для повторного использования последнего использованного регулярного выражения (см. следующий пункт).[1]
Если вы объедините 0,/re/
адрес с s/.../.../
(замещения) вызов, который использует такой же регулярное выражение, ваша команда фактически выполнит замену только первый строка, которая соответствует re
.
sed
обеспечивает удобный ярлык для повторного использования последнего примененного регулярного выражения:а пустой пара разделителей, //
.
$ sed '0,/foo/ s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo
Только функции POSIX sed
например BSD (macOS) sed
(также будет работать с ГНУ sed
):
С 0,/re/
нельзя использовать и форма 1,/re/
не обнаружит re
если это происходит в самой первой строке (см. выше), требуется специальная обработка для 1-й линии.
Ответ МихаилВС упоминает технику, приведенную здесь в конкретном примере:
$ sed -e '1 s/foo/bar/; t' -e '1,// s//bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar # only 1st match of 'foo' replaced
Unrelated
2nd foo
3rd foo
Примечание:
Пустое регулярное выражение
//
ярлык здесь используется дважды:один раз для конечной точки диапазона и один раз вs
вызов;в обоих случаях регулярное выражениеfoo
неявно используется повторно, что позволяет нам не дублировать его, что делает код короче и удобнее в сопровождении.ПОСИКС
sed
нужны настоящие символы новой строки после определенных функций, например, после имени метки или даже ее отсутствия, как в случае сt
здесь;стратегически разбить сценарий на несколько-e
options — это альтернатива использованию фактического символа новой строки:заканчивать каждый-e
фрагмент сценария, в котором обычно должен находиться перевод строки.
1 s/foo/bar/
заменяет foo
только в 1-й строке, если она там найдена.Если так, t
переходит в конец скрипта (пропускает оставшиеся команды в строке).( t
функция переходит к метке, только если самая последняя s
вызов выполнил фактическую замену;при отсутствии метки, как в данном случае, разветвляется конец скрипта).
Когда это произойдет, адрес диапазона 1,//
, который обычно находит первое вхождение начиная со второй строки, воля нет совпадение, и диапазон будет нет быть обработан, поскольку адрес оценивается, когда текущая строка уже 2
.
И наоборот, если в первой строке нет совпадения, 1,//
воля будет введен и найдет истинное первое совпадение.
Конечный эффект такой же, как и при использовании GNU. sed
's 0,/re/
:заменяется только первое вхождение, независимо от того, встречается ли оно в 1-й строке или в любой другой.
НЕдиапазонные подходы
ответ Потонга демонстрирует петля методы что обойти необходимость в диапазоне;поскольку он использует ГНУ sed
синтаксис, вот POSIX-совместимые эквиваленты:
Техника петли 1:В первом матче выполните замену, затем введите цикл, который просто печатает оставшиеся строки как есть:
$ sed -e '/foo/ {s//bar/; ' -e ':a' -e '$!{n;ba' -e '};}' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo
Техника петли 2, для только небольшие файлы: прочитать весь ввод в память, затем выполнить над ним одну замену.
$ sed -e ':a' -e '$!{N;ba' -e '}; s/foo/bar/' <<<$'1st foo\nUnrelated\n2nd foo\n3rd foo'
1st bar
Unrelated
2nd foo
3rd foo
[1] 1.61803 приводит примеры того, что происходит с 1,/re/
, с последующим и без него s//
:
- sed '1,/foo/ s/foo/bar/' <<<$'1foo\n2foo'
урожайность $'1bar\n2bar'
;то есть, оба строки были обновлены, поскольку номер строки 1
соответствует первой строке и регулярному выражению /foo/
- конец диапазона - тогда ищется только начиная с следующий линия.Поэтому, оба в этом случае выбираются линии, а s/foo/bar/
замена производится на обоих из них.
- sed '1,/foo/ s//bar/' <<<$'1foo\n2foo\n3foo'
терпит неудачу:с sed: first RE may not be empty
(BSD/macOS) и sed: -e expression #1, char 0: no previous regular expression
(GNU), поскольку в момент обработки 1-й строки (из-за номера строки 1
начиная с диапазона), регулярное выражение еще не применялось, поэтому //
ни к чему не относится.
За исключением GNU sed
особенный 0,/re/
синтаксис, любой диапазон, который начинается с номер строки эффективно исключает использование //
.
Вы можете использовать awk, чтобы сделать что-то подобное.
awk '/#include/ && !done { print "#include \"newfile.h\""; done=1;}; 1;' file.c
Объяснение:
/#include/ && !done
Запускает оператор действия между {}, когда строка соответствует «#include», и мы еще не обработали ее.
{print "#include \"newfile.h\""; done=1;}
Это печатает #include "newfile.h", нам нужно экранировать кавычки.Затем мы устанавливаем для переменной Done значение 1, чтобы не добавлять дополнительные включения.
1;
Это означает «распечатать строку» — пустое действие по умолчанию печатает $0, что печатает всю строку.Однострочный и его легче понять, чем sed IMO :-)
Довольно обширная коллекция ответов на Часто задаваемые вопросы по linuxtopia sed.Также подчеркивается, что некоторые ответы, предоставленные людьми, не будут работать с версией sed, отличной от GNU, например
sed '0,/RE/s//to_that/' file
в версии, отличной от GNU, должно быть
sed -e '1s/RE/to_that/;t' -e '1,/RE/s//to_that/'
Однако эта версия не будет работать с gnu sed.
Вот версия, которая работает с обоими:
-e '/RE/{s//to_that/;:a' -e '$!N;$!ba' -e '}'
бывший:
sed -e '/Apple/{s//Banana/;:a' -e '$!N;$!ba' -e '}' filename
Просто добавьте количество вхождений в конце:
sed s/#include/#include "newfile.h"\n#include/1
#!/bin/sed -f
1,/^#include/ {
/^#include/i\
#include "newfile.h"
}
Как работает этот скрипт:Для строк между 1 и первой #include
(после строки 1), если строка начинается с #include
, затем добавьте указанную строку.
Однако, если первый #include
находится в строке 1, затем обе строки 1 и следующая за ней #include
будет добавлена строка.Если вы используете GNU sed
, у него есть расширение, где 0,/^#include/
(вместо 1,
) поступит правильно.
Возможное решение:
/#include/!{p;d;}
i\
#include "newfile.h"
:
n
b
Объяснение:
- читаем строки, пока не найдем #include, распечатываем эти строки и начинаем новый цикл
- вставьте новую строку включения
- введите цикл, который просто читает строки (по умолчанию sed также печатает эти строки), отсюда мы не вернемся к первой части скрипта
Я знаю, что это старый пост, но у меня было решение, которое я использовал:
grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file
По сути, используйте grep, чтобы найти первое вхождение и остановиться на нем.Также напечатайте номер строки, т.е. 5:line.Перенесите это в sed и удалите:и все, что после этого, у вас останется только номер строки.Передайте это в sed, который добавляет s/.*/replace в конец, что дает однострочный сценарий, который передается в последний sed для запуска как сценарий в файле.
поэтому, если regex = #include и replace = blah и первое обнаруженное grep вхождение находится в строке 5, тогда данные, передаваемые по конвейеру в последний sed, будут 5s/.*/blah/.
Если кто-то пришел сюда, чтобы заменить символ при первом вхождении во всех строках (как я), используйте это:
sed '/old/s/old/new/1' file
-bash-4.2$ cat file
123a456a789a
12a34a56
a12
-bash-4.2$ sed '/a/s/a/b/1' file
123b456a789a
12b34a56
b12
Например, изменив 1 на 2, вы можете вместо этого заменить только все вторые буквы.
я бы сделал это с помощью awk-скрипта:
BEGIN {i=0}
(i==0) && /#include/ {print "#include \"newfile.h\""; i=1}
{print $0}
END {}
затем запустите его с помощью awk:
awk -f awkscript headerfile.h > headerfilenew.h
может быть небрежно, я новичок в этом.
В качестве альтернативного предложения вы можете посмотреть ed
команда.
man 1 ed
teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'
# for in-place file editing use "ed -s file" and replace ",p" with "w"
# cf. http://wiki.bash-hackers.org/howto/edit-ed
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
H
/# *include/i
#include "newfile.h"
.
,p
q
EOF
Наконец я заставил это работать в сценарии Bash, который используется для вставки уникальной отметки времени в каждый элемент RSS-канала:
sed "1,/====RSSpermalink====/s/====RSSpermalink====/${nowms}/" \
production-feed2.xml.tmp2 > production-feed2.xml.tmp.$counter
Это изменяет только первое вхождение.
${nowms}
это время в миллисекундах, установленное скриптом Perl, $counter
счетчик, используемый для управления циклом внутри скрипта, \
позволяет продолжить команду на следующей строке.
Файл считывается, и стандартный вывод перенаправляется в рабочий файл.
Насколько я это понимаю, 1,/====RSSpermalink====/
сообщает sed, когда остановиться, устанавливая ограничение диапазона, а затем s/====RSSpermalink====/${nowms}/
это знакомая команда sed для замены первой строки на вторую.
В моем случае я заключил команду в двойные кавычки, потому что использую ее в сценарии Bash с переменными.
С использованием FreeBSD ed
и избегать ed
ошибка «нет совпадения» в случае отсутствия include
оператор в файле, который будет обработан:
teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'
# using FreeBSD ed
# to avoid ed's "no match" error, see
# *emphasized text*http://codesnippets.joyent.com/posts/show/11917
cat <<-'EOF' | sed -e 's/^ *//' -e 's/ *$//' | ed -s <(echo "$teststr")
H
,g/# *include/u\
u\
i\
#include "newfile.h"\
.
,p
q
EOF
Это может сработать для вас (GNU sed):
sed -si '/#include/{s//& "newfile.h\n&/;:a;$!{n;ba}}' file1 file2 file....
или если память не проблема:
sed -si ':a;$!{N;ba};s/#include/& "newfile.h\n&/' file1 file2 file...
С помощью GNU sed -z
вы можете обработать весь файл, как если бы это была всего лишь одна строка.Таким образом s/…/…/
заменит только первое совпадение во всем файле.Помнить: s/…/…/
заменяет только первое совпадение в каждой строке, но с -z
вариант sed
рассматривает весь файл как одну строку.
sed -z 's/#include/#include "newfile.h"\n#include'
В общем случае вам придется переписать выражение sed, поскольку пространство шаблонов теперь содержит весь файл, а не только одну строку.Некоторые примеры:
s/text.*//
можно переписать какs/text[^\n]*//
.[^\n]
соответствует всему кроме символ новой строки.[^\n]*
будет соответствовать всем символам послеtext
пока не будет достигнута новая строка.s/^text//
можно переписать какs/(^|\n)text//
.s/text$//
можно переписать какs/text(\n|$)//
.
Следующая команда удаляет первое вхождение строки в файле.Он также удаляет пустую строку.Он представлен в виде XML-файла, но будет работать с любым файлом.
Полезно, если вы работаете с XML-файлами и хотите удалить тег.В этом примере удаляется первое вхождение тега isTag.
Команда:
sed -e 0,/'<isTag>false<\/isTag>'/{s/'<isTag>false<\/isTag>'//} -e 's/ *$//' -e '/^$/d' source.txt > output.txt
Исходный файл (source.txt)
<xml>
<testdata>
<canUseUpdate>true</canUseUpdate>
<isTag>false</isTag>
<moduleLocations>
<module>esa_jee6</module>
<isTag>false</isTag>
</moduleLocations>
<node>
<isTag>false</isTag>
</node>
</testdata>
</xml>
Файл результатов (output.txt)
<xml>
<testdata>
<canUseUpdate>true</canUseUpdate>
<moduleLocations>
<module>esa_jee6</module>
<isTag>false</isTag>
</moduleLocations>
<node>
<isTag>false</isTag>
</node>
</testdata>
</xml>
пс:у меня это не сработало на Solaris SunOS 5.10 (довольно старом), но работает на Linux 2.6, sed версии 4.1.5
Ничего нового, но, возможно, немного более конкретный ответ: sed -rn '0,/foo(bar).*/ s%%\1%p'
Пример: xwininfo -name unity-launcher
производит вывод, например:
xwininfo: Window id: 0x2200003 "unity-launcher"
Absolute upper-left X: -2980
Absolute upper-left Y: -198
Relative upper-left X: 0
Relative upper-left Y: 0
Width: 2880
Height: 98
Depth: 24
Visual: 0x21
Visual Class: TrueColor
Border width: 0
Class: InputOutput
Colormap: 0x20 (installed)
Bit Gravity State: ForgetGravity
Window Gravity State: NorthWestGravity
Backing Store State: NotUseful
Save Under State: no
Map State: IsViewable
Override Redirect State: no
Corners: +-2980+-198 -2980+-198 -2980-1900 +-2980-1900
-geometry 2880x98+-2980+-198
Извлечение идентификатора окна с помощью xwininfo -name unity-launcher|sed -rn '0,/^xwininfo: Window id: (0x[0-9a-fA-F]+).*/ s%%\1%p'
производит:
0x2200003
POSIXly (также допустимо в sed), Только один используется регулярное выражение, нужна память только для одной строки (как обычно):
sed '/\(#include\).*/!b;//{h;s//\1 "newfile.h"/;G};:1;n;b1'
Объяснено:
sed '
/\(#include\).*/!b # Only one regex used. On lines not matching
# the text `#include` **yet**,
# branch to end, cause the default print. Re-start.
//{ # On first line matching previous regex.
h # hold the line.
s//\1 "newfile.h"/ # append ` "newfile.h"` to the `#include` matched.
G # append a newline.
} # end of replacement.
:1 # Once **one** replacement got done (the first match)
n # Loop continually reading a line each time
b1 # and printing it by default.
' # end of sed script.