12

I'm trying to change the order of lines in a specific pattern. Working with a file with many lines (ex. 99 lines). For every three lines, I would like the second line to be the third line, and the third to be the second line.

EXAMPLE.

1- Input:

gi_1234
My cat is blue.
I have a cat.
gi_5678
My dog is orange.
I also have a dog.
...

2- Output:

gi_1234
I have a cat.
My cat is blue.
gi_5678
I also have a dog.
My dog is orange.
...

9 Answers 9

23
$ seq 9 | sed -n 'p;n;h;n;G;p'
1
3
2
4
6
5
7
9
8

That is, print the current line, get the next one, hold it, get the next one, Get the held line (append it to the pattern space) and print that 2-line pattern space with the third and second lines swapped.

12

Using awk and integer maths:

awk 'NR%3 == 1 { print } NR%3 == 2 { delay=$0 } NR%3 == 0 { print; print delay; delay=""} END { if(length(delay) != 0 ) { print delay } }' /path/to/input

The modulus operator performs integer division and returns the remainder, so for each line, it will return the sequence 1, 2, 0, 1, 2, 0 [...]. Knowing that, we just save the input on lines where the modulus is 2 for later -- to wit, just after printing the input when it's zero.

2
  • We've a small flaw here. See my answer, minor improvement part Jun 1, 2017 at 11:29
  • Thanks for the good catch; I have incorporated a fix into my answer in the form of NR%3 == 0 { print; print delay; delay=""} END { if(length(delay) != 0 ) { print delay }.
    – DopeGhoti
    Jun 1, 2017 at 15:14
3

Another awk approach:

awk '{print $0; if ((getline L2)>0 && (getline L3)>0){ print L3 ORS L2 }}' file

The output:

gi_1234
I have a cat.
My cat is blue.
gi_5678
I also have a dog.
My dog is orange.

  • (getline L2)>0 && (getline L3)>0 - extracts next 2 records if they exist

  • each 2nd and 3rd records are assigned to L2 and L3 variables respectively

2
  • 1
    I'm assuming those variables start with the letter L (lower case). They are poor choices for readability because they look like the numerals for twelve and thirteen. A better choice might be line2, etc. Jun 1, 2017 at 16:23
  • @DennisWilliamson, changed to uppercase Jun 1, 2017 at 17:45
2

Using perl and a short script:

user@pc:~$ cat input.txt 
gi_1234
My cat is blue.
I have a cat.
gi_5678
My dog is orange.
I also have a dog.

user@pc:~$ perl -ne '$l2=<>; $l3=<>; print $_,$l3,$l2;' input.txt 
gi_1234
I have a cat.
My cat is blue.
gi_5678
I also have a dog.
My dog is orange.

The script process the whole file, for each line (stored in $_) it will get the next two lines ($l2 and $l3) and print them in the requested order: line1, line3, line2.

1

One way could be as follows:

sed -e '
   /\n/s/\(.*\)\(\n\)\(.*\)/\3\2\1/;//b
   $!N;$q;N;                            # load up the pattern space with 3 lines provided eof not reached
   P;D;                                 # first just print the first line then interchange the two and print them
' yourfile

Alternatively,

perl -ne 'print $_, reverse scalar <>, scalar <>' yourfile

Results

gi_1234
I have a cat.
My cat is blue.
gi_5678
I also have a dog.
My dog is orange.
1

Why not just make a while loop? In expanded form:

( while read a
  do
    read b
    read c
    echo "$a"
    echo "$c"
    echo "$b"
  done
) < input.txt

In "single line format":

( while read a ; do read b ; read c ; echo "$a" ; echo "$c" ; echo "$b" ; done) < input.txt

Outputs:

gi_1234
I have a cat.
My cat is blue.
gi_5678
I also have a dog.
My dog is orange.
1

Perl

perl -ne 'print if $.%3==1;$var=$_ if $.%3==2;print $_ . $var if $.%3==0' input.txt

The idea here is that we use modulo operator % with line number $. variable, to figure out which one is every first, which one is every second, and which one is every 3rd line. For every 3rd line remainder is 0, while for every 1st and 2nd line it will have corresponding numbers.

Test:

$ cat input.txt                                                                                                          
gi_1234
My cat is blue.
I have a cat.
gi_5678
My dog is orange.
I also have a dog.

$ perl -ne 'print if $.%3==1;$var=$_ if $.%3==2;print $_ . $var if $.%3==0' input.txt                                    
gi_1234
I have a cat.
My cat is blue.
gi_5678
I also have a dog.
My dog is orange.

Minor improvement

The approach with storing second line into a variable has a flaw. What if the last line is the "second" one , i.e. for that line number remainder is 2 ? The original code in my and DopeGhoti's answer won't print My dog is orange if we leave out the last line. The fix for that in both cases is to use END{} code block, with unsetting the temporary variable after printing. In other words:

$ awk 'NR%3 == 1 { print } NR%3 == 2 { delay=$0 } NR%3 == 0 { print; print delay;delay=""}END{print delay}' input.txt

and

$ perl -ne '$s=$_ if $.%3==2;print $_ . $s and $s="" if $.%3==0 or $.%3==1;END{print $s}' input.txt 

This way, the code will work for arbitrary number of lines in a file, not just those divisible by 3.

Additional fix for issue mentioned in comments

In awk's case, if the last line in the file produces output of 1 for $. % 3, the previous code has issue of outputting blank newline because of unconditional printing of END{print delay}, since print function mentioned in the comments always appends newline to whatever variable it is operating on. In case of perl version this issue doesn't occur, since with -ne flags print function doesn't append the newline.

Nonetheless, the fix in awk's case is to make conditional, as mentioned by Dope Ghoti in the comments is to verify the length of the temporary variable. The perl version of the same fix would be:

$ perl -ne '$s=$_ if $.%3==2;print $_ . $s and $s="" if $.%3==0 or $.%3==1;END{print $s if length $s}' input.txt 
2
  • 1
    Your fix has a potential minor flaw of its own in that it will append a blank line of output for files with the 'wrong' number of lines. I have fixed this in my incorporation of your improvement in my answer with (for awk) NR%3 == 0 { print; print delay; delay=""} END { if(length(delay) != 0 ) { print delay }.
    – DopeGhoti
    Jun 1, 2017 at 15:22
  • 1
    @DopeGhoti The issue doesn't occur with perl, since perl's print with -ne flags doesn't output a newline. It does indeed print, but it's a null string, no trailing newline. Nontheless, I've added the mention of the issue and the same fix into my answer. Thanks ! Jun 1, 2017 at 22:28
1

Vim

Not suitable for long files, but still handy if you were just editing a file and wanted, for example, to reorder some yaml stanzas.

First record a macro:

gg qq j ddp j q

And then repeat desired number of times:

@q @q @q ...

Or just e.g.

3@q

Explanation:

  • gg - go to the first line
  • qq - start recording a macro
  • j - go to the second line
  • ddp - swap the second and the third line
  • j - go to the fourth line, i.e. to the first of the next three lines
  • q - stop recording
  • @q - replay the macro once
  • 3@q - replay the macro three times
1
  • 1
    Instead manual repeating @q @q @q, it is possible doing by this way 3@q - repeat three times. 100@q - repeat the macro 100 times.
    – MiniMax
    Jun 1, 2017 at 19:28
0

Usage: ./shuffle_lines.awk input.txt

Check shebang #!/usr/bin/awk -f, because the awk location may differ on your system.

#!/usr/bin/awk -f

{
    if ((NR + 1) % 3 == 0) {
        buffer = $0;
    } else if (NR % 3 == 0) {
        print $0 ORS buffer;
        buffer = "";
    } else {
        print;
    }
}

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.