Simple minds think alike

より多くの可能性を

【Golang】cobraでコマンドラインツール(CLI)を作る

Go言語でコマンドラインツール(CLI)を作る際cobraというライブラリを使うと、ヘルプや設定ファイルの読み込みなど一般的な機能を持ったCLIを簡単に作れるようになっていて便利です。

といった様々なツールがcobraを使って作られています。

cobra.dev

cobraにはコマンドラインジェネレーターというのがあり、CLIのボイラープレートを生成することで比較的簡単にCLIを作れるようになっているので、その実装方法を紹介してみたいと思います。

前提

以下のバージョンで確認しています。

インストール

まず、cobraのジェネレーターをインストールします。

$ go get github.com/spf13/cobra/cobra

このコマンドラインジェネレーターは initadd という2つのサブコマンドがあります。

  • cobra init: 空のボイラープレートを生成する
  • cobra add: サブコマンドを追加

順番に実行してみていきます。

CLIのボイラープレートを生成する

設定ファイル使わないシンプルなCLIを作りたいので、 --viper=false を指定して cobra init でボイラープレートを生成します。

$ mkdir -p helloWorldApp && cd helloWorldApp
$ cobra init --pkg-name github.com/moritamori/helloWorldApp --viper=false
Your Cobra application is ready at
/home/takashi/go/src/github.com/helloWorldApp

tree でファイル構成を見てみると以下のようになっています。

$ tree
.
├── LICENSE
├── cmd
│   └── root.go
└── main.go

main()関数があるmain.go、ルートコマンドの実装cmd/root.go の2つが生成されています。

それぞれのコードを見ていきます。載せるコード量が多くなってしまうので、コメントの部分を除いています。

main.goを開いてみる

main()関数の中から単純にcmdパッケージの中の Execute() 関数を実行しているだけです。 Excute() 関数はルートコマンドにのみあります。

package main

import "github.com/moritamori/helloWorldApp/cmd"

func main() {
       cmd.Execute()
}

cmd/root.goを開いてみる

ルートコマンドのファイルです。基本的にジェネレータのボイラープレートのままですが、コード内にあるcobra.CommandRun の処理が動くようにコメントアウトを外して、 Hello world! という文字列の標準出力を追加しています。

実際には、このcmdパッケージの中にコマンドラインの処理を書いていきます。

package cmd

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

var rootCmd = &cobra.Command{
    Use:   "helloWorldApp",
    Short: "A brief description of your application",
    Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    Run: func(cmd *cobra.Command, args []string) { 
        fmt.Println("hello world!")
    },
}

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

func init() {
    rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

実行してみる

実行するとHello world!が表示されます。

$ go run main.go
hello world!

ちなみに、Run の部分のコメントを外さずに以下のようにして再度実行すると、 Long に登録しているHelpのメッセージが標準出力に表示されます。

var rootCmd = &cobra.Command{
    Use:   "helloWorldApp",
    Short: "A brief description of your application",
    Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    // Run: func(cmd *cobra.Command, args []string) { },
}
$ go run main.go
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.

この挙動は以下のロジックの中で決まっていて

ので、Runが定義されていない状態だと、ヘルプのメッセージが表示されるという動きになります。

サブコマンドを追加

次にcalkコマンドを追加してみます。

$ cobra add calc

再度、treeを実行しファイル構成を見てみると、新しく cmd/calc.go が追加されていることが分かります。

$ tree
.
├── LICENSE
├── cmd
│   ├── calc.go
│   └── root.go
└── main.go

cmd/calc.goを開いてみる

生成されたファイルを見てみます。

package cmd

import (
    "fmt"

    "github.com/spf13/cobra"
)

var calcCmd = &cobra.Command{
    Use:   "calc",
    Short: "A brief description of your command",
    Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println("calc called")
    },
}

func init() {
    rootCmd.AddCommand(calcCmd)
}

ルートコマンドのサブコマンドの違いは

  • Excute()関数
    • ルートコマンドだけにある
  • init()関数
    • ルートコマンド
      • フラグの設定
    • サブコマンド
      • ルートコマンドにサブコマンドを追加

という2点です。

では、サブコマンドを実行してみます。

$ go run main.go calc
calc called

サブコマンドのRunが実行されたことが分かります。

バイナリを作って実行するには

プロジェクトのディレクトリでgo install するとソースコードをビルドし、バイナリが $GOPATH/bin に出来ます。

$ go install

$GOPATH/bin にパスが通っていることを確認し、バイナリを実行するとgo run で実行した時と同じ実行結果になることを確認できます。

$ helloWorldApp
hello world!
$ helloWorldApp calc
calc called

$ which helloWorldApp
/home/takashi/go/bin/helloWorldApp

cobra initのフラグをファイルで指定

上記のサンプルで cobra init 実行時に --viper=false を指定することで、設定ファイル関連の処理がルートコマンドに入らないようにしました。

などは、毎回フラグを付けるのは煩雑なので .cobra.yaml ファイルを用意すると便利です。

デフォルトでは、ホームディレクトリ( $HOME/.cobra.yaml )に配置すると自動的に読み込まれるようになっています。

author: Takashi Morita
year: 2021
license:
  header: This file is part of CLI application foo.
  text: |
    {{ .copyright }}

    This is my license. There are many like it, but this one is mine.
    My license is my best friend. It is my life. I must master it as I must
    master my life.
useViper: false

Githubでコード管理したい場合など、他のディレクトリに配置したい場合もあるかと思います。 --config フラグを指定することで、ホームディレクトリ以外の場所に .cobra.yaml を設置できます。

$ cobra init --pkg-name github.com/moritamori/helloWorldApp2 --config ./.cobra.yaml

コマンドライン引数を追加するには

cobra.CommandArgs を指定すると引数を渡せるようになります。詳しくは公式のREADMEをご参照ください。

var rootCmd = &cobra.Command{
    〜〜〜
    // 最低1つのコマンドライン引数を受け取る
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
        fmt.Println(args[0])
    },
}
$ go run main.go arg_string
arg_string

フラグを追加するには

別の記事にまとめてみたので、よろしければこちらもご参照ください。

simple-minds-think-alike.hatenablog.com

参考資料