Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Succinct way to print all lines up until the last line that matches a given pattern

I'm trying to find a succinct shell one-liner that'll give me all the lines in a file up until some pattern.

The use case is dumping all the lines in a log file until I spot some marker indicating that the server has been restarted.

Here's a stupid shell-only way that:

tail_file_to_pattern() {
    pattern=$1
    file=$2

    tail -n$((1 + $(wc -l $file | cut -d' ' -f1) - $(grep -E -n "$pattern" $file | tail -n 1 | cut -d ':' -f1))) $file
}

A slightly more reliable Perl way that takes the file on stdin:

perl -we '
    push @lines => $_ while <STDIN>;
    my $pattern = $ARGV[0];
    END {
        my $last_match = 0;
        for (my $i = @lines; $i--;) {
            $last_match = $i and last if $lines[$i] =~ /$pattern/;
        }
        print @lines[$last_match..$#lines];
    }
'

And of course you could do that more efficiently be opening the file, seeking to the end and seeking back until you found a matching line.

It's easy to print everything as of the first occurrence, e.g.:

sed -n '/PATTERN/,$p'

But I haven't come up with a way to print everything as of the last occurance.

like image 867
Ævar Arnfjörð Bjarmason Avatar asked Jan 22 '12 16:01

Ævar Arnfjörð Bjarmason


2 Answers

Here's a sed-only solution. To print every line in $file starting with the last line that matches $pattern:

sed -e "H;/${pattern}/h" -e '$g;$!d' $file

Note that like your examples, this only works properly if the file contains the pattern. Otherwise, it outputs the entire file.

Here's a breakdown of what it does, with sed commands in brackets:

  • [H] Append every line to sed's "hold space" but do not echo it to stdout [d].
  • When we encounter the pattern, [h] throw away the hold space and start over with the matching line.
  • When we get to the end of the file, copy the hold space to the pattern space [g] so it will echo to stdout.

Also note that it's likely to get slow with very large files, since any single-pass solution will need to keep a bunch of lines in memory.

like image 158
Rob Davis Avatar answered Oct 11 '22 13:10

Rob Davis


Load the data into an array line by line, and throw the array away when you find a pattern match. Print out whatever is left at the end.

 while (<>) {
     @x=() if /$pattern/;
     push @x, $_;
 }
 print @x;

As a one-liner:

 perl -ne '@x=() if /$pattern/;push @x,$_;END{print @x}' input-file
like image 32
mob Avatar answered Oct 11 '22 14:10

mob