Sed заменить последний шаблон соответствия линии

Для такого файла:

a
b
a
b

Я бы хотел использовать sed для замены только последней строки, содержащей экземпляр "a" в файле. Поэтому, если бы я захотел заменить его на "c", результат должен выглядеть так:

a
b
c
b

Обратите внимание, что мне нужно, чтобы это работало независимо от того, сколько совпадений он может встретить, или подробности о том, какими должны быть требуемые шаблоны или содержимое файла. Спасибо заранее.

Ответ 1

Не совсем sed:

tac file | sed '/a/ {s//c/; :loop; n; b loop}' | tac

Тестирование

% printf "%s\n" a b a b a b | tac | sed '/a/ {s//c/; :loop; n; b loop}' | tac
a
b
a
b
c
b

Отмените файл, затем для соответствия first, выполните замену, а затем безоговорочно разложите остальную часть файла. Затем переверните файл.

Обратите внимание: пустое регулярное выражение (здесь как s//c/) означает повторное использование предыдущего регулярного выражения (/a/)

Я не большой поклонник сериала, за пределами очень простых программ. Я бы использовал awk:

tac file | awk '/a/ && !seen {sub(/a/, "c"); seen=1} 1' | tac

Ответ 2

Другой:

tr '\n' ' ' | sed 's/\(.*\)a/\1c/' | tr ' ' '\n'

в действии:

$ printf "%s\n" a b a b a b | tr '\n' ' ' | sed 's/\(.*\)a/\1c/' | tr ' ' '\n'
a
b
a
b
c
b

Ответ 3

Это может сработать для вас (GNU sed):

sed -r '/^PATTERN/!b;:a;$!{N;/^(.*)\n(PATTERN.*)/{h;s//\1/p;g;s//\2/};ba};s/^PATTERN/REPLACEMENT/' file

или другим способом:

sed '/^PATTERN/{x;/./p;x;h;$ba;d};x;/./{x;H;$ba;d};x;b;:a;x;/./{s/^PATTERN/REPLACEMENT/p;d};x' file

или, если хотите:

sed -r ':a;$!{N;ba};s/^(.*\n?)PATTERN/\1REPLACEMENT/' file

Ответ 4

Другой подход:

sed "`grep -n '^a$' a | cut -d \: -f 1 | tail -1`s/a/c/" a

Преимущество этого подхода заключается в том, что вы дважды запускаете файл дважды, а не читаете его в памяти. Это может иметь смысл в больших файлах.

Ответ 5

Решение с двумя проходами при буферизации всего ввода недопустимо:

sed "$(sed -n /a/= file | sed -n '$s/$/ s,a,c,/p' )" file

(более ранняя версия этой ошибки попала в ошибку с расширением истории, обнаруженным при установке redhat bash -4.1, таким образом избегает $!d, который ошибочно расширялся.)

Однопроходное решение, которое буферизирует как можно меньше:

sed '/a/!{1h;1!H};/a/{x;1!p};$!d;g;s/a/c/'

Простейшие:

tac | sed '0,/a/ s/a/c/' | tac

Ответ 6

Здесь все сделано в одном awk

awk 'FNR==NR {if ($0~/a/) f=NR;next} FNR==f {$0="c"} 1' file file
a
b
c
b

Это дважды читает файл. Сначала запустите, чтобы найти последний a, второй запустить, чтобы изменить его.

Ответ 7

Здесь много хороших ответов; здесь концептуально простое двухпроходное sed решение с поддержкой tail, которое совместимо с POSIX и не считывает весь файл в память, подобно подход Эрана Бен-Натана:

sed "$(sed -n '/a/ =' file | tail -n 1)"' s/a/c/' file
  • sed -n '/a/=' file выводит номера строк (функция =), соответствующие regex a, а tail -n 1 извлекает выходную последнюю строку, то есть номер строки в файле file, содержащий последнее вхождение регулярного выражения.

  • Размещение замены подстановки $(sed -n '/a/=' file | tail -n 1) непосредственно перед ' s/a/c' приводит к внешнему sed script, например 3 s/a/c/ (с образцом ввода), который выполняет желаемую замену только на последнем, на котором возникло регулярное выражение.

Если шаблон не найден во входном файле, вся команда является эффективной no-op.

Ответ 8

tac infile.txt | sed "s/a/c/; ta ; b ; :a ; N ; ba" | tac

Первый tac меняет линии infile.txt, выражение sed (см. fooobar.com/questions/51302/...) заменяет первое совпадение 'a' с 'c' и печатает оставшиеся строки, а последний tac возвращает строки обратно в исходный порядок.

Ответ 9

Вот только с помощью awk:

awk '{a[NR]=$1}END{x=NR;cnt=1;while(x>0){a[x]=((a[x]=="a"&&--cnt==0)?"c <===":a[x]);x--};for(i=1;i<=NR;i++)print a[i]}' file
$ cat f
a
b
a
b
f
s
f
e
a
v
$ awk '{a[NR]=$1}END{x=NR;cnt=1;while(x>0){a[x]=((a[x]=="a"&&--cnt==0)?"c <===":a[x]);x--};for(i=1;i<=NR;i++)print a[i]}' f
a
b
a
b
f
s
f
e
c <===
v

Ответ 10

Это также можно сделать в perl:

perl -e '@a=reverse<>;END{for(@a){if(/a/){s/a/c/;last}}print reverse @a}' temp > your_new_file

Испытано:

> cat temp
a
b
c
a
b
> perl -e '@a=reverse<>;END{for(@a){if(/a/){s/a/c/;last}}print reverse @a}' temp
a
b
c
c
b
> 

Ответ 11

Здесь команда:

sed '$s/.*/a/' filename.txt

И здесь он находится в действии:

> echo "a
> b
> a
> b" > /tmp/file.txt

> sed '$s/.*/a/' /tmp/file.txt
a
b
a
a

Ответ 12

Вот еще один вариант:

sed -e '$ a a' -e '$ d' file 

Первая команда добавляет a, а вторая удаляет последнюю строку. На странице sed(1):

$ Соответствует последней строке.

d Удалить пространство шаблонов. Начать следующий цикл.

a text Добавить текст, в котором каждая внедренная новая строка предшествует обратную косую черту.

Ответ 13

awk - единственное решение:

awk '/a/{printf "%s", all; all=$0"\n"; next}{all=all $0"\n"} END {sub(/^[^\n]*/,"c",all); printf "%s", all}' file

Объяснение:

  • Когда строка соответствует a, все строки между предыдущим a до (не включая) текущим a (т.е. содержимое, хранящееся в переменной all) печатается
  • Если строка не соответствует a, она добавляется к переменной all.
  • Последняя строка, соответствующая a, не сможет напечатать свой контент all, поэтому вы вручную распечатаете ее в блоке END. До этого вы можете подставить строку, соответствующую a, тем, что вы хотите.

Ответ 14

Дано:

$ cat file
a
b
a
b

Вы можете использовать POSIX grep для подсчета совпадений:

$ grep -c '^a' file
2

Затем введите это число в awk, чтобы напечатать замену:

$ awk -v last=$(grep -c '^a' file) '/^a/ && ++cnt==last{ print "c"; next } 1' file
a
b
c
b