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

StackOverflow https://stackoverflow.com/questions/148451

Вопрос

Я хотел бы обновить большое количество исходных файлов 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.
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top