コマンドラインプログラムにおける引数、オプションなどの標準仕様

コマンドラインプログラムの引数、オプションといったインターフェースには ちゃんと仕様、ガイドラインといったものが存在しています。 Unix(Linux) では慣習としてなんとなく合うものなのですが、 Windows や Java のプログラムなどでは、インターフェースがめちゃくちゃなプログラムも結構あります。
今回はコマンドラインプログラムの標準的なインターフェースの仕様、動作を紹介します。 プログラムを作る際の参考にしてもらえればと思います。


標準インターフェースはよくあるケースを考慮されて作られているものです。 自分の作っているプログラムは標準的なケースと違うといったことがあるかもしれません。
しかし、 標準のスタイルは合わせること自体に意味があります。
作成する場合にはなるべく標準インターフェースにあわせて作るべきだと思います。

Windows or Unix スタイル

コマンドラインのオプションは Windows では "/"(スラッシュ)、 Unix では "-"(ハイフン) という慣習があります。

コマンドラインはクロスプラットフォームで作りやすいプログラムですし、 もとは Unix 用プログラムでも Windows で使われるものも多いです。 その際、 / は Unix ではディレクトリーの区切りに使われるため 使うことはできませんし、環境で切り替えるのも面倒です。

そのため、コマンドラインプログラムは Windows であっても Unix スタイルで作成するべき でしょう。

仕様、ガイドライン

まずはコマンドラインプログラムのインターフェースに関する規格について説明します。

POSIX 規格

POSIX とは Unix を始めとした各種の OS で移植しやすいプログラムを作成するための国際規格で、 IEEE, The Open Group によって策定、認証されています。 コマンドラインプログラムのインターフェースについてもPOSIX で規定されています。 ここでは、仕様とガイドラインが記述されており、当然これらには従うべきでしょう。 細かい点は上記のリンクを見てもらうとして、重要な点を抜粋してみます。

コマンド名 [-a] [-b] [-c オプション引数] [-d|-e] [-fオプション引数] コマンド引数...
  1. オプションは"-" で始める
  2. オプションは英数字一文字(なるべく小文字)
  3. - を省略して繋げられる ("-a -b -d" == "-abd")
  4. オプションとオプション引数の間には空白を入れる
    • くっつけて書く形式も例外的に認められるところがあるので、作成する側としては両方に対応した方がよいでしょう。
  5. オプションはコマンド引数の前
    • こちらも作成する側としては後や間に書く場合にも対応した方がよいです。
  6. すべてのオプションは省略できなければならない
    • 仕様上は認められていますが、ガイドラインでは禁止です。

このうち特に注意して欲しい点が 2 点あります。

java のプログラムなどで -jar や -output のようなロング名が使われることがあります。 しかし、上記の 2, 3 のルールがあるため、 ロング名は仕様上、完全にアウトです。
ロング名を使いたい場合は、次節の GNU スタイルで書く必要があります。

また、"必須オプション" などといって省略できないオプションを作る人がまれにいます。 しかし、そもそもデフォルトの動作を変えるためのものが "オプション" です。
仕様上は認められていますが、「オプションの言葉の意味わかってるの?」 と言われかねないので、 オプションは必ず省略できるようにしておくべきです。

GNU スタイル

多くのコマンドラインプログラムを提供している GNU でも、 インターフェースの仕様は慣習として定義されています。 これは POSIX の規格を一部拡張した形式になっています。 POSIX の仕様には最低限従うべきです。 これに加えて GNU の仕様にもなるべく合わせた方がいいと思います。
以下、 GNU の拡張部分について説明していきます。


ロング名

オプションのロング名は POSIX 規格上はアウトと書きましたが、ロング名を使いたいことも多いです。 GNU の仕様では次の形式でロング名を認めています。
  • オプションの前を "--"(ハイフン 2 つ)
アルファベットが足りない場合は別ですが、 -o, --output のようになるべくロング、ショートの両方とも用意した方が良いです。


また、オプション引数はショート形式と同様に空白でわけます。 ただし、 くっつけて書く "--name=value" の形も認められています。


対応すべきオプション

GNU の仕様では次の 2 つのオプションを最低限つけるようになっています。 これらのオプションをつけてプログラムを起動すると、 それぞれの表示を標準出力に出力して終了します。
また、最低限必要なのはロング名だけですが、 それぞれのショート形式 -v, -h にも対応しておいた方がいいでしょう。


引数を全て省略するとヘルプ(Usage)を表示するようになっているプログラムも多いです。 それはそれでいいことだと思いますが、引数を取らずに動作するプログラムもあります。 統一した形式でヘルプを表示できることは大事なことだと思います。

また、バージョンも 他のプログラムなどから呼び出して、バージョンを取得したいという場合があるので、 表示方法は統一されているべきです。


なお、この 2 つ以外のオプション名もなるべく GNU の他のプログラムに合わせた方がいいと思います。

エラーメッセージの書式

前節は起動する際のインプットの話でしたが、 アウトプットに関しても、 GNU ではエラーメッセージの書式をガイドラインで規定しています。
ファイル解析エラーのスタイル

ファイル解析エラーというのはコンパイラーなどのプログラムで、 フォーマットの書式に誤りがあった場合に出すエラーです。 これはコンパイラーでなくとも、設定ファイルの解析などで出すことはあると思います。

これらの出力は Emacs など他のプログラムで利用することが多いため、 ファイル解析エラーのメッセージは特に標準スタイルに合わせるべきです。

標準のスタイルは次の形式です。
ファイル名:行番号:エラー種別(Error, Warning 等): メッセージ
(例) /foo/bar/baz.txt:89:Error: Syntax error.

正確にはガイドラインには"エラー種別"は含まれていないのですが、 種別も入れるのが一般的だと思います。


通常エラーのスタイル

ファイル解析ではなく通常のエラーの場合は、最初にプログラム名を入れます。
プログラム名:エラー種別(Error, Warning 等): メッセージ
(例) foo.exe:Error: No such a file or directory. ("/bar/baz.txt")

こちらもガイドライン上は"エラー種別"は含まれていません。

終了ステータス

アウトプットのインターフェースの一つである終了ステータスにも仕様や慣例があります。 最低限のルールは 正常終了では 0 、異常終了では 0 以外 ということです。 これが守られていないとシェルスクリプトなど他のプログラムから呼び出された場合に成功したかどうかの判断ができず、困ることがあります。

異常終了の場合は 0 以外ですが、これは 1 ~ 255 の値を使って、エラー等の状態を表します。 この値はプログラム側で自由に設定していいのですが、基本は 1 で POSIX システム上の C 言語ではマクロで次のように定義されています。

マクロ 状態
EXIT_SUCCESS 0 正常終了
EXIT_FAILURE 1 異常終了



また、慣例として 128 以上は特殊な値 とされています。 この範囲の値は外部からシグナルを受け取って終了したことを意味しており、 さらに 128 を引いた値がシグナル値となるようにします。
例えば SIGHUP(1), SIGINT(2), SIGTERM(15) のシグナルを受け取ったのであれば、 終了ステータスはそれぞれ 129, 130, 143 です。

標準的なプログラミングの動作

前章は仕様やガイドラインで規定されている部分です。 以降は補足や私が慣例上やっていた方がよいと思う点について書いていきます。

ログ出力

ここではアウトプットの動作についてをまとめてみます。


基本方針

Unix 系のコマンドラインプログラムでは基本的に次のような方針で作られます。
正常終了の場合は何も出力せず、エラーがあった場合にメッセージを出力する

基本的にこの方針でよいと思うのですが、 問題なのは処理時間が長い場合です。

こういった場合、ユーザーに固まっていると思われないように "r"(キャリッジリターン, CR)を使って、 進捗表示を出すといったテクニックもあります。
しかし、これは他のプログラムから呼び出したり、ログをファイルに保存したりする時には非常に邪魔になります。 こういう場合は現在の処理内容をログとして出力するといったことがよくやられます。


ログ出力系オプション

基本方針としてはエラーメッセージ以外は出さないものなので、 処理内容を出力する場合、次のようにすることが多いです。
  1. 標準でログ出力して、 quite モード(-q, --quite)を用意
  2. 標準では出力せず、 verbose モード(-V, --verbose)を用意
個人的には処理が長いプログラムに関しては、ユーザーを不安にさせないために 1 つ目の quite モードを用意した方がよいと思います。

ただ、処理が短い場合でも verbose モードはデバッグ用として用意されることも多いです。
また、この verbose のオプションはショート形式が -v にされがちですが、 --version にも -v が使われるので、 ショート形式は大文字の -V にするべきだと思います。


制御コード

ターミナルの出力では制御コードを使って、太字にしたり、色を変えたりといった装飾ができます。
しかし、 これは先ほどの "r"(CR) と同様にログ取りなどで邪魔です。 また、制御コードによる装飾に対応していないターミナルもあります。
出力に制御コードは使うべきではないと思います。

ただし、なるべくわかりやすく表示したいというのも大事なことです。 制御コードを使いたい場合にはオプションで使用しないモードも必ず用意しておいた方がいいでしょう。

標準入出力

コマンドラインプログラムはパイプでつなげたり、 Web サービスなどの他のプログラムから呼び出されることがあります。 このため、ファイルの入出力だけでなく、 なるべく 標準入力、 標準出力にも対応しておかなければなりません。

出力に関しては -o(--output) のオプションで出力ファイル名を指定することが多いと思います。 この際、オプションの省略時に標準出力にしておけば、必須オプションといったものを作る必要もなくなります。

入力に関しては cat プログラムのように入力ファイルはコマンド引数で指定して、 引数を全て省略した場合に標準入力になるといった形式が多いのではないかと思います。

標準エラー出力

先ほどのログ出力のところの方針で正常終了の場合には何も出力しないのは、 標準出力が生成物の出力先でもあるためです。 また、そのためエラー出力用に標準エラー出力も用意されています。

ちゃんとエラーメッセージの出力は標準エラー出力にしておくべきでしょう。
この際、標準エラー出力は標準出力とバッファリングが違ったり、出力先を変えていたりします。 そのため、ログ出力と合わせてみないとわからないようなものではなく、 標準エラー出力だけで独立して理解できるメッセージにしておく必要があります。

サブコマンド スタイル

svn, git といったバージョン管理や gem, lein などのパッケージ管理のプログラムなどでは機能が多かったり、 色々な用途で呼び出されることがあります。
こういった場合には、コマンド引数の最初をサブコマンドとして後の指定を変えるスタイルが取られることが多いです。
コマンド [共通オプション] サブコマンド [サブコマンド別のオプション] コマンド引数...
(例) svn --username foo add -m "message" bar.cpp

引数とオプションの順番など厳密には POSIX 規格から外れていますが、結構認められてきているスタイルです。そのため、機能が多すぎて規格に合わせられないといった場合には サブコマンドのスタイルを検討してみるのもよいと思います。

まとめ

最後にコマンドラインプログラム作成において守っておいた方が良い点をまとめておきます。
  • インターフェース仕様
    • Unix スタイル(オプションは - ) で作る
    • オプションにロング名は使わない (GNU スタイルの -- を使う)
    • 省略できない必須オプションは作らない
    • --help, --version (-h, -v) のオプションを用意する
    • ファイル解析時のエラーメッセージは標準スタイルに合わせる
    • 終了ステータスは 正常終了で 0、 異常終了で 1 ~ 127
  • 推奨動作
    • 余計なメッセージ出力はしない (モードで切り替えるようにしておく)
    • 出力に制御コードは使わない (使う場合は使わないモードも用意)
    • 標準入力、出力に対応する
    • エラーメッセージは標準エラー出力に出す


スポンサーサイト
 

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.
 
このページをシェア
アクセスカウンター
アクセスランキング
[ジャンルランキング]
コンピュータ
60位
アクセスランキングを見る>>

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

03月 | 2017年04月 | 05月
- - - - - - 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 - - - - - -


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

yohshiy

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

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

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