普段遣いの言語がpythonとgoの人がrubyを使いたくなるときのこと

備忘録ということでメモしておくことにする。

いつrubyが欲しくなるか?

今現在では日常的に書く言語、つまるところの普段遣いの言語がpythonとgoなのだけれど、時折rubyが欲しくなる。

それがいつかというと、ワンライナーがほしいとき。タイトルを見れば想像がつく人もいるかもしれない。

より厳密に言うなら、「行をパースして値を取り出す際に、興味の対象となる箇所が2箇所以上のワンライナーが欲しくなったとき」にrubyが欲しくなる。

perlでもおそらく良いのだろうけれど、手持ちの道具箱の中にperlは入っていなかったのでrubyを例にあげる(実はプログラミングを真面目に始めたときの最初の言語がrubyだった。そしてperlには触れずに済んで生きてきたのでperlをコピペ以外で使った経験がない)。

ワンライナーを使う上での前提

自分自身の持つ道具箱から、いつどういうタイミングでどの道具を取り出すか?ということを整理するために、例題ベースでその対応を考えてみることにする。つまりrubyに至るまでの道筋をもう少し丁寧にメモしておくことにする。

基本的には、シェルスクリプトで済む範囲で生活しようしているようだ。一方で、原理主義ではないので、あまり複雑な機能を使おうとすることもないし。真面目に丁寧に使うこともない気がする。基本的な機能だけで生活している。もっと言えば、ループと条件分岐ですらなるべく使わない範囲で生活しようとしている。

いたるところで使われるgrep

ワンライナーの基本はgrepなような気がする。そしてwindowsではこの体験がし辛いので苦労していたことがあった。

(powershellが使いづらいという話は、結構いろいろ話せることがある気がする。すごく雑に言うと、表現や状態やアクションの数が複数ということは中を覗かなくてはいけない、この機能とパイプでつなぎ合わせる動作が衝突するというような話。また別の機会に愚痴のような形でメモにするかもしれない)

普通のgrep

行中から特定のパターンに合致する行を取り出すときにはgrepを使う。

例えば以下の様な内容のファイルから、箇条書きに対応する行を取り出してみる。

00sentence.txt

# section

- foo
- bar
- boo

箇条書きは、-で始まる行なので^-grepする。正規表現を使うときには概ね常に-Pオプションを付ける。 (他に-を含んだ行がなさそうな場合には、雑に grep '-' でごまかすこともある)

$ cat 00sentence.txt | grep -P "^-"
- foo
- bar
- boo

特定のファイル名のものだけを対象に、なにか処理を行いたい、と言うときには、findなどと組み合わせる事がある。 例えばテストファイルだけを取り出したいときには、findだけで済むが、そのうち特定のファイル名のみからなるファイルだけが欲しかった場合には、grepで絞り込む。

$ find . -name "test*.py"

# ファイル名にfooを含むようなもののみ。
$ find . -name "test*.py" | grep foo

xargsやループ

このようにして集めたファイル名の一覧に対して処理をしたい場合には xargsを使うか、forループを回す、この方法のforループだけをシェル中の構文として利用を許している。先程はループも使わないなどと言ってしまったが。

例えば、gofmtやblackのようなフォーマッターを掛けることや、lintを掛けること、もしくはsedなどで正規表現にマッチした部分を書き換えること。wcなどで行数を数えたりといった処理をよく後段につなげる。

基本的にはxargsで済ませられる分にはxargs派なのだけれど、forループの方がechoなどで途中の状態を確認したりなどの試行錯誤がしやすい事がある。またバッククォートを使った形式はネストした呼び出しができないが、Makefile中で利用する文にはMakeの構文と衝突しづらいので時折便利。

# xargsでsedをつなげてooを@@に変換する。
$ find . -name "test*.py" | grep foo | xargs -I{} sed -i 's/oo/@@/g'

# あるいは以下のように
$ for i in `find . -name "test*.py" | grep foo`; do echo $i; done
$ for i in $(find . -name "test*.py" | grep foo); do echo $i; done

-lオプション

grepでは-lオプションもよく使う。これは逆に特定のパターンを含んだファイル名を返す。-rオプションを込みで利用して再帰的に探索してfindの代わりとして使うこともある。あるいはgit grep -lを同様のものとして使うこともある。

# fooをimportしているpythonファイルだけを集める。 (from fooには対応していない)
$ grep -rP "import.*foo" .

# 集めたファイルをformatに掛ける
$ grep -rP "import.*foo" . | xargs gofmt -i

-oオプション

マッチした部分だけがほしい場合には-oオプションを使う。例えば特定の形式のパターンで抽出した部分に対してカウントしたい場合に、uniqとsortを込みで使う。

例えば、gitで更新があったファイルに対して、部分的にformatterをあてるというような使い方をすることもある。この場合にはgit diff --name-statusの結果などをparseする。

$ git diff --name-status
D   heh
M   daily/20200314/example_ast/q.py
M   daily/20200315/example_pygraphql/Makefile
M   daily/20200315/example_pygraphql/requirements.txt
M   daily/20200315/readme.md

$ git diff --name-status | grep -P "^(M|A)" | grep -P -o "[^/]+$"
q.py
Makefile
requirements.txt
readme.md

# 拡張子で集計するだとか
$ git diff --name-status | grep -P "^(M|A)" | grep -P -o "[^/]+$" | grep -P -o "\..+$" | sort | uniq -c
   1 .md
   1 .py
   1 .txt

# あるいはsedで無理やり潰す場合もある
$ git diff --name-status | grep -P "^(M|A)" | sed "s@.*/@@g"| grep -P -o "\..+$" | sort | uniq -c
   1 .md
   1 .py
   1 .txt

-h

複数のファイルを対象にした際に、-lの逆が欲しい場合もある。そのときには-hオプションを使うこともある。

cut, sed, grep

興味の対象が1箇所だけの場合にはcutとsedを組み合わせる事が多い。

$ ps
  PID TTY           TIME CMD
  403 ttys000    0:00.07 -bash
 1073 ttys000    0:07.91 screen
16317 ttys001    0:00.00 pbcopy
66189 ttys001    0:09.14 bash
 5852 ttys002    0:00.06 /usr/local/bin/bash --noediting -i
 5857 ttys003    0:28.95 /opt/local/Library/Frameworks/Python.framework/Versions/3.8/Resources/Python.app/Contents/MacOS/Python $HOME/.emacs.d/.python-environments/default/bin/jediepcserver --virtual-env $HOME/vboxshare/venvs/my/
 5859 ttys003    0:02.49 /opt/local/Library/Frameworks/Python.framework/Versions/3.8/Resources/Python.app/Contents/MacOS/Python $HOME/emacs-sandbox/emacs.d/.python-environments/default/lib/python3.8/site-packages/jedi/inference/compiled/subprocess/__main__.py $HOME/emacs-sandbox/emacs.d/.python-environments/default/lib/python3.8/site-packages 3.8.1
17968 ttys007    0:17.31 bash
36914 ttys008    0:03.66 bash
59247 ttys011    0:00.06 bash

# bashのprocess idだけを集める
$ ps | grep bash | cut -d " " -f 1 | grep -v "^$"
66189
16343
56094
17968
36914
59247

ruby

ようやくここで本題。

先程のcutの例で他にprocessの生存時間の情報も欲しいときなどにrubyを使う。つまりprocess idと生存時間の2箇所の値を取り扱いたくなったとき(2は2以上)。

ここで、冒頭の表現を思い出すと、rubyが欲しくなるのは、「行をパースして値を取り出す際に、興味の対象となる箇所が2箇所以上のワンライナーが欲しくなったとき」、ということでこれが対応する。ようやく伏線が回収された。

使うのは -ne正規表現正規表現と文字列の式展開が組み合わさってとても便利。

$ ps | grep bash | ruby -ne 'puts "pid:#{$1}\ttime:#{$2}" if $_ =~ /^\s*(\d+)\s+\S+\s+(\S+)/'
pid:403 time:0:00.07
pid:66189   time:0:09.17
pid:5852    time:0:00.15
pid:16612   time:0:00.00
pid:56094   time:0:08.27
pid:17968   time:0:17.31
pid:36914   time:0:03.66
pid:59247   time:0:00.06

また、パスのような /で区切られたよう文字列を持つもに対しては、%r!<pattern>!みたいな感じで正規表現リテラルを変えて記述できるので、バックスラッシュの数を節約できて便利。

$ git diff --name-status
M   daily/20200314/example_ast/q.py
M   daily/20200315/example_pygraphql/Makefile
M   daily/20200315/example_pygraphql/requirements.txt
M   daily/20200315/readme.md

# status付きでexampleだけを取り出す
$ git diff --name-status | grep example | ruby -ne 'puts "status:#{$1}  path:#{$2}" if $_ =~ %r!^([A-Z]).+/(example_.+)!'
status:M    path:example_ast/q.py
status:M    path:example_pygraphql/Makefile
status:M    path:example_pygraphql/requirements.txt

こういうときにrubyを使う。竜頭蛇尾っぽいけれど。メモなので。備忘録なので。おしまい。

追記: awkは。。?

awkを使っても良いと思います。