golangのcobraで、サブコマンドとして外部コマンドを実行する

新年が明けたのでgolangに入門し、お試しにとコマンドラインツールを作っているところです。

その際に、外部コマンドをサブコマンドのように実行するにはどうすれば…と思って見つけたのが以下の記事です。

[Go言語]codegangsta/cliで、サブコマンドとして外部コマンドをとれるようにしてみた - Qiita

urfave/cli*1を使えばまさにやりたいことができそう…と思っていたのですが、上記記事のコードでは引数に未知のflagが含まれているとエラーになってしまいます。

どうにか解決できないか、あるいは自分の勘違いなのではと調査したものの進展なし。 早々に諦めて別のライブラリを調査することに。

https://github.com/spf13/cobra

結果、わりと知名度が高そうなcobraで同様のことができたので掲載しておきます。 importやコマンド名は適宜置き換えてください。

// main.go
package main

import "sample/cmd"

func main() {
    cmd.Execute()
}

main.goは特筆すべきことなどありません。

// cmd/root.go
package cmd

import (
    "fmt"
    "github.com/spf13/cobra"
    "os"
    "os/exec"
)

var rootCmd = &cobra.Command{
    Use:  "sample",
    Run: func(cmd *cobra.Command, args []string) {},
}

func Execute() {

    args := os.Args
    if len(args) > 1 {
        trySearchSubCommand(args)
    }

    err := rootCmd.Execute()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func trySearchSubCommand(args []string) {
    subcommand := args[1]

    for _, c := range rootCmd.Commands() {
        if c.Use == subcommand {
            return
        }
    }

    path, err := exec.LookPath(rootCmd.Use + "-" + subcommand)
    if err != nil {
        return
    }

    sub := &cobra.Command{
        Use:                subcommand,
        DisableFlagParsing: true,
        RunE: func(cmd *cobra.Command, args []string) error {
            c := exec.Command(path, args...)
            c.Stdout = os.Stdout
            c.Stdin = os.Stdin
            c.Stderr = os.Stderr
            return c.Run()
        },
    }
    rootCmd.AddCommand(sub)
}

func init() {}
  • rootCmd.Execute実行よりも前に探索を行う
  • rootCmd.Commandsで既存サブコマンド一覧を取得し、名前が一致するなら何もしない
  • exec.LookPathを使ってPATHの通った実行可能ファイルの絶対パスを探す
  • 見つかったならrootCmd.AddCommandでサブコマンドとして登録
    • DisableFlagParsing: trueにしてflagの解析を行わないようにする
  • 見つからない場合はrootコマンドの可能性が高いので何もしない

かなり無理矢理感がある、これで本当に良いのでしょうかね…golangcliライブラリに詳しい方にご教授いただきたいところ。

追記

今回の実装はrootコマンドのflagはparseしてほしい、かつ外部コマンドをサブコマンドとして実装したい場合の方法です。 rootにflagがない場合は下記で指摘されている通り、rootコマンドのRunで解析したほうが楽です。

objectxplosiveさん、指摘ありがとうございました。

…と、ここまでかいたタイミングで「rootの実態すら別コマンドとして実装して、root.Runではそいつを呼び出せばよかったのでは?」と思ったわけですが、試したわけではないので動くかわかりません。

*1:いつの日かcodegangsta/cliからrenameされたらしい