第27回シェル芸勉強会に参加してきました(復習)

第27回シェル芸勉強会に行ってきたので、その復習。

今回はsedコマンドの機能について学ぼうということで、sed縛りとなっていた。 sedコマンドというと基本は置換にしか使わないが、今回はラベルだったりパターンスペース・ホールドスペースを使ったりする。 正直、今回は全然ついていけてなかった…(´・ω・`)。 ラベルやパターンスペース・ホールドスペースの存在は知っていたのだが、それらをうまく利用できなかったと思う。 sedは奥が深い…。

問題・模範解答はこちら。 使用するファイルは1個だけなのだが、以下のコマンドでダウンロードしておこう。

git clone https://github.com/ryuichiueda/ShellGeiData.git

Q1.

Q1は、echoで取得した文字列の偶数番目のみ大文字にするというもの。 後方参照を使って偶数のみを大文字にする。

echo abcdefghijklmn | sed 's/\(.\)\(.\)/\1\u\2/g'
echo abcdefghijklmn | sed -r 's/(.)(.)/\1\u\2/g' # -rで拡張正規表現対応になる
blacknon@BS-PUB-UBUNTU-01:~$ echo abcdefghijklmn | sed 's/\(.\)\(.\)/\1\u\2/g'
aBcDeFgHiJkLmN
blacknon@BS-PUB-UBUNTU-01:~$ echo abcdefghijklmn | sed -r 's/(.)(.)/\1\u\2/g'
aBcDeFgHiJkLmN

Q2.

seqで1~100までを出力し、sedでFizzBuzz(数字が3の倍数ならFizz、5の倍数ならBuzz、3×5=15の倍数ならFizzBuzzと置換する数字遊び)をしてみる、という問題。 なんの気なしに「sed FizzBuzz」で検索してみたところ、ebanさんの過去のブログが普通にヒット。 考える前に理想的な解答見ちゃったような気持ちになったが、気を取り直して解答。 sで置換を行うパターンとcで行ごと置き換えるパターン、2通りが考えられた。

seq 1 100 | sed -e '3~3s/^.*$/Fizz/' -e '5~5s/^.*$/Buzz/g' -e '15~15s/^.*$/FizzBuzz/g'
seq 1 100|sed -e '15~15cFizzBuzz' -e '5~5cBuzz' -e '3~3cFizz' # 置き換え版

sedでは、実行コマンド(sとかcとか)の前に'first~step'を記述することで、指定された行ごとに処理が行われるようにできる。 例えば「15~15」だと15行ごとに処理が行われるので、それを利用してやるとFizzBuzzが行える。

Q3.

seqで1~10まで出力して、3行目を7行目に代入する、という問題。

ここでホールドスペース・パターンスペースの話が出てくる。 これについては、@gin_135さんの記述したこちらが詳しいと思う。 (実は見たときにホールドスペース・パターンスペースなしで解けないかと思ったのだが、どうあがいても使ったほうが短くかける)

seq 1 10 | sed '3h;3d;7G'
seq 1 10 | sed '3{h;d};7G' # 「3h;3d;」もこう記述して省略できるらしい…
blacknon@BS-PUB-UBUNTU-01:~$ seq 1 10 | sed '3h;3d;7G'
1
2
4
5
6
7
3
8
9
10
blacknon@BS-PUB-UBUNTU-01:~$ seq 1 10 | sed '3{h;d};7G' # 「3h;3d;」もこう記述して省略できるらしい…
1
2
4
5
6
7
3
8
9
10

それぞれの記述だが、以下のような意味がある。

  • 3h ... 3行目をパターンスペース(あまり意識しないが、いつも使ってるとこ)からホールドスペースにコピーする
  • 3d … 3行目をパターンスペースから削除する
  • 7G ... ホールドスペースに入ってる内容を7行目に追記する(7gだと7行目を入れ替える)

イメージ的には、クリップボードに近いだろうか。

Q4.

C++のコードについて、関数の位置を入れ替えてコンパイル、実行するという問題(たまにあるな、このコンパイルするって問題)。 関数自体の位置を入れ替える必要があるので、複数行に対して位置の差し替えをする必要がある。 模範解答より。

cat aho.cc | sed '/ main(/,/^}/H;/ main(/,/^}/d;$G'
cat aho.cc | sed '/ main(/,/^}/H;/ main(/,/^}/d;$G' | g++ -x c++ - && ./a.out # そのままコンパイルして実行するパターン
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ cat aho.cc
#include 
using namespace std;

int main(int argc, char const* argv[])
{
        aho();
        return 0;
}

void aho(void)
{
        cout << "aho" << endl;
}
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ cat aho.cc | sed '/ main(/,/^}/H;/ main(/,/^}/d;$G'
#include 
using namespace std;

void aho(void)
{
        cout << "aho" << endl;
}

int main(int argc, char const* argv[])
{
        aho();
        return 0;
}

で、「なにやってるのコレ?」っていうのが、↑の状態だと正直よくわからないので分解。 記号だらけなのでぱっと見だと分からないのだが、分解してしまえば結構単純だ。

cat aho.cc | \
sed '/main(/,/^}/H; # sedでは「/開始位置/,/終了位置/」 で、対象範囲を指定できるので、それを使って「main(」~「^}」までをホールドスペースにコピー(^で行頭を意味する)
     /main(/,/^}/d; # 「main(」~「^}」までを削除
     $G' # $(最終行)にホールドスペースの内容を追記

Q5.

「seq 1 10 |」から始めて、奇数行と偶数行を入れ替えるという問題。 ここでもパターンスペース・ホールドスペースが用いられる。

seq 1 10 | sed '1~2{h;d};G'
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ seq 1 10 | sed '1~2{h;d};G'
2
1
4
3
6
5
8
7
10
9

Q6.

「echo 1」からはじめて、行の文字数が10になるまで1を増加させていくという内容。 ラベルを用いるのだが、よくコードゴルフだと"a"とかで一文字になってる(一応以下でも"A"というラベルを用いている)。

echo 1 | sed -r ':A;p;s/./&&/;/.{10}/!b A'
blacknon@BS-PUB-UBUNTU-01:~/ShellGeiData/vol.27$ echo 1 | sed -r ':A;p;s/./&&/;/.{10}/!b A'
1
11
111
1111
11111
111111
1111111
11111111
111111111
1111111111

分解した内容は以下。

echo 1 | \
sed -r '# Aラベル開始(宣言)
        :A;
        p;
        s/./&&/; # 条件に一致した文字を増やす(1個のみ)
        # Aラベルの処理終了
        /.{10}/!b A' # 文字数が10個ではない場合、Aラベルに戻る

Q7.

aというファイルを作って、それをコピーしてa1~a10というファイルを作成するという問題。 ただし、

  • 使うコマンドはseq、cp、sedだけ
  • ワンライナー中で数字を使わない

という縛りあり(両立しなくてもいいらしい)。 で、ここでかなり刺激があった解答が以下。

上記回答には、個人的な学びポイントが色々と組み込まれていた。

aというファイルに関しては、/dev/nullからコピーして生成してやればcpコマンドしか使わない。

cp /dev/null ./a &&seq inf | sed '/../q' | sed 's/.*/cp a a&/e'
blacknon@BS-PUB-UBUNTU-01:/tmp/test$ ls
blacknon@BS-PUB-UBUNTU-01:/tmp/test$ cp /dev/null ./a &&seq inf | sed '/../q' | sed 's/.*/cp a a&/e'

blacknon@BS-PUB-UBUNTU-01:/tmp/test$ ls
a  a1  a10  a2  a3  a4  a5  a6  a7  a8  a9

Q8.

Q6の内容について、さらに今度は逆順の出力を得るという問題(今度は10個じゃなくて5個だけど)。 tacが使えると楽なのだが、実はsedだけでtacのような処理も行えるらしい。なので、それを使ってやるとよいようだ。

echo 1 | sed -r ':A;p;s/./&&/;/.{5}/!b A' | sed 'p;1!G;h;$!d'
blacknon@BS-PUB-UBUNTU-01:~$ echo 1 | sed -r ':A;p;s/./&&/;/.{5}/!b A' | sed 'p;1!G;h;$!d'
1
11
111
1111
11111
11111
1111
111
11
1

今回は全体的に難しい…というより、sedのパターンスペース・ホールドスペースとラベルを理解しているかどうかで明暗が分かれた気がする。 普段使いだと触ってなかったのだが、今後はこの辺の機能も触っておこうかな。