sed地獄へようこそ――「第27回シェル芸勉強会 大阪サテライト」の記録

はじめに

2月11日(土)に開催された「第27回シェル芸勉強会 大阪サテライト」に参加しました。

今季最強の寒波による大雪が予想されるということで、一時は大阪サテライトの開催が危ぶまれましたが、当日の天気は荒れることもなく無事に開催されることになりました。

今回も、朝9時45分から夜18時までひたすらsh行に打ち込む一日でした。

……sed地獄へようこそ。

メイン会場および他サテライト会場

各会場の様子

問題ならびに模範解答

ライブストリーム(YouTube)


午前の部: シェルに関する勉強会

黒い画面と戯れよう

講師: 鳥海秀一さん(USP友の会)

  • 「黒い画面怖い」から、どのようにCLIを攻略するか
    • 遊び心を持っていろいろ試す
    • キーボードと黒い画面の2つに分けて考える
  • 黒い画面を攻略
    • 文字はゲシュタルトであり、フォント等を変えることで文字が持つ情報以上の何かを伝えることができる
    • bashから見れば単なるファイル
    • エスケープシーケンスで文字表示以外の制御ができる
  • エスケープシーケンスに関連するコマンド
    • clear
    • tput
    • stty
  • エスケープシーケンスの応用例
    • エスケープシーケンスを名前に持つファイルやディレクトリの作成
    • 第25回勉強会のQ8を解く(サインカーブの描画)

エスケープシーケンスを使ってターミナル画面でいろいろ試してみるという内容で、具体的にはtputコマンドでterminfoデータベースを操作することによってターミナル画面を制御しています。

ちなみに、応用例での

$ touch $(clear)
$ ls -1
?[3;J?[H?[2J

という謎ファイル作成は、いたずらにピッタリだと思います。

あと、サインカーブの描画はただただスゴい!!

シェル芸入門 日常会話編

講師: 石井久治さん(USP友の会)

  • シェル芸とは
    • curl -s https://blog.ueda.asia/?page_id=1434 | sed 's/<[^<]\+>//g' | sed '/^シェル芸の定義/,+1!d;s/\x0b//g'
  • シェルとは
    • ユーザとカーネル間のインターフェイスとなるソフトウェア
  • シェル芸の思想とUNIX哲学
  • シェルとコマンドについて
    • 標準入出力
    • リダイレクト
    • パイプ
    • フィルタ
  • シェル(bash)で使える要素
    • 組込みコマンド
    • コマンドの複合
    • 制御構造
    • 変数
    • 展開
      • パラメータ展開
      • (…略…)
      • ブレース展開(実演: 表の描画)
      • プロセス置換
    • 外部コマンド
      • (…略…)
      • cut
      • tac
      • tee
      • (…以下略…)
  • 参考書籍

シェル芸を基礎から学んで身につけるという初心者向けの内容ですが、中盤からのブレース展開を使った表の描画はまさに「芸」といってよいでしょう。

……しかし、シェル芸の(奥|闇)は深い。


休憩

午後からのsed地獄に備えるべく、man sedsedのマニュアルを読みながらの昼食となりました。

……これは果たして、休憩なのだろうか?


午後の部: シェル芸勉強会

  • 解答にあるsedのコード片に対して可読性が高いとはお世辞にも言えないものの、参考資料として「sed, a stream editor」をあらかじめ読んでおけば少しは分かりやすくなるかと思います。
  • いずれの問題も、Debian 8「jessie」の32ビットPC版で解答しています。

A1: 偶数番目の文字だけ大文字にする

$ echo abcdefghijklmn | sed -r 's/(.)(.)/\1\u\2/g'
aBcDeFgHiJkLmN

sedsコマンドで置換をする際に利用できる、GNU拡張の

‘\u’
    Turn the next character to uppercase,

という特別なエスケープシーケンスを使い、該当する部分を大文字にしています。

奇数番目の文字を大文字にする場合は、

$ echo abcdefghijklmn | sed -r 's/(.)(.)/\u\1\2/g'
AbCdEfGhIjKlMn

のようになります。

ちなみに、正規表現を書くときに

-r, –regexp-extended
    スクリプトで拡張正規表現を使用する

というオプションを使うと、バックスラッシュ(\)の数を減らすことができて便利です。

A2: sedだけでFizzBuzz

$ seq 1 100 | sed '3~3s/.*/Fizz/;5~5s/.*/Buzz/;15~15s/.*/FizzBuzz/'
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
(...以下略...)

GNU拡張にある、次のアドレス指定方法

first~step
    first 行からはじまる step 行おきの行にマッチする。
    (…以下略…)

でFizzBuzzを行っています。

A3: 3行目を7行目の下に移動

$ seq 1 10 | sed -n '3!p;3h;7x;7p'
1
2
4
5
6
7
3
8
9
10

sedの部分の手順は、

  1. 3行目以外を表示
  2. 3行目の内容をホールドスペースにコピー
  3. 7行目でホールドスペースとパターンスペースの内容を交換
  4. 7行目でパターンスペース(3行目の内容)を出力

となっています。

なお、ホールドスペースを使ったsedのコードを書く場合、

-n, –quiet, –silent
    パターンスペースの自動出力を抑制する

というオプションを使うことで、想定外の出力を可能な限り抑えることができます。

A4: コードの位置を入れ替える

$ cat aho.cc | sed -rz 's/(int main[^}]+})(\n\n)(void aho[^}]+})/\3\2\1/'
#include <iostream>
using namespace std;

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

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

GNU拡張のオプション

-z, –null-data
    NUL 文字で行を分割する

を使うと、正規表現の中で改行文字(\n)を扱うことができますので、これを利用してコードブロックを置換しています。

パターンスペース・ホールドスペースの扱いが苦手でも、工夫次第で何とかなるものです。

A5: 奇数行と偶数行を入れ替える

$ seq 1 10 | sed -rz 's/\n/ /g;s/([^ ]+) ([^ ]+)/\2 \1/g;s/ /\n/g'
2
1
4
3
6
5
8
7
10
9

A4とほぼ同じ手法で解いています。

なお、次のシェル芸のように、

$ seq 1 10 | sed -rz 's/([^\n]+)\n([^\n]+)/\2\n\1/g'
2
1
4
3
6
5
8
7
10
9

改行文字(\n)をそのまま正規表現で扱ったほうが、コードゴルフ的には良かったかと思います。

A6: 「1」で図形を出力――その1

$ echo 1 | sed -r 'p;:_;s/(.)$/\1\1/p;t_;' | sed '10q'
1
11
111
1111
11111
111111
1111111
11111111
111111111
1111111111

1つ目のsedの部分では、手順として

  1. パターンスペースを出力
  2. 末尾の文字を1ヶ重複して出力
  3. 1.(ラベル「_」)に戻る

となっていますが、このままだと無限に出力し続けるので、2つ目のsedの部分で出力を止めています。

また、より短い別解として、

$ echo 1 | sed -nr ':_;/^.{11}$/b;p;s/.$/&&/;t_'
1
11
111
1111
11111
111111
1111111
11111111
111111111
1111111111

を紹介しておきます。手順としては、

  1. パターンスペースの文字数を確認し、10ヶを超えれば終了するという条件を設定
  2. パターンスペースを出力
  3. パターンスペースの末尾の文字を1ヶ重複させる
  4. 1.(ラベル「_」)に戻る

というふうになっています。

A7: 縛りsed――複数回のファイルのコピー

aというファイルを作成した後、このファイルをa1a10まで連番でコピーするわけですが、

  • 縛り1: 使うコマンドはseqcpsedだけ
  • 縛り2: ワンライナー中で数字を使わない

という2つの縛りに従って問題を解く必要があります。

ちなみに、縛り1と縛り2は「AND」の関係ではなく、それぞれの条件に基づいて解答するとのことでしたが、

$ ls
$ sed -n 'w a' /etc/passwd; sed '=' a | sed '/^[a-z]/d' | sed -n '/^.$/,/^..$/!d;s/.*/cp a a&/e'
$ ls
a  a1  a10  a2  a3  a4  a5  a6  a7  a8  a9

のように両方の条件を同時に満たした解答ができてしまいましたので、これを説明します。

さて、肝心の手順ですが、

  1. ほとんどのシステムでは/etc/passwdの行数が10行以上あることを利用し、wコマンドで/etc/passwdからaに書き込む
  2. =コマンドで行数を出力
  3. 英文字で始まる行を削除し、行数の部分だけ残す
  4. 数字1ヶだけの行と最初に数字2ヶになる行以外を削除した後、eフラグ付きのsコマンドでcp用文字列を生成して実行

という、強引きわまりないものになっています。

実用性はほとんどないと思いますが、よい頭の体操になりました。

A8: 「1」で図形を出力――その2

訂正: 当日の解答に誤りがありましたので、次の通り訂正します。

$ echo 1 | sed -nr ':A;p;/^.{5}$/bB;s/.$/&&/;tA;:B;p;/^.$/b;s/.$//;tB'
1
11
111
1111
11111
11111
1111
111
11
1

sedの部分の処理を、ラベル「A」で示されるループと、ラベル「B」で示されるループの2つに分けて説明すると次のようになります。

  • ラベル「A」:
    1. パターンスペースを出力
    2. パターンスペースの文字数が5ヶであれば、ラベル「B」に移る
    3. パターンスペースの末尾の文字を1ヶ重複させる
    4. 1.(ラベル「A」)に戻る
  • ラベル「B」:
    1. パターンスペースを出力
    2. パターンスペースの文字数が1ヶであれば、sedの処理を終了
    3. パターンスペースの末尾の文字を1ヶ消す
    4. 1.(ラベル「B」)に戻る

sedというものが、実はスクリプト言語であることがよく分かる問題でした。


LT大会

Q7の解答の説明

発表者: 日柳 光久(@mikkun_jp)

  • (…内容はA7での説明と同じため、省略…)

sedで知る矢印キーのキーコード

発表者: T.Motooka(@t_motooka)さん

  • 矢印キーが何のコードを送っているか?
  • sed -n lで確認できる
  • コナミコマンドを送れると役に立つ?

午前の部で学んだtouch $(clear)と並んで、sed -n lもターミナル画面を使ったいたずらに有用な気がします。

我は放つ、危険のシェル芸!!

発表者: nmrmsys(@nmrmsys)さん

「IoTの次は何か?」という話も世の中にはあるようですが、「KoT(Kiken-shellgei on Things)」が来たとしても決しておかしくないでしょう。

30億のデバイスで走る危険シェル芸の世界は、すぐそこなのかも知れません。


おわりに

エクシェル芸の前回が「地獄の一丁目」だとすれば、sed縛りの今回は「地獄の二丁目」といったところでしょうか。

それはともかく、今まで良く分からなかったsedの各種コマンドや、ホールドスペース・パターンスペースの使い方を学ぶことができた有意義な一日でした。

最後に、

  • 大阪サテライトの主催をされている、くんすと(@kunst1080)さん、T.Motooka(@t_motooka)さん
  • 大阪サテライト会場を提供してくださっている、フェンリル株式会社 大阪本社
  • 午前の部の講師を務めてくださった、鳥海秀一さん、石井久治(@hisaharu)さん
  • Ryuichi Ueda(@ryuichiueda)さんをはじめとする、メイン会場スタッフの皆さん

に改めてお礼を申し上げます。ありがとうございました。

……そういえば、Vimシェル芸という何やら不穏な文字列が……。

Written on February 15, 2017