5

I'm trying to work out how to replicate a single range of lines in a text file. The range starts with a line that is unique in the file but the range ends with a line that can exist in multiple places in the file.

Here's some example input I need to process:

I have no imagination
so this sample text will
Common
be boring. But it does
demonstrate the problem
I am trying to solve.
Common
Hi mom!
This is a unique line.
And here is some more
text that should be copied
as well.
Common
Followed by text that should
not be copied.

The lines I need to duplicate and modify are in bold to point them out here.

The output I need is:

I have no imagination
so this sample text will
Common
be boring. But it does
demonstrate the problem
I am trying to solve.
Common
Hi mom!
This is a changed line.
And here is different more
text that should be copied
as well.
Common
This is a unique line.
And here is some more
text that should be copied
as well.
Common
Followed by text that should
not be copied.

The additional output is in bold to make it clear.

I need to get the range of lines starting with the line:

This is a unique line

and ending with the line:

Common

That range of lines must be inserted before just before the original range of lines. The copy of the matching range of lines will need to be modified slightly.

The "Common" line that ends the range can itself occur in many places within the file.

I came up with a working awk script but it seems far more complicated than it needs to be. My awk skills are non-existent.

/This is a unique line/{flag=1}
/Common/{
    if (flag > 0) {
        n=m;
        sub("some","different",n);
        sub("unique","changed",n);
        print n "\n" $0 "\n" m;
        m=""
    };
    flag=0
};
flag{
    if (length(m) > 0) {
        m=m "\n" $0
    } else {
        m=$0
    }
}
!flag{ print }

Is there a cleaner, less verbose way to implement this? I'm open to other options besides awk. It just needs to be a standard command available on macOS.

6
  • 1
    FYI your Awk skills are far from nonexistent. Your code works and is readably formatted. That puts you ahead of many people writing Awk code.
    – Wildcard
    Commented May 18, 2017 at 2:00
  • How does Here change to This in your example? is the change in indentation part of the requirement as well? Commented May 18, 2017 at 2:41
  • @steeldriver Sorry, that was a typo when I tweaked the question. It should be This, not Here all the way through. I've fixed the question.
    – rmaddy
    Commented May 18, 2017 at 2:47
  • @steeldriver As stated in the question, the indent is just to highlight the relevant text. The indent isn't actually in the source or output. I tried to make those lines bold but the double-asterisk doesn't work in preformatted text.
    – rmaddy
    Commented May 18, 2017 at 2:50
  • Sorry - missed that Commented May 18, 2017 at 2:53

3 Answers 3

5

Use ex, the POSIX-specified file editor (and the non-visual form of vi).

printf '%s\n' '/This is a unique line' '.,/Common/copy -' %p | ex file.txt

This prints the modified file contents but doesn't save changes.

Here is the command to save the changes:

printf '%s\n' '/This is a unique line' '.,/Common/copy -' x | ex file.txt

Unlike Awk and Sed, ex isn't limited to only modified lines in order. Rather, it can operate on the entire buffer instead of only moving forward.

The first command /This is a unique line is just a motion command. It moves the cursor to the first line of the file that has This is a unique line.

The next command is the copy command. It operates on the address range from current line (.) to the next line from there matching Common, and copies them to the line preceding the current line (-).


Update: For modification after copying, let's first mark the line we started at. The first line to be copied will be marked with "b" and the line before it will be marked as "a." Then the copied lines will be put in between 'a and 'b, and we can use those in addresses for our "substitute" commands. And t is a synonym for copy, by the way.

printf "/Here is a unique line
kb
-ka
.,/Common/t 'a
'a+,'b-s/unique/changed/g
'a+,'b-s/some/different/g
x
" | ex file.txt
2
  • Thanks but please note that besides copying the few specific lines, the copy needs to be modified slightly. I updated my question with a specific, testable example to make it easier.
    – rmaddy
    Commented May 18, 2017 at 1:22
  • @rmaddy, see update.
    – Wildcard
    Commented May 18, 2017 at 2:13
5
sed -e '
   /This is a unique line/,/Common/!b
   H
      /Common/!d
   g
      s/^\n//
   h
      s/unique/changed/
      s/some/different/
   G
' yourfile

Explanation

  • We first reject any non-range bunch of lines.
  • For every line in range, we append it to the hold space.
  • We go delete all lines read in till we see the Common line. At which point, we retrieve the hold area (which should have the whole range + a leading "\n").
  • We delete this leading newline and update the hold as well. It is due to the H command BTW.
  • Now we make the edits: unique -> changed , some -> different, ...
  • Append the hold space(unchanged version) to the pattern space(changed version).
  • sed will autoprint what's in the pattern space.
4
awk '/This is a unique line/,/Common/{
   H = H RS $0
   if ( $0 ~ /Common/ ) {
      g = H
      sub("\n","",g)
      sub("some","different",g)
      sub("unique","changed",g)
      $0 = g H
   } else { next }
}1'   inputfile

Here's the sed code(I showed in the Answer section) translated into awk.

Note that, the code you are having you are taking on the responsibility of turning ON/OFF the awk variable flag to keep track of lines. But whereas, awk already does it for you under the hood the exact same thing when you use it's range operator ,

1
  • This is what I was hoping for. A clean way to use a range. My feeble attempts to use a range weren't working. Thanks.
    – rmaddy
    Commented May 18, 2017 at 4:49

You must log in to answer this question.

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