SNSへはこちら

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

はじめに

この記事はQiitaに投稿している記事です。今後はこちらを最新のものに更新していこうと思っています。
私は過去に第27回シェル芸勉強会へ参加しました。そのときに sed 祭りがあったので、便乗してコマンド作ってみました。

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

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

sed のホールドスペースとかパターンスペースとか知らない方へ

dotinstallsedのパターンスペース・ホールドスペースの動作を図で学ぶ - Qiita とかで学習してください。多分紙とペンを用意したほうが頭に入って来やすいと思います。

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

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

  • macOS Sierra
  • Zsh 5.3.1
  • GNU sed 4.4

コマンド再現

実行(./)

$ sed 'e' sh.sh

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

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

cat

$ sed '' eval.awk

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

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

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

echo

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

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

tr

$ cat arr | sed 'y/abc/xyz/g'

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

expand

$ cat a | sed 's/\t/    /g'

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

wc -w

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

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

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

wc -l

$ cat data | sed -n '=' | sed '$!d'

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

grep

$ cat data | sed '/9999/!d'

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

grep -v

$ cat data | sed '/9999/d'

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

grep -o .

$ echo アンメルツ タテタテ | sed 's/./&\n/g' | sed '$d'

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

nl -ba -nln

$ cat test | sed '=' | sed 'N;s/\n/\t/'

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

nl

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

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

tac

$ cat arr | sed '1!G;h;$!d'

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

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

yes

$ sed ':a p;ba' <<<y

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

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

head

$ cat doc | sed '10q'

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

tail

$ cat doc | sed '1!G;h;$!d' | sed '10q' | sed '1!G;h;$!d'

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

seq inf

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

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

seq

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

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

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

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

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

basename

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

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

dirname

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

paste - -

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

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

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

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

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

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

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

xargs

$ seq 10 | sed ':a N; s/\n/ /;ba'

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

イディオム(?)

コマンドではないですが一応。

FizzBuzz

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

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

$ 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

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

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}' は、スペースで区切られた2つの数の表示順番を変えるコマンドです。ですから sed 's/\(.*\) \(.*\)/\2 \1/' とでもすればいいでしょう。
  • 最後の xargs -n1 ですが、これは横に2つ並べた文字列に対してスペースを \n に置換すればいいわけですから、sed 's/ /\n/' くらいでいいでしょう。

以下をまとめて

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

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

おわりに・雑感

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