コマンドライン引数を簡易にパースする

2017-04-24 追記:
改良版を作りました → その2

目的

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

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

使い方

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

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

オプショナル引数の1文字目で区切り文字を指定しているのがミソで、パラメータ内に出現しない文字をユーザが任意に指定できます。

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

ruby my_cmd.rb arg1 arg2

実装例

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

def parse_optionals(optional_args)
  delim = optional_args[0]     # 1番目の文字=区切り文字
  body  = optional_args[1..-1] # 2番目の文字から最後まで
  kvs = body.split(delim)
  optionals = {}

  kvs.each{ |kv|
    # 1つ目の "=" で key と value を分ける
    idx = kv.index("=")
    # assert idx >= 1
    k = kv[0 ... idx]
    v = kv[(idx + 1) .. -1]
    optionals[k] = v
  }

  return optionals
end

def parse_args(args, names)
  opts = {}

  if ARGV.size == names.size
    # オプショナル引数なし
  elsif ARGV.size == names.size + 1
    # オプショナル引数あり
    opts = parse_optionals(ARGV[names.size])
  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"}

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

備考

  • ライブラリではなくスニペットという扱い。コピペして使う。
  • とにかくサッと書きたいときに毎回適当に書いたりするんだけど、毎回モヤモヤするのでいったん固めて使いまわしたかった。ひとまず自己満足できたので良し。
  • この形に落ち着いた後で「これ Ruby のメソッドのシグネチャだなあ」と思った。解が似ているということは、解こうとしていた問題が似ていたということではないかと。
  • 区切り文字を "&" にすると URL のクエリ文字列っぽくなる。枯れたフォーマットがあればそれに乗っかりたいので、実際クエリ文字列フォーマットで渡すのも検討したけど、区切り文字固定だと結局エスケープで悩むことになる。他に LTSV や JSON も検討したけど、同様に区切り文字とエスケープの問題とか JSON は煩雑とか言語によっては標準でライブラリがないとかで採用しなかった。
  • たとえば区切り文字をスペースにすると ' opt1=o1 opt2=o2' のように書ける。値にスペースが含まれないならこの方が見やすいかも。