コマンドライン引数を簡易にパースする(その2)

その1 を書いた後、もうちょっと良さそうな方法を思いついたのでその2。
こっちの方がオプショナル引数の書き方が自然(シェルでの一般的な書き方からの逸脱が少ない)で、他の人に使ってもらうとしても拒否反応が少ないだろうし説明も楽かなと。

自然ということでいえば、その1では必ず "key=value" のフォーマットで書く仕様にしていましたが、やはりフラグとしての指定もできた方がいいかと思ってそこも変更しました。

目的

  • 使い捨てのスクリプト、プロトタイピング、自分だけ〜何人かだけで使う程度の軽いツールなどを作るときにサッと使いたい
  • なるべく言語非依存にしたい

高機能でなくていいので yak shaving なしでサッと使いたい。

使い方

例として arg1, arg2 が必須で、あとはオプショナルなパラメータとして opt1, opt2, opt3 が指定できるが opt2 は渡さないという場合、次のように指定します。
必須引数 arg1, arg2 の順番は固定。オプショナル引数はその後に置く。

ruby my_cmd.rb arg1 arg2 opt1=11 opt3=33

オプショナル引数はまるごと省略可:

ruby my_cmd.rb arg1 arg2

key=value の形になっていない("=" を含まない)オプショナル引数はフラグとして扱われる。

ruby my_cmd.rb arg1 arg2 enable-foo

実装例

Ruby の場合(説明用コードなので一部 Ruby っぽくない書き方になっていますが意図的なものです)。

def parse_optionals(args)
  optionals = {}

  args.each{ |arg|
    if arg.include? "="
      idx = arg.index("=")
      # assert idx >= 1
      k = arg[0 ... idx]
      v = arg[(idx + 1) .. -1]
      optionals[k] = v
    else
      optionals[arg] = true
    end
  }

  return optionals
end

def parse_args(args, names)
  opts = {}

  if ARGV.size == names.size
    # オプショナル引数なし
  elsif ARGV.size > names.size
    # オプショナル引数あり
    opts = parse_optionals(ARGV[names.size .. -1])
  else
    $stderr.puts "invalid arguments size"
    # print_usage
    exit 1
  end

  names.each_with_index{ |name, i|
    opts[name] = args[i]
  }

  return opts
end

# コマンドライン引数をパース
opts = parse_args( ARGV, ["arg1", "arg2"] )

# 必要に応じてバリデーションとか

# パースされた引数を使ったプログラム本体
do_someting( opts )

パース結果を pretty print しただけの動作例:

$ ruby my_cmd.rb a1 a2 opt1=o1 opt2=o2 opt3=o3
{"opt1"=>"o1", "opt2"=>"o2", "opt3"=>"o3", "arg1"=>"a1", "arg2"=>"a2"}

$ ruby my_cmd.rb a1 a2 opt1=o1 opt3=o3
{"opt1"=>"o1", "opt3"=>"o3", "arg1"=>"a1", "arg2"=>"a2"}

$ ruby my_cmd.rb a1 a2
{"arg1"=>"a1", "arg2"=>"a2"}

$ ruby my_cmd.rb a1 a2 enable-foo
{"enable-foo"=>true, "arg1"=>"a1", "arg2"=>"a2"}

パース結果は Java でいえば Map<String, String> で、そこから先のバリデーションとかは使う側が適宜行います。

備考

  • ライブラリではなくスニペットという扱い。コピペして使う。
  • とにかくサッと書きたいときに毎回適当に書いたりするんだけど、毎回モヤモヤするのでいったん固めて使いまわしたかった。ひとまず自己満足できたので良し。
  • この形に落ち着いた後で「これ Ruby のメソッドのシグネチャだなあ」と思った。解が似ているということは、解こうとしていた問題が似ていたということではないかと。