Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems. Join them; it only takes a minute:

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

I have a file containing text in paragraphs (lines with text separated by one or more empty lines). I would like to reverse the order of paragraphs (i.e. last paragraph will become the first, ...), preferably by using sed.

I am looking for a sed command which would do to a file of paragraphs, what tac would do to a file of lines.

share|improve this question
up vote 5 down vote accepted

Using sed isn't quite as straight-forward as mentioned by Joseph R.. However, you could say:

sed '/./{H;d;};x;s/\n/={NL}=/g' inputfile | \
sed -e 's/^={NL}=//' -e '1!G;h;$!d' | \
sed G | sed 's/={NL}=/\'$'\n/g'

Given a sample input:

Para 1 line 1
Para 1 line 2
Para 1 line 3

Para 2 line 1
Para 2 line 2
Para 2 line 3

Para 3 line 1
Para 3 line 2
Para 3 line 3

this would produce:

Para 3 line 1
Para 3 line 2
Para 3 line 3

Para 2 line 1
Para 2 line 2
Para 2 line 3

Para 1 line 1
Para 1 line 2
Para 1 line 3

It's worth mentioning that this solution (as well as the alternate Perl one) require a blank line at the end of the input file in order to work as expected.

share|improve this answer

This solution uses both tac and perl to read a paragraph at a time. It does not require reading the whole file into memory.

tac file | perl -00 -lpe '$_ = join "\n", reverse split /\n/'

Reverse all the lines of the file, then for each reversed paragraph, reverse the lines.

share|improve this answer
    
This looks very elegant and efficient. However, this solution also condenses multiple empty (i.e separating) lines into one – Martin Vegter Feb 16 '14 at 14:08

There might be a way to do this with sed, but I doubt it will be simple. Here's how I would do it in Perl:

perl -n00e 'push @paragraphs,$_; END{print for reverse @paragraphs}' your_file

This works because defining the input record separator as the null character (-00) tells Perl to operate in paragraph mode. Perl's definition of a paragraph1 matches your definition exactly.


1Look under the heading Other values for $/

share|improve this answer
    
this works indeed. The only small problem is, it does not preserve multiple empty lines separating the paragraphs. Instead, all paragraphs are separated by exactly one empty line. – Martin Vegter Feb 16 '14 at 13:41

If your paragraphs are always separated by a single empty line:

sed '/^$/s/^/\x02/' infile | tr \\n$'\002' $'\003'\\n | \
sed 's/^\x03//;1s/\x03$//;1!G;h;$!d;$a\' | tr $'\003' \\n

It's quite easy to see how it works if you break it into pieces and run sed '/^$/s/^/\x02/' infile then sed '/^$/s/^/\x02/' infile | tr \\n$'\002' $'\003'\\n and so on...


If your paragraphs are separated by one or more empty lines, e.g.

Para 1 line 1
Para 1 line 2

Para 2 line 1


Para 3 line 1
Para 3 line 2

Para 4 line 1
Para 4 line 2



Para 5 line 1

and you want to reverse the order of paragraphs but preserve the order of "empty blocks" you could read the file twice:
1st: turn paragraphs into single lines (removing empty blocks in-between) and reverse them and
2nd: turn the empty blocks into single lines, "indexing" the number of empty lines in each block (and removing non-empty lines)
then paste the results and process the output to restore newlines:

paste -d $'\004' <(sed '/^$/s/^/\x02/' infile | tr \\n$'\002' $'\003'\\n | \
sed -e '/^\x03$/d;s/^\x03//;s/\x03$//;1!G;h;$!d;$a\') \
<(sed -E '/^$/!d;//{:a;N;/^(\n){1,}$/ba;s/\n/\x02/g;s/(.*)\x02.*/\1/}' infile) \
| sed '$!s/\x04/\n/;$s/\x04$//' | tr $'\003\002' \\n\\n

which outputs:

Para 5 line 1

Para 4 line 1
Para 4 line 2


Para 3 line 1
Para 3 line 2

Para 2 line 1



Para 1 line 1
Para 1 line 2

If you don't mind an extra trailing line in the output you could drop the last sed:

paste -d $'\n' <(sed '/^$/s/^/\x02/' infile | tr \\n$'\002' $'\003'\\n | \
sed -e '/^\x03$/d;s/^\x03//;s/\x03$//;1!G;h;$!d;$a\') \
<(sed -E '/^$/!d;//{:a;N;/^(\n){1,}$/ba;s/\n/\x02/g;s/(.*)\x02.*/\1/}' infile) | \
tr $'\003\002' \\n\\n

These assume that first and last line aren't empty (and no \x02, \x03 or \x04 in the input).

share|improve this answer

You CAN do it with a single instance of sed; no pipes necessary. Since sed only makes one pass through the document and since the portion of the file required as the beginning of the output is at the end of the file, it will require holding the entire file in memory inside sed (in the hold space)—so it may not scale well. But it answers the question exactly:

:getpara
   ${
      s/$/\
/
      G
      s/\n\n$//
      q
   }
   N
   /\n$/!bgetpara
G
h
$!d
s/\n\n$//
q

If there is no trailing newline, this still works fine. If there is a single trailing newline, it is suppressed in the output (i.e. there will not be a leading newline in the output). If there are (for example) 5 trailing newlines in the input, there will be 4 leading newlines in the output.

The gaps between paragraphs are preserved.

Whitespace on an otherwise empty line is NOT treated as a paragraph break, but that's a feature, not a bug. :)

You can also do this as the much less readable one-liner:

sed ':k;${;s/\(\(\n\).*\)$/\1\2/;G;s/\n\n$//;q;};N;/\n$/!bk;G;h;$!d;s/\n\n$//;q' inputfile

Although this only works with GNU sed. (Note the tricky use of backreferences to perform s/$/\n/. Without this it wouldn't be a literal one-liner as it would contain a backslash-newline.)

share|improve this answer
    
so you slurp the file, right? it looks like you put the whole thing in hold space. w/ G;h. you might mention something about input restrictions or similar. – mikeserv Oct 29 '15 at 20:17
    
+1 for the effort but the gaps aren't preserved (regardless of whether the OP wants only the paragraphs reversed or also the gaps; it doesn't preserve the order of the gaps and doesn't reverse their order either); try it with the sample in my post. – don_crissti Oct 29 '15 at 21:19
    
I didn't test the one-liner because I'm working from my Mac and don't have GNU sed handy, but the script version definitely preserves the gaps between paragraphs. I just tested it on your input. Did you test the script version? – Wildcard Oct 29 '15 at 22:09
    
@mikeserv: Definitely true. (Will update tonight.) – Wildcard Oct 29 '15 at 22:10
    
I've tested both and they both mix up the gaps. Here's what the output looks like when using the script. The one liner is similar just that it eats one line from the last gap. – don_crissti Oct 30 '15 at 8:50
gem install facets

ruby -r facets/string \
     -e 'puts $stdin.read.strip.shatter(/\n\n+/).reverse.join("")' < file

This should preserve your paragraph spacing (while being more readable than sed :) ) Although, props to devnull for an awesome answer.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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