この投稿は、Clojure Contrib Library Advent Calendar 2013の6日目の記事です。
tools.cliとは
tools.cliは、コマンドライン引数のパーサライブラリです。
Contribがまだひとつの巨大なライブラリであったときのclojure.contrib.command-line
に相当するものです。シンプルなコマンドライン引数のパース機能を提供するもので、オプションの設定、ヘルプの自動生成などを行えます。v0.3.0からはClojureScriptでも使用できるようになりました。
インストール
Leiningenを使っているなら、project.clj内のdependenciesに以下を加えましょう。
[org.clojure/tools.cli "0.3.5"]
使い方
tools.cliが提供するのは基本的にparse-opts
関数のみです。このparse-opts
関数に、引数とオプションなどの設定を渡すことでパースを行います。
(ns my.program
(:require [clojure.tools.cli :refer [parse-opts]])
(:gen-class))
(def cli-options
;; 引数が必要なオプション
[["-p" "--port PORT" "Port number"
:default 80
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
;; 複数回入力可能なオプション
["-v" nil "Verbosity level"
:id :verbosity
:default 0
:assoc-fn (fn [m k _] (update-in m [k] inc))]
;; デフォルトがnilのbooleanオプション
["-h" "--help"]])
(defn -main [& args]
(parse-opts args cli-options))
上のプログラムに対して、例えば次のようなコマンドを実行したとします。
my-program -vvvp8080 foo --help --invalid-opt
するとparse-opts
関数は次のマップを返します。
{:options {:port 8080
:verbosity 3
:help true}
:arguments ["foo"]
:summary " -p, --port PORT 80 Port number
-v Verbosity level
-h, --help"
:errors ["Unknown option: \"--invalid-opt\""]}
このマップを用いて、内部の処理を切り替えたり、ヘルプを表示したりするわけです。
オプション
オプションは1種類につき、ひとつのベクタで設定します。
booleanオプション
デフォルトでは、オプションは指定された際にtrue
となるbooleanオプションになります。
["-h" "--help"]
2番目の引数はロングオプション名です。これからハイフンを除いたものが、返り値マップのキー名として使用されます。上の例では:help
となります。ロングオプション名を指定しない場合、:id
を使って明示的に返り値マップのキー名を指定する必要があります。
["-h" nil "Show help"
:id :help]
3番目の引数はオプションの説明文です。
引数ありオプション
ロングオプション名にパラメータ名を書くことで、引数が必要なオプションとなります。
["-p" "--port PORT"]
デフォルト値
オプションが指定されなかったときのデフォルト値は:default
で指定します。
["-p" "--port PORT" "Port number"
:default 80]
オプション引数のパース
コマンドライン引数は全て文字列として扱われますが、ポート番号の場合は数値として解釈して欲しいですよね。その場合は:parse-fn
を使います。
["-p" "--port PORT" "Port number"
:default 80
:parse-fn #(Integer. %)]
複数回指定時の挙動
同一オプションが複数回指定された場合、デフォルトでは上書きされます。:assoc-fn
を設定すると、そのオプションが呼ばれた数だけ数値をカウントするようなオプションも作れます。
["-v" nil "Verbosity level"
:id :verbosity
:default 0
:assoc-fn (fn [m k _] (update-in m [k] inc))]
上記は-v
の回数をカウントし、:verbosity
キーで返り値マップから回数を取り出せます。
オプション引数のチェック
オプションの引数が特定の条件を満たす必要があるなど、validationを設定することもできます。例えば、ポート番号は0〜65536の整数でなければならない場合、
["-p" "--port PORT" "Port number"
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
のように書けます。
オプションサマリー
parse-opts
関数はオプションのサマリー文字列を返します。
-p, --port PORT 80 Port number
-v Verbosity level
-h, --help
このサマリーは、より大きなヘルプテキストの一部として使われることを想定されています。ちなみに、デフォルトのサマリー文字列のフォーマットが気に入らなければ、:summary-fn
で好きなように変更できます。
エラーハンドリング
parse-opts
関数はエラーをthrowする代わりに、エラーメッセージを含んだベクタを返します。
{:options {:help ...}
:arguments ["foo"]
:summary " -p, --port PORT 80 Port number ..."
:errors ["Unknown option: \"--invalid-opt\""]}
エラーメッセージの表示などに活用することができます。
使用例
それでは、Leiningenで適当なプロジェクトを作成し、tools.cliを試してみましょう。
(ns cli-example.core
(:require [clojure.string :as string]
[clojure.tools.cli :refer [parse-opts]])
(:import java.net.InetAddress)
(:gen-class))
(def cli-options
[["-p" "--port PORT" "Port number"
:default 80
:parse-fn #(Integer/parseInt %)
:validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]]
["-H" "--hostname HOST" "Remote host"
:default (InetAddress/getByName "localhost")
:default-desc "localhost"
:parse-fn #(InetAddress/getByName %)]
[nil "--detach" "Detach from controlling process"]
["-v" nil "Verbosity level; may be specified multiple times to increase value"
:id :verbosity
:default 0
:assoc-fn (fn [m k _] (update-in m [k] inc))]
["-h" "--help"]])
(defn usage [options-summary]
(->> ["This is my program. There are many like it, but this one is mine."
""
"Usage: program-name [options] action"
""
"Options:"
options-summary
""
"Please refer to the manual page for more information."]
(string/join \newline)))
(defn error-msg [errors]
(str "The following errors occurred while parsing your command:\n\n"
(string/join \newline errors)))
(defn exit [status msg]
(println msg)
(System/exit status))
(defn -main [& args]
(let [{:keys [options arguments errors summary]} (parse-opts args cli-options)]
(cond
(:help options) (exit 0 (usage summary))
(not= (count arguments) 1) (exit 1 (usage summary))
errors (exit 1 (error-msg errors)))
(println options)
(println arguments)))
lein run
で実行してみましょう。
$ lein run -- -h
This is my program. There are many like it, but this one is mine.
Usage: program-name [options] action
Options:
-p, --port PORT 80 Port number
-H, --hostname HOST localhost Remote host
--detach Detach from controlling process
-v Verbosity level; may be specified multiple times to increase value
-h, --help
Please refer to the manual page for more information.
$ lein run -vvvp8080 foo
{:verbosity 3, :hostname #<Inet4Address localhost/127.0.0.1>, :port 8080}
[foo]
サブコマンド
コマンドラインツールを作ろうとしたとき、サブコマンドを複数用意して、機能を分けたいときがあります。例えばGitはサブコマンドを用いてgit add
, git commit
, git push
というように使えますね。
v0.3.0以降のtools.cliでは:in-order
を使用することで、サブコマンドを設定することができます。また、他のライブラリを利用する方法もあります。
tools.cli with :in-order
例えば次のようなコマンドが実行されたとき、
git --git-dir=/other/proj/.git log --oneline --graph
:in-order
が設定されていなかったり、false
だった場合には、parse-opts
は次のようにパースします。
options: [[--git-dir /other/proj/.git]
[--oneline]
[--graph]]
arguments: [log]
しかし、:in-order
をtrue
に設定した場合には、
options: [[--git-dir /other/proj/.git]]
arguments: [log --oneline --graph]
になります。
:in-order
をtrue
にすると、未設定の引数が見つかった時点でコマンドライン引数のパースを止め、残りを全てarguments
に格納します。ここではlog
が未設定の引数なので、それ以降の--oneline --graph
がarguments
に入っています。
これを利用すれば、(first arguments)
をサブコマンドとしてディスパッチすることができます。サブコマンドに対する引数(rest arguments)
を用いて、再度parse-opts
関数でパースすれば良いわけです。
clj-sub-command
clj-sub-commandは私が作成したライブラリで、tools.cliと併用することを前提に作られています。
関数の引数やヘルプ文字列を極力tools.cliに似せてあります。また、サブコマンドの前に共通のオプションを指定することもできます。
使用するには、以下のdependencyを追加します。
[clj-sub-command "0.3.0"]
sub-command
関数を用いて、サブコマンドをパースします。
(ns foo.core
(:require [clj-sub-command.core :refer [sub-command candidate-message]]))
(defn -main [& args]
(let [[opts cmd args help cands]
(sub-command args
"Usage: foo [-h] {cmd1,cmd2} ..."
:options [["-h" "--help" "Show help" :default false :flag true]]
:commands [["cmd1" "Description for cmd1"]
["cmd2" "Description for cmd2"]])]
(when (:help opts)
(println help)
(System/exit 0))
(case cmd
:cmd1 (f1 args)
:cmd2 (f2 args)
(do (println (str "Invalid command. See 'foo --help'.\n\n"
(candidate-message cands)))
(System/exit 1)))))
ヘルプはこんな感じです。
Usage: foo [-h] {cmd1,cmd2} ...
Options Default Desc
------- ------- ----
-h, --no-help, --help false Show help
Command Desc
------- ----
cmd1 Description for cmd1
cmd2 Description for cmd2
間違ったコマンドを打ち込むと、候補を出力してくれます。
$ foo cmd3
Invalid command. See 'foo --help'.
Did you mean one of these?
cmd1
cmd2
各コマンドに渡される引数のパースには、別途tools.cliを用いる想定です。例えば上の例では、f1
, f2
関数内でparse-opts
関数を使います。
tools.cliの回はこれにて終了です。
お役に立てば幸いです。
明日は@athos0220さんの『tools.traceで実行トレースをとる』です。