Swift ArgumentParser におけるプロパティの定義順の重要性

今回はタイトル通り ArgumentParser を使っている時のプロパティの定義順の重要性について書きます。v0.0.2 で実際に経験していた範囲ですが、まだ ArgumentParser のソースコードを読んだ内容ではないこと、今後のバージョンで変わりうることをご了承ください。

前回のエントリのつづき

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()

上記サンプルコードは前回のエントリで紹介した、必須引数とバージョンフラグの両立をするためのコード例です。詳しい内容は前回のエントリを参照ください。

実はこの上記のコードでも前回解決した問題を再び発生させることが出来ます。

問題再燃サンプル

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") }
        }
    }

    @Argument()
    var text: String

    @OptionGroup()
    var version: Version

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

MyEcho.main()

何が変わったかわかりますか?

text プロパティと version プロパティの定義順を変更して text プロパティを先に持ってきました。これだけで

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

というエラーになります。

結論(経験上)

プロパティの定義順に処理が進むので先に解決して欲しいものは先に書くべし。

もしくは @Argument() は必ず最後に書くべしとも言えます。

@OptionGroup() の Property Wrapper の init 時に ParsableArgumentsfunc validate() throws が実行されます。上からプロパティを埋めていくように ArgumentParser は動作しているようなので、引数を処理している最中に struct Version のイニシャライズの段階でバリデーション処理でこける(バージョン番号を出力して正常扱いで終了)ために、@Argument() var text: String に到達しない。これが、期待する動作を手に入れていたころのコードでした。

プロパティの定義順を text を先にすると、ArgumentParser が引数の処理をしているときに text プロパティに代入するべき値が存在しないため version プロパティの処理をする前に必須項目が欠如しているとしてエラーになります。

Help や Usage にも影響あり

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

このエラー時の Usage の出力をみると引数 text の後ろに version フラグが書かれています。

ためしに前回のエントリの完成形コードを引数もフラグもなしで実行すると

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

このように version のあとに text が来ています。

プロパティの定義順によって出力される説明の順番にも影響を与えています。

さらに僕がハマった罠

僕が実際に書いていたコードで、外から引数として与えられはしない固定値としてプロパティに保持したいものがありました。今手元で再現できないのですが、たしか Decodable に対応してないというコンパイルエラーが出ていました。ParsableCommandParsableArguments に準拠しています。さらに ParsableArgumentsDecodable に準拠しています。よって、 ParsableCommandDecodable である必要があるのです。ただし、別に Decodable である必要性がなかったプロパティだったので CodingKey を定義して除外できるようにしました。

するとさっきまで動いていたバージョン番号の出力が引数がないと怒られるようになりました。

ずっと謎だったのですが、最終的に分かったことは CodingKey に列挙している case がプロパティの定義順通りになっていなかったからでした。

このせいで結構な時間が溶けていました。

まとめ

プロパティの定義順は

  • パース処理の順番
  • Usage や Help の出力の順番
  • CodingKey を定義する場合はプロパティの定義順に揃える

ということでした。

常に順番を意識してコードを書きましょう!