tomyamaのブログ

日記・雑記。

検索にマッチした行の前後を表示する

前にPerlで書いたフィルタースクリプトです。

 

主な機能は、指定したキーワードを見易いようにANSIエスケープシーケンスでマーキングすることです。ログを読むときに、蛍光ペンを引いたようにキーワード部分を読み易くしたくて作成しました。

 

また、-fオプションを指定することで、行指向フィルターとしても機能します。

 

プログラムのソースを検索するときは、grepのようにキーワードにマッチした行だけでは文脈を理解できません。そのため、マッチした前後の行も表示させるように実装しています。

デフォルトではマッチした行の前後を5行ずつ表示します。このデフォルトの行数を変更するには、-fの次に数字を指定します。

  • 例)「-f」 数値を指定しない場合はデフォルト(前後5行を表示)
  • 例)「-f 3」 前後3行を表示
  • 例)「-f 0,5」 前は0行,後ろは5行を表示

 

他にもオプションスイッチがあるのですが、grepのオプションに似せた仕様にしています。ただし、キーワードに使える正規表現grepとは異なり、Perl拡張正規表現です。

 

実行例

$ mark -inf 0,3 tty /usr/local/bin/mark
        *** skip ***
     48:                        if( $main::bIsATty ){
     49:                                if ($main::ignorecase) {
     50:                                        if ($mybuff =~ s/($main::re)/\033[1m$1\033[0m/igo) {
     51:                                                $match_flg = 1 ;
        *** skip ***
     97:                                        if( $main::bIsATty ){
     98:                                                print("        \033[34m",
     99:                                                        "*** skip ***",
    100:                                                        "\033[0m\n") ;
        *** skip ***
    164:        $main::bIsATty = -t STDOUT;
    165:        ##############
    166:}
    167:
        *** skip ***
    183:        &prt_variable ('bIsATty',       $main::bIsATty) ;
    184:        &prt_variable ('numbering',     $main::numbering) ;
    185:        &prt_variable ('prt_fname',     $main::prt_fname) ;
    186:        &prt_variable ('opt_nofname',   $main::opt_nofname) ;
$

 

コンソールに表示する際は色付けした表示が見易いのですが、ファイルにリダイレクトする際にはANSIエスケープシーケンスの制御コードが邪魔になる事が多いです。その為、ANSIエスケープシーケンスを抑止するコードを入れています。下記のコードは、抑止するか否かを判定する為のフラグを設定している行です。

        $main::bIsATty = -t STDOUT;

ANSIエスケープシーケンスの抑止については『ANSIエスケープシーケンスを使えない環境に対応する』という記事も参照してみて下さい。

 

実行例(色付けされていない=ANSIエスケープシーケンスの制御コードの出力が抑止されている)

$ mark -inf 0,3 tty /usr/local/bin/mark > /tmp/mark_summary.txt
$ cat /tmp/mark_summary.txt
        *** skip ***
     48:                        if( $main::bIsATty ){
     49:                                if ($main::ignorecase) {

…(省略)…
    167:
        *** skip ***
    183:        &prt_variable ('bIsATty',       $main::bIsATty) ;
    184:        &prt_variable ('numbering',     $main::numbering) ;
    185:        &prt_variable ('prt_fname',     $main::prt_fname) ;
    186:        &prt_variable ('opt_nofname',   $main::opt_nofname) ;
$

 

[mark] Perlスクリプト
  • マーカーペンをイメージ、かつ人の名前っぽいので「mark」という名前にしています。

#!/usr/bin/perl -w
################################################################################
## MARK -- emphasizes part matching a pattern
##
## - Author: tomyama  2006-2022
## - Only for personal use !
##
################################################################################

use strict ;
use File::Basename ;

exit (&pl_main (@ARGV)) ;

##########
## スクリプトのエントリポイント
sub pl_main {

        ## 初期化処理
        &init_script();

        ## 引数解析
        &parse_arg (@_) ;

        ## デバッグ用 : パラメータ出力
        &prt_param () if ($main::debug) ;

        for (my ($i, $m) = (0, scalar (@main::fi_in)) ; $i < $m ; $i++) {
                my $opn_fname = "$main::fi_in[$i]" ;

                print STDERR (qq{ ***** $opn_fname *****\n}) if ($main::debug) ;

                open (FI_IN, "<$opn_fname") ||
                        die (qq{$main::appname: `$opn_fname': } .
                                qq{could not open file: $!\n}) ;
                my $nr = 0 ;
                my $num = 8 ;
                if ($main::prt_fname) {
                        $num = (((int (length ($opn_fname) / 8)) + 1) * 8) - 1 ;
                }
                while (<FI_IN>) {
                        $nr++ ;
                        my $mybuff = $_ ;
                        $mybuff =~ s/\r?\n$//o ;

                        ## マーキングする
                        my $match_flg = 0 ;
                        if( $main::bIsATty ){
                                if ($main::ignorecase) {
                                        if ($mybuff =~ s/($main::re)/\033[1m$1\033[0m/igo) {
                                                $match_flg = 1 ;
                                        }
                                } else {
                                        if ($mybuff =~ s/($main::re)/\033[1m$1\033[0m/go) {
                                                $match_flg = 1 ;
                                        }
                                }
                        }else{
                                if( $main::ignorecase ){
                                        if( $mybuff =~ m/($main::re)/igo ){
                                                $match_flg = 1;
                                        }
                                }else{
                                        if( $mybuff =~ m/($main::re)/go ){
                                                $match_flg = 1;
                                        }
                                }
                        }

                        ## マッチしていなかったら
                        if ($main::opt_filter && ! $match_flg) {
                                ## 後方行の出力
                                $main::opt_filter_e-- ;
                                if ($main::opt_filter_e >= 0) {
                                        goto PRINTOUT ;
                                }

                                ## 前方行をバッファに溜めておく
                                ## opt_filter_pre=0 の場合はバッファは必要無し
                                if ($main::opt_filter_pre == 0) {
                                        next ;
                                ## バッファが満杯であれば整理しておく
                                } elsif (scalar (@main::opt_filter_buff) >=
                                                $main::opt_filter_pre) {
                                        shift (@main::opt_filter_buff) ;
                                }

                                ## バッファに溜める
                                push (@main::opt_filter_buff, $mybuff) ;
                                next ;
                        ## マッチしていたら
                        } elsif ($main::opt_filter) {
                                ## 読み易いように、skip...を出力
                                if ($main::opt_filter_e +
                                                scalar (@main::opt_filter_buff) <
                                                0) {
                                        if( $main::bIsATty ){
                                                print("        \033[34m",
                                                        "*** skip ***",
                                                        "\033[0m\n") ;
                                        }else{
                                                print("        *** skip ***\n");
                                        }
                                }

                                ## バッファを吐き出す
                                for (my ($i, $nri, $m) = (
                                        0,
                                        $nr - scalar (@main::opt_filter_buff),
                                        $#main::opt_filter_buff);
                                                $i <= $m; $i++, $nri++) {
                                        ## 必要に応じてファイル名を出力
                                        if ($main::prt_fname) {
                                                printf ("%-${num}s:",$opn_fname) ;
                                        }
                                        ## 必要に応じて行番号を出力
                                        if ($main::numbering) {
                                                printf ("%7d:", $nri) ;
                                        }
                                        ## 出力する
                                        print ("$main::opt_filter_buff[$i]\n") ;
                                }
                                undef (@main::opt_filter_buff) ;

                                ## 後方行出力用のカウンタをセットする
                                $main::opt_filter_e = $main::opt_filter_post ;
                        }

                        PRINTOUT:
                        ## 必要に応じてファイル名を出力
                        printf ("%-${num}s:",$opn_fname) if ($main::prt_fname) ;

                        ## 必要に応じて行番号を出力
                        printf ("%7d:", $nr) if ($main::numbering) ;

                        ## 出力する
                        print ("$mybuff\n") ;
                }
                close (FI_IN) ;
        }

        return 0 ;
}

##########
## 初期化処理
sub init_script{
        ### GLOBAL ###
        $main::apppath = dirname ($0) ;
        $main::appname = basename ($0) ;
        $main::debug = 0 ;
        $main::numbering = 0 ;
        $main::prt_fname = 0 ;
        $main::opt_nofname = 0 ;
        $main::opt_filter = 0 ;
        $main::opt_filter_pre  = 0 ;
        $main::opt_filter_post = 0 ;
        #@main::opt_filter_buff ;
        $main::opt_filter_e = 0 ;
        $main::ignorecase = 0 ;
        $main::re = undef ;
        #@main::fi_in ;
        ## [ANSIエスケープシーケンス]を使うか否かの判定で使う
        $main::bIsATty = -t STDOUT;
        ##############
}

##########
## デバッグ用 : パラメータ出力
sub prt_param {
        my $c = 20 ;
        local *prt_variable = sub ($$) {
                printf STDERR (qq{PARAM : %-${c}s = "%s"\n}, uc ($_[0]),
                        $_[1] ? 'True' : 'False') ;
        } ;

        print  STDERR (qq{ ***** PARAMETER *****\n}) ;
        printf STDERR (qq{PARAM : %-${c}s = "%s"\n},
                uc ('apppath'), $main::apppath) ;
        printf STDERR (qq{PARAM : %-${c}s = "%s"\n},
                uc ('appname'), $main::appname) ;
        &prt_variable ('debug',         $main::debug) ;
        &prt_variable ('bIsATty',       $main::bIsATty) ;
        &prt_variable ('numbering',     $main::numbering) ;
        &prt_variable ('prt_fname',     $main::prt_fname) ;
        &prt_variable ('opt_nofname',   $main::opt_nofname) ;
        &prt_variable ('opt_filter',    $main::opt_filter) ;
        printf STDERR (qq{PARAM : %-${c}s = "%s"\n},
                uc ('opt_filter_pre'),  $main::opt_filter_pre) ;
        printf STDERR (qq{PARAM : %-${c}s = "%s"\n},
                uc ('opt_filter_post'), $main::opt_filter_post) ;
        &prt_variable ('ignorecase',    $main::ignorecase) ;
        printf STDERR (qq{PARAM : %-${c}s = '%s'\n},
                uc ('Regular_Expressions'), $main::re) ;
        for (my ($i, $m) = (0, scalar (@main::fi_in)) ; $i < $m ; $i++) {
                printf STDERR (qq{PARAM : %-${c}s = "%s"\n},
                        uc ("fi_in($i)"), $main::fi_in[$i]) ;
        }
}

##########
## 引数解析
sub parse_arg {
        my @val = @_ ;

        ## 引数分のループを回す
        while (my $myparam = shift (@val)) {
                if ($myparam =~ s/^-([a-zA-Z])([a-zA-Z]+)$/-$1/o) {
                        unshift (@val, "-$2") ;
                }

                ## デバッグモードOn
                if      ($myparam eq '-d' || $myparam eq '--debug') {
                        $main::debug = 1 ;
                } elsif ($myparam eq '-f') {
                        $main::opt_filter       = 1 ;
                        $main::opt_filter_pre   = 5 ;
                        $main::opt_filter_post  = 5 ;
                        if (defined ($val[0]) && $val[0] =~
                                        m/^([0-9]+)(?:,([0-9]+))?$/o) {
                                ## 捨てる
                                shift (@val) ;

                                $main::opt_filter_pre  = $1 ;
                                if (defined ($2)) {
                                        $main::opt_filter_post = $2 ;
                                } else {
                                        $main::opt_filter_post = $1 ;
                                }
                        }
                } elsif ($myparam eq '-H' || $myparam eq '--with-filename') {
                        $main::prt_fname = 1 ;
                } elsif ($myparam eq '-h' || $myparam eq '--no-filename') {
                        $main::opt_nofname = 1 ;
                } elsif ($myparam eq '--help') {
                        &usage (0) ;
                        exit (0) ;
                } elsif ($myparam eq '-i' || $myparam eq '--ignore-case') {
                        $main::ignorecase = 1 ;
                } elsif ($myparam eq '-n' || $myparam eq '--line-number') {
                        $main::numbering = 1 ;
                } else {
                        if (! defined ($main::re)) {
                                $main::re = $myparam ;
                        } else {
                                if ("$myparam" eq '-') {
                                        ## dummy
                                } elsif (! -f "$myparam") {
                                        warn (qq{$main::appname: `$myparam': },
                                                "file not found.\n") ;
                                        &usage (1) ;
                                        exit (1) ;
                                } elsif (! -r "$myparam" && ! -l "$myparam") {
                                        warn (qq{$main::appname: `$myparam': },
                                                "permission denied.\n") ;
                                        &usage (1) ;
                                        exit (1) ;
                                }
                                push (@main::fi_in, $myparam) ;
                        }
                }
                print STDERR (qq{ARGV : "$myparam"\n}) if ($main::debug) ;
        }

        if (! defined ($main::re)) {
                warn ("$main::appname: ",
                        "Please specify the Regular Expressions.\n") ;
                &usage (1) ;
                exit (1) ;
        }

        if ($#main::fi_in < 0) {
                #&usage (1) ;
                #exit (1) ;
                push (@main::fi_in, '-') ;
        } elsif ($#main::fi_in > 0) {
                $main::prt_fname = 1 if (! $main::opt_nofname) ;
        }
}

##########
## 書式表示
sub usage ($) {
        my $msg = "usage: " .
        "$main::appname [<OPTIONS...>] <PATTERN> [<FILE...>]\n" .
        "Try `perldoc $main::apppath/$main::appname' for more information.\n" ;

        if ($_[0]) {
                print STDERR ($msg) ;
        } else {
                print STDOUT ($msg) ;
        }

        return 0 ;
}
__END__

=pod

=head1 NAME

MARK - emphasizes part matching a pattern

=head1 SYNOPSIS

$ mark [I<OPTIONS...>] I<PATTERN> [I<FILE...>]

=head1 DESCRIPTION

The "B<mark>" behaves like the marker pen.
The specified I<PATTERN> is searched out and that part is emphasized.

The I<PATTERN> can be described by the Regular-Expression equal with B<Perl>.

I<FILE>s specifies the input file name.
If it is a standard input, "B<->" is given.

=head1 OPTIONS

=over 4

=item -d, --debug

Debugging mode is on.

=item -f [I<num-forward>[,I<num-rear>]]

It behaves like the filter program.
The back and forth 5 lines are displayed in default.

=item --help

Simple help is displayed.

=item -h, --no-filename

Suppress the prefixing of filenames on output when multiple files are searched.

=item -H, --with-filename

Print the filename for each match.

=item -i, --ignore-case

Ignore case distinctions in the I<PATTERN>.

=item -n, --line-number

Prefix each line of output with the line number within its input file.

=back

=head1 ADVANCED USAGE

$ rpm -qa | mark '-[0-9]+[a-z]?\..+$'

$ mark '\b\d{1,3}(?:\.\d{1,3}){3}\b' /var/log/maillog

$ mark -nf 5,0 '(ServerName|DocumentRoot|Log)\s+.*$' /etc/httpd/conf/httpd.conf

$ mark -iHnf 0,10 '^[^\s].*$' *.{c,h}

$ mark -ni '</?(table|tr|th|td)[^>]*>' index.html | S<less -R>

$ man perlfunc | mark -nf 5,10 -i 'regular expr' | S<less -R>

$ man awk | perl -ne 's/.\010//go; print' | S<mark -nf 0,3 '^[A-Z]'>

$ tail -f /var/log/httpd/access_log | S<mark -f 0 '"(GET|POST) /(?:\S+(?:\.html|\.htm|/))? [^"]+"'>

$ ls -tr /var/log/messages.?.gz | xargs gzip -dc | mark -ihf 10 'error' - /var/log/messages > /tmp/report.txt

=head1 SEE ALSO

When you want to examine the regular expression,
please refer to an online manual of B<Perl>.

=over 4

=item L<perlre>(1)

Perl regular expressions

=item L<perlrequick>(1)

Perl regular expressions quick start

=item L<perlreref>(1)

Perl Regular Expressions Reference

=item L<perlretut>(1)

Perl regular expressions tutorial

=item L<perlfaq6>(1)

Regular Expressions

=item regex(7)

POSIX 1003.2 regular expressions

=back

To look for the above-mentioned, I executed the following command.

`man -k regular'

Other more basic references

L<perl>(1), grep(1)

=head1 BUGS

* none noted

Report bugs to tomyama.

=head1 AUTHOR

Written by tomyama at Oct 2006

=cut

 

NAME

    MARK - emphasizes part matching a pattern

 

SYNOPSIS

    $ mark [OPTIONS...] PATTERN [FILE...]

 

OPTIONS

    -f [num-forward[,num-rear]]
        It behaves like the filter program. The back and forth 5 lines are
        displayed in default.

    --help
        Simple help is displayed.

    -h, --no-filename
        Suppress the prefixing of filenames on output when multiple files
        are searched.

    -H, --with-filename
        Print the filename for each match.

    -i, --ignore-case
        Ignore case distinctions in the PATTERN.

    -n, --line-number
        Prefix each line of output with the line number within its input
        file.

 

ADVANCED USAGE

    $ rpm -qa | mark '-[0-9]+[a-z]?\..+$'

    $ mark '\b\d{1,3}(?:\.\d{1,3}){3}\b' /var/log/maillog

    $ mark -nf 5,0 '(ServerName|DocumentRoot|Log)\s+.*$'
    /etc/httpd/conf/httpd.conf

    $ mark -iHnf 0,10 '^[^\s].*$' *.{c,h}

    $ mark -ni '</?(table|tr|th|td)[^>]*>' index.html | less -R

    $ man perlfunc | mark -nf 5,10 -i 'regular expr' | less -R

    $ man awk | perl -ne 's/.\010//go; print' | mark -nf 0,3 '^[A-Z]'

    $ tail -f /var/log/httpd/access_log |
    mark -f 0 '"(GET|POST) /(?:\S+(?:\.html|\.htm|/))? [^"]+"'

    $ ls -tr /var/log/messages.?.gz | xargs gzip -dc | mark -ihf 10 'error'
    - /var/log/messages > /tmp/report.txt