Swift ArgumentParser でバージョン番号を出力する方法

コマンドラインツールを作っているとそのツールのバージョン番号を出力したい事があります。ここでは引数で渡した文字列をそのまま標準出力に出力する myEcho というコマンドを作るというのを例にしてみます。

完成イメージはこんな感じです。

% myEcho Hello
Hello
% myEcho --version
1.0.0

引数の文字をそのまま Print し、--version フラグが立っているときはバージョン番号を出力するだけのコマンドです。

コード例その1

import ArgumentParser

struct MyEcho: ParsableCommand {

    static var configuration = CommandConfiguration(commandName: "myEcho")

    @Flag()
    var version: Bool

    @Argument()
    var text: String

    func run() throws {
        if version {
            print("1.0.0")
            return
        }

        print(text)
    }
}

MyEcho.main()

素直に今回の仕様を ArgumentParser を使って実装すると上記のような感じで書けると思います。

ヘルプを出力してみるとこんな感じ

% myEcho --help
USAGE: myEcho [--version] <text>

ARGUMENTS:
  <text>

OPTIONS:
  --version
  -h, --help              Show help information.

いけてそうです。

引数を渡してみます。

% myEcho Hello
Hello

動いてますね。バージョン番号を出力させてみます。

% myEcho --version
Error: Missing expected argument '<text>'
Usage: myEcho [--version] <text>

エラーが出ました。必須引数がないってことで怒られています。とはいえよくあるコマンドラインツールはそのツールが引数を必須としていても --version フラグが有効な場合は必須として扱いません。

直します。

コード例その2

import ArgumentParser

struct MyEcho: ParsableCommand {

    static var configuration = CommandConfiguration(commandName: "myEcho")

    @Flag()
    var version: Bool

    @Argument()
    var text: String?

    func run() throws {
        if version {
            print("1.0.0")
            return
        }

        guard let text = text else {
            throw ValidationError("need argument")
        }

        print(text)
    }
}

MyEcho.main()

@Argument() にしている text プロパティをオプショナルにして、実行時に version フラグが立っていなければ text プロパティをチェックしてエラー処理したうえで出力します。

実際動かしてみると echo 部分も --version も期待通りに動きます。引数なしで実行すると以下のような感じに怒られます。

% myEcho
Error: need argument
Usage: myEcho [--version] [<text>]

補足すると ValidateionError は ArgumentParser 側で v0.0.1 のころから定義されているエラーです。このエラーを throw するとエラーコードを履きながらエラーメッセージを表示しつつ Usage を出力してくれます。便利ですね。

とはいえ個人的には

  • func run() throws 実行時には引数関連のエラー処理は終わった状態であってほしい
  • Unwrap を後続の処理の中であまりやりたくない

という思いもありモヤモヤします。

このモヤモヤを解決するために issue で良い方法がないか聞いてみた回答が次のコードです。

完成形

import ArgumentParser

struct MyEcho: ParsableCommand {

    static var configuration = CommandConfiguration(commandName: "myEcho")

    struct Version: ParsableArguments {
        @Flag()
        var version: Bool

        func validate() throws {
            if version { throw CleanExit.message("1.0.0") }
        }
    }

    @OptionGroup()
    var version: Version

    @Argument()
    var text: String

    func run() throws {
        print(text)
    }
}

MyEcho.main()

変更点は地味に多いですが以下の通り

  • version: Bool を Version 型 に変更
  • struct Version 内で
    • Bool フラグを持たせる
    • ParsableArguments プロトコルfunc validate() throws を実装
    • func validate() throws 内でフラグが立っていればバージョン番号を出力
      • 出力時には正常終了するために ArgumentParser が定義する CleanExit 型のエラーをスローする
  • text プロパティはオプショナルをやめる
  • func run() throws ではただ text プロパティを出力するのみ

これが現状のスッキリ終わらせるための Workaround です。

func validate() throws が run よりも先に実行されているので func run() throws 内では version のことは気にせずに済むようになりました。

コード量は増えましたが僕が元々感じていたモヤモヤはすべて解消されています。また、バージョン番号を出力する処理が Version 型にまとまっています。今回は MyEcho.Version とネストさせて定義していますが、ネストさせる必要性は特にないのでので単体のファイルとして定義していれば使い回しもしやすいです。

まとめ

この記事を読んで気付いた方がいるかも知れませんが、ArgumentParser ではバージョン番号出力はデフォルトで提供されていません。Issue 内でも提供して欲しいと他の方がリクエストしていました。将来的には提供されるようになるかも知れませんね。