简体   繁体   中英

sed replace last line matching pattern

Given a file like this:

a
b
a
b

I'd like to be able to use sed to replace just the last line that contains an instance of "a" in the file. So if I wanted to replace it with "c", then the output should look like:

a
b
c
b

Note that I need this to work irrespective of how many matches it might encounter, or the details of exactly what the desired pattern or file contents might be. Thanks in advance.

Not quite sed only:

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

testing

% 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

Reverse the file, then for the first match, make the substitution and then unconditionally slurp up the rest of the file. Then re-reverse the file.

Note, an empty regex (here as s//c/ ) means re-use the previous regex ( /a/ )

I'm not a huge sed fan, beyond very simple programs. I would use awk:

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

Many good answers here; here's a conceptually simple two-pass sed solution assisted by tail that is POSIX-compliant and doesn't read the whole file into memory, similar to Eran Ben-Natan's approach :

sed "$(sed -n '/a/ =' file | tail -n 1)"' s/a/c/' file
  • sed -n '/a/=' file outputs the numbers of the lines (function = ) matching regex a , and tail -n 1 extracts the output's last line, ie the number of the line in file file containing the last occurrence of the regex.

  • Placing command substitution $(sed -n '/a/=' file | tail -n 1) directly before ' s/a/c' results in an outer sed script such as 3 s/a/c/ (with the sample input), which performs the desired substitution only on the last on which the regex occurred.

If the pattern is not found in the input file, the whole command is an effective no-op.

This might work for you (GNU sed):

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

or another way:

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

or if you like:

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

On reflection, this solution may replace the first two:

sed  '/a/,$!b;/a/{x;/./p;x;h};/a/!H;$!d;x;s/^a$/c/M' file

If the regexp is no where to found in the file, the file will pass through unchanged. Once the regex matches, all lines will be stored in the hold space and will be printed when one or both conditions are met. If a subsequent regex is encountered, the contents of the hold space is printed and the latest regex replaces it. At the end of file the first line of the hold space will hold the last matching regex and this can be replaced.

Another one:

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

in action:

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

Another approach:

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

The advantage of this approach is that you run sequentially on the file twice, and not read it to memory. This can be meaningful in large files.

A two-pass solution for when buffering the entire input is intolerable:

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

(the earlier version of this hit a bug with history expansion encountered on a redhat bash-4.1 install, this way avoids a $!d that was being mistakenly expanded.)

A one-pass solution that buffers as little as possible:

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

Simplest:

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

Here is all done in one single awk

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

This reads the file twice. First run to find last a , second run to change it.

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

第一个tac反转infile.txt的行, sed表达式(参见https://stackoverflow.com/a/9149155/2467140 )用 'c' 替换 'a' 的第一个匹配项并打印剩余的行,并且last tac将行反转回原来的顺序。

Here is a way with only using 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

It can also be done in perl:

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

Tested:

> 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
> 

Here's the command:

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

And here it is in action:

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

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

Here's another option:

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

The first command appends an a and the second deletes the last line. From the sed(1) man page :

$ Match the last line.

d Delete pattern space. Start next cycle.

a text Append text, which has each embedded newline preceded by a backslash.

awk -only solution:

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

Explanation:

  • When a line matches a , all lines between the previous a up to (not including) current a (ie the content stored in the variable all ) is printed
  • When a line doesn't match a , it gets appended to the variable all .
  • The last line matching a would not be able to get its all content printed, so you manually print it out in the END block. Before that though, you can substitute the line matching a with whatever you desire.

Given:

$ cat file
a
b
a
b

You can use POSIX grep to count the matches:

$ grep -c '^a' file
2

Then feed that number into awk to print a replacement:

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

Could someone please help me on how to pass variable to the below command?

sed "$(sed -n '/110600002019/ =' tmuser.cf | tail -n 1)"' s/110600002019 /120700002019/' tmuser.cf

I want variable to be passed like below.

sed "$(sed -n '/$a/ =' tmuser.cf | tail -n 1)"' s/$a /$c/' tmuser.cf

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM