A line starting with Fred Flintstone shall be appended with some string.  Look for specified occurrence of Fred Flintstone and append it.

How do I use this command for anyone of the occurrences of such a pattern?  I tried

sed '/Fred Flintstone/ s/$/ someString/2' filename

Apparently the above one isn't working.  It works well for all occurrences, but not a specific occurrence. (Say I want to replace first or second or third [any one of them])

Example File1:

Fred Flintstone 
Johnson Stone
Fred Flintstone
Fred Flintstone
Michael Clark

Desired Output File1:

Fred Flintstone 
Johnson Stone
Fred Flintstone someString
Fred Flintstone
Michael Clark
up vote 7 down vote accepted

Although you've mentioned sed, these are sort of awk-y tasks:

awk -v pat="Fred Flintstone" '$0 ~ pat {count++;\
               if (count == 2) { $0 = $0" someString" ;} ;}; 1' file.txt
  • -v pat="Fred Flintstone" saves the Regex pattern to match as variable pat to be used inside awk expressions

  • $0 ~ pat checks the record against pat for a match; if matched, the count variable is increased by 1 and if count is 2 the record is reset as having the current content plus someString ({count++; if (count == 2) { $0 = $0" someString" ;} ;})

  • 1 is an idiom; as it is truthy, all the records would be printed

Example:

% cat file.txt
Fred Flintstone
Johnson Stone
Fred Flintstone
Fred Flintstone
Michael Clark

% awk -v pat="Fred Flintstone" '$0 ~ pat {count++; if (count == 2) { $0 = $0" someString" ;} ;}; 1' file.txt
Fred Flintstone
Johnson Stone
Fred Flintstone someString
Fred Flintstone
Michael Clark

This simple sed command allows you to selectively make the change without using loops (it does use a branch-to-end) or needing GNU extensions or reading the whole file at once:

sed -r '/Fred Flintstone/ {x; s/$/#/; /^#{2}$/ {x; s/.*/& someString/; b}; x}'

Explanation:

  • -r - use extended regex
  • /Fred Flintstone/ - for lines that match this pattern:
    • x - exchange pattern space and hold space (activate the counter)
    • s/$/#/ - add a character to the counter
    • /^#{2}$/ - when the counter is length 2 (substitute any value)
      • x exchange the pattern space and hold space (activate the counted input line)
      • s/.*/& someString/ - append the string to the desired line
      • b - skip to the end of processing for this line so it can be printed
    • x - exchange the pattern space and hold space (activate lines that match the string but not the count)

Indentation levels in the explanation indicate levels of curly brace nesting.

All other lines pass through without processing and are printed.

Revised answer to prevent code injection on back-slashes as well as slashes, quotes when using awk variables awk -v variable ... or a shell awk '...' variable="$value" since awk does C escape sequence processing on values passed with -v variable= or shell variable=$value; so the shell variable='\\n' will change to \n within awk).

pattern="${patt//\\/\\\\}" awk '
    $0 ~ ENVIRON["pattern"]{ seen++ } seen==2 { $0 = $0 "something"};1' infile

for the below input and the pattern:

another break line
Johnson Stone
\"Fred //\\Flintstone' SOMETHING will goes here ...
\"Fred //\\Flintstone'
Michael Clark

pattern is in a variable called patt

$ echo "$patt"
\"Fred //\\Flintstone'

The output is:

\"Fred //\\Flintstone'
another break line
Johnson Stone
\"Fred //\\Flintstone' SOMETHING will goes here ...something
\"Fred //\\Flintstone'
Michael Clark

Explanation:

later might you will needed to pass something string as a variable, and you will needed to use something like edit=$somesting and use ENVIRON["edit"] there.

pattern="${patt//\\/\\\\}" edit="$something" awk '
    $0 ~ ENVIRON["pattern"]{ seen++ } seen==2 { $0 = $0 ENVIRON["edit"]};1' infile

Here's a way to do it in sed without slurping the whole file into memory:

  • find the first occurrence of the pattern
  • append the next line N to the pattern space
  • attempt to replace the second occurrence of the pattern
  • branch back and append another line if the match fails Ta

The $q quits the loop if the end of file is reached without a match.

So

sed '/Fred Flintstone/ {:a; $q; N; s//& someString/2; Ta;}' File1
Fred Flintstone 
Johnson Stone
Fred Flintstone someString
Fred Flintstone
Michael Clark

Although T is a GNU extension, you can do the same in POSIX sed using t; ba


Hmm... after thinking about this some more, it will actually append the new text to every other occurrence - not only the second. If you really need to use replace only the second occurrence, then the only way I can think to do it would be:

  • address and replace the first instance with some unique string
  • address and modify the new first instance
  • address and replace the unique string with the original pattern

GNU sed provides a trick to address the first instance of a pattern

0,/pattern/

So

sed -e '0,/Fred Flintstone/ s//Barney Rubble/' \
    -e '0,/Fred Flintstone/ s//& someString/' \
    -e '0,/Barney Rubble/ s//Fred Flintstone/
' File1
Fred Flintstone
Johnson Stone
Fred Flintstone someString
Fred Flintstone
Michael Clark

If you don't mind using ed, then you can address the second instance more directly by going to the first line and then matching forward from one instance to the next:

ed -s File1 << \EOF                                                                                                              
1;#
/Fred Flintstone/,/Fred Flintstone/ s//& someString/
,p
q
EOF
Fred Flintstone
Johnson Stone
Fred Flintstone someString
Fred Flintstone
Michael Clark

or as a one-liner

printf '1;#\n/Fred Flintstone/,/Fred Flintstone/ s//& someString/\n,p\n' | ed -s File1

You can make the ed version in-place by replacing ,p with w

Another use case for ex, the POSIX-specified file editor

(It's the predecessor of vi and is still part of vi.)

printf '%s\n' '0/Fred Flintstone///s/$/ someString/' x | ex filename

The magic here is that unlike Sed, Awk and similar tools, ex doesn't run code on every line. You can move the cursor around, give commands, etc.

In this case, we give a line number 0 (which ensures that Fred Flintstone on the first line will be taken into account), followed by a regex /Fred Flintstone/ which refers the the first line in the file matching that regex, followed by another regex // which, being empty, reuses the last regex and so refers to the second line in the file which matches; and then we use the s command which you already know.

The x command means to save any changes and exit.

We use printf to feed commands to ex.

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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