読者です 読者をやめる 読者になる 読者になる

My Favorite Things - Coding or die.

とある技術者の経験記録、的な。

Carthageコードリーディング::3日目

Carthage Swift コードリーディング iOS

さて3日目。

前回は「VersionCommand」まわりの実装を見た。 今回は実際にコマンドを実行している周りを見ていく。

エントリポイント(復習)

1日目でも触れたが、エントリポイントであるmain.swiftの以下でhelpコマンドをデフォルトに実行している。

registry.main(defaultVerb: helpCommand.verb) { error in
    fputs(error.description + "\n", stderr)
}

CommandRegistry::main

mainメソッドの中身を見ると、以下のようになっている。

@noreturn public func main(defaultVerb defaultVerb: String, errorHandler: ClientError -> ()) {
        main(arguments: Process.arguments, defaultVerb: defaultVerb, errorHandler: errorHandler)
}

標準のProcess.argumentsを使ってコマンドライン引数を取得した上で、別のメソッドに転送している。

ちなみにProcess.arguments[String]を返し、最初の要素はコマンド自身の名前となる。 つまりcarthage helpとした場合は、["carthage", "help"]という結果が得られるはずである。

転送先のメソッドを見てみると・・・ちょっと長い。

@noreturn public func main(arguments arguments: [String], defaultVerb: String, errorHandler: ClientError -> ()) {
  assert(arguments.count >= 1)

  var arguments = arguments

  // Extract the executable name.
  let executableName = arguments.remove(at: 0)

  let verb = arguments.first ?? defaultVerb
  if arguments.count > 0 {
    // Remove the command name.
    arguments.remove(at: 0)
  }

  switch runCommand(verb, arguments: arguments) {
  case .Success?:
    exit(EXIT_SUCCESS)

  case let .Failure(error)?:
    switch error {
    case let .UsageError(description):
      fputs(description + "\n", stderr)

    case let .CommandError(error):
      errorHandler(error)
    }

    exit(EXIT_FAILURE)

  case nil:
    if let subcommandExecuted = executeSubcommandIfExists(executableName, verb: verb, arguments: arguments) {
      exit(subcommandExecuted)
    }

    fputs("Unrecognized command: '\(verb)'. See `\(executableName) help`.\n", stderr)
    exit(EXIT_FAILURE)
  }
}

端折ってもいいけれど一応順番に見ていく。 (今後、繰り返しになりそうであれば割愛していく感じで)

コマンド名と引数の取得

assert(arguments.count >= 1)

var arguments = arguments

// Extract the executable name.
let executableName = arguments.remove(at: 0)

まずassertargumentsの要素数が1以上であることを確認している。 前述したが、引数無しで$ carthageと打った場合でもcarthageが格納されるはずなのでこれは正当なassert。

そして次に「コマンド名」と「コマンド引数」に分離すべく、argumentsvarで宣言しなおしている。 (ここで同じ名前で宣言しなおしているのは賛否ありそうだが、あえて新しい名前をつけるシチュエーションでもなかろう)

そして最初の引数であるはずのcarthageを除外して、executableNameに入れている。 (最初、これは使わないから_でいいかと思ったのだが、あとで使っているようだ)

let verb = arguments.first ?? defaultVerb
if arguments.count > 0 {
  // Remove the command name.
  arguments.remove(at: 0)
}

ここでverbにコマンド名、あるいはデフォルトであたえたhelpCommand.verb(すなわちhelp)が入る。

つまり$ carthage versionであればversion$ carthageであればhelpがデフォルトコマンドとして格納されるわけだ。

そしてargumentsの最初の要素を削って、コマンドの本来の引数としている。 すなわち$ carthage update --platform macであれば、["--platform", "mac"]となるはずだ。

実行

次に実際にコマンドを実行しているようだ。

switch runCommand(verb, arguments: arguments) {
case .Success?:
  exit(EXIT_SUCCESS)

case let .Failure(error)?:
  switch error {
  case let .UsageError(description):
    fputs(description + "\n", stderr)

  case let .CommandError(error):
    errorHandler(error)
  }

  exit(EXIT_FAILURE)

case nil:
  if let subcommandExecuted = executeSubcommandIfExists(executableName, verb: verb, arguments: arguments) {
    exit(subcommandExecuted)
  }

  fputs("Unrecognized command: '\(verb)'. See `\(executableName) help`.\n", stderr)
  exit(EXIT_FAILURE)
}

runCommand(verb, arguments: arguments)メソッドで先ほど解決した、コマンド名と引数を渡している。 そしてその結果(成功 or 失敗 or nil?)で処理結果を分離している。

そのまま読んでみると、以下のようになるだろうか。

  • 成功:プロセス終了
  • 失敗(引数の指定が正しくない):usage(正しい使い方)を出力して終了
  • 失敗(コマンド失敗):登録されたエラーハンドラを実行
  • nil:サブコマンドがあれば実行し、なければエラーとする

nilのケースが良くわからなかった。 これは「Commandantモジュール」の中なので、Carthageでは使用していない部分の可能性もある。

追記: 直後に分かったが登録されていないコマンドを与えた場合にnilのケースになるようだ。

$ carthage foo
$ Unrecognized command: 'foo'. See `carthage help`.

CommandRegistry::runCommand

runCommandの中身に飛ぶと、以下のようになっている。

public func runCommand(verb: String, arguments: [String]) -> Result<(), CommandantError<ClientError>>? {
  return self[verb]?.run(ArgumentParser(arguments))
}

self[verb]でsubscriptを呼び出して、CommandWrapperを取得している。

public subscript(verb: String) -> CommandWrapper<ClientError>? {
        return commandsByVerb[verb]
}

commandsByVerbは以下のように宣言されており、registerメソッドを呼び出した際に更新されている。

private var commandsByVerb: [String: CommandWrapper<ClientError>] = [:]

public func register<C: CommandType where C.ClientError == ClientError, C.Options.ClientError == ClientError>(command: C) {
  commandsByVerb[command.verb] = CommandWrapper(command)
}

つまりcommandsByVerbはコマンド名をキーに、CommandoTypeのラッパーを値として格納するDictionaryだ。 これによってコマンド名に対応するCommandWrapper(とやら)を取得して、.run(ArgumentParser(arguments))を呼び出している。

ArgumentParserは大体想像するとおりのこと(つまり引数文字列のパース)をやっていると思うので、とりあえず放置する。

CommandWrapper

まずコード全体を見てみる。

public struct CommandWrapper<ClientError: ClientErrorType> {
    public let verb: String
    public let function: String

    public let run: ArgumentParser -> Result<(), CommandantError<ClientError>>

    public let usage: () -> CommandantError<ClientError>?

    /// Creates a command that wraps another.
    private init<C: CommandType where C.ClientError == ClientError, C.Options.ClientError == ClientError>(_ command: C) {
        verb = command.verb
        function = command.function
        run = { (arguments: ArgumentParser) -> Result<(), CommandantError<ClientError>> in
            let options = C.Options.evaluate(.Arguments(arguments))

            if let remainingArguments = arguments.remainingArguments {
                return .Failure(unrecognizedArgumentsError(remainingArguments))
            }

            switch options {
            case let .Success(options):
                return command
                    .run(options)
                    .mapError(CommandantError.CommandError)

            case let .Failure(error):
                return .Failure(error)
            }
        }
        usage = { () -> CommandantError<ClientError>? in
            return C.Options.evaluate(.Usage).error
        }
    }
}

これもちょっとややこしそうだ。

verb、function

とりあえずイニシャライザを覗いてみると、、、 verbfunctionは、単にCommandTypeの値を転記しているだけのようだ。

verb = command.verb
function = command.function

run

runArgumentParserを受け取ってResultを返すクロージャが格納されている。

run = { (arguments: ArgumentParser) -> Result<(), CommandantError<ClientError>> in
  let options = C.Options.evaluate(.Arguments(arguments))

  if let remainingArguments = arguments.remainingArguments {
    return .Failure(unrecognizedArgumentsError(remainingArguments))
  }

  switch options {
  case let .Success(options):
    return command
      .run(options)
      .mapError(CommandantError.CommandError)

  case let .Failure(error):
    return .Failure(error)
  }
}

個人的にはクロージャではなくて普通にメソッドでも良いと感じたのだが、何かメリットがあるのだろうか?

とりあえずオプション引数のパースを試みて、成功したらcommand.run(options)で実行し、 失敗した場合は.Failureで失敗を返しているようだ。(まぁその辺の詳細はおいおい)

command.run(options)で実行される具体例は、前回見た以下となる。

public func run(options: NoOptions<CarthageError>) -> Result<(), CarthageError> {
    let versionString = NSBundle(identifier: CarthageKitBundleIdentifier)?.objectForInfoDictionaryKey("CFBundleShortVersionString") as! String
    carthage.println(versionString)
    return .Success(())
}

これでコマンド引数のパースから、前回のVersionCommandrun()までつながった。

usage

usageも同じようにクロージャが格納されている。

usage = { () -> CommandantError<ClientError>? in
  return C.Options.evaluate(.Usage).error
}

CommandTypeに紐づくOptions.Usageを評価させて、.errorを返しているらしい。 難しく見えるけれど、要はオプション引数のusage(使い方)を返す処理のようだ。

また新しいテーマに入ってしまうので深追いはやめておくが、参考までにHelpCommandevaluateは以下のようになっている。

public static func evaluate(m: CommandMode) -> Result<HelpOptions, CommandantError<ClientError>> {
  return create
    <*> m <| Argument(defaultValue: "", usage: "the command to display help for")
}

まとめ

とりあえず今日はここまで! なんかCarthageというよりも依存ライブラリに対するコードリーディングになっている気もするが・・・。

続く・・・のかもしれない。