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

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

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

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

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

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

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

Ответ 1

 # 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 

Источник

Ответ 2

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

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

     Apple       Banana
     Orange      Orange
     Apple       Apple

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

sed '0,/Apple/{s/Apple/Banana/}' filename

Ответ 3

sed '0,/pattern/s/pattern/replacement/' filename

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

пример

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

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

Ответ 4

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

Приведенные здесь примеры используют упрощенный вариант использования: замените слово "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/ address с 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-й строки.

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

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

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

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

И наоборот, если в 1-й строке нет совпадений 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

Техника цикла 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/ - конец диапазона - затем ищется только начиная со следующей строки.Следовательно, в этом случае выбираются обе строки, и подстановка 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 special 0,/re/ syntax, любой диапазон, начинающийся с номера строки, эффективно исключает использование //.

Ответ 5

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

Ответ 6

Довольно полный набор ответов на 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

Ответ 7

#!/bin/sed -f
1,/^#include/ {
    /^#include/i\
#include "newfile.h"
}

Как работает этот script: для строк между 1 и первым #include (после строки 1), если строка начинается с #include, затем добавьте указанную строку.

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

Ответ 8

Просто добавьте число вхождений в конец:

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

Ответ 9

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

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

Пояснение:

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

Ответ 10

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

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

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

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

Ответ 11

Если кто-то пришел сюда, чтобы заменить символ для первого появления во всех строках (например, я сам), используйте это:

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, вы можете вместо этого заменить только вторую.

Ответ 12

Я сделал бы это с помощью awk script:

BEGIN {i=0}
(i==0) && /#include/ {print "#include \"newfile.h\""; i=1}
{print $0}    
END {}

затем запустите его с помощью awk:

awk -f awkscript headerfile.h > headerfilenew.h

может быть неаккуратным, я новичок в этом.

Ответ 13

В качестве альтернативного варианта вы можете посмотреть команду 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

Ответ 14

Наконец, я получил это, чтобы работать в Bash script, который использовался для вставки уникальной метки времени в каждый элемент в фиде RSS:

        sed "1,/====RSSpermalink====/s/====RSSpermalink====/${nowms}/" \
            production-feed2.xml.tmp2 > production-feed2.xml.tmp.$counter

Он изменяет только первое вхождение.

${nowms} - это время в миллисекундах, установленное Perl script, $counter - это счетчик, используемый для управления контуром в script, \ позволяет продолжить эту команду на следующей строке.

Файл читается, а stdout перенаправляется в рабочий файл.

Как я понимаю, 1,/====RSSpermalink====/ сообщает sed, когда останавливается, устанавливая ограничение диапазона, а затем s/====RSSpermalink====/${nowms}/ - это знакомая команда sed, которая заменяет первую строку вторым.

В моем случае я помещаю команду в двойные кавычки, потому что я использую ее в Bash script с переменными.

Ответ 15

Использование FreeBSD ed и избежать ошибки ed "no match" в случае, если в обрабатываемом файле нет include:

teststr='
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
'

# using FreeBSD ed
# to avoid ed "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

Ответ 16

Это может сработать для вас (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...

Ответ 17

С опцией GNU sed -z вы можете обработать весь файл, как если бы он был только одной строкой. Таким образом, s/…/…/ заменит только первое совпадение во всем файле. Помните: s/…/…/ заменяет только первое совпадение в каждой строке, но с -z опции -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|$)//.

Ответ 18

Следующая команда удаляет первое вхождение строки внутри файла. Он также удаляет пустую строку. Он представлен в 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>

ps: он не работал у меня в Solaris SunOS 5.10 (довольно старый), но он работает на Linux 2.6, sed версии 4.1.5

Ответ 19

Ничего нового, но, возможно, немного более конкретного ответа: 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

Ответ 20

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.

Ответ 21

Возможный вариант использования может заключаться в том, что ваши случаи распространяются по всему файлу, но вы знаете, что ваша единственная проблема - в первых 10, 20 или 100 строках.

Тогда простая адресация этих строк устраняет проблему - даже если формулировка ОП касается только первой.

sed '1,10s/#include/#include "newfile.h"\n#include/'

Ответ 22

sed имеет очень простой синтаксис для этого: "-i" является интерактивным (нет необходимости в новом файле). Чтобы заменить только первый экземпляр:

sed -i 's/foo/bar/' file

чтобы заменить глобально, вы использовали бы

sed -i 's/foo/bar/g' file

В вашем примере я бы использовал (^ и $- начало и конец строки соответственно)

sed -i 's/^#include/#include\n#include/' file