Perl One Linerのススメ

# TL;DR

自分はPerl One Linerを多用するが、使っているところを見られた時にだいたい「意味わからん」とかよく言われるので、なんとなく入門的なものを書こうかと。

* どうしてPerl One Linerを使っているか
* 基本的な使い方

あたりを、ざっくり簡単な書いていきます。


# なぜPerl One Linerか

* Linux環境なら、Perlはおよそインストールされているので、どこでも使える
* 環境によって、正規表現の方言に悩まされない
    * 過去に実行環境における`sed`の拡張正規表現の方言に悩まされたこととかある
* *Nixコマンド的に使えるので、パイプと組み合わせていろいろできて便利
* その気になれば、Perlでできることはほぼ書ける
    * OSコマンド実行とか
    * ハッシュ使って集計とか


# 個人的な使い方

* `sed`、`awk`の代替として
* 少し凝った条件で絞り込みをしたい時には、`grep`の代替として
* で
    * `find`の結果を整形する
    * `xargs`をお供に

といった感じで。

即興でのログの集計や、`find`の結果をPerl One Linerで整形してコマンドを作って実行、などもよくやります。


# 基本形

次が、基本形です。

```shell
$ perl -w[なにか] -e '[One Line Program]' [ファイル名] [ファイル名]...

## ファイルを省略すると、標準入力から読み取る
$ ... | perl -w[なにか] -e '[One Line Program]'
```

* `-w` … 警告を有効にする（より良いスクリプトを書く時には、基本お供で。`use warnings;`）
* `-e` … このオプションの後に、実行するスクリプトを指定する

「なにか」の部分には`-p`や`-n`を指定します。

これは、ざっくり以下のイメージで実行されます。

```shell
while (<>) { ## 指定されたファイルの中身や、パイプで渡された標準入力の内容でループ
    # ここで、「-e」で指定したスクリプトが実行される
}
```

`-p`と`-n`の差は、`print`が入るかどうかです。

これを、覚えておきましょう。

ちなみに、`-e`で指定するスクリプトは`"`（ダブルクォート）ではなく、`'`（シングルクォート）で囲うようにしましょう。`$`がPerlの前にシェルに評価されるのを抑止します。
※これを理解したうえで、意図的に使うこともたまにありますが…

ユースケースとともに、簡単な使い方を書いていってみましょう。

サンプルに、こんなテキストファイルを用意。

sample.txt

```
foo,bar
hoge,fuga
shell,shell
```

こちらを使って、少し例を書いていきます。


## `sed`の代わりに

個人的には、1番よく使います。

```shell
$ perl -wp -e 's/[pattern]/[replace]/' [ファイル名]
$ ... | perl -wp -e 's/[pattern]/[replace]/'

# 「-i」を付けると、対象のファイル自体を書き換える
$ perl -wpi -e 's/[pattern]/[replace]/' [ファイル名]

# 「-i」に引数を追加すると、指定の拡張子でバックアップを作ってくれる
$ perl -wp -i.bk -e 's/[pattern]/[replace]/' [ファイル名]
```

`-p`を付けると、実行時に`print`を含めるようになります。見た目、ほとんど`sed`ですね。

例。

```shell
# 「foo」を「var」に置換
$ perl -wp -e 's/foo/var/' sample.txt 
var,bar
hoge,fuga
shell,shell


# gを使った例
# 「shell」という文字を、すべて「perl」に置換
$ perl -wp -e 's/shell/perl/g' sample.txt 
foo,bar
hoge,fuga
perl,perl

# gなしの場合
$ perl -wp -e 's/shell/perl/' sample.txt 
foo,bar
hoge,fuga
perl,shell

# ファイルそのものを書き換える場合（バックアップ付き）
$ perl -wp -i.bk -e 's/shell/perl/g' sample.txt
$ diff sample.txt sample.txt.bk 
3c3
< perl,perl
---
> shell,shell
```

## `awk`の代わりに

`awk`や`cut`の代わりとして。

```shell
$ perl -wnal -e 'print $F[...]' [ファイル名]
$ ... | perl -wnal -e 'print $F[...]'

# splitのパターンを指定するには、「-F」（正規表現可）を使う
$ perl -wnal -F'...' -e 'print $F[...]' [ファイル名]
$ ... | perl -wnal -F'...' -e 'print $F[...]'
```

`-n`は`print`が入らないので、自分で出力する必要があります。splitした結果は`@F`配列に入るので、`$F[index]`で指定します。

`-l`は、`print`に改行を追加するオプションです。

例。

```shell
$ perl -wnal -F',' -e 'print $F[0]' sample.txt 
foo
hoge
shell
```

## `grep`の代わりに

`print`と`if`の組み合わせで、`grep`のようにも使えます。

```
$ perl -wnl -e 'print if ...' [ファイル名]
$ ... | perl -wnl -e 'print if ...'
```

`print`の後ろには、`$_`（現在処理している行）が隠れています。

例。

```shell
$ perl -wn -e 'print if /foo|shell/' sample.txt 
foo,bar
shell,shell

$ perl -wn -e 'print if /foo/ and /bar/' sample.txt 
foo,bar
```

`if`文が使えるので、それなりに柔軟な表現が可能です。

`grep`よりもキータイプ数が増えてしまうのですが、`grep`だとANDを表現するには

```shell
$ grep ... | grep ... | grep ...
```

とパイプを介することになって、やややりづらいのと、数が増えてくるとプロセス数のオーバーヘッドも大きくなるのでPerl One Linerで書いた方が高速になります。

もちろん、「そんな複雑な条件をOne Linerで書くな」というのはもっともですが…。


# ひとこと

このあたりのオプション（`-p`、`-n`、`-a`、`-l`）と`print`の組み合わせで書いていくのが、Perl One Linerの基本だと思います。

ファイルを直接読むだけではなくて、標準入力からも読み込めるので、あるコマンドの結果を整形するなどもよくやります。

Perl自体を知っていると、`;`で区切って複数の行を書いたり、変数を宣言できたりしていろいろ広がりますが、ほどほどに。


# 知っておくと便利なPerlの変数

他にもいろいろありますが、まずはこのくらい。

* `$_` … 現在処理している行の内容
* `$.` … 現在処理している行番号

# 少し応用

たとえば、以下のようなアクセスログがあるとしましょう。

```
108.81.70.158 - - [16/Jun/2015:13:59:34 +0000] "GET /item/electronics/3717 HTTP/1.1" 200 86 "-" "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"
176.33.96.225 - - [16/Jun/2015:13:59:35 +0000] "GET /category/cameras HTTP/1.1" 200 88 "-" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"
32.129.29.109 - - [16/Jun/2015:13:59:35 +0000] "GET /item/toys/2278 HTTP/1.1" 200 129 "/search/?c=Toys" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1"
192.75.60.93 - - [16/Jun/2015:13:59:35 +0000] "POST /search/?c=Giftcards+Electronics HTTP/1.1" 200 127 "/category/giftcards" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"
72.42.173.76 - - [16/Jun/2015:14:01:35 +0000] "GET /category/software HTTP/1.1" 200 66 "-" "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"
168.180.150.153 - - [16/Jun/2015:14:01:35 +0000] "GET /category/books HTTP/1.1" 200 107 "/item/software/3528" "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:10.0.1) Gecko/20100101 Firefox/10.0.1"
172.171.186.182 - - [16/Jun/2015:14:15:35 +0000] "POST /search/?c=Software+Networking HTTP/1.1" 200 80 "-" "Mozilla/5.0 (Windows NT 6.0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11"
212.147.150.123 - - [16/Jun/2015:14:15:35 +0000] "GET /category/electronics HTTP/1.1" 200 49 "/category/books?from=20" "Mozilla/5.0 (Windows NT 6.0; rv:10.0.1) Gecko/20100101 Firefox/10.0.1"
140.210.67.42 - - [16/Jun/2015:15:00:35 +0000] "GET /item/electronics/1466 HTTP/1.1" 200 80 "/category/software" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0.1) Gecko/20100101 Firefox/9.0.1"
104.33.70.151 - - [16/Jun/2015:15:00:35 +0000] "GET /category/garden?from=10 HTTP/1.1" 200 89 "/item/garden/2967" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
152.90.29.208 - - [16/Jun/2015:15:30:35 +0000] "GET /category/electronics HTTP/1.1" 200 41 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:9.0.1) Gecko/20100101 Firefox/9.0.1"
```

これは、[こちら](https://inokara.hateblo.jp/entry/2015/06/21/225143)に記載されていたものを、少しいじったものです。

ここから、分あたりのアクセス数を出力してみます。

まず、アクセスログの時間の部分から、分までを抜き出します。

```shell
$ perl -wp -e 's/.+ \[([^:]+:\d\d:\d\d):\d\d \+\d+\] .+/$1/g' accesslog 
16/Jun/2015:13:59
16/Jun/2015:13:59
16/Jun/2015:13:59
16/Jun/2015:13:59
16/Jun/2015:14:01
16/Jun/2015:14:01
16/Jun/2015:14:15
16/Jun/2015:14:15
16/Jun/2015:15:00
16/Jun/2015:15:00
16/Jun/2015:15:30
```

あとは、`sort`と`uniq`コマンドを組み合わせれば、あっさり出せたりします。

```shell
$ perl -wp -e 's/.+ \[([^:]+:\d\d:\d\d):\d\d \+\d+\] .+/$1/g' accesslog | sort | uniq -c
      4 16/Jun/2015:13:59
      2 16/Jun/2015:14:01
      2 16/Jun/2015:14:15
      2 16/Jun/2015:15:00
      1 16/Jun/2015:15:30
```

`print`する範囲を変えれば集計範囲が変わるので、例えば時間とパスを出力して、時間別のURLパス単位に集計することも可能です。

自分はたいてい、他のコマンドと組み合わせて使うことが多いのですが参考までに。
※とはいえ、この例だと`sed`でも十分ですね。