9

let's say, I have this file with five hundred lines of text.

one
two
three
four
five
six
seven
eight
nine
ten
.
.
.
five-hundred

If I wanted to replace the fifth line after match with some string, I would just do this:

sed '/two/{n;n;n;n;n;s/.*/MODIFIED/}' inputfile

output:

one
two
three
four
five
six
MODIFIED
eight
nine
ten
.
.
.
five-hundred

But what if I want to replace the 60th line after match? I don't want to write 'n' sixty times.

I tried playing around with x, h/H, g/G, and ranges, but I can't still get my desired output.

4
  • Does this answer your question? Relative line number in the range match in sed Commented May 14, 2021 at 2:07
  • @Quasímodo OK, but there end could be replaced with $, so for example seq 10|sed '/2/,${/2/,+5{/2/,+4!s/.*/replaceString/}}' from accepted answer or seq 10| sed '/2/{:a;N;$!ba;s/[^\n]*/replaceString/6}' from the second answer. no such big difference, but easy adjustable if one could understood how they works. BTW, I retracted my close vote. Commented May 14, 2021 at 5:00
  • 1
    Just use 10 lines of sample input/output instead of 500 and show us what the output should be if you want to print the 5th lines after every line that contains t. Different answers you have will give you the same output for two but different output for t. It would also be good if you showed if you want a regexp or string match.
    – Ed Morton
    Commented May 14, 2021 at 13:33
  • 1
    Another edge case to consider - what if the string you're adding contained the string you're searching for, e.g. instead of MODIFIED it were fifty-two? Should THAT line then match with two and so you'd change the 5th line after THAT and every 5th line from then on?
    – Ed Morton
    Commented May 14, 2021 at 13:47

9 Answers 9

13

Workaround with awk:

awk '/two/{ n=NR+5 } NR==n{ sub(/.*/, "MODIFIED") }1' file

or if you want to replace the line

awk '/two/{ n=NR+5 } NR==n{ $0="MODIFIED" }1' file
0
10

For this kind of tasks, you may have to consider what to do if the search pattern occurs again before the offset.

awk -v offset=60 '
    /two/ {x[NR + offset]}
  NR in x {delete x[NR]; $0 = "MODIFIED"}
          {print}'

Would make sure a line is replaced whenever the line 60 rows above contained two.

1
  • Good point on the possibility of the search pattern re-occurring within the window.
    – Ed Morton
    Commented May 14, 2021 at 13:29
7
/two/{
    :loop
    N
    /\(\(.*\n\)\{5\}\).*/{
        s//\1Modified/
        b
    }
    b loop
}

Save that in x.sed and execute it as

sed -f x.sed file

or use the one-liner for GNU Sed:

sed '/two/{:a;N;/\(\(.*\n\)\{5\}\).*/{s//\1Modified/;b;};ba;}' file

Line by line analysis:

  • 1-2: When the match is found, start a loop. In this loop,
    • 3: Add a line to pattern space with the N command.
    • 4: If there are 5 newline characters — the basic regex is \(.*\n\)\{5\} —, put that in a capture group, leave the last line (.*) uncaptured and
      • 5: Substitute the whole pattern space* with the capture group followed by the Modified line.
      • 6: Break the loop.
    • 8: Else loop again.

*An empty regex slot is equivalent to the previously used regex, so that 5th line is equivalent to s/\(\(.*\n\)\{5\}\).*/\1Modified/.


Less esoteric is to use Awk:

awk '{c--};/two/{c=5};c==0{$0="Modified"};{print}' file

That starts by decrementing a counter, and because, as all variables, c is initially zero, it only becomes positive if a match is found.

An even more explicit approach would be

awk 'BEGIN{c=-1};/two/{c=5};c==0{$0="Modified"};{print};c>=0{c=c-1}' file

Useful Sed resources:

2
  • Does this regex \(.*\n\)\{5\} match only the 5th line character? if it does, why does it need .* before \n which matches anything before the new line character? I'm confused because the back-reference \1 contains nothing, but the capture group which is \(.*\n\)\{5\} has .* in it. I hope my question does make sense.
    – cjijcb
    Commented Jul 30, 2021 at 2:12
  • 1
    @cjijcb No, it .*\n matches a whole line, which consists of its "contents" .* and the newline character \n. Thus \(.*\n\)\{5\} matches 5 whole lines, all of which are reproduced by the capture group \1.
    – Quasímodo
    Commented Jul 30, 2021 at 18:11
6

A sed (but only GNU sed) solution without loops and using simple ranges is:

sed '/^two$/,+5{/^two$/,+4{b};s/.*/& hello/}'

The only thing to adjust is the match regexps and the +5 and +4 counters.

For 100 lines after the match:

sed '/two/,+100{/two/,+99{b};s/.*/& Modified/}'

Maybe this could clarify how it works:

$ seq 10 | sed '/4/,+3{/4/,+2{s/.*/&changed/;b};s/.*/& Modified.../}'
1
2
3
4changed
5changed
6changed
7 Modified...
8
9
10

4

Doing a full-line string match and assuming such a match cannot re-occur within the 5 line range, using any awk in any shell on every Unix box:

$ awk 'c&&!--c{$0="MODIFIED"} $0=="two"{c=5} 1' file
one
two
three
four
five
six
MODIFIED
eight
nine
ten
.
.
.
five-hundred

See https://stackoverflow.com/questions/17908555/printing-with-sed-or-awk-a-line-following-a-matching-pattern/17914105#17914105.

4

If you're editing a file, use a proper editor instead of a stream editor to edit in-place. sed is for when you need streaming capabilities.

In this case, eds ranges and commands are stronger that POSIX seds:

printf '%s\n' 'g/two/+5s/.*/MODIFIED/' wq | ed -s file

And it's easy to spot/change the 5 to, say, 60.

(You can make this a source for a stream if you change wq to %p q, two separate arguments; you don't have to print the whole file, though. You can also write to a different file with 'w otherfile' q, again, two separate arguments.)

7
  • 1
    Did not know you could do +5 to g//'s regex. That's pretty cool. Unfortunately when printing to stdout, even with -s, ? will be output at the end, and also Newline appended at the beginning if the file isn't newline-terminated.
    – JoL
    Commented May 15, 2021 at 0:04
  • 1
    @JoL well it’s really doing a relative range for s: with spaces for separability, g/two/ +5s/…/. with ex/vim you can do some funky business with the pattern, true. But this is a range Commented May 15, 2021 at 0:27
  • 1
    Oh! +5 is part of the [cmd] argument to g//. It never ocurred to me that you could use a range there. That kind of opens up possibilities.
    – JoL
    Commented May 15, 2021 at 0:31
  • 1
    @JoL Note that -s has nothing to with printing to standard output and only mean that ed will be in streaming mode, reading an editing script from standard input and the file in question from the given argument.
    – Kusalananda
    Commented Jan 26 at 12:32
  • 1
    @Kusalananda I wouldn't say it has nothing to do with printing to stdout when the complete description of the option in the manpage is "suppress byte counts and '!' prompt". ed would read instructions from stdin and the file in question from the argument regardless of whether -s is used.
    – JoL
    Commented Jan 28 at 3:29
2

Using GNU sed in extended regex -E we setup a 59 newlines pattern space and then keep shifting out from the top till we hit the pattern line. At that point the Target line is the last line in the pattern space.

sed -Ee ':top
  1,59{
    $q;N;b top
  }
  $!N
  /^[^\n]*two/s/[^\n]*$/MODIFIED/
  P;D
' file

Note:

  • Change the offset from 59 to whatever required.
  • Change the search pattern from two to whatever desired keeping in mind the rules for writing a regex
  • with minor tweaks it can be made POSIX sed compliant.
4
  • 1
    Note that if the match was between the 495th and 498th line that would modify the 499th line. The asker has not specified what should happen, but I think not modifying any line would be the most natural result in that case. In which case a simple solution is $!NN I reckon.
    – Quasímodo
    Commented May 14, 2021 at 12:15
  • The issue of the last line is fixed now. Now it is catching the last line.
    – guest_7
    Commented May 18, 2021 at 3:45
  • Changing the last line, even though it is not the 60th (or whatever offset one chooses) counting from the match also does not look in line with the asker's expectations. Don't you think changing $!N to N in the original answer would be a good suggestion? Note that it is not unspecified behavior if N is issued on the last line of the file, in case you're trying to avoid that.
    – Quasímodo
    Commented May 18, 2021 at 19:36
  • When the OP did not explicitly specify the requirements, so I took my interpretation of it. Yes the change from $!N to N ...but like you mentioned, I did not use a bareword N due to differing behavior in POSIX and gnu versions.YMMV.
    – guest_7
    Commented May 21, 2021 at 1:52
0

Compact, but perhaps not very obvious solution which should work with any sed flavour and doesn't require looping:

sed -e 'H;/^two$/h;x;/^two\(\n.*\)\{5\}/{s//MODIFIED/;x;}' -e x

Idea: Collect lines in in hold space, beginning with the matching line. If there are n newlines in the hold space, we just added the line to be replaced, so we replace the entire hold space and switch spaces.

In detail:

  • H will append any line to the hold space
  • /^two$/h will copy the line with the matching pattern to the hold space, overriding all lines we collected before and making sure, the hold space starts with the given pattern one we did pass it
  • x exchanges both buffers so we can deal with what we collected in the hold space. Note that we will change buffers back at the end of the script
  • /^two\(\n.*\)\{5\}/ is a pattern starting with the trigger line and at least 5 more lines (replace 5 by your desired value). So this pattern will match as soon as we appended five lines after the matching one. Heureka! What to do now? We want to print the replacement AND we need to destroy the hold space so it will not fit again unless another trigger line is found. We can do both things at the same time by replacing the whole stuff by the replacement:
  • s//MODIFIED/ replaces our whole hold space (which is in the pattern space since the x command) with the replacement. Leaving out the first pattern of the s command makes it use the last pattern.
  • Now another x changes buffers in case we did the replacement, so changing back buffers in the last step will bring our replacement to the pattern space again to get printed. The new hold space will contain the removed line then, not triggering the match in future unless it has been two again
4
  • 1
    In POSIX sed syntax, you can't have anything after }. You'd need two -expressions or use a newline instead of ; after }. Commented May 14, 2021 at 14:38
  • To my knowledge, POSIX allows implementations to support the use of ; after } and all living sed flavours I ever met removed this meaningless exception. But if anyone can prove me wrong, you are the one.
    – Philippos
    Commented May 14, 2021 at 14:47
  • That won't work with SysV sed like on Solaris for instance. Commented May 14, 2021 at 16:06
  • I have no Solaris machine anymore, but I trust you, so I changed it. Thank you, @StéphaneChazelas
    – Philippos
    Commented May 16, 2021 at 16:25
0
awk -v l=60 '$0~/two/ && !s {s=1} s && ++s == l {$0 = "MODIFIED"; s=0}1'

Looking for a match (only if not found yet) and when find it set a start position, then increment it until a limit. When the limit is reached do the replacement and set again the start position to 0.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .