普段遣いの言語が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を使っても良いと思います。