sedで各種コマンドを作ってみよう

# はじめに
久々の投稿です。この間初めてシェル芸勉強会に参加しました。
その第27回シェル芸勉強会で sed 祭りがあったので、便乗してコマンド作ってみました。

**sedコマンドはあるけど他のコマンド無いよ〜＞＜；**って言う謎状態に陥らないとも限らないので必見です。

更に、(きっとおそらく多分)簡単順に出してます。また、解説が雑なところなどは気が向き次第ちょくちょく更新しますのでお待ちいただければなと。

# sed のホールドスペースとかパターンスペースとか知らない方へ
[dotinstall](http://dotinstall.com/) や [sedのパターンスペース・ホールドスペースの動作を図で学ぶ - Qiita](http://qiita.com/gin_135/items/773fec1343a69c9f90d6) とかで学習してください。多分紙とペンを用意したほうが頭に入って来やすいと思います。

あとは `man sed` でもして読書しましょう。(ホールド|パターン)スペース、正規表現について勉強された後の方は man ページがいい教科書になると思います。

実行環境は以下のとおりです。

- macOS Sierra
- Zsh 5.3.1
- GNU sed 4.4

# コマンド再現
## 実行(`./`)
```bash
$ sed 'e' sh.sh
```

スクリプトファイルの実行です。実行権限？？なにそれ美味しいの？ただし環境変数等は引き継がれない恐れあり。

イキナリですが `e` コマンドで実行しています。`eval` のようなもので、文字列そのもの(置換後含め)をコマンドとして実行できるものとなります。反則に近いですが、字面は `sed` のみなので許してください。なおこのコマンドは、man ページには GNU 版では一切の記述なし。BSD 版にはあります。

## cat
```bash
$ sed '' eval.awk
```

はい。何もコマンドは書いていません() つまり何も余計なことはするなと言うことです。

これ以降は、この冗長な cat ではなく、素直に `cat` 使います。

すべて sed にするんだーー！という **sed 原理主義者・sed審議委員会の方** はこちらに置き換えてお考えください。

## echo
```bash
$ sed '#任意のスクリプト群' <<<Fxxk
```

Bash ベタベタの Here String。バックスラッシュで遊びたい**変態**(褒め言葉)**シェル芸人**にはあまり向いていないようで、完全互換とはいかないようです。

## tr
```bash
$ cat arr | sed 'y/abc/xyz/g'
```

`y` コマンドは、tr の持つ機能と同等であると考えられます。すなわち、a が x に置換され、b が y に置換されるといった感じです。

## expand
```bash
$ cat a | sed 's/\t/    /g'
```

ハードタブをスペースに展開するコマンド。スペース4個で置換しました。

## wc -w
```bash
$ echo i am a perfect human | sed 's/[^ ]*/1\n/g' | sed -n '$d;=' | sed '$!d'
```

いや〜、うまく短くならない。

`[^ ]*` の部分を `\w*` としてしまうと `std::cout` 等をカウントにかけたときに `wc -w` と挙動が異なってしまうので、こうするしかありませんでした。

## wc -l
```bash
$ cat data | sed -n '=' | sed '$!d'
```

`=` コマンド。現在の行数を、入力行の１行前に追加します。実際中身はいらないので `-n` オプションで本文の出力は抑制。

## grep
```bash
$ cat data | sed '/9999/!d'
```

マッチする行以外は削除、という極めてシンプルなやり方です。`/pattern/command` でパターンマッチができます。否定は上のようにスラッシュとコマンドの間に `!` を置くことに注意。

## grep -v
```bash
$ cat data | sed '/9999/d'
```

上と真逆のことをすればオーケー。

## grep -o .
```bash
$ echo アンメルツ タテタテ | sed 's/./&\n/g' | sed '$d'
```

とりあえず1文字ごとに改行文字を追加しています。最後の文字ではそれは不要なため、最終段の `sed` で削除しました。

## nl -ba -nln
```bash
$ cat test | sed '=' | sed 'N;s/\n/\t/'
```

今回実装したのは `-ba` オプションと `-nln` オプションが付いたもの。`-ba` は、「空行があっても行番号を付加してくださいよ〜」というもの、`-nln` という怪しげなものは「行番号を左揃えで出しましょう」というものになっています。

## nl
```bash
$ cat test | sed 's/^$/@@@/' | sed ':a N; /\n@@@/!ba;s/\n@@@/ @@@/;ba' | sed '=' | sed 'N;s/\n/\t/g' | sed 's/@@@/\n/g'
```

できたけど・・・ふえぇ・・・こんなになっちゃったよう

## tac
```bash
$ cat arr | sed '1!G;h;$!d'
```

こんなのすぐに書ける気がしない...

脳みそを柔らかくすれば理解は出来るはず。

## yes
```bash
$ sed ':a p;ba' <<<y
```

無限ループ(無条件ジャンプ)を使っています。

here string で `echo` コマンドの使用は避ける。

## head
```bash
$ cat doc | sed '10q'
```

まあ簡単。`q` は出力を停止するコマンド。

## tail
```bash
$ cat doc | sed '1!G;h;$!d' | sed '10q' | sed '1!G;h;$!d'
```

まあ `cat | tac | head | tac` ですよね誰が見ても(←

## seq inf
よくわからないですけど、<del>こわい</del>シェル芸人の方がこの `seq inf` を使っていました。ということで、sed でも実装するしか無いですね

```bash
$ sed ':a p;ba' <<<y | sed -n '='
```

## seq
```bash
$ sed ':a p;ba' <<<y | sed -n '=;10q'
```

これは上の自作 yes コマンドと自作 head コマンドをもじったものの組み合わせ。

こうしてみると分かるんですが、完全にモジュール化してますね。

また、**キーボードはあるけど数字キーが無いよ〜＞＜；**という方にも、以下のワンライナーがあれば怖くありません。

```bash
$ sed ':a p;ba' <<<y | sed -n '=' | sed -n 'p;/../q'
```

## basename
```bash
$ echo 'a/b/c/d/e' | sed ':a s:[^/]/*\([^/]/*\):\1:; /\//ba'
```

`なんとか/` を2つ同時にマッチさせてます。スラッシュがなくなるまでループ。

## dirname
```bash
$ echo 'a/b/c/d/e' | sed 's;/[^/]*$;;'
```

## paste - -
一応説明ですが、入力を横に並べられるコマンドです。

```bash
$ seq 1 10 | paste - -
1	2
3	4
5	6
7	8
9	10
```

これは `xargs` を使ってもほぼ同様にできます。

```bash
$ seq 1 10 | xargs -n2
1 2
3 4
5 6
7 8
9 10
```

さて、これを `sed` でやるとこんな感じです。

```bash
$ seq 10 | sed 'N;s/\n/ /'
```

`N` コマンドは次の行をパターンスペースに追加するもの。`sed` は文末の改行を削除・置換できないのでこのような措置を取りました。

## xargs
```bash
$ seq 10 | sed ':a N; s/\n/ /;ba'
```

ループとの組み合わせです。

# イディオム(?)
コマンドではないですが一応。
## FizzBuzz

```bash
$ sed -n '=;100q' /dev/urandom | sed 0~3cFizz | sed 0~5cBuzz | sed 0~15cFizzBuzz
```

`urandom` なんてけしからん！という方は既存の `seq` と組み合わせましょう。

```bash
$ sed ':a p;ba' <<<y | sed -n '=;100q' | sed 0~3cFizz | sed 0~5cBuzz | sed 0~15cFizzBuzz
```

## 隔行入れ替え
`seq 1 10` から以下の出力を得ることを目的とします。

```
2
1
4
3
6
5
8
7
10
9
```

通常は以下のワンライナーで動かすでしょう。

```bash
seq 1 10 | paste - - | awk '{print$2,$1}' | xargs -n1
```

これについて作っていきます。

- `seq 1 10` は上の例より `sed ':a p;ba' <<<y | sed -n '=;10q'`です。
- `paste - -` も同様に `sed 'N;s/\n/ /'`。
- `awk '{print$2,$1}'` は、スペースで区切られた２つの数の表示順番を変えるコマンドです。ですから `sed 's/\(.*\) \(.*\)/\2 \1/'` とでもすればいいでしょう。
- 最後の `xargs -n1` ですが、これは横に2つ並べた文字列に対してスペースを `\n` に置換すればいいわけですから、`sed 's/ /\n/'` くらいでいいでしょう。

以下をまとめて

```bash
$ sed ':a p;ba' <<<y | sed -n '=;10q' | sed 'N;s/\n/ /' | sed 's/\(.*\) \(.*\)/\2 \1/'
```

となります。別のテキストを使う場合はもともとスペースが入っていることもあるでしょうから、ヌル文字で区切ったほうがいいと思います。

# おわりに・雑感
コマンドを作っている時はいいんですけれど、作り終わってふと気を抜いた後にこのスクリプトを見直すとなんだコレ状態になりますね。やはり `sed` は難読化シェル芸に向いているのか...