Apple のオープンソースライブラリ ArgumentParser

Swift.org - Announcing ArgumentParser

2月末に Swift.org でアナウンスされた ArgumentParser をいろいろと触ってみたので紹介的なものを書いてみようと思います。

基本的にはわかりやすい英語とサンプルコードで書かれている 本家 Documentation が一次情報なのでそちらを読んでください。リポジトリには 動作するサンプルコード もあるのでそちらを動かすのもおすすめです。

この記事の執筆時点では ver 0.0.2 がリリースされていますが僕が試したのは v0.0.1 なのでご了承ください。

はじめに

3rd Party 製の Command Line Tool 向けの Argument Parser だったりフレームワークのようなものは以前からありました。Apple 自身も Swift Package Manager 内部で使われていた ArgumentParser (名前が同じでややこしい)は存在しており、apple/swift-tools-support-core (以下 TSC)として切り出されていました。今回アナウンスされたものは TSC の ArgumentParser とも別物の単体のオープンソースライブラリとしてリリースされました。

特徴としては Swift 5.1 で追加された Property Wrapper を使ってコマンドのパラメータとプロパティの対応が宣言的に書けるようなライブラリになっています。ある種のフレームワーク的なものとしても捉えられ、仕組みに沿ってコードを書いていけば簡単にコマンドラインツールが書けるようになっています。おかげで本質的な処理部分に時間をかけられるのはうれしいところです。

Apple が作っているいくつかのオープンソースプロダクトは TSC から ArgumentParser に乗り換える方向のようです。(indexstore-dbswift-format で PR が出されていてまだマージはされていない)

今回は僕がドキュメントを読んだり少し触った範囲で気になったことを書いていきます。

僕の背景として、公開はしていませんが5個ほど TSC を使ったコマンドラインツールを個人的に書いたことがあります。他の言語だと Go と Node.js で少しコマンドラインツールを使ったことがあります。

できること

  • よくあるパターンの引数のパース処理を Property Wrapper を使って簡単に書ける
    • フラグ
      • --verbose みたいなただのフラグ・スイッチ的なオプションの指定方法
    • オプション
      • swift build --configuration debug--configuration debug のように名前付きの引数みたいなオプションの指定方法
    • 引数
      • ls ~/ みたいに何もつかない引数
  • help もデフォで最低限は生成される
    • -h --help がデフォルトで実装される
  • エラーメッセージもデフォで最低限を返してくれる
    • 型チェックとか必須パラメータの不足など
  • ParsableCommand に準拠した型でコマンドを定義する
    • func run() throws メソッド内にコマンドの処理を書く
  • 実行自体は static メソッド main を呼ぶだけで、引数のパースや ParsableCommand の run をよしなに呼んでくれる
  • オプションのショートネームも定義できる
    • Swift で一般的な Lower camel case は自動的に Kebab case に変換されたオプション名になる
      • @Flag() var printSupportedFile: Bool--print-supported-file
    • @Flag(name: .shortAndLong) var printSupportedFile: Bool とすれば自動的に頭文字を使ったショートネームが追加されて -p, --print-supported-file が使えるようになる
    • @Flag(name: [.customShort("s") .customLong("support-file")]) var printSupportedFile: Bool とすれば自動生成じゃなく自分で任意のパラメータ名にできる。この場合は -s, --support-file が使えるようになる
  • long オプションは基本的に -- だけど withSingleDash: true すれば - 形式にできるけど推奨はされていない
  • 独自の型もパースできる(してもらえる)
    • ExpressibleByArgument に準拠して init?(argument:) を実装する
    • enum も RawRepresentable に対応してるような奴は ExpressibleByArgument に準拠してると宣言するだけで済む。
    • transform 関数を実装していればパースできたりもする
  • フラグの反転も指定できる
    • @Flag(inversion: .prefixedNo) var index: Bool だと --index--no-index が定義される
    • @Flag(inversion: .prefixedEnableDisable) だと --enable-hogehoge--disable-hogehoge
  • CaseIterable と String の RawRepresentable に対応した enum を使えばフラグをまとめたりとかもできる
  • Flag オプションを Int 型に適用すると呼び出された回数が入ってくる
    • -vvvv とかしたら verbose プロパティの値は 4 になってるみたいなことができる
  • 引数のパース方法は選べるけどほとんどの場合ややこしいことになる
    • @Option(parsing: . upToNextOption) var file: [String] のケースだけはちょっと便利かもしれない。毎回 --file hoge ってしなくても連続して --file のオプションを渡せるから。
  • サブコマンドの実装ができる
    • ツリー上にパースされていく
    • コマンド名は基本的に型名が使われるが以下のように明示もできる swift static var configuration = CommandConfiguration( commandName: "stdev", abstract: "Print the standard deviation of the values.")
    • サブコマンドのサブコマンドも作れる
    • 共通のオプション(設定)は ParsableArguments に準拠した Struct とかを用意して、@OptionGroup() var options: Math.Options と書くことでまとめられる
      • ちょっとオプションの多いコマンドを作ったときにはオプションのカテゴライズをしてまとめられてコードが見やすくなるし、排他処理も書きやすくなる
  • ヘルプのカスタマイズ
    • @Flagとかの help 引数に文字列を明示的にあたえることでコメントを追加できる
    • より細かいカスタムは上記で渡していた文字列の代わりに ArgumentHelpインスタンスを渡すことで可能
    • コマンドやサブコマンドのヘルプは static var configuration = CommandConfiguration( XXXX ) でできる
    • デフォルトの -h, --help のパラメータ名はカスタム可能。Root レベルのコマンドに対する CommandConfiguration で helpName: で指定可能
    • ヘルプに出力したくない場合には @Flag(help: .hidden) とかすれば、試験的に追加した機能とかをヘルプに出力せずにすませられる
  • バリデーション
    • ParsableCommandParsableArgumentsvalidate() を実装することでバリデーションできる
    • throw すればそのメッセージが標準エラーに出力されてエラーコードで終了する
    • runメソッド内でも実行時エラーを throw できる
      • v0.0.2 では ExitCode という Error が用意されたので自前でエラーメッセージを出力して throw ExitCode.failure することで綺麗にエラーからの終了が実装できるようになった
  • マニュアルでパース
    • きっちりとコマンドラインツールとして作るのではなくスクリプトっぽく書きたいとき
      • ParsableArguments に準拠した struct にこれまど同様にオプションを書いておく
      • static method の parseOrExit() を呼べば、成功してればインスタンスが返ってくるし失敗したら終了してくれる
      • parse メソッドもあってこちらは throw されるので自分でエラーハンドリングができる。
    • コマンドやサブコマンドもマニュアルでパースできるけどややめんどくさい parseAsRoot() とかつかえばいい

できないこと(提供されてないこと)

  • コマンドラインツール的に便利なパスの処理
  • 外部のコマンドの呼び出しのサポート

これらはライブラリの名前から連想する対応範囲を超えているように思えるので別のライブラリなどを使った方が良いかもしれないです。僕は個人的にパス周りは Path.swift, パスまわりや外部のコマンド呼び出しは前述の TSC を使ったりしています。

それ以外は特別困っていません。

まとめ

コマンドラインツールはいろんな宗派があると思うので好みが分かれると思います。細かいことを言えば切りがないかも知れません。とはいえ

  • Apple 純正
  • サブコマンドも含めてコマンドラインツール作成における大枠の仕組み、パース処理が提供されている
  • 独特な実装ではなく Swift らしい素直な実装

Swift でコマンドラインツールを書く場合、これらは利点だと思うのでぜひ皆さんも使って見たらいいと思います。