4

I need to add a few lines of text before the last occurrence of a word (SearchWord in my example) in multiple files.

  • search should be case insensitive.
  • the word is present in multiple lines of the file.

Existing Data in a file

SearchWord abc defgh
SEARCHWORD abc1234
sEarchWord abcd
sometext 1
sometext 2

Required Output

SearchWord abc defgh
SEARCHWORD abc1234 
NEWLINE_1_ADDED
NEWLINE_2_ADDED
NEWLINE_3_ADDED
sEarchWord abcd
sometext 1
sometext 2
  • The keyword SEARCHWORD only occurs once in your data... – Kusalananda Jan 20 '17 at 15:04
  • @don_crissti Then my question would be whether the keyword should be matched case-sensitively or not, and if not, could there still be multiple matches? – Kusalananda Jan 20 '17 at 15:07
  • @don_crissti Of course (leaving sed aside), but we still don't know where the extra data should come from... – Kusalananda Jan 20 '17 at 15:17
  • SeachWord should not be case sensitive. This searchword present in multiple lines in a file. But New text should be added before last matched searchword – ksb Jan 20 '17 at 15:18
  • @don_crissti : No we don't have those 3 lines in any file – ksb Jan 20 '17 at 15:23
3

Use ex, the POSIX file editor

ex is the non-visual form of the vi file editor. It actually predates vi. vi (and Vim) still use ex commands; any time you type a colon and enter a command, that is an ex command (though Vim has many custom extensions).

Since ex opens the whole file for reading, rather than operating in a stream, addressing can work backwards as well as forwards, unlike in Sed or Awk.

For this particular edit, use the following one liner:

printf '%s\n' 'set ignorecase' '$+?searchword?i' 'NEWLINE_1_ADDED' 'NEWLINE_2_ADDED' 'NEWLINE_3_ADDED' . x | ex input.txt

To test the command (print to standard output rather than saving to file), use %p rather than x as the final command:

printf '%s\n' 'set ignorecase' '$+?searchword?i' 'NEWLINE_1_ADDED' 'NEWLINE_2_ADDED' 'NEWLINE_3_ADDED' . %p | ex input.txt 

To handle multiple files:

for f in *.txt; do
    printf '%s\n' 'set ignorecase' '$+?searchword?i' 'NEWLINE_1_ADDED' 'NEWLINE_2_ADDED' 'NEWLINE_3_ADDED' . x | ex "$f"
done

Explanation

Running the printf command by itself, you can see the commands which are passed to ex:

set ignorecase
$+?searchword?i
NEWLINE_1_ADDED
NEWLINE_2_ADDED
NEWLINE_3_ADDED
.
x

The set command is self-explanatory.

$+ refers to the line after the last line, and ?...? means to search backward from there. (The + makes it so a file with a matching last line will be handled correctly.) i means to "insert" text before the line found.

The . on a line by itself ends the text to be inserted.

x saves and exits.

  • 1
    Thank you @wildcard..This solution perfectly works for all files in a folder and saving the file after changes. – ksb Jan 20 '17 at 20:43
2

Easier if you saved those lines in a text file, e.g. infile then used ed to edit your files in-place:

for f in ./*.txt; do
ed -s "$f" <<\IN
.t.
?[kK][eE][yY][wW][oO][rR][dD]?-1 r infile
$d
w
q
IN
done

If you just wanted to insert some text (not the content of a file), it's similar:

for f in ./*.txt; do
ed -s "$f" <<\IN
.t.
?[kK][eE][yY][wW][oO][rR][dD]?-1 s/$\
some_text\
more_text\
last\\_\&_line
$d
w
q
IN

Replace w with ,p to see what it does without modifying the files.
Note that backslashes, ampersands and delimiters have to be escaped in the RHS of a substitution. The same applies to newlines, except the last one.
How it works ? Well, duplicate the last line, search for your keyword backwards, insert the text or the content of the file before that line, delete the duplicated last line, write, quit.


Sure, if you don't need to edit those files in place and just want to print the modified versions, like Zanna does, you don't need arcane text editors with a syntax from another age... You can do it with a single sed invocation - and because Wildcard kindly reminded me that in this family of editors there's a dedicated command to insert text before a match let's use that and some branching:

sed -s '1{h;$!d;b end
}
/keyword/I{H;$!d
}
//{x;p;$!d
}
:end
${x;/keyword/Ii\
some_text_here\
more_text_here\
and_a\\_backslash
}' ./*.txt

This only works with gnu sed though... with other seds you'll have to run a loop (this time without the branch part so as to have a blank line between each file content):

for f in ./*.txt; do
sed -s '/[kK][eE][yY][wW][oO][rR][dD]/{H;$!d
}
//{x;p;$!d
}
${x;/[kK][eE][yY][wW][oO][rR][dD]/i\
some_text_here\
more_text_here\
and_a\\_backslash
}' "$f"
done

As you can see even with insert, one still have to escape backslashes and newlines (except the last one). Other than that, it's simple: it just accumulates lines in the hold buffer and exchanges when encountering a match; on last line it exchanges again and inserts the text before the match, if any.

  • ex is much simpler. Although I see that ed has the "insert" command also. Using the s command here makes things more complicated than insert, because it requires special character escaping. (Why not just use "insert"?) Also, does ed not have the compound address feature that I use in my ex answer ($+?regex?i)? – Wildcard Jan 20 '17 at 19:21
  • 1
    @Wildcard - why not use insert... well, that's a good question buddy... This may sound really retarded but... I don't have an answer :) – don_crissti Jan 20 '17 at 19:24
2

If you have tac and GNU sed you could use

$ tac file | sed '0,/searchword/I s/.*searchword.*/&\nNEWLINE_3_ADDED\nNEWLINE_2_ADDED\nNEWLINE_1_ADDED/I' | tac
SearchWord abc defgh
SEARCHWORD abc1234
NEWLINE_1_ADDED
NEWLINE_2_ADDED
NEWLINE_3_ADDED
sEarchWord abcd
sometext 1
sometext 2

Explanation

  • tac print out the file backwards
  • 0,/pattern/ operate on first occurence of pattern only
  • I case insensitive search
  • s/old/new/ replace old with new
  • .*searchword.* match the whole line containing searchword
  • & the whole matched pattern in replacement
  • \n newline

Maybe more readable:

tac file | sed '0,/searchword/I s/.*searchword.*/&\
NEWLINE_ADDED_3\
NEWLINE_ADDED-2\
NEWLINE_ADDED_1/I' | tac

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.