2

I'm trying to reverse the lines in a file, but I want to do it two lines by two lines.

For the following input:

1
2
3
4
…
97
98

I would like the following output:

97
98
…
3
4
1
2

I found lots of ways to reverse a file line by line (especially on this topic: How can I reverse the order of lines in a file?).

  • tac. The simplest. Doesn't seem to have an option for what I want, even if I tried to play around with options -r and -s.
  • tail -r (not POSIX compliant). Not POSIX compliant, my version doesn't seem to have anything to do that.

Remains three sed formula, and I think a little modification would do the trick. But I'm not even understanding what they're doing, and thus I'm stuck here.

sed '1!G;h;$!d'
sed -n '1!G;h;$p'
sed 'x;1!H;$!d;x'

Any help would be appreciated. I'll try to understand these formula and to give answer to this question by myself.

Community
  • 1
  • 1
Niols
  • 627
  • 1
  • 4
  • 13
  • Have you tried breaking the `sed` commands down to understand what each step is doing? That's how you decipher programs. – Barmar May 09 '15 at 15:48
  • That's what I'm trying to do right now. If someone comes knowing the answer, it'll help me. If nobody does, and if I finaly understand them, I'll answer by myself. – Niols May 09 '15 at 15:49
  • 1
    I think you'll learn much better by figuring it out yourself than having it fed to you. – Barmar May 09 '15 at 15:57
  • Of course, I agree. But I'm working on an other project right now, that depends on this sed trick. And I don't really like splitting myself into two fully different things. But I think time to learn sed has come, and my other project will have to wait. – Niols May 09 '15 at 16:04
  • 1
    It seems like it would be much easier to do in `awk`. Put all the lines in an array, then go through the array in reverse by multiples of 2. – Barmar May 09 '15 at 16:10

3 Answers3

3

Okay, I'll bite. In pure sed, we'll have to build the complete output in the hold buffer before printing it (because we see the stuff we want to print first last). A basic template can look like this:

sed 'N;G;h;$!d' filename     # Incomplete!

That is:

N    # fetch another line, append it to the one we already have in the pattern
     # space
G    # append the hold buffer to the pattern space.
h    # save the result of that to the hold buffer
$!d  # and unless the end of the input was reached, start over with the next
     # line.

The hold buffer always contains the reversed version of the input processed so far, and the code takes two lines and glues them to the top of that. In the end, it is printed.

This has two problems:

  1. If the number of input lines is odd, it prints only the last line of the file, and
  2. we get a superfluous empty line at the end of the input.

The first is because N bails out if no more lines exist in the output, which happens with an odd number of input lines; we can solve the problem by executing it conditionally only when the end of the input was not yet reached. Just like the $!d above, this is done with $!N, where $ is the end-of-input condition and ! inverts it.

The second is because at the very beginning, the hold buffer contains an empty line that G appends to the pattern space when the code is run for the very first time. Since with $!Nwe don't know if at that point the line counter is 1 or 2, we should inhibit it conditionally on both. This can be done with 1,2!G, where 1,2 is a range spanning from line 1 to line 2, so that 1,2!G will run G if the line counter is not between 1 and 2.

The whole script then becomes

sed '$!N;1,2!G;h;$!d' filename

Another approach is to combine sed with tac, such as

tac filename | sed -r 'N; s/(.*)\n(.*)/\2\n\1/'     # requires GNU sed

That is not the shortest possible way to use sed here (you could also use tac filename | sed -n 'h;$!{n;G;};p'), but perhaps easier to understand: Every time a new line is processed, N fetches another line, and the s command swaps them. Because tac feeds us the lines in reverse, this restores pairs of lines to their original order.

The key difference to the first approach is the behavior for an odd number of lines: with the second approach, the first line of the file will be alone without a partner, whereas with the first it'll be the last.

Wintermute
  • 42,983
  • 5
  • 77
  • 80
  • Oh! I had not read anything about `N` command, which is really interesting in my case. Thanks a lot for this complete answer, this is even clearer than the tutorial I was reading on the use of ranges, `!` and their combination with commands. Thanks again! – Niols May 09 '15 at 16:37
  • @Niols You may be interested in http://www.grymoire.com/Unix/Sed.html -- very good tutorial; I still use it as a reference when things slipped my mind. – Wintermute May 09 '15 at 16:54
2

I would go with this:

tac file | while read a && read b; do echo $b; echo $a; done
Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
1

Here is an awk you can use:

cat file
1
2
3
4
5
6
7
8

awk '{a[NR]=$0} END {for (i=NR;i>=1;i-=2) print a[i-1]"\n"a[i]}' file
7
8
5
6
3
4
1
2

It store all line in an array a, then print it out in reverse, two by two.

Jotne
  • 40,548
  • 12
  • 51
  • 55