This is IT

技術、日常

OptionParser.parse!を2回使ったら、オプションがなくなってしまった話

概要

破壊的メソッドは元のオブジェクトも変更してしまいます。

なので当然なのですが、同じ変数に、同じ破壊的メソッドを複数回使ってしまうと、1回目と2回目以降では実行結果が変わってしまいます。

普通の破壊的メソッドだと気を付けられるのですが、OptionsParserクラスのオブジェクトだとあまり意識できなかったので自戒のメモ

コードと実行結果

コード

require 'optparse'

def main
  p load_options
  p ARGV

  p load_options
  p ARGV
end

def load_options
  opt = OptionParser.new

  params = {}

  opt.on('-l') { |v| params[:l] = v }

  opt.parse!(ARGV)

  params
end

main

実行結果

> sample.rb -l foo
{:l=>true}
["foo"]

{}
["foo"]

バグの理由

mainメソッド内の、1回目と2回目のp load_optionsの実行結果が異なってしまっています。

そもそも、load_optionsメソッド内にある、options.parseは、

  • opt.onメソッドのブロックの実行
  • 配列ARGVからオプション値を取り除く

という仕様です。

今回はparse!という破壊的メソッドを利用しているため、mainメソッド内で、ARGVは下記のように遷移しています。

def main
# この時のARGVは、`ARGV = ["-l", "foo"]
  p load_options

# load_options内の、`parse!`メソッドにより、`ARGV = ["foo"]`に遷移
  p ARGV

# 上記で破壊的メソッドによって、この段階では`ARGV = ["foo"]`のまま!
  p load_options

  p ARGV
end

となり、今回の実行結果では、

> sample.rb -l foo
{:l=>true}
["foo"]

{}
["foo"]

のように、2回目のp load_optionsでは、ARGV =['foo']を対象に処理を行うので、「オプションは何もないよ!」といった結果になってしまいます。

まとめと解決策

僕みたいな初心者はよく言われますが、『破壊的メソッドは出来るだけ使わない』ように心がけた方が、思わぬバグを減らせるかもしれませんね。

今回はサンプルのコードなので大分簡略化しましたが、実際に実装したコードでは、一度変数に代入をすることで、破壊的メソッドを伴う処理が何度も呼ばれないように修正をしました。

# 修正前
options =
  load_options.empty? { l: true, w: true} : load_options

# 修正後
options = load_options

options = 
  options.empty? { l: true, w: true} : options

まぁ良い設計なのかはさておき、『破壊的メソッドを伴う処理は、戻り値を一度変数に入れる』というのも1つの解決策なのだと思います🥰

あとそもそも破壊的メソッド使わなければいいんじゃないか?という話ですが、そうすると今度は逆に、ARGV配列が常に["-l", "foo"]のまま変化しなくなってしまうので、諸々支障が出ます(割愛)