💎

rbコマンドを使ったrubyワンライナーの世界

2023/11/27に公開

本記事ではワンライナーでrubyを使うためのrbというコマンドについて紹介する記事です。

モチベーション

1日1問、半年以内に習得 シェル・ワンライナー160本ノックという技術書の中でrubyを使用したワンライナーが紹介されていました。Mac標準のsedやawkといったコマンドはBSD系のコマンドであり、LinuxなどのGNU系のコマンドと挙動が違うことが多く、オプション覚えるのも大変だったのでそういったときはrubyを使用してワンライナーを書いてみました。

PerlやPythonによるワンライナーも紹介されていたのですが、Perlはまったく知識0だったので今から新たに学ぶのがしんどかったのとPythonhはワンライナーで書きづらそうだったためrubyなら少しはわかるのでrubyを選択しました。

rubyをワンライナーで使用したのは初めてだったのですが、書いてみたらとても楽しく、rubyでいい感じにワンライナーを書くためのrbというコマンドの存在を知ったので布教と備忘録をかねて本記事を書いています。

rubyでワンライナー

先に簡単にrubyでのワンライナーの書き方の紹介です。

-eオプション rubyスクリプトの実行

基本的にはrubyコマンドに-eオプションを付け、実行したいスクリプトを渡してあげるとrubyで書いた処理を実行することができます。

% ruby -e 'puts "Hello Ruby Oneliner!!"'
Hello Ruby Oneliner!!

ワンライナーでrubyを使う場合は以下のように標準入力を処理することが多くなると思います。標準入力はARGFというファイルオブジェクトを扱うことで処理することができます。ARGFは特殊なオブジェクトであり、Enumerableモジュールをミックスインしてるためeachmapといった処理が可能です。

% seq 1 10 | ruby -e 'ARGF.each{|num| puts num.to_i * 2}'
2
4
6
8
10
12
14
16
18
20

-n, -l, -pオプション 行ごとに処理

-nオプションをつけると標準入力を1行ずつ処理することができます。この場合$_という変数に文字列で行データが格納されることになります。

% seq 1 10 | ruby -ne 'puts $_.to_i * 2'                   
2
4
6
8
10
12
14
16
18
20

-nオプションと合わせて使われるのに-lオプションがあります。このオプションは行単位で処理するときに行データの末尾の改行を自動で取り除いてくれるオプションのようなのですが筆者はいまいち理解できなかったので以下のようなワンライナーを考えてみます。

% seq 1 10 | ruby -ne 'BEGIN{a=""};a+=$_ if $_.to_i % 2 == 0;END{puts a}'  
2
4
6
8
10

1−10の範囲の数値を標準入力で渡して偶数のみを変数に格納して出力しています。この場合、各行データの末尾には改行が付いているので改行されて出力されます。これに-lオプションをつけてみます。

% seq 1 10 | ruby -lne 'BEGIN{a=""};a+=$_ if $_.to_i % 2 == 0;END{puts a}'
246810

改行されずに横一列で出力されました。これは各行データの末尾の改行が除去された状態で変数に追加されていったためです。また、-lオプションをつけるとprintなどで出力するときに改行を自動で入れてくれます。改行があることで予期せぬ挙動を生むこともあるので基本-nオプションを使う時はつけてもいいかもしれません。もしくは、-nオプションを使うときに思った挙動にならないときにつけてもいいかもしれません。

awkなどに慣れている方は問題ないと思いますが上記の例のようにBEGINENDブロックを使用することで集計処理なども書くことができます。

-pオプションは-nオプションの機能に加え最後にprint $_を実行してくれます。標準入力に対して破壊的変更を加えるときに便利。

% echo {a..z} | ruby -pe '$_.upcase!'
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

-nオプションまたは-pオプションを使用する時は$.という変数に行番号が格納されます。

% seq 11 20 | ruby -lne 'print "#{$.} #{$_}"'
1 11
2 12
3 13
4 14
5 15
6 16
7 17
8 18
9 19
10 20

-aオプション 標準入力を分割

-aオプションは-nもしくは-pオプションと合わせて使用することで$Fという変数に$.splitの結果が代入される。このオプションを使用するときの区切り文字は空白もしくはタブですが-Fオプションを使用することで区切り文字を変更することが可能です。

% echo {a..z} | ruby -ane '$F.each{|str|print str.upcase};puts'
ABCDEFGHIJKLMNOPQRSTUVWXYZ

% ruby -e 'puts ("a".."z").to_a.join(",")' | ruby -F, -ane '$F.each{|str|print str.upcase}'
ABCDEFGHIJKLMNOPQRSTUVWXYZ

-rオプション ライブラリの使用

-rオプションを使用することでrubyの標準ライブラリを読み込んで使用することができます。

# unライブラリを使用してwebサーバーを起動
% ruby -run -e httpd -- -p 8888 .

# jsonライブラリを使用
% echo '{"lang": "Ruby"}' | ruby -rjson -ne 'puts JSON.parse($_)["lang"]'
Ruby

-0オプション 入力データを一つのまとまりとして処理

-0オプションを使用すると-nオプションなどと合わせて使用したときに標準入力の区切りを改行以外にすることができる。そのため以下のような複数行の文字列データを一つの文字列として扱いたい場合などに使用することができる。

# 句読点が行の先頭にならないように置換するワンライナー
% ruby -0ne 'print $_.gsub(/\n([、。])/, "\\1\n")' <<EOF
シェルワンライナーにrubyを使用してみましたが
、いろいろできることが多くて便利ですね
。
EOF

シェルワンライナーにrubyを使用してみましたが、
いろいろできることが多くて便利ですね。

以上がrubyでワンライナーを書くための基本操作です。

rbコマンド

rbコマンドとはrubyで感覚的にワンライナーを書くためのCLIツールです。CLIツールといっても本体はrubyで書かれた数行のスクリプトです。これまでに説明させていただいたようにrubyをワンライナーとして使うには多くのオプションが存在し、覚えるのが大変ですよね?このrbコマンドに存在するオプションは-lオプションのみです。余計なことを覚えずに直感的にrubyを書くことができます。

rbの中身
#!/usr/bin/env ruby
File.join(Dir.home, '.rbrc').tap { |f| load f if File.exist?(f) }

def execute(_, code)
  puts _.instance_eval(&code)
rescue Errno::EPIPE
  exit
end

single_line = ARGV.delete('-l')
code = eval("Proc.new { #{ARGV.join(' ')} }")
single_line ? STDIN.each { |l| execute(l.chomp, code) } : execute(STDIN.each_line, code)

上記rbコマンドの中身を少し詳しく見てみましょう。

File.join(Dir.home, '.rbrc').tap { |f| load f if File.exist?(f) }

これは設定ファイルの読み込みを行なっています。ユーザーのホームディレクトリに.rbrcという設定ファイルが存在していればそれをロードします。

def execute(_, code)
  puts _.instance_eval(&code)
rescue Errno::EPIPE
  exit
end

ここではexecuteという関数を定義しています。この関数は引数を二つとり、第一引数の任意のオブジェクトに対して第二引数の処理を実行します。

single_line = ARGV.delete('-l')

この行では-lというオプションが指定されていればそれを削除して、削除結果を変数に格納しています。結果として-lオプションが指定されればsingle_lineはtrueになります。

code = eval("Proc.new { #{ARGV.join(' ')} }")

この行で残りのコマンドライン引数を文字列に連結させてProcオブジェクトに変換します。とりあえずcodeという変数に実行したいrubyコードが格納されることになります。

single_line ? STDIN.each { |l| execute(l.chomp, code) } : execute(STDIN.each_line, code)

最後に-lオプションの指定があった場合は標準入力を行ごとに処理をし、-lオプションの指定がなかった場合は標準入力をそのままexecute関数に渡して実行しています。-lオプションの指定があった場合は標準入力を行ごとに評価し、文字列として扱い、オプションの指定がなかった場合は標準入力をEnumeratorとして扱います。

インストール

なにはともあれインストールしてみます。上記のスクリプトをコピーしてパスの通っている場所に配置するか以下のコマンドを実行することで使用することができます。

sudo curl https://raw.githubusercontent.com/thisredone/rb/master/rb -o /usr/local/bin/rb && sudo chmod +x /usr/local/bin/rb

パスを通すことができたら以下のコマンドを実行してみてHELLOが表示されればOKです!

% echo hello | rb -l upcase
HELLO

rbの使用例

前述したrubyコマンドの例をrbコマンドで書いてみると以下のような感じになります。

# seq 1 10 | ruby -e 'ARGF.each{|num| puts num.to_i * 2}'
seq 1 10 | rb 'map{|num|num.to_i * 2}'
2
4
6
8
10
12
14
16
18
20

# seq 1 10 | ruby -ne 'puts $_.to_i * 2' 
% seq 1 10 | rb -l 'to_i * 2'
2
4
6
8
10
12
14
16
18
20

# seq 1 10 | ruby -ne 'BEGIN{a=""};a+=$_ if $_.to_i % 2 == 0;END{puts a}' 
% seq 1 10 | rb 'filter{|num|num.to_i % 2 == 0}'
2
4
6
8
10

# echo {a..z} | ruby -pe '$_.upcase!'
% echo {a..z} | rb -l upcase
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

# seq 11 20 | ruby -lne 'print "#{$.} #{$_}"'
% seq 11 20 | rb 'map.with_index{|num,i|"#{i+1} #{num}"}'
1 11
2 12
3 13
4 14
5 15
6 16
7 17
8 18
9 19
10 20

rbコマンドを使う利点としてはオプションについて何も考えなくていいことだと思います。行ごとに文字列として処理がしたければ-lオプションをつけ、それ以外は標準入力がEnumeratorとなるのでmapやfilterといった処理をするといった感じです。rubyのコードを書くだけでいいです。

ただ、rbコマンドだけだとrubyコマンドで実行していたような少し複雑な処理が逆に複雑になってしまうこともあるかもしれないのでそのときは無理にrbコマンドを使わずにrubyコマンドでワンライナーを実行するでいいかなと思います。

上記例だけだと何の意味もないコード例になってしまうので以下にrbのREADMEに載っていたコード例を一部載せておきます。

> docker ps -a | rb grep /Exited/ | rb -l 'split.last.ljust(20) + " => " + split(/ {2,}/)[-2]'

# angry_hamilton      => Exited (0) 18 hours ago
# dreamy_lamport      => Exited (0) 3 days ago
# prickly_hypatia     => Exited (0) 2 weeks ago

> find . -type f | rb 'group_by(&File.method(:extname)).map { |ext, o| "#{ext.chomp}: #{o.size}" }'

# : 3
# .rb: 19
# .md: 1

設定ファイル

rbコマンドの中身のところで軽く触れましたがrbコマンドはホームディレクトリに設定ファイルがあればそれを読み込みます。試しに以下のような設定ファイルを作成してみました。

.rbrc
require 'json'

class String
  def black; "\033[30m#{self}\033[0m" end
  def red;   "\033[31m#{self}\033[0m" end
end

この設定ファイルでは標準ライブラリであるjsonライブラリを読み込んでいるのとStringクラスを拡張する関数を定義しています。これを作成しホームディレクトリに配置すると以下のように使用することができます。

% echo '{"lan":"Ruby"}' | rb 'map{|x|JSON.parse(x)["lan"]}'
Ruby

% echo Ruby | rb -l red
Ruby # 見えないけど赤く表示されています

このように設定ファイルを作成することで独自の関数を定義したり標準ライブラリを読み込んで使うなども可能になります。

まとめ

今回はrubyを使用したワンライナーとより直感的にワンライナーを書くためのrbコマンドについて紹介させていただきました。rubyやrbが実際の開発現場で使用できるかは正直わかりませんが、rubyを使ったワンライナーは狙ったものができるととても楽しかったので興味がある方はぜひ使ってみてください!普段スクリプト言語を書かない筆者にとっては新鮮で非常に面白かったです。

この記事がどなたかのお役に立てれば幸いです。
今回は以上です🐼

参考

rubyコマンドについて

https://qiita.com/Fuyutsubaki/items/b07ad4fb537c8b042b6a

https://maeharin.hatenablog.com/entry/20130113/ruby_oneliner

rubyワンライナーの実用例

https://ryuichi1208.hateblo.jp/entry/2021/09/07/234018

rbについて

https://obelisk.hatenablog.com/entry/2019/01/01/104040

GitHubで編集を提案

Discussion