第41回シェル芸勉強会に参加しました。

第41回シェル芸勉強会の参加レポートです。
だいぶ久々に参加させていただいたように思います。

午前の部

ついに文字コードのお話が最終回になりました。今回は主に Unicode の結合文字列の話でした。

結合文字列をざっくり言うと、 Unicode 上では同じ「ポ」を表すにも以下の 2 通りの方法があるということです。

  • ポ: U+30DD
  • ホ + ゜(半濁点): U+30DB U+309A

また、上の単一のコードでひとつの文字になる表現を合成済み文字、下の複数コードの組み合わせで表現するものを結合文字列と呼びます。

同一文書中に符号化方式が混在していると不便なので統一することを Unicode 正規化といい、合成済み文字に統一した形式を NFC、結合文字列に分解した形式を NFD と言います。
CLIUnicode 正規化するには uconv が便利 (uconv -x NFDuconv -x NFC) ということを知りました。

$ echo -n ポ | uconv -x NFC | iconv -t ucs-2be | xxd -ps
30dd
$ echo -n ポ | uconv -x NFD | iconv -t ucs-2be | xxd -ps
30db309a

余談ですが結合文字列といえば Mac OS X がめんどくさい印象です。Windows から OS X に濁点 (・半濁点) がファイル名に入ったファイルを送って、編集して送り返してもらうと濁点が分割されます。
メールなどであれば、おそらくファイル名を変更してやり取りするのでまだ良いですが、DropBox などのファイル同期を取るソフトウェアで OS が混在すると同名のファイルが 2 つできるという悪夢が発生します。
しかも保存しないと増えないのでタイミングがよくわからなくなって原因究明が大変です。

問題と私の回答

私の回答のうち、 Twitter のものはその場での回答です。
pre タグで囲まれている部分は記事執筆の際に WSL で再度実行しています。

Q1

 次のファイルについて、2列目をキーにしてエクセルの横列の記号(A, B, ..., Z, AA, AB, ...のやつ)順に並べ替えてください。

$ cat excel
114514 B
1192296 AA
593195 CEZ
4120 TZ
999 QQQ
A1


$ cat ShellGeiData/vol.41/excel | xargs printf '%s %4s\n' | tr \  % | sed 's#%%# %#' | sort -k2 | tr -d %
114514 B
593195 AA
4120 TZ
1192296 CEZ
999 QQQ

解説

列番号の桁数を合わせてソートしたらできました。

他の方の回答を見ていると、 awk の length 関数で列番号の桁数を求める解法が多かったみたいですね。

Q2

 次のファイルのレコードを干支順にソートしてください。

$ cat eto_yomi
申 さる
子 ね
寅 とら
卯 う
巳 み
辰 たつ
丑 うし
酉 とり
戌 いぬ
亥 い
午 うま
未 ひつじ

ただし、次のファイルを補助に使って良いこととします。

$ cat eto
子丑寅卯辰巳午未申酉戌亥
A2.1


$ cat ShellGeiData/vol.41/eto | grep -o . | sed 's#.#grep & ShellGeiData/vol.41/eto_yomi#e'
子 ね
丑 うし
寅 とら
卯 う
辰 たつ
巳 み
午 うま
未 ひつじ
申 さる
酉 とり
戌 いぬ
亥 い

解説

sed の e を外すとなにをしたかわかるかと思います。

$ cat ShellGeiData/vol.41/eto | grep -o . | sed 's#.#grep & ShellGeiData/vol.41/eto_yomi#'
grep 子 ShellGeiData/vol.41/eto_yomi
grep 丑 ShellGeiData/vol.41/eto_yomi
grep 寅 ShellGeiData/vol.41/eto_yomi
grep 卯 ShellGeiData/vol.41/eto_yomi
grep 辰 ShellGeiData/vol.41/eto_yomi
grep 巳 ShellGeiData/vol.41/eto_yomi
grep 午 ShellGeiData/vol.41/eto_yomi
grep 未 ShellGeiData/vol.41/eto_yomi
grep 申 ShellGeiData/vol.41/eto_yomi
grep 酉 ShellGeiData/vol.41/eto_yomi
grep 戌 ShellGeiData/vol.41/eto_yomi
grep 亥 ShellGeiData/vol.41/eto_yomi
A2.2


$ cat ShellGeiData/vol.41/eto_yomi | awk '{print "/"$1"/s_.*_"$0"_"}' | sed -f - <(sed 's_._&\n_g' ShellGeiData/vol.41/eto)
子 ね
丑 うし
寅 とら
卯 う
辰 たつ
巳 み
午 うま
未 ひつじ
申 さる
酉 とり
戌 いぬ
亥 い

解説

grepを使わないことを目標にしました。

awksed のパターンを作って、eto を縦にしたものに適用しています。

感想

以下のようにしたら grep -o . みたいなことしてる部分も消せますね。

$ cat ShellGeiData/vol.41/eto_yomi | awk '{print "s_"$1"_"$0" _"}' | sed -f - ShellGeiData/vol.41/eto | xargs -n 2
子 ね
丑 うし
寅 とら
卯 う
辰 たつ
巳 み
午 うま
未 ひつじ
申 さる
酉 とり
戌 いぬ
亥 い

A2.1 の sed 部分を xargs でやるのが方がいました。そっちのほうがスマートですね。

Q3

 次のファイルのレコードを数字(第一フィールドの計算結果)が小さい順に並べてください。

$ cat kim_calc
1+2+4 金正日
4*3 金正男
3-1-5 金日成
495/3 金正恩
0x1F 金正哲
A3


$ cat ShellGeiData/vol.41/kim_calc | sed 's_\(.*\) \(.*\)_echo $((\1)) \1 \2_e' | sort -n | cut -d\  -f2-3
3-1-5 金日成
1+2+4 金正日
4*3 金正男
0x1F 金正哲
495/3 金正恩

解説

まずbcで計算しようとしたところ0x1Fが変換できませんでした。

$ cat ShellGeiData/vol.41/kim_calc | cut -d\  -f1 | bc
7
12
-3
165
(standard_in) 5: syntax error

bash で式評価すれば 0x 形式を変換できたのでそうしました。

感想

printf でも 0x の変換はできますが計算はできないので、今回の問題では手順が増えますね。

$ printf %d\\n 0x1F
31

Q4

 次のファイルはシフトJISのテキストですが、これを1) 辞書順、2) 数字の小さい順、にソートしてください。出力もシフトJISとします。

$ cat sjis | nkf -g
Shift_JIS
$ cat sjis | nkf -wLux
123 ずんごるももう
31 こきたてひーひー
9 ほじぱんふんじこみ
2242 たまもとやろう
A4-1


A4-2


$ cat ShellGeiData/vol.41/sjis | iconv -f cp932 | sed 'y/01234/01234/' | sort -n | sed 'y/01234/01234/'
20 ほじぱんふんじこみ
31 こきたてひーひー
123 ずんごるももう
2242 うえってきたかるとらまん

解説

cp932 は
Microsoftコードページ932 - Wikipedia のことで、 Windows の Shift JIS 拡張コードです。
Shift JIS と言ってるときは、ほとんどの場合「Windows で作られた」「Windows で表示したい」ことを指してるので cp932 を指定するのが無難だと思っています。

Q5

 サイズの小さい順にソートしてください。

$ cat size 
2GB
1.2GB
40000MB
1000000000kB
0.4GB
410MB
A5


$ cat ShellGeiData/vol.41/size | sed p | sed '2~2s_.*_echo & | tr -d B | tr k K | numfmt --from=si_e' | paste - - | sort -k2 -n | cut -f1
0.4GB
410MB
1.2GB
2GB
40000MB
1000000000kB
感想

以下のツイートを見てnumfmtを知ったのでそのまま使ったら解けました。


Q6

sleepと内部コマンドだけを使って次の数を小さい順にソートしてください。

$ cat nums
5.4
0.34
2.3
0.9
6
A6


$ (while read a; do (sleep $a; echo $a) & done) < ShellGeiData/vol.41/nums
$ 0.34
0.9
2.3
5.4
6

解説

スリープソートです。スリープソートはバケットとしてタイムスロットを使うバケットソートの一種と見なせるそうです。
バケットソート - Wikipedia

非同期で動くので表示がずれるのが気になる場合はwaitをかませば良いです。() でサブシェル内で実行すればジョブ開始のメッセージも出ないのでより見やすくなります。

$ (while read a; do (sleep $a; echo $a) & done && wait) < ShellGeiData/vol.41/nums
0.34
0.9
2.3
5.4
6
感想

sleepソートを見た瞬間スリープソートだと思いました。

Q7

 次のローマ数字をソートしてください。

$ cat roman
IV
XI
LXXXIX
IX
XLIII
XX
VIII
A7


$ cat ShellGeiData/vol.41/roman | sed -E 'p;s_I([XV])_(-1)\1_;s_X([LC])_(-10)\1_;s_I_(1)_g;s_V_(5)_g;s_X_(10)_g;s_L_(50)_g' | sed 's_)(_)+(_g; 2~2s_.*_echo "&" | bc_e' | paste - - | sort -k2 -n | cut -f1
IV
VIII
IX
XI
XX
XLIII
LXXXIX

解説

アラビア数字への変換は以下の手順で行いました。

  1. 減算則の部分 (IVXL など) を先に負の値に置き換える
  2. 他の記号を対応する正の値に置き換える
  3. 総和を求める式に変形する
  4. 計算する

上記手順を偶数列で行って、paste - - で行を揃えてソートする。という流れですね。

今見ると seds_I([XV])_(-1)\1_;s_X([LC])_(-10)\1_ 部分を s_I[XV]_(-1)*&_;s_X[LC]_(-1)*&_; に置き換えたいですね。つまり、負の値そのものに置き換えるのではなく -1 をかけるのでもできます。
動作としては、以下の途中結果がわかりやすいかと思います。

$ cat ShellGeiData/vol.41/roman | sed 'p;s_I[XV]_(-1)*&_;s_X[LC]_(-1)*&_;s_I_(1)_g;s_V_(5)_g;s_X_(10)_g;s_L_(50)_g' | sed 's_)(_)+(_g; 2~2s_.*_echo "&" | bc_'
IV
echo "(-1)*(1)+(5)" | bc
XI
echo "(10)+(1)" | bc
LXXXIX
echo "(50)+(10)+(10)+(10)+(-1)*(1)+(10)" | bc
IX
echo "(-1)*(1)+(10)" | bc
XLIII
echo "(-1)*(10)+(50)+(1)+(1)+(1)" | bc
XX
echo "(10)+(10)" | bc
VIII
echo "(5)+(1)+(1)+(1)" | bc
感想

手元では動いていたのですがシェル芸 bot では動いていませんでした。
Ubuntu の sh が dash だったのが動かなかった理由だったようです。

numconv がすごい便利そうでした。珍しく AUR になかったのでその場では試せませんでした。 (AUR: Arch Linux の半公式ユーザーリポジトリ。ソースからビルドしたり、.debを展開してインストールするエキセントリックなパッケージがあったりする。)

numconv だと以下のように解けます。

$ cat ShellGeiData/vol.41/roman | sed 'p; s_.*_echo & | numconv_e' | paste - - | sort -k2n | cut -f1
IV
VIII
IX
XI
XX
XLIII
LXXXIX

Q8

 次のファイルを辞書順にソートしてください。ただし、濁点がついているものが先に来るようにしてください。できる人はワンライナー中で「かきくけこがぎぐげご」の文字を使わないでください。

$ cat gagigugego 
かき氷
ぎ・おなら吸い込み隊
きつねうどん
ぐりこもりなが事件
きききりん
がきの使い
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ
がきの使い
かき氷
ぎ・おなら吸い込み隊
きききりん
きつねうどん
ぐりこもりなが事件
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ
A8


$ cat ShellGeiData/vol.41/gagigugego | uconv -x NFD | iconv -t utf16be | xxd -ps | tr -d \\n | sed 's_000a_&\n_g' | sed 's_...._& _g' | sed 's_3099 _-&_g' | sort -n | sed 's_-3099_3099_g' | xxd -r -p | iconv -f utf16be | uconv -x NFC
がきの使い
かき氷
ぎ・おなら吸い込み隊
きききりん
きつねうどん
ぐりこもりなが事件
くその役にも立たない
げんしりょく発電
ごりらいも
こじんてきにはクソ

解説

手順は以下です。

  1. NFD にする (結合文字列に分解する)
  2. UTF-16 のビッグエンディアンに変換する
  3. 16 進表現に変換する
  4. 濁点 (UTF-16 では 0x3099) をソートで上位に並ぶように置き換える (3099 -> -3099)
  5. ソートする
  6. 濁点を戻したり (-3099 -> 3099) など、いろいろ逆手順で戻す
    • uconv -x NFD | iconv -t utf16be | xxd -ps | ... | sed 's_3099 _-&_g' -> sed 's_-3099_3099_g' | xxd -r -p | iconv -f utf16be | uconv -x NFC

その場では -3099 ではなく 13099 に置き換えていましたが、U+0xxx の文字が 2 文字目にあると期待どおりにソートされないので -3099 のほうが汎用的です。

感想


ってツイートを悠長に打ってる間に答え
が出るという不思議体験を味わいました。

U+3099(゛) が先にソートされる理由がよくわかりませんが・・・・・・

総括

久々の参加だったのですが、今回は比較的簡単で良かったです。(スリープソート以外は「これを知ってないと解けない」問題がないので。)
簡単でしたがそれでも疲れるのと、相変わらず全然知らないこと (uconvnumfmtnumconv など) が出てくるのがすごいです。

あと、もとの表現を残したまま計算したいときに sed 'p;s/.*/echo & | hoge/e' | paste - - のパターンが使えますね。A5・A7で使用しています。