4

I have following text file.

banana
apple
juice
mango
something

I am searching for pattern juice, and I want to find the 2nd line from that matching pattern in reverse order (i.e 2 lines above the matching pattern) and replace it with coconut.

Expected output:

coconut
apple
juice
mango
something

I tried with following, but it just deletes the above two line and not the exact one I'm looking for.

tac foo.txt |sed '/juice/I,+2 d' |tac
mango
something

I think tweaking above script would do the job, but I am not sure.

Note: There will not be any re-occurrence of the match, and it needs not to be an exact match (meaning, the match can be found in a long line as well). The match should be case-sensitive.

2

Following your approach,

tac file|sed '/juice/{n;n;s/.*/coconut/}'|tac
  • /juice/ matches a line with juice.
  • n;n; prints the current and the next line.
  • s/.*/coconut/ makes the substitution.

Apparently you have GNU sed, so you could also use -z to get the whole file into memory and directly edit the line two above juice,

sed -rz 's/[^\n]*(\n[^\n]*\n[^\n]*juice)/coconut\1/' file

[^\n] means "not a newline" and the parenthesis () capture the group reproduced by the \1 back-reference.

| improve this answer | |
6

If ed is okay, you need to edit a file, not a stream, and there is only one juice:

$ more <<-EOF | ed -s ./tmp.txt
	/juice/
	-2
	c
	coconut
	.
	w
	q
EOF
$

Find the line, go two lines up, change, write, and quit.


An even more compact variation, suggested by @d-ben-knoble in the comments:

$ printf '%s\n' '/^juice$/-2s/.*/coconut/' w q | ed -s ./tmp.txt
| improve this answer | |
3
$ tac file | awk 'c&&!(--c){$0="coconut"} /juice/{c=2} 1' | tac
coconut
apple
juice
mango
something
| improve this answer | |
2

With bash, assuming your file is not large:

mapfile -t lines < file
for (( i = 0; i < ${#lines[@]}; i++ )); do 
  if [[ ${lines[i]} == *"juice"* ]]; then
    lines[i-2]="coconut"
    break
  fi
done
printf '%s\n' "${lines[@]}"

If you would want case insensitive matching, add this before the loop:

shopt -s nocasematch

With perl:

perl -0777 -pe 's/^.*(?=\n.*\n.*juice)/coconut/m' file
| improve this answer | |
1

This can also be done in a single, POSIX awk:

awk '/juice/{l2="coconut"}NR>2{print l2}{l2=l1;l1=$0}END{print l2;print l1}' file

The idea is to always have 2 lines captured, l2 (two above current line) and l1 (one above current line). If current line matches juice, change l2 to coconut.

| improve this answer | |
1

Here is yet another awk approach, this time a double-pass method:

awk 'NR==FNR&&/juice/{m=FNR} NR>FNR&&FNR==m-2{$0="coconut"} (NR>FNR)' file file
  • We specify the file two times as command-line arguments, so that it gets processed twice.
  • In the first pass (where FNR, the per-file line-counter, is equal to NR, the global line counter), we simply identify in which line the search pattern juice occurs, and store it in a variable m.
  • In the second pass, we set line number m-2 to the replacement text coconut.
  • As a general rule, we print the lines including any modifications, but only in the second pass (where the condition NR>FNR evaluates to "true").

If you have GNU awk (some other awk implementations support this, too) you can speed up the process a little by aborting the first pass as soon as the match is found using the nextfile command:

awk 'NR==FNR&&/juice/{m=FNR;nextfile} NR>FNR&&FNR==m-2{$0="coconut"} (NR>FNR)' file file
| improve this answer | |
0

You could use the ex line editor fir this task :

$ echo '/juice/-2s/.*/coconut/|x' | ex -s file

$ sed -Ee '
    1N;$!N
    s/.*(\n.*\n.*juice)/coconut\1/;t
    P;D
' file 

PS : not in front of my terminal so they are untested for sintax and or some issues.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.