OptionParser底力+ARGFを使ったコマンド作り
前回からの続き。マニュアルがあるのだから、ちゃんと読めばそこにすべてが書かれているのだけど、自分はマニュアルを読むのが苦手。それよりもサンプルコードを実行して、その結果を体感しながら覚える方が好き。自分にとってはそうやって覚えた方が、忘れずに記憶が後々まで残るのだ。
RubyのOptionParserの使い方は、大体分かった。すると便利な機能を知った後では、以前のコードがずいぶん無駄な努力をしているように思えてくる。もっと楽して、もっと使いやすいコマンドに仕上げるのだ。
サンプルコード(悪い例)
- 以下のコードは、以前作ったコマンドのRubyコードである。
- Closure Compilerを利用してJavaScriptを圧縮・整形する。
#!/usr/bin/ruby require 'optparse' require 'net/http' require 'uri' compilation_level_names = Hash.new{|h,k| k} compilation_level_names.merge!({'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}) option_hash = {} OptionParser.new do |opt| opt.on('-l','--compilation_level=STR|NUM', 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)', '1 | 2 | 3 (Default:1 )') {|v| option_hash[:compilation_level] = compilation_level_names[v]} opt.on('--output_format=STR', 'text | xml | json (Default:text)') {|v| option_hash[:output_format] = v } opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| option_hash[:formatting] = 'pretty_print'} opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -l1 --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' js-compile.rb --pretty_print "`cat FILE_PATH`"', 'The above commands output same compiled codes.') opt.parse!(ARGV) end url = URI('http://closure-compiler.appspot.com/compile') input = URI.decode(ARGV[0] || STDIN.gets(nil)) params = {js_code:input, compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} res = Net::HTTP.post_form(url, params.merge(option_hash)) puts res.body STDERR.puts "", "Before: #{input.length}", "After : #{res.body.length}", "Rate : #{res.body.length.to_f / input.length.to_f * 100}"JavaScriptを圧縮・整形するコマンド作り - ザリガニが見ていた...。
- optparseを使っているのに、その機能を使いこなしていないのだ。
- もっと楽できるはずなのに、苦労して自前のコードで処理している。
オプション--compilation_level=STR|NUMの処理
現状
- このオプションで設定したいことは、JavaScriptの圧縮レベルである。
- Closure Compilerには3つの圧縮レベルがある。
- WHITESPACE_ONLY
- SIMPLE_OPTIMIZATIONS
- ADVANCED_OPTIMIZATIONS
- --compilation_levelに上記圧縮レベルのどれかを設定したのだ。
- 圧縮レベルの名称の入力が面倒なので、数値1・2・3でも指定できるようにした。
- それを実現するため、compilation_level_namesというハッシュを用意している。
...中略... compilation_level_names = Hash.new{|h,k| k} compilation_level_names.merge!({'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}) option_hash = {} OptionParser.new do |opt| opt.on('-l','--compilation_level=STR|NUM', 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)', '1 | 2 | 3 (Default:1 )') {|v| option_hash[:compilation_level] = compilation_level_names[v]} ...中略...
- compilation_level_namesハッシュの動作
- 1・2・3に対しては、WHITESPACE_ONLY・SIMPLE_OPTIMIZATIONS・ADVANCED_OPTIMIZATIONSを返し、
- 1・2・3以外に対しては、指定されたキーそのものを値(文字列)として返す。
irb(main):001:0> compilation_level_names = Hash.new{|h,k| k} => {} irb(main):002:0> compilation_level_names.merge!({'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}) => {"1"=>"WHITESPACE_ONLY", "2"=>"SIMPLE_OPTIMIZATIONS", "3"=>"ADVANCED_OPTIMIZATIONS"} irb(main):003:0> compilation_level_names['1'] => "WHITESPACE_ONLY" irb(main):004:0> compilation_level_names['WHITESPACE_ONLY'] => "WHITESPACE_ONLY"
改善
- 実は、optparseのonメソッドでは、オプション引数の選択肢を与えることが可能である。
- 配列で渡せば、配列に含まれる文字列を返す。
- ハッシュで渡せば、キーに対応する値を返す。
- よって、以下のように書けば、現状の仕様は満たされることになるのだ。
...中略... option_hash = {} OptionParser.new do |opt| opt.on('-l','--compilation_level=STR|NUM', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], {'1'=>'WHITESPACE_ONLY', '2'=>'SIMPLE_OPTIMIZATIONS', '3'=>'ADVANCED_OPTIMIZATIONS'}, 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)', '1 | 2 | 3 (Default:1 )') {|v| option_hash[:compilation_level] = v} ...中略...
- と同時に便利な機能が追加される。
- オプション引数に、配列に含まれる値以外、ハッシュに含まれるキー以外が指定されると、エラーになる。
- オプション引数は、すべてを入力しなくてもよい。他の選択肢と区別できる必要最小限の入力でOK。
- よって、WHITESPACE_ONLY・SIMPLE_OPTIMIZATIONS・ADVANCED_OPTIMIZATIONSは、W・S・Aだけの入力でもOKなのだ。
- ならば、ショートカット用の選択肢1・2・3は不要になる!
...中略... option_hash = {} OptionParser.new do |opt| opt.on('-l','--compilation_level=STR', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| option_hash[:compilation_level] = v} ...中略...
これでいいのだ!
オプション--output_format=STRの処理
現状
opt.on('--output_format=STR', 'text | xml | json (Default:text)') {|v| option_hash[:output_format] = v }
オプション--no-newlineを追加する
現状
- Closure Compilerは長いJavaScriptコードを圧縮する時に、所々に改行を追加する。(pritty_printでなくても)
- それらの改行は、JavaScriptコードを実行する時には不要なので、現状まではパイプで繋いで削除していた。
$ js-compile.rb FILE_PATH | perl -pe 's/\n//g'
改善
- --[no-]newline
require 'optparse' option_hash = {} OptionParser.new do |opt| opt.on('--[no-]newline', 'With new line, or without new line.') {|v| option_hash[:newline] = v} opt.parse!(ARGV) end p option_hash
- '--[no-]newline'というオプション定義すると、--newlineと--no-newline両方のオプション指定が可能になる。
$ js-compile-test.rb --newline {:newline=>true} $ js-compile-test.rb --no-newline {:newline=>false}
- --no-newline
require 'optparse' option_hash = {} OptionParser.new do |opt| opt.on('--no-newline', 'With new line, or without new line.') {|v| option_hash[:newline] = v} opt.parse!(ARGV) end p option_hash
- '--no-newline'というオプション定義すると、--no-newlineのみオプション指定が可能になる。--newlineではエラーになる。
$ js-compile-test.rb --newline js-compile-test.rb:8:in `block in': invalid option: --newline (OptionParser::InvalidOption) from js-compile-test.rb:6:in `new' from js-compile-test.rb:6:in ` ' $ js-compile-test.rb --no-newline {:newline=>false}
ARGFの利用
- 定数ARGFには、配列ARGVに含まれる値をファイルパスとみなして、各ファイルの中身をまとめて受け取る仮想ファイル
- 定数ARGVには、コマンド引数が収められている配列。
- もし、ARGVが空っぽなら、ARGFは標準入力を指し示す。
- よって、ARGFが良きに計らいコマンド引数、あるいは標準入力から受け取ってくれるのだ。
$ echo -e "abc\ndef\nghi" > sample.txt $ cat sample.txt abc def ghi $ ruby -e 'puts ARGF.gets(nil)' sample.txt abc def ghi $ cat sample.txt | ruby -e 'puts ARGF.gets(nil)' abc def ghi
- よって、現状のコマンド引数、あるいは標準入力から受け取るコードの部分は、もっとシンプルに書き換えられる。
現状
- コマンド引数は、JavaScriptコードとみなされる。
- 標準入力も、JavaScriptコードとみなされる。
input = URI.decode(ARGV[0] || STDIN.gets(nil))
改善
ARGF.gets(nil)とすることで...
- コマンド引数は、ファイルパスとみなされる。
- 標準入力は、JavaScriptコードとみなされる。
input = URI.decode(ARGF.gets(nil))
- さらに、getsはデフォルトでARGFから読み込むことになっているので、ARGFは省略できる。
input = URI.decode(gets(nil))
- コマンド引数の解釈が、JavaScriptコードからファイルパスに変化してしまったが、
- ファイルパスと解釈する方が、コマンドインターフェースとしては一般的だと思う。
- こちらの方が処理の幅も広がる。
サンプルコード(改善後)
- 以上の改善をコードに反映して、サンプルコード(悪い例)は以下のようになった。
#!/usr/bin/ruby require 'optparse' require 'net/http' require 'uri' # デフォルト設定 params = {compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} option_hash = {newline:true} # オプション解析 OptionParser.new do |opt| opt.banner = 'Usage: js-compile.rb [options] [FILE_PATH]' opt.separator('') opt.on('-l','--compilation_level=STR', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| params[:compilation_level] = v} opt.on('--output_format=STR', ['text', 'xml', 'json'], 'text | xml | json (Default:text)') {|v| params[:output_format] = v} opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| params[:formatting] = 'pretty_print'} opt.on('--[no-]newline', 'With new line, or without new line.(Default:--newline)') {|v| option_hash[:newline] = v} opt.separator('') opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -lW --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' # The above commands output same compiled codes.', '', ' js-compile.rb FILE_PATH # Argument receive file path.', ' cat FILE_PATH | js-compile.rb # STDIN receive JavaScript code.') opt.parse!(ARGV) end input = params[:js_code] = URI.decode(gets(nil)) # コンパイル url = URI('http://closure-compiler.appspot.com/compile') res = Net::HTTP.post_form(url, params) output = option_hash[:newline] ? res.body : res.body.gsub(/\n/, '') # 出力 puts output STDERR.puts "", "Before: #{input.length} Byte", "After : #{output.length} Byte", "Rate : #{output.length * 100 / input.length}%"
- ヘルプオプション-hあるいは--helpを指定すると、以下のように出力される。
$ js-compile.rb -h Usage: js-compile.rb [options] [FILE_PATH] -l, --compilation_level=STR WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY) --output_format=STR text | xml | json (Default:text) --pretty_print Add new line and indent for readable code. --[no-]newline With new line, or without new line.(Default:--newline) Example: cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print cat FILE_PATH | js-compile.rb -lW --pretty_print cat FILE_PATH | js-compile.rb --pretty_print # The above commands output same compiled codes. js-compile.rb FILE_PATH # Argument receive file path. cat FILE_PATH | js-compile.rb # STDIN receive JavaScript code.
完成!
複数ファイル対応
- コマンド引数に複数のファイルパスを指定できるようにしてみた。
- 複数ファイルを一つのJavaScriptコードにまとめる仕様である。
#!/usr/bin/ruby require 'optparse' require 'net/http' require 'uri' # デフォルト設定 params = {compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} option_hash = {newline:true} # オプション解析 OptionParser.new do |opt| opt.banner = 'Usage: js-compile.rb [options] [FILE_PATH...]' opt.separator('') opt.on('-l','--compilation_level=STR', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| params[:compilation_level] = v} opt.on('--output_format=STR', ['text', 'xml', 'json'], 'text | xml | json (Default:text)') {|v| params[:output_format] = v} opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| params[:formatting] = 'pretty_print'} opt.on('--[no-]newline', 'With new line, or without new line.(Default:--newline)') {|v| option_hash[:newline] = v} opt.separator('') opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -lW --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' # The above commands output same compiled codes.', '', ' js-compile.rb FILE_PATH # Argument receive file path.', ' cat FILE_PATH | js-compile.rb # STDIN receive JavaScript code.') opt.parse!(ARGV) end while gets(nil) input = params[:js_code] = URI.decode($_) # コンパイル url = URI('http://closure-compiler.appspot.com/compile') res = Net::HTTP.post_form(url, params) output = option_hash[:newline] ? res.body : res.body.gsub(/\n/, '') # 出力 print output STDERR.puts "", "Before: #{input.length} Byte", "After : #{output.length} Byte", "Rate : #{output.length * 100 / input.length}%" end
- 差分
$ colordiff -u <(pbpaste) /usr/local/bin/js-compile.rb
--- /dev/fd/63 2014-08-21 11:43:46.000000000 +0900
+++ /usr/local/bin/js-compile.rb 2014-08-21 11:42:13.000000000 +0900
@@ -12,7 +12,7 @@
# オプション解析
OptionParser.new do |opt|
- opt.banner = 'Usage: js-compile.rb [options] [FILE_PATH]'
+ opt.banner = 'Usage: js-compile.rb [options] [FILE_PATH...]'
opt.separator('')
opt.on('-l','--compilation_level=STR', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'],
'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| params[:compilation_level] = v}
@@ -31,13 +31,15 @@
opt.parse!(ARGV)
end
-input = params[:js_code] = URI.decode(gets(nil))
+while gets(nil)
+input = params[:js_code] = URI.decode($_)# コンパイル
url = URI('http://closure-compiler.appspot.com/compile')
res = Net::HTTP.post_form(url, params)
output = option_hash[:newline] ? res.body : res.body.gsub(/\n/, '')
# 出力
-puts output
+print output
STDERR.puts "", "Before: #{input.length} Byte", "After : #{output.length} Byte", "Rate : #{output.length * 100 / input.length}%"
+end
--no-newline不要
- よく考えたら、pretty_print以外は余分な改行は不要な訳なので、
- pretty_printの有無によって、改行の出力をコントロールすればいい。
#!/usr/bin/ruby require 'optparse' require 'net/http' require 'uri' # デフォルト設定 params = {compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} # オプション解析 OptionParser.new do |opt| opt.banner = 'Usage: js-compile.rb [options] [FILE_PATH...]' opt.separator('') opt.on('-l','--compilation_level=STR', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| params[:compilation_level] = v} opt.on('--output_format=STR', ['text', 'xml', 'json'], 'text | xml | json (Default:text)') {|v| params[:output_format] = v} opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| params[:formatting] = 'pretty_print'} opt.separator('') opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -lW --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' # The above commands output same compiled codes.', '', ' js-compile.rb FILE_PATH # Argument receive file path.', ' cat FILE_PATH | js-compile.rb # STDIN receive JavaScript code.') opt.parse!(ARGV) end while gets(nil) input = params[:js_code] = URI.decode($_) # コンパイル url = URI('http://closure-compiler.appspot.com/compile') res = Net::HTTP.post_form(url, params) output = params[:formatting] == 'pretty_print' ? res.body : res.body.gsub(/\n/, '') # 出力 print output STDERR.puts "", "Before: #{input.length} Byte", "After : #{output.length} Byte", "Rate : #{output.length * 100 / input.length}%" end
- 差分
$ colordiff -u <(pbpaste) /usr/local/bin/js-compile.rb
--- /dev/fd/63 2014-08-22 08:51:53.000000000 +0900
+++ /usr/local/bin/js-compile.rb 2014-08-22 08:48:33.000000000 +0900
@@ -8,7 +8,6 @@
params = {compilation_level:'WHITESPACE_ONLY',
output_format:'text',
output_info:'compiled_code'}
-option_hash = {newline:true}
# オプション解析
OptionParser.new do |opt|
@@ -18,7 +17,6 @@
'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| params[:compilation_level] = v}
opt.on('--output_format=STR', ['text', 'xml', 'json'], 'text | xml | json (Default:text)') {|v| params[:output_format] = v}
opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| params[:formatting] = 'pretty_print'}
- opt.on('--[no-]newline', 'With new line, or without new line.(Default:--newline)') {|v| option_hash[:newline] = v}
opt.separator('')
opt.on('Example:',
' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print',
@@ -38,7 +36,7 @@
# コンパイル
url = URI('http://closure-compiler.appspot.com/compile')
res = Net::HTTP.post_form(url, params)
- output = option_hash[:newline] ? res.body : res.body.gsub(/\n/, '')
+ output = params[:formatting] == 'pretty_print' ? res.body : res.body.gsub(/\n/, '')
# 出力
print output
エラー処理追加
- オプションエラー発生時に、トレース表示しないように修正した。
#!/usr/bin/ruby require 'optparse' require 'net/http' require 'uri' # デフォルト設定 params = {compilation_level:'WHITESPACE_ONLY', output_format:'text', output_info:'compiled_code'} # オプション解析 OptionParser.new do |opt| opt.banner = 'Usage: js-compile.rb [options] [FILE_PATH...]' opt.separator('') opt.on('-l','--compilation_level=STR', ['WHITESPACE_ONLY', 'SIMPLE_OPTIMIZATIONS', 'ADVANCED_OPTIMIZATIONS'], 'WHITESPACE_ONLY | SIMPLE_OPTIMIZATIONS | ADVANCED_OPTIMIZATIONS (Default:WHITESPACE_ONLY)') {|v| params[:compilation_level] = v} opt.on('--output_format=STR', ['text', 'xml', 'json'], 'text | xml | json (Default:text)') {|v| params[:output_format] = v} opt.on('--pretty_print', 'Add new line and indent for readable code.') {|v| params[:formatting] = 'pretty_print'} opt.separator('') opt.on('Example:', ' cat FILE_PATH | js-compile.rb --compilation_level=WHITESPACE_ONLY --pretty_print', ' cat FILE_PATH | js-compile.rb -lW --pretty_print', ' cat FILE_PATH | js-compile.rb --pretty_print', ' # The above commands output same compiled codes.', '', ' js-compile.rb FILE_PATH # Argument receive file path.', ' cat FILE_PATH | js-compile.rb # STDIN receive JavaScript code.') begin opt.parse!(ARGV) rescue => e puts e exit end end while gets(nil) input = params[:js_code] = URI.decode($_) # コンパイル url = URI('http://closure-compiler.appspot.com/compile') res = Net::HTTP.post_form(url, params) output = params[:formatting] == 'pretty_print' ? res.body : res.body.gsub(/\n/, '') # 出力 print output STDERR.puts "", "Before: #{input.length} Byte", "After : #{output.length} Byte", "Rate : #{output.length * 100 / input.length}%" end
- 差分
$ colordiff -u <(pbpaste) /usr/local/bin/js-compile.rb
--- /dev/fd/63 2014-08-29 10:06:09.000000000 +0900
+++ /usr/local/bin/js-compile.rb 2014-08-29 10:01:14.000000000 +0900
@@ -27,7 +27,12 @@
' js-compile.rb FILE_PATH # Argument receive file path.',
' cat FILE_PATH | js-compile.rb # STDIN receive JavaScript code.')
- opt.parse!(ARGV)
+ begin
+ opt.parse!(ARGV)
+ rescue => e
+ puts e
+ exit
+ end
end
while gets(nil)