Replace a letter with another from the last word from the last two lines of a text file

Viewed 362

How could I possibly replace a character with another, selecting the last word from the last two lines of a text file in shell, using only a single command? In my case, replacing every occurrence of a with E from the last word only.

Like, from a text file containing this:

tree;apple;another
mango.banana.half
monkey.shelf.karma

to this:

tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE

I tried using sed -n 'tail -2 'mytext.txt' -r 's/[a]+/E/*$//' but it doesn't work (my error: sed expression #1, char 10: unknown option to 's).

9 Answers

Could you please try following, tac + awk solution. Completely based on OP's samples only.

tac Input_file | 
awk 'FNR<=2{if(/;/){FS=OFS=";"};if(/\./){FS=OFS="."};gsub(/a/,"E",$NF)} 1' | 
tac

Output with shown samples is:

tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE

NOTE: Change gsub to sub in case you want to substitute only very first occurrence of character a in last field.

This might work for you (GNU sed):

sed -E 'N;${:a;s/a([^a.]*)$/E\1/mg;ta};P;D' file

Open a two line window throughout the length of the file by using the N to append the next line to the previous and the P and D commands to print then delete the first of these. Thus at the end of the file, signified by the $ address the last two lines will be present in the pattern space.

Using the m multiline flag on the substitution command, as well as the g global flag and a loop between :a and ta, replace any a in the last word (delimited by .) by an E.

Thus the first pass of the substitution command will replace the a in half and the last a in karma. The next pass will match nothing in the penultimate line and replace the a in karmE. The third pass will match nothing and thus the ta command will fail and the last two lines will printed with the required changes.

If you want to use Sed,

tac input_file | sed -E '1,2{h;s/.*[^a-zA-Z]([a-zA-Z]+)/\1/;s/a/E/;x;s/(.*[^a-zA-Z]).*/\1/;G;s/\n//}' | tac

One tiny detail. In your question you say you want to replace a letter, but then you transform karma in kErme, so what is this? If you meant to write kErma, then the command above will work; if you meant to write kErmE, then you have to change it just a bit: the s/a/E/ should become s/a/E/g.

With tac+perl

$ tac ip.txt | perl -pe 's/\w+\W*$/$&=~tr|a|E|r/e if $.<=2' | tac
tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE
  • \w+\W*$ match last word in the line, \W* allows any possible trailing non-word characters to be matched as well. Change \w and \W accordingly if numbers and underscores shouldn't be considered as word characters - for ex: [a-zA-Z]+[^a-zA-Z]*$
  • $&=~tr|a|E|r change all a to E only for the matched portion
  • e flag to enable use of Perl code in replacement section instead of string

To do it in one command, you can slurp the entire input as single string (assuming this'll fit available memory):

perl -0777 -pe 's/\w+\W*$(?=(\n.*)?\n\z)/$&=~tr|a|E|r/gme'

Using GNU awk forsplit() 4th arg since in the comments of another solution the field delimiter is every sequence of alphanumeric and numeric characters:

$ gawk '
BEGIN {
    pc=2                        # previous counter, ie how many are affected
}
{
    for(i=pc;i>=1;i--)          # buffer to p hash, a FIFO
        if(i==pc && (i in p))   # when full, output 
            print p[i]
        else if(i in p)         # and keep filling
            p[i+1]=p[i]         # above could be done using mod also
    p[1]=$0
}
END {
    for(i=pc;i>=1;i--) {
        n=split(p[i],t,/[^a-zA-Z0-9\r]+/,seps)   # split on non alnum
        gsub(/a/,"E",t[n])                       # replace
        for(j=1;j<=n;j++) {
            p[i]=(j==1?"":p[i] seps[j-1]) t[j]   # pack it up
        }
        print p[i]                               # output
    }
}' file

Output:

tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE

Would this help you ? on GNU awk

$ cat file
tree;apple;another
mango.banana.half
monkey.shelf.karma

$ tac file | awk 'NR<=2{s=gensub(/(.*)([.;])(.*)$/,"\\3",1);gsub(/a/,"E",s); print gensub(/(.*)([.;])(.*)$/,"\\1\\2",1) s;next}1' | tac
tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE

Better Readable version :

$ tac file | awk 'NR<=2{
   s=gensub(/(.*)([.;])(.*)$/,"\\3",1);
   gsub(/a/,"E",s); 
   print gensub(/(.*)([.;])(.*)$/,"\\1\\2",1) s;
   next
}1' | tac

With GNU awk you can set FS with the two separators, then gsub for the replacement in $3, the third field, if NR>1

awk -v FS=";|[.]"  'NR>1 {gsub("a", "E",$3)}1' OFS="." file

tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE

With GNU awk for the 3rd arg to match() and gensub():

$ awk -v n=2 '
    NR>n { print p[NR%n] }
    { p[NR%n] = $0 }
    END {
        for (i=0; i<n; i++) {
            match(p[i],/(.*[^[:alnum:]])(.*)/,a)
            print a[1] gensub(/a/,"E","g",a[2])
        }
    }
' file
tree;apple;another
mango.banana.hElf
monkey.shelf.kErmE

or with any awk:

awk -v n=2 '
    NR>n { print p[NR%n] }
    { p[NR%n] = $0 }
    END {
        for (i=0; i<n; i++) {
            match(p[i],/.*[^[:alnum:]]/)
            lastWord = substr(p[i],1+RLENGTH)
            gsub(/a/,"E",lastWord )
            print substr(p[i],1,RLENGTH) lastWord 
        }
    }
' file

If you want to do it for the last 50 lines of a file instead of the last 2 lines just change -v n=2 to -v n=50.

The above assumes there are at least n lines in your input.

You can let sed repeat changing an a into E only for the last word with a label.

tac mytext.txt| sed -r ':a; 1,2s/a(\w*)$/E\1/; ta' | tac
Related