tail, grepを使って特定キーワードを含む行以下を表示する

この記事は[SRA Advent Calendar 2018](https://qiita.com/advent-calendar/2018/sra) の6日目の記事です。
カレンダーが空いていたので埋め草です。

### やりたいこと
ログとかを調査していて、「あー、この日付以降の行だけ見たいんだけどなあ」ということがありませんか。
わたしはよくあります。ログがつねに適切な量でローテーションされていたらこんな苦労ないのに。。。

### こうすればできる
```bash

tail -n +$(grep -n "キーワード" ファイル名 |head -1| sed 's/:.*$//') ファイル名
```

#### 適当に解説
まず、バッククォートの中身からです。
`grep -n "キーワード" ファイル名` でもって、対象ファイルからそのキーワードがある行を行番号付きでgrepしています。
ex.

```bash

$  grep -n "Dec 06" catalina.out
204325:Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
204327:Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
204329:Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
:
```

次にパイプで `|head -1` が続くので、特定キーワードを含む行の先頭だけを取り出しています。
さらにパイプで`|sed 's/:.*$//'` が続いているので、特定キーワードを含む行がでてきた行番号だけ取り出せます。

```bash
$ grep -n "Dec 06" catalina.out  |head -1
204325:Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
$ grep -n "Dec 06" catalina.out  |head -1|sed 's/:.*$//'
204325
```

で、この行数が ` tail -n +行数 ファイル名` に入ってくるので、特定キーワードを含む行以降だけ出力できるわけです。

```bash

$ tail -n +$(grep -n "Dec 06" catalina.out  |head -1|sed 's/:.*$//') catalina.out  | head
Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server version:        Apache Tomcat/7.0.86
Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server built:          Apr 9 2018 20:16:54 UTC
Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server number:         7.0.86.0
:
```

しかし、毎回、これを打つのも面倒です。ファイル名二回も打たなくちゃいけないし。
よし、functionにしよう。

### .bashrc にfunctionとして登録する
これを.bashrcの最後にいれました。

```bash

function greptail() {
  if [ -p /dev/stdin  ]; then
    tmpf=$(mktemp "/tmp/greptail.tmp.XXXXXX")
    cat > $tmpf
    tail -n +$(grep -n "$1" $tmpf|head -1 | sed 's/:.*$//') $tmpf
    rm -f $tmpf
  else
    tail -n +$(grep -n "$1" $2 |head -1| sed 's/:.*$//') $2
  fi
}

```
#### 使い方
ファイル名を指定する場合はこう。greptailの第一引数にキーワード、第二引数にファイル名を与えます。

```bash

$ greptail "Dec 06" catalina.out
Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server version:        Apache Tomcat/7.0.86
Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server built:          Apr 9 2018 20:16:54 UTC
:
```

標準入力を利用することも一応できます。

```bash
$ cat catalina.out |greptail "Dec 06" 
Dec 06, 2018 10:44:52 AM org.apache.catalina.startup.VersionLoggerListener log
INFO: Server version:        Apache Tomcat/7.0.86
:
```

キーワードがgrepで存在しなかったりすると、こうでますが、ご愛嬌。

```bash

$ greptail "naiyo-" catalina.out
tail: invalid number of lines: ‘+’
```

#### 軽く解説（いいわけ）
if文の前半は後回しにしておいて、else節の中身は先程のコマンドのものとまるきり同じです。

```bash
    tail -n +$(grep -n "$1" $2 |head -1| sed 's/:.*$//') $2
```
grepに渡す$1の周りをダブルクォートで囲い忘れると途端に動かなくなる。


さて、後回しにしたif文の方です。
せっかくfunctionにするのだから、標準入力もとれるようにしたいと思ったのです。パイプでつなげるのが理想。

なので、functionには`[ -p /dev/stdin ]` で標準入力を開いているときと、開いていないときとで分岐が入っています。標準入力が開いてないときはelse節のほうにいっています。

しかし、標準入力でを開いているときのほうが、問題でした。これ、二回標準入力をなめないといけないんですね。

if文の前半を` tail -n +$(grep -n "$1" - |head -1| sed 's/:.*$//') -` こういうふうにするとどうなるかというと、何も返ってこない。
$() の中身のgrepで標準入力が終わりまで読まれているので、 `tail -n +行数 -` したときには標準入力は空っぽなのです。
標準入力を読み終わったあと頭に戻せないかいろいろ考えてみましたが、今の所見つからず。やむを得ず、標準入力が開かれていたときは一旦tmpファイルに内容を書き出して、それを使って読み込みしてます。
どでかいファイルを標準入力で渡したときに、大きいtmpファイルを作っちゃうじゃないか、とか、tmpファイルが残っちゃったらどうするか、とか、いろいろ問題はあるんですが。

### 別解
こうすればできるとコメントをいただきました。ありがとうございます。

#### awk

```bash
$ awk '/キーワード/,/*/ {print}' ファイル名 
```
なるほどー。
awkの範囲パターンを使って、キーワードのある行からすべての行をパターンに指定しているんですね。これは便利。

ただし、このawk、一部のawkでは動きませんでした(mawk 1.3.3 でNG)。試した範囲で、GNU Awk 3.1.7, GNU Awk 4.2.1 とかでは大丈夫。

#### sed

```bash
$ sed -e '/キーワード/,$p' -e d ファイル名
```

sedでキーワードのある行から、最後の行まで($)をpで出力し（行の複製）、次の -e d でもともとの行を消しているんですね。
全く思いつきませんでした。
awk力もsed力も高めなければ。

### パフォーマンス

やり方が複数あったら計測するよね、ということで。
functionにいれたgreptail, awk, sedを実施して timeコマンドで実行時間をとってみました。

#### 計測1
対象ファイル総行数：205,778行
キーワードから末尾までの行数 :41行

|  | greptail | greptail(stdin) | awk | sed |
|:-:|:-:|:-:|:-:|:-:|
| real  | 0m0.096s  | 0m0.305s | 0m2.549s  | 0m0.194s |
| user  | 0m0.040s  | 0m0.044s |0m1.788s  | 0m0.124s |
| sys   | 0m0.026s  | 0m0.130s |0m0.030s  | 0m0.013s  |

#### 計測2
対象ファイル総行数：2,185,828行
キーワードから末尾までの行数 :15行

|  | greptail | greptail(stdin) | awk | sed |
|:-:|:-:|:-:|:-:|:-:|
| real  | 0m11.194s| 0m20.023s | 0m26.310s| 0m5.772s |
| user  | 0m0.378s| 0m0.378s | 0m18.071s  | 0m1.417s |
| sys   | 0m0.497s| 0m1.390s | 0m0.342s | 0m0.281s  |

sedが安定した速さという感じですね。