tools.cli を使った Clojure でのコマンド引数のオプション解析

今回は Clojure でコマンドライン引数のオプション解析を行う方法について説明します。

with-command-line から tools.cli へ

Clojure でオプション解析する場合、以前は contrib の with-command-line が使われていたようです。
しかし、 これが contrib から分離して、今は tools.cli になっています。 with-command-line の頃は Java の悪習を引きずって、 POSIX 規格を無視した -(ハイフン) が一つのロング名が使われていました。 これが、 tools.cli ではちゃんと GNU スタイルでのオプションの指定になっています。

基本的な使い方

サンプルを使って tools.cli の使い方を説明していきます。
対象 リンク
ブラウズ cmdprs
圧縮ファイル cmdprs.zip

プロジェクトへの追加

ライブラリーを使うので、 Leiningen でプロジェクトを作って利用します。
Leiningen の使い方に関して詳しくは以前の記事を参考にしてください。
~/clojure/commandline $ lein new app cmdprs
project.cli に追記
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [org.clojure/tools.cli "0.3.1"]]
おそらく tools.cli はよく使うので、プロファイル($LEIN_HOME/profiles.clj)に追加してもいいのではないかと思います。

使い方の流れ

tools.cli で使う関数は parse-opts のみで、次のような流れで使用します。
  1. オプション仕様を定義
  2. 解析する引数とオプション仕様の定義を渡して parse-opts を実行
  3. 戻り値の解析結果を利用
core.clj :
;; オプション仕様定義
(def option-spec
  [["-h" "--help" "Show help."]
   ["-v" "--version" "Show program version."]
   [nil "--verbose" "Output log verbosity."]
   ;; 引数付き
   ["-o" "--output FILE" "Output file path (Default: \"a.out\")"
    :default "a.out"
    ]
   ])

(defn -main
  "Command line option parse sample program."
  [& args]
  ;; 引数を解析 (parse-opts 引数リスト オプション仕様)
  (let [info (parse-opts args option-spec)]
    ;; 解析結果を使用
    (pprint info))
  )
~/clojure/commandline/cmdprs $ lein run -- -v -k -o test.out foo bar baz
{:options {:version true, :output "test.out"},
 :arguments ["foo" "bar" "baz"],
 :summary
 "  -h, --help                Show help.\n  -v, --version             Show program version.\n      --verbose             Output log verbosity.\n  -o, --output FILE  a.out  Output file path (Default: \"a.out\")",
 :errors ["Unknown option: \"-k\""]}
なお、 通常 lein run の後の引数がプログラムの引数として渡されます。 lein 側の引数と紛らわしい場合など "--" を使って明示的に区切ることができます。

解析結果

parse-opts の解析結果は次のようなマップです。
キー
:options オプションのキー(ロング名)と値の map
:arguments オプションを取り除いたコマンド引数の vector
:summary オプションの説明(Usage で使用)
:errors エラーメッセージの vector

オプション仕様の定義

オプション仕様の定義の仕方についてもう少し詳しく説明します。

フラグ

オプション仕様の定義は次のような形式の vector の vector となります。
["ショート名" "ロング名" "説明"]

   [["-h" "--help" "Show help."]
    ["-v" "--version" "Show program version."]]
引数を指定していない場合はフラグで、 -v のようにオプションを指定すると true が設定されます。


ショート、ロング形式は nil を指定するとどちらかのみになります。
   [nil "--verbose" "Output log verbosity."]
ロング名は options のマップのキーにもなるので、 ロング名を省略した場合には :id でキーを指定する必要があります。
ただ、 個人的にはショートはアルファベットの数の制限上のために指定しないことはあっても、 ロング名は付けておくべきだと思います。

引数をとるオプション

引数をとるオプションを定義するためには、ロング形式の後に引数を書きます。
["-o" "--output FILE" "Output file path"]
指定した引数は文字列として格納されます。
これを :parse-fn で変換関数を指定して数値として格納したり、 :validate で引数の妥当性チェックもできます。
ただ、個人的には細かいエラー処理がやりずらくなるので、 これはオプション解析が終わった後に別途やってもよいのではないかと思います。

デフォルト値

定義の vector のオプションの説明の後に、 キーと値を書くことによって拡張した設定を書くことができます。
:default で省略時のデフォルト値を設定となります。
   ["-o" "--output FILE" "Output file path (Default: \"a.out\")"
    :default "a.out"
    ]

:assoc-fn を使った拡張

オプションを複数回指定すると後のもので上書きされます。 通常はこれで問題ありませんが、 grep の -e オプションのように指定されたものを全部使いたい場合などもあると思います。
こういった場合には :assoc-fn でデフォルトの動作を変更することができます。

まずはデフォルトの動作を確認するため、ダンプ処理と通常の assoc を行う関数にしてみます。
   ["-e" "--expr PATTERN" "Regular expression pattern"
    :assoc-fn
    (fn [m k v]
      (println (str "m=" m ", k=" k ", v=" v))
      (assoc m k v)
      )
    ]
~/clojure/commandline/cmdprs $ lein run -- -e foo -e bar -e baz
m={:output "a.out"}, k=:expr, v=foo
m={:expr "foo", :output "a.out"}, k=:expr, v=bar
m={:expr "bar", :output "a.out"}, k=:expr, v=baz
{:options {:expr "baz", :output "a.out"},
 :arguments [],
 :summary
 "...",
 :errors nil}
これを次のように変更すると複数回指定したものを vector にためていきます。
   ["-e" "--expr PATTERN" "Regular expression pattern"
    :assoc-fn
    (fn [m k v]
      (assoc m k
             (let [vec (get m k)]
               (if vec (conj vec v) [v])))
      )
    ]
~/clojure/commandline/cmdprs $ lein run -- -e foo -e bar -e baz
{:options {:expr ["foo" "bar" "baz"], :output "a.out"},
 :arguments [],
 :summary
 "...",
 :errors nil}

その他

説明しなかった変換、妥当性チェックやサブコマンドタイプへの対応など tools.cli では、その外にもいろいろと機能があります。 それらについては以下のサイト等を参考にしてください。

サンプルコード

もう少し実際にコマンドラインプログラムで使いそうなサンプルを書いてみました。
ここでの挙動はなるべく標準的な動作にそって実装しています。
対象 リンク
ブラウズ cmdsamp
圧縮ファイル cmdsamp.zip

core.clj :
  :
(defn -main [& args]
  (let [{:keys [options arguments errors summary]} (parse-opts args option-spec)]
    ;; 中断処理
    (cond
     ;; --help
     (:help options)
     (do (print-usage summary) (System/exit 0))
     ;; --verion
     (:version options)
     (do (print-version) (System/exit 0))
     ;; 解析時のエラー
     errors
     (do (print-err-msg errors) (System/exit 1))
     ;; 引数が 0 個
     (< (count arguments) 1)
     (do (print-err-msg "FILE isn't specified.")
         (print-usage summary)
         (System/exit 1))
     )
    ;; アプリの処理
    (println "Options   : " options)
    (println "Arguments : " arguments)
    ))

変数の格納

解析結果をそのまま格納するのではなく、バインディングを使って個々の変数に格納します。
  (let [{:keys [options arguments errors summary]} (parse-opts args option-spec)]

ヘルプ、バージョン処理

解析結果を元にエラー等で中断するかチェックしています。
--help--version は必ず用意しておかなければならないオプションです。 それぞれ指定するとヘルプやバージョン情報を標準出力に表示して終了します。
    (cond
     ;; --help
     (:help options)
     (do (print-usage summary) (System/exit 0))
     ;; --verion
     (:version options)
     (do (print-version) (System/exit 0))
ヘルプではオプションの説明は tools.cli が用意してくれるのでそれを使います。
(defn print-usage [options-summary]
  (println
   "Usage:" program-name "[Options] FILE [...]\n"
   "FILE: Input file path.\n"
   (str "Options:\n" options-summary)))
~/clojure/commandline/cmdsamp $ lein run -- -h
Usage: cmdsamp [Options] FILE [...]
 FILE: Input file path.
 Options:
  -h, --help                Show help.
  -v, --version             Show program version.
      --verbose             Output log verbosity.
  -o, --output OUT_FILE  a.out  Output file path (Default: "a.out")

エラー処理

オプション解析エラーは vector で格納され、エラーが発生していない場合は nil です。
     ;; 解析時のエラー
     errors
     (do (print-err-msg errors) (System/exit 1))
エラーメッセージは標準エラー出力(*err*)に出力しています。メッセージスタイルも標準のスタイルに合わせています。
(defn print-err-msg [errors]
  (let [errmsgs (if (vector? errors) errors [errors])]
    (.println *err* (join \newline
                          (map #(str program-name ":Error:" %1) errmsgs)))
    ))
~/clojure/commandline/cmdsamp $ lein run -- -u foo -o
cmdsamp:Error:Unknown option: "-u"
cmdsamp:Error:Missing required argument for "-o OUT_FILE"


また、サンプルではコマンド引数が 1 個以上ないとエラーにし、 Usage も表示しています。
     ;; 引数が 0 個
     (< (count arguments) 1)
     (do (print-err-msg "FILE isn't specified.")
         (print-usage summary)
         (System/exit 1))
~/clojure/commandline/cmdsamp $ lein run --
Usage: cmdsamp [Options] FILE [...]
 FILE: Input file path.
 Options:
  -h, --help                Show help.
  -v, --version             Show program version.
      --verbose             Output log verbosity.
  -o, --output OUT_FILE  a.out  Output file path (Default: "a.out")
cmdsamp:Error:FILE isn't specified.
スポンサーサイト
 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
36位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
3位
アクセスランキングを見る>>
カレンダー(アーカイブ)
プルダウン 降順 昇順 年別

02月 | 2014年03月 | 04月
- - - - - - 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 - - - - -


はてな新着記事
はてな人気記事
ブロとも申請フォーム
プロフィール

yohshiy

Author:yohshiy
職業プログラマー。
仕事は主に C++ ですが、軽い言語マニアなので、色々使っています。

はてブ:yohshiy のブックマーク
Twitter:@yohshiy

サイト紹介
プログラミング好きのブログです。プログラミング関連の話題や公開ソフトの開発記などを雑多に書いてます。ただ、たまに英語やネット系の話になることも。