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コマンドの可能性が高いので何もしない
かなり無理矢理感がある、これで本当に良いのでしょうかね…golangのcliライブラリに詳しい方にご教授いただきたいところ。
追記
今回の実装はrootコマンドのflagはparseしてほしい、かつ外部コマンドをサブコマンドとして実装したい場合の方法です。 rootにflagがない場合は下記で指摘されている通り、rootコマンドのRunで解析したほうが楽です。
これ、ちょっと調べていて気付いたんですが、 cobra は sub-command に match する物が無い場合、Root .Run を叩きに来ます。なのでそこで実行するのが簡単だと…
— 眼力 玉壱號 (@objectxplosive) 2018年1月13日
多少調整が必要なんですが、こんな感じの root にすると flag 含めてそのままやってきます… pic.twitter.com/PbchC6IKY4
— 眼力 玉壱號 (@objectxplosive) 2018年1月14日
objectxplosiveさん、指摘ありがとうございました。
…と、ここまでかいたタイミングで「rootの実態すら別コマンドとして実装して、root.Runではそいつを呼び出せばよかったのでは?」と思ったわけですが、試したわけではないので動くかわかりません。