de[v|b]log

ShellScript, Coffee, iOS/OSX Dev
Origin: Himajinworks.
About.

久々に(リモートだが)参加させていただいた。
今回は治安が悪かった。(褒め言葉)

気づいたこととして、そこまでしっかり nl を使っていないこと(日々の生活で使えていない)と、 sed の使い方を知らなかった事がある。

nl をしっかり使っていなかったこと

これについては単純で、例えばファイルの行数を出したいとかと言うときは

$ cat -n file

でだいたい片付けていたから。
stdoutに対しても

$ ./command | cat -n -

で済ませていたから、使えていなかった。
nl は空行をカウントしないとか様々な機能があるので、できればこっちを使っていきたいなぁという気持ち。

sed の使い方を知らなかった

まずは man sed 。(今回は GNU sed をベースに)

普段使いだと置換だとか、特定行の削除だとかしか使っていなかった。ましてやパターンスペース、ホールドスペースの名前はしっていても何をするものなのかわかっていなかった。
今回ので(にわかな理解にはなるが)ある程度理解できた。

またアドレスの使い方だとか、コマンドの使い方だとかもある程度はわかった気になっている。

読みづらい

実際 sed スクリプトは読みづらい感じがしているが、これはワンライナーで書いているからなのでは?と思いスクリプト形式で取り扱うようにした。またそうすると、コメントも書けるため、(シェル芸の概念とは異なるが)将来的にメンテナンスしようとしたときに少し便利になる。

そうするとスクリプトはこのようになる。

Before

$ echo 'foo\nbar\nbaz' | sed '1h;1d;3G'
bar
baz
foo

After

script.sed

1
2
3
4
5
6
7
8
# 1行目をhold spaceにコピーする
1h

# 1行目を削除する
1d

# hold spaceから3行目にコピーする
3G
$ echo 'foo\nbar\nbaz' | sed -f script.sed
bar
baz
foo

コメントを書きながら進めていけるという点ではすごい良い気がする。勉強期間はこれでいきたい。

正規表現マッチ対象の大文字小文字変換

知らなかった。

$ echo 'abcde' | sed 's/./\u&/g'

gsed なら、これで大文字になる。対応するように \l も存在している。

Hold space と Pattern space

まだ雑な理解だから間違っているかもしれない。

[input] ---> [pattern space] ---> [hold space]

という位置づけで、普段 sed を使っているときにいじっているのはパターンスペースである。

$ echo 'foo\nbar' | sed '2p'

foo
bar
bar

とかで使っている p コマンドは、現在のパターンスペース(正規表現にマッチしている箇所)を吐き出している。(2はアドレス。アドレスについては後述)

$ echo 'foo\nbar\nbaz' | sed '1h;1d;3G'
bar
baz
foo

これは以下の動作をしている。

  1. 1行目の foo をホールドスペースに入れる
  2. 1行目を削除する
  3. ホールドスペースから3行目にコピーする
1
2
3
4
5
6
7
8
# 1行目の `foo` をホールドスペースに入れる
1h

# 1行目を削除する
1d

# hold spaceから3行目にコピーする
3G

ここで、 hH ではホールドスペースに対して上書きを行うのか追記を行うのか、 gG も同様な違いがあることに注意する。

アドレス

さっきから 1h3G のように書いているが、このとき出てきている 13 はアドレスとして扱われる。今の理解だと行数。
パターンを渡した場合は、それが対応する行番号として扱われるようだ。(観察から)

ここで、シェル芸勉強会の間に出てきた解答を解読してみる。

$ cat ./input | sed '/int/,/^}/{H;d};$G'
  • アドレス先頭 : /int/
  • アドレス末尾 : /^}/
  • 区間指定 : /int/,/^}/
  • Hold spaceに追加して対象を削除 : {H;d}
  • Hold spaceの中身を吐く : $G
    • ここが G のみだと、ホールドスペースに入った増分が順に出力されるようだ。

このような構造になっている。

これだけだとわかりづらいので入力ファイルもしっかり定義しつつ、もう少しシンプルな例を書いてみる。

  • input.txt (入力ファイル)
1
2
3
- name: K. Murakami
- id: kmhjs
- love: zsh
  • expected.txt (期待する出力)
1
2
3
- id: kmhjs
- love: zsh
- name: K. Murakami
  • script.sed (適用するsedスクリプト)
1
2
3
4
5
6
7
8
# - name: から始まる行をhold spaceにコピー(上書き)する
/^- name:/ h

# - name: から始まる行を削除する
/^- name:/ d

# - love: から始まる行の次に、現在のhold spaceの中身を貼り付ける
/^- love:/ G

ここまでのを簡単にまとめてみる

例題に沿って。

  • input.txt (入力ファイル)
1
2
3
abcde xyz
123
sample input
  • expected.txt (期待する出力)
1
2
3
4
abcde xyz
456
abcde xyz
sample input
  • script.sed (適用するsedスクリプト)
1
2
3
4
5
6
7
8
# ^ab.*z$ のパターンをホールドスペースにコピーする(上書き)
/^ab.*z$/ h

# 123 を 456 に置換する
s/123/456/

# 456 の下にホールドスペースに入っているものを貼り付ける
/456/ G

動きのイメージはこんな感じ。

ラベル

: ラベル名 で定義できる。
bt コマンドを使うことで goto: のような使い方ができる。

例題として「 a を1つ入力し、10個 a が連なった文字列(= aaaaaaaaaa )を生成する。」を考える。

このとき分岐とループのみを用いて行うこととしては、「 a が10個連なった文字列が得られるまで、末尾に a を付加する」となる。
よって以下では b (多分branch)を使って、分岐を行うことでループを行う。

  • input.txt (入力ファイル)
1
a
  • expected.txt (期待する出力)
1
aaaaaaaaaa
  • script.sed (適用するsedスクリプト)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ループ用のラベルを定義する
: loop_label

# ----- ループ内での処理 ここから -----

# 現在の入力列の末尾にaを1つ付加する
s/.*/&a/

# ----- ループ内での処理 ここまで -----

# ループを抜けるかの判定を行う
#
# ここではaが10個出力されているか(/a\{10\}/にマッチするか)を判定し、
# マッチしない場合は次のループへ入り、aを末尾に付加する。
/a\{10\}/! b loop_label

こうなる。

範囲指定とステップ実行

上で出したアドレスを基に、範囲指定やステップ実行ができる。

範囲指定

{先頭アドレス},{末尾アドレス} で範囲指定となる。
例えば、以下の入力ファイルの begin から end の間の文字列を foo bar baz に置き換えたいとなったら、以下のような書き方をすることになる。(雑)

  • input.txt (入力ファイル)
1
2
3
4
5
6
7
8
9
10
HEADER

begin

this is content.
Replace this with "foo bar baz"

end

FOOTER
  • expected.txt (期待する出力)
1
2
3
4
5
6
7
8
9
10
HEADER

begin

foo bar baz
foo bar baz

end

FOOTER
  • script.sed (適用するsedスクリプト)
1
2
# begin から end までの範囲に対して、begin, end以外の文字列を foo bar baz に置換する
/^begin$/,/^end$/ s/[^(begin|end)].\+/foo bar baz/

上のsedスクリプトのアドレスについては、行数を指定していいならば

1
2
# begin から end までの範囲に対して、begin, end以外の文字列を foo bar baz に置換する
3,8 s/[^(begin|end)].\+/foo bar baz/

ともできる。
尚、行数指定的には、「あるパターンを持つ行から10行分」と指定したいときは

1
/pattern/,+10 tasks...

とできるらしい。(manより)

ステップ実行

~N (Nは数字)として実現できる。

例えば、「3行目から3行ごと」と書きたければ、

1
3~3 tasks...

となる。
よって以下の書き方をすることで、(1から任意の数までのシーケンス列の入力に対して、)3の倍数判定ができる。

  • input.txt (入力ファイル)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • expected.txt (期待する出力)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3N line!
4
5
3N line!
7
8
3N line!
10
11
3N line!
13
14
3N line!
  • script.sed (適用するsedスクリプト)
1
2
# 3行目から3行ごとに、文字列を 3N Line! に置き換える
3~3 s/.*/3N line!/

ということができる。

ここまでの例から、シェル芸勉強会で出た問題として、「sedのみでfizzbuzzを行え」を再度やってみる。

  • input.txt (入力ファイル)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • expected.txt (期待する出力)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
fizz
4
buzz
fizz
7
8
fizz
buzz
11
fizz
13
14
fizzbuzz
16
  • script.sed (適用するsedスクリプト)
1
2
3
4
5
6
7
8
# 3行目から3行ごとに fizz に置き換える
3~3 s/.*/fizz/

# 5行目から5行ごとに buzz に置き換える
5~5 s/.*/buzz/

# 15行目から15行ごとに fizzbuzz に置き換える
15~15 s/.*/fizzbuzz/

ヒャッホイ。理解した。

グルーピング

まだ理解しきれていないが、 範囲 { その範囲に適用したいこと } を行える。
以下は同じパターンが続く入力列に対して、「6~10行目の範囲の、input2からinput4の範囲のinput3をREWRITEに書き換える」という、回りくどいサンプル。

  • input.txt (入力ファイル)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input1
input2
input3
input4
input5
input1
input2
input3
input4
input5
input1
input2
input3
input4
input5
  • expected.txt (期待する出力)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
input1
input2
input3
input4
input5
input1
input2
REWRITE
input4
input5
input1
input2
input3
input4
input5
  • script.sed (適用するsedスクリプト)
1
2
3
4
5
6
7
8
# 6から10行目の範囲に対して
6,10 {
  # input2からinput4から始まる範囲に対して
  /^input2$/,/^input4$/ {
    # input3をREWRITEに置換する
    s!input3!REWRITE!
  }
}

まとめ

  • sed をちょっとだけ使えるようになった
  • sed やばい

どうでもいい話

確認を行うため、以下のテストを適用していた。

Makefile

1
2
test: ./test.zsh ./expected.txt ./execute.zsh
	@ ./test.zsh

test.zsh

1
2
3
#! /usr/bin/env zsh

diff <(./execute.zsh ./input.txt ./script.sed) ./expected.txt

execute.zsh

1
2
3
4
5
6
#! /usr/bin/env zsh

typeset input_file_path=$1
typeset sed_script_path=$2

gsed ${input_file_path} -f ${sed_script_path}

こうして、上で書いていた input.txt , expected.txt , script.sed を用意して、

1
$ make test

こう。