Как использовать sed, чтобы заменить только первое вхождение в файле? - PullRequest
183 голосов
/ 29 сентября 2008

Я бы хотел обновить большое количество исходных файлов C ++ с помощью дополнительной директивы include перед любыми существующими #include. Для такого рода задач я обычно использую небольшой скрипт bash с sed, чтобы переписать файл.

Как получить sed, чтобы заменить только первое вхождение строки в файле, а не заменять каждое вхождение?

Если я использую

sed s/#include/#include "newfile.h"\n#include/

заменяет все #include.

Альтернативные предложения для достижения того же самого также приветствуются.

Ответы [ 20 ]

251 голосов
/ 26 февраля 2012

Напишите сценарий sed, который заменит только первое появление «Apple» на «Banana»

Пример ввода: Вывод:

     Apple       Banana
     Orange      Orange
     Apple       Apple

Это простой скрипт: Примечание редактора: работает только с GNU sed.

sed '0,/Apple/{s/Apple/Banana/}' filename
114 голосов
/ 29 сентября 2008
 # 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---

или, если хотите: Примечание редактора: работает только с GNU sed.

sed '0,/RE/s//to_that/' file 

Источник

53 голосов
/ 17 августа 2010
sed '0,/pattern/s/pattern/replacement/' filename

это сработало для меня.

пример

sed '0,/<Menu>/s/<Menu>/<Menu><Menu>Sub menu<\/Menu>/' try.txt > abc.txt

Примечание редактора: оба работают только с GNU sed.

35 голосов
/ 29 октября 2015

Обзор из множества полезных существующих ответов , дополненный пояснениями :

В приведенных здесь примерах используется упрощенный вариант использования: замените слово «foo» на «bar» только в первой соответствующей строке.
Из-за использования строк в кавычках ANSI C ($'...') для предоставления примерных строк ввода в качестве оболочки предполагается bash, ksh или zsh.


GNU sed только:

Ответ Бен Хоффштейна показывает нам, что GNU предоставляет расширение спецификации POSIX для sed, которая допускает следующую двухадресную форму: 0,/re/ (re представляет здесь произвольное регулярное выражение).

0,/re/ позволяет регулярному выражению соответствовать в самой первой строке также . Другими словами: такой адрес создаст диапазон от 1-й строки до и включая строку, которая соответствует re - независимо от того, встречается ли re в 1-й строке или в любой последующей строке.

  • Сравните это с POSIX-совместимой формой 1,/re/, которая создает диапазон, который совпадает с 1-й строки до и включая строку, которая соответствует re на , следующих линии; другими словами: этот не будет обнаруживать первое вхождение совпадения re, если оно произойдет в 1-й строке , а также предотвращает использование сокращения // для повторного использования последнего использованного регулярного выражения (см. Следующий пункт). [1]

Если вы объедините адрес 0,/re/ с вызовом s/.../.../ (подстановка), который использует регулярное выражение с тем же , ваша команда будет эффективно выполнять подстановку только для first строка, которая соответствует 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

A только для POSIX-функций sed, например, BSD (macOS) sed (также будет работать с GNU sed):

Поскольку 0,/re/ нельзя использовать и форма 1,/re/ не обнаружит re, если это произойдет в самой первой строке (см. Выше), требуется специальная обработка для 1-й строки .

В ответе MikhailVS упоминается техника, приведенная в конкретном примере здесь:

$ 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 используется неявно, что позволяет нам не дублировать его, что делает как более короткий, так и более понятный код.

  • POSIX sed нужны фактические символы новой строки после определенных функций, например после имени метки или даже ее пропуска, как в случае с t здесь; Стратегическое разделение сценария на несколько вариантов -e является альтернативой использованию фактических символов новой строки: заканчивайте каждый фрагмент сценария -e там, где обычно требуется переход на новую строку.

1 s/foo/bar/ заменяет foo только на 1-й строке, если она там найдена. Если это так, t ветвится до конца скрипта (пропускает оставшиеся команды в строке). (Функция t переходит к метке, только если последний вызов s выполнил фактическую замену; при отсутствии метки, как в данном случае, конец сценария разветвляется).

Когда это произойдет, адрес диапазона 1,//, который обычно находит первое вхождение , начиная со строки 2 , будет не совпадать, а диапазон будет не обрабатываться, потому что адрес вычисляется, когда текущая строка уже 2.

И наоборот, если в 1-й строке нет совпадений, 1,// будет введено и найдет истинное первое совпадение.

Чистый эффект такой же, как у GNU sed 0,/re/: заменяется только первое вхождение, происходит ли оно в 1-й строке или в любом другом.


Подходы без дальности действия

ответ Потонга демонстрирует цикл техники , которые обходят необходимость в диапазоне ; поскольку он использует синтаксис GNU 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

Loop техника 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 соответствует 1-й строке, а регулярное выражение /foo/ - конец диапазона - затем ищется только для запуска на next линия. Следовательно, обе строки выбраны в этом случае, и замена 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 запускается range), регулярное выражение еще не применено, поэтому // ни к чему не относится.
За исключением специального синтаксиса 0,/re/ GNU sed, любой диапазон , начинающийся с номера строки , фактически исключает использование //.

23 голосов
/ 29 сентября 2008

Вы можете использовать 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: -)

16 голосов
/ 12 июля 2012

Довольно полный набор ответов на linuxtopia sed FAQ . Это также подчеркивает, что некоторые ответы, которые предоставили люди, не будут работать с не-GNU версией sed, например

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
12 голосов
/ 29 сентября 2008
#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}

Как работает этот сценарий: для строк от 1 до первого #include (после строки 1), если строка начинается с #include, то перед указанной строкой ставится.

Однако, если первый #include находится в строке 1, то и строка 1, и следующая последующая #include будут иметь префиксную строку. Если вы используете GNU sed, у него есть расширение, где 0,/^#include/ (вместо 1,) будет делать правильные вещи.

12 голосов
/ 29 сентября 2008

Просто добавьте номер вхождения в конце:

sed s/#include/#include "newfile.h"\n#include/1
8 голосов
/ 29 сентября 2008

Возможное решение:

    /#include/!{p;d;}
    i\
    #include "newfile.h"
    :
    n
    b

Пояснение:

  • читать строки, пока мы не найдем #include, вывести эти строки и начать новый цикл
  • вставить новую строку включения
  • введите цикл, который просто читает строки (по умолчанию sed также будет печатать эти строки), отсюда мы не вернемся к первой части скрипта
3 голосов
/ 24 мая 2015

Я знаю, что это старый пост, но у меня было решение, которое я использовал:

grep -E -m 1 -n 'old' file | sed 's/:.*$//' - | sed 's/$/s\/old\/new\//' - | sed -f - file

В основном используйте grep, чтобы найти первый случай и остановиться там. Также выведите номер строки, т.е. 5: строка. Передайте это в sed и удалите: и что-нибудь после, чтобы у вас остался номер строки. Передайте это в sed, который добавляет s / .*/ replace до конца, что дает 1-строчный скрипт, который передается в последний sed для запуска в качестве сценария в файле.

, так что если regex = #include и replace = blah и первый поиск grep вхождения находится в строке 5, то данные, переданные в последний sed, будут 5s /.*/ blah /.

...