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

第36回シェル芸勉強会の参加レポートです。
最近あまりシェル芸してないからか、別解を思いつかなかったり、似たような解法を使ったりしてますね・・・・・・

参考リンク等

午前の部の資料


午前の部

午前の部は文字コードと銘打たれていましたが、文字コードの前提となる知識みたいな内容でした。次回に続くそうです。

locale

こういう遊びを知りました。


iconv

文字コードを変換する iconv の紹介がありました。

Windows-31J と仲良くするために日常的に iconv -t cp932 を使っている自分には馴染み深いコマンド。
TL を眺めていると、文字コードには nkf を使ってる人が多いみたいでした。

バイナリをテキストに変換する方法

文字コードはもちろんバイナリなので、シェル芸で扱うにはテキスト化する必要があります。
講師の鳥海さんは od や hexdump も紹介されておられましたが、正味ふだん使っている xxd しか覚えてません・・・・・・

xxd -psxxd -r -p が逆変換になってくれてるのが個人的にすごい嬉しい。

基数変換

bc の基数変換は私は良く使ってますが、その他に bash 組み込みの $(()) でも基数変換できるそうです。

$ echo $((25#1000))
15625


私の問題と解答

Q1 は全然わからなくて断念しました。
また、Q8 も途中まではいい感じでしたが結局最後までは行けず・・・

Q1

welcome.txt に隠されたメッセージを読み取ってください。また、welcome.txtワンライナーで作ってみてください。

A1 (断念)
cat welcome.txt | xxd -ps | tr -d \\n | fold -w 84 | awk 1

f:id:nogiro_iota:20180711222005p:plain

解説

cat で見ると、アンダースコアしか存在しない不思議なファイルだったので、とりあえず xxd -psしてみました。

すると 0x5f と 0x00 からなるファイルだとわかったので、幅を調整すれば絵が出そうだと当たりを付けました。

最終行まで幅がすべて同じである保証はありませんが、揃っている方がきれいだと思ったので、揃う幅(因数)を知るために factor を通しました。

$ cat welcome.txt | wc -c | factor
1680: 2 2 2 2 3 5 7

3*7 の倍数を中心に追ってる間に時間切れ。

感想

toilet の出力が幅 70 文字だから幅は 70文字ということでした。
figlet も toilet もあまりちゃんと使ったことがないのが仇になりました。

Q2

次のファイル群について、全てファイル名を N年M組.docNは半角数字、Mは半角大文字)に揃えて徳を積んでください。

$ ls
1-B.doc     3年C組.doc  3年A組.doc  1ーC.doc    4年C組.doc
1A.doc      4年a組.doc   3年B組.doc  1ーD.doc
3年D組.doc  5年A組.doc   4年B組.doc  1年E組.doc

A2
cd worst/ && ls | sed p | sed '0~2y/1234ABCDEFal/1234ABCDEFA1/' | sed '0~2s#[^1-5A-F]##g' | sed '0~2s#\(.\)\(.\)#\1年\2組.doc#' | paste -d\  - - | sed 's#.*#mv &#' | sh 

https://twitter.com/nogiro_iota/status/1015453781394223104

解説

| sh に投げれば何でもできる」系の解き方をしてます。
というか mv のような破壊的な処理を扱うときは、最終的に実行されるスクリプトを眺めるために、 | sh に投げれる形に積極的にしたいですね。

今回は汚い元データとキレイになりつつある成形中データを同一のパイプラインに流して処理しています。
やり方としてはすべて sed で偶数行だけ (0~2) 処理したあとに paste - - で横に並べています。

キレイなデータを作る部分は概ね sedy コマンドで置き換えています。
マルチバイト文字が含まれていると tr でできないのがちょっと面倒ですね・・・・・・

感想

いま考えると 1 B のように空白の入っているファイル名があったらちゃんとできてないですね。
そういう場合だったとすると paste の前に sed 's%.*%"&"%' を挟むとうまくいきます。

Q3

2018年のすべての日付について、2,3,5,7が4つ含まれる日付を列挙してください(例: 2018年3月22日など)。

A3.1
echo 2018{01..12}{01..31} | tr \  \\n | xargs -n 1 date +%Y%m%d -d 2> /dev/null | sed p | sed '0~2s%[^2357]%%g' | awk 'length($0)==4{print p}{p=$0}'

https://twitter.com/nogiro_iota/status/1015457900951568384

解説

日付を列挙するのにはイディオムがあって以下のコマンドで出ます。

echo 2018{01..12}{01..31} | tr \  \\n | xargs -n 1 date +%Y%m%d -d 2> /dev/null

2,3,5,7 の抽出部分は Q2 と似たようなやり方を使っています。
事前に sed p で行を 2 つにしておいて、0~2 で偶数(後ろの方の)行だけから 2,3,5,7 を抽出します。

そうすると文字幅が 4 である行の前の行が答えになるので、 awk で抜き出しています。

A3.2
echo 2018{01..12}{01..31} | tr \  \\n | xargs -n 1 date +%Y%m%d -d 2> /dev/null | python3 -c 'import functools;any(print(i) if(functools.reduce(lambda a,b:a+i.count(b), ["2","3","5","7"], 0) == 4)else 0 for i in (input() for null in iter(int, 1)))'

https://twitter.com/nogiro_iota/status/1015466634222292992

解説

Python ワンライナーが得意そうな問題だったのでやったらできました。

3 項演算子で、入力文字列中の 2,3,5,7 の数の和が 4 だったら出力し、4でなかったら何もしないという分岐を行っています。
Python のリスト内包表記ワンライナーらしさが出ています。

reduce() を使わず i.count("2") + i.count("3") + ... とやると Twitter の文字制限に引っかかったので、重複部分を削るのはやっぱり大事です。
※ 執筆中に reduce()sum(map()) に置き換えたほうが見やすいし、import もなくなるので圧倒的に短いことに気づきましたが・・・

functools.reduce(lambda a,b:a+i.count(b), ["2","3","5","7"], 0) == 4

sum(map(lambda a:i.count(a), ["2","3","5","7"])) == 4
感想

dateutils という日付を扱うためのコマンドがあるらしく、イディオム的に出力処理を作った日付部分を代替できるみたいです。
他にも日付の足し算などもできるみたいで普通に便利そう。

Q4

俳句を考え、次の短冊に縦書きで入れてください。

$ cat tanzaku
┏ ーー-┷-ーー┓
┃       ┃
┃       ┃
┃       ┃
┃       ┃
┃       ┃
┃       ┃
┃       ┃
┗ーーーーーー┛
A4
echo あいうえお かきくけこさし すせそたち | grep -o . | awk 'BEGIN{r=1;c=6}/ /{--c;r=1}!/ /{print ++r"s%.%"$0"%"c}' | sed -f - tanzaku

https://twitter.com/nogiro_iota/status/1015462783352107008

解説

最後の sed -f - tanzaku に渡す前の出力を見れば目指すところがわかりやすいかと思います。

$ echo あいうえお かきくけこさし すせそたち | grep -o . | awk 'BEGIN{r=1;c=6}/ /{--c;r=1}!/ /{print ++r"s%.%"$0"%"c}' | head -n 6
2s%.%あ%6
3s%.%い%6
4s%.%う%6
5s%.%え%6
6s%.%お%6
2s%.%か%5

(17行は長いので列の変わる 6 行目まで出力しています。)

これがなんなのかは下の感想をご確認ください。(感想ってなんだ???)

感想

これはもう、昔やったことあるから解けたみたいな感じです。
昔下のようなビットマスクの 2 次元版みたいな処理がしたかったときがあって、


(2 回めの cat file1はコピペミスです)

下のようなことをしてました。


見やすくすると下のようになります。

$ cat a
abcd
efgh
$ echo -e '0010\n1010' | sed 's/./& /g' | awk '{for (i=1;i<=NF;i++){if ($i=="0"){print NR"s_._0_"i}}}' | sed -f - a
00c0
e0g0

最後の sed -f - a をなくしてみるとわかるのですが、

$ echo -e '0010\n1010' | sed 's/./& /g' | awk '{for (i=1;i<=NF;i++){if ($i=="0"){print NR"s_._0_"i}}}'
1s_._0_1
1s_._0_2
1s_._0_4
2s_._0_2
2s_._0_4

これは、一番左の数字が縦軸で、一番右の数字が横軸の文字を 0 に置き換える sed コマンドを生成しています。

今回は tanzaku ファイルに文字の埋まったデータがあるので、この縦軸・横軸を入れ替えてやれば tateyoko コマンドみたいなことができるわけになります。

それはそれとして until 検索が便利ですね。
from:nogiro_iota * until:2015-11-26 - Twitter Search

Q5

cowsayの牛を右向きにして吹き出しの位置を調整して下さい。(万が一右向きオプションがあったら、それは使わずにお願いします。)

              ________________________________
             < あなたとJava今すぐダウンロード >
              --------------------------------
               ^__^   /
       _______/(oo)  /
   /\/(       /(__)
      | w----||
      ||     ||
A5
cowsay あなたとjava今すぐ | sed 's#$#                         #' | sed 's#^\(.\{30\}\).*#\1#' | rev | tr '\\/()' '/\\)(' | awk 'NR!=2{print}NR==2{for(i=length;i>0;i--){printf(substr($0,i,1))}print ""}' | sed '2s%\(.*>\)\( *\)$%\2\1%'

https://twitter.com/nogiro_iota/status/1015472438652440576

解説

まず、後ろをパディングして、幅を揃えてから反転します。
それだけだとメッセージ部分も反転してしまうので、メッセージ部分を別個に反転しなおして、空白を追加した分吹き出しがずれるので移動しています。

感想

sede コマンドを使えばスッキリ書けたと思うのですが、書き方がわからず・・・・・・
メッセージ部分の空白が、全角文字でズレてるのが若干嫌ですね。

Q6

seq 20の出力について、次のように素数を丸囲みしてください。

1
②
③
4
⑤
6
⑦
8
9
10
⑪
12
⑬
14
15
16
⑰
18
⑲
20
A6
seq 20 | factor | awk '{gsub(":","",$1)}NF==2{print "echo '\''print \"246\";"$1"'\'' | cat <(echo obase=16) - | bc | sed '\''s%^2461%247%'\'' | xxd -r -p | iconv -f utf16be | awk 1"}NF!=2{print "echo "$1}' | bash

https://twitter.com/nogiro_iota/status/1015480448590360576

解説

① の Unicode コードポイントを見ると、 0x2461 (間違ってます。正しくは 0x2460)だったので、16 進数にに変換してから 246 を前に付ければ望む結果が得られます。
ただし、16 以上の数は桁上りがあるので、 5 桁になっているものは置換して対応しました。

UTF-16 エンコードのときにサロゲートペアを含まない Unicode は iconv で utf16be にエンコード/デコードすると xxd で変換/逆変換したときに扱い易いです。

感想

最初は Unicode の結合文字である U+20DD (前の文字を丸囲いする文字)をつければいいかと思い、それがどのように見えるか確認しました。


しかし、環境によるのかも知れませんが別々の文字として表示され ② などにならなかったので別のアプローチにしました。
いま考えると、もしできていたとしても ⑪ などの 2 桁のものは 1① になってしまっていたということになるので、どのみち正解にはなりませんでした。

あと、① → 0x2460 で 0x2461 ではなかったために 1 ズレてしまっていました。
正しい解は下になります。

seq 20 | factor | awk '{gsub(":","",$1)}NF==2{$1=$1"-1";print "echo '\''print \"246\";"$1"'\'' | cat <(echo obase=16) - | bc | sed '\''s%^2461\\([0-9]\\)%247\\1%'\'' | xxd -r -p | iconv -f utf16be | awk 1"}NF!=2{print "echo "$1}' | bash

Q7

text には、文字や空白、改行として認識されないバイナリが含まれています。どの行にどんなものがあるか調査してください。

A7
cat text | iconv -t utf16be | xxd -ps | fold -w 4 | grep ^0 | tr -d \\n | sed 's#000a#\n#g' | nl -nln -ba

https://twitter.com/nogiro_iota/status/1015483095007367169

解説

Unicode コードポイントに変換しながら見ていくと、grep -v ^3 などをして普通に見える文字を削っていった結果、残ったのが grep ^0 でした。

感想

こんなことをのたまってましたが、tr はマルチバイト文字を扱えないので、制御文字もマルチバイト文字も一緒くたに扱っているだけですね。

sed でやれば普通に出てきました。

$ cat text | sed 's/[[:print:]]//g' | xxd -ps
000a0a0a0a020a0a0a060a0a160a0a0a0a0a0a0a0a

Q8

$ echo 嘘は嘘であると見抜ける人でないと(掲示板を使うのは)難しい

から始めて、出力で次のようにルビを打ってください。多少ずれたりスペースが入っても構いません。

ウソ  ウソ        ミヌ      ヒト          ケイジバン  ツカ        ムズカ
嘘は嘘であると見抜ける人でないと(掲示板 を使うのは)難しい
A8 (途中)
echo 嘘は嘘であると見抜ける人でないと(掲示板を使うのは)難しい | pee cat 'mecab | sed '\''/^[はであとでなをの]/!s#.*,##'\'' | sed '\''s%[[:space:]].*%%'\'' | sed '\''/^[はであとでなをの()]/s#.# #g'\'' | sed \$d | tr -d \\n | awk 1' | tac

https://twitter.com/nogiro_iota/status/1015488362214387713

感想

全角カタカナを半角カタカナにしたりする nkf のオプションが覚えきれない。。。と思ってるうちに終わっていました。

元の文と揃える方法は全く考えずに進めていて、column -t も初めて見るものだったので色々新鮮でした。

これ楽しいですね。

$ echo これは、一番左の数字が縦軸で、一番右の数字が横軸の文字を 0 に置き換える sed コマンドを生成しています。 | mecab | sed 's#,[^,]*$##' | sed 's#\t.*,# #' | nkf --hiragana | sed 's%\(.*\) \(.*\)\1%\1 \2%' | sed \$d | awk 'NF==1{$2="*"}{a1[NR]=$1;a2[NR]=$2}END{for(i=1;i<=NR;++i){printf a1[i]" "}print "";for(i=1;i<=NR;++i){printf a2[i]" "}print ""}' | pee 'nkf --katakana | nkf -Z4' cat | sed '2,3!d' | column -t | sed 's%  \([^ ]\)%\1%g' | sed 's%\*% %g'
        イチバンヒダリ  スウジ  タテジク    イチバンミギ  スウジ  ヨコジク  モジ      オキカ                    セイセイ
これは、一番 左  の数字が縦軸 で、一番 右 の数字が横軸 の文字を0に置き換えるsedこまんどを生成しています。