なぜ Go 言語で CLI ツールを作るのか
以下の 4 点がぱっと思いつきますね。
また、みなさんがよく使ってるコマンドも、実は go でできてた、なんてことも多いと思います。
作り方
主に『サブコマンドを取る形式にするかどうか』で大きく 2 つに分類されます。
サブコマンドを取らない場合
サブコマンドを取らない場合というのは、ls
コマンドのように『コマンド + オプション』で完結するタイプのコマンドを指しています。
この手のコマンドを作成する場合、公式の flag の package だけ事足りますが、『ロングオプションとショートオプションを手軽に記述したい』などの要望があるときは spf13/pflag などのパッケージを利用すると便利です。
サブコマンドを取る場合
サブコマンドを取る場合というのは、git
コマンドのように『コマンド + サブコマンド + オプション』のように、複数のサブコマンドを取るタイプのコマンドを指しています。
(git add xxx
, git commit
では、add
, commit
の部分をサブコマンドと呼ぶことにします)
この場合は、サードパーティのパッケージにおとなしく頼るのが吉です。
その中でも spf13/cobra と urfave/cli が有名です。
どちらも数多くの使用例があり github を参考にできます。
cobra の使用例
『Projects using Cobra』 にみせびらかすように使用例が列挙されており、そのすごさが見てとれます。
kubectl や docker, hugo, github-cli がこちらのパッケージを使って出来ているらしいです(強すぎ)。
urfave/cli の使用例
こちらは特に公式にまとめられてるとかはなかったのですが、ghq (や opencontainers/runc, ovh/cds)が実はそうみたいですね。
以前 git に関する cli を作った時はこちらを使いました。
実際にやってみる
今回は参考にできるリポジトリの多さから cobra でサブコマンド付きの cli を作成してみたいと思います。
(最近個人で作ってみてる cli は github にあげてます。)
setup
# go プロジェクトを初期化 $ go mod init gitlab.tokyo.optim.co.jp/takahiro.tominaga/cobra-example # のちに使うため、cobra に加えて cli もインストールする $ go install github.com/spf13/cobra-cli@latest $ go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
初期化
先ほどインストールした cobra-cli を使って、cobra プロジェクトとしての初期化を行います。
# init 前 . ├── README.md └── go.mod # init $ cobra-cli init # init 後: いくつかのファイルが生成されている $ ls . ├── LICENSE ├── README.md ├── cmd │ └── root.go ├── go.mod ├── go.sum └── main.go
この状態で適当に動かしてみましょう
$ 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.
きちんと起動はしてそうですが、これ以上何もできないので、お待ちかねのサブコマンドを追加してみます。
サブコマンドの追加
# cobra-cli add <コマンド名> $ cobra-cli add hello # cmd/hello が追加された . ├── LICENSE ├── README.md ├── cmd │ ├── hello.go │ └── root.go ├── go.mod ├── go.sum └── main.go # hello サブコマンドを叩く $ go run main.go hello hello called
今回はこの hello コマンドを変更し、『name
オプションに名前を取り、その名前を使って出力』させてみたいと思います。
cmd/hello
の中身は以下のようになっています。
どうやら fmt.Println("hello called")
を変更したらよさそうです。
package cmd import ( "fmt" "github.com/spf13/cobra" ) // helloCmd represents the hello command var helloCmd = &cobra.Command{ Use: "hello", Short: "A brief description of your command", Long: `デフォルトの説明長すぎてカット.`, Run: func(cmd *cobra.Command, args []string) { fmt.Println("hello called") }, } func init() { rootCmd.AddCommand(helloCmd) }
フラグを追加してみます。
フラグは spf13/pflag が使われているので、慣れているとサブコマンド無しの cli も作りやすいと思います。
// helloCmd represents the hello command var helloCmd = &cobra.Command{ ... Run: func(cmd *cobra.Command, args []string) { // 先ほどのフラグを受け取る。 name, err := cmd.Flags().GetString("name") if err != nil { fmt.Println(err) } fmt.Printf("Hello %s!\n", name) }, } func init() { // **対象のコマンドに対し**フラグを追加 // ロングオプション, ショートオプション, デフォルト値, 説明 の順 // documentation: https://pkg.go.dev/github.com/spf13/pflag#StringP helloCmd.Flags().StringP("name", "n", "john doe", "your name") rootCmd.AddCommand(helloCmd)- [なぜ Go 言語で CLI ツールを作るのか](#なぜ-go-言語で-cli-ツールを作るのか) }
実行してみます。
$ go run main.go hello --name kotlin Hello kotlin! # していないときはデフォルト値が入る $ go run main.go hello Hello john doe!
なお、オプションの型を bool にすると -a
などのみで値を与えることが可能です。
注意点
2. クロスコンパイル可能なので、複数プラットフォームへの対応が容易
の良さを生かすためには、OS に固有の表現を使わないことが大切です。
例えば、パスの記述などでを string の結合で書くのはやめましょう。
パスや URL の扱いは、まともな言語なら標準のパッケージがあるのでそれを使うように意識します(com)。
pwd, _ := os.Getwd() // dir + "/" + fileName とかで結合しない joined := filepath.Join(pwd, "./README.md") fmt.Printf("path to README.md: %s\n", joined)
実行例
# linux 環境 $ go run main.go readme path to README.md: /home/tominaga/stamp/cobra-example/README.md # windows 環境 (cmd) >go run main.go readme path to README.md: C:\Users\OPM004972\Documents\work\memo_dx\stamp\cobra-example\README.md
なお、ci 環境で複数プラットフォーム向けバイナリを作成するには goreleaser が便利です(gh-actions の例)。