Goのcobraで作るコマンドラインツール(Hello World)

## はじめに

Goでコマンドラインツールを作るフレームワーク[spf13/cobra](https://github.com/spf13/cobra)を使うと簡単にコマンドを実装することができるので、ハンズオン形式で紹介します。

今回は以下のような、表示させたい名前をコマンドライン引数で指定すると、
`Hello <引数で指定した名前>!!!`と出力するHello World的なコマンドを実装していきます。

```shell:Usage
> go run main.go -h
greet is a simple command line tool that greets you in English.

Usage:
  greet [flags]

Flags:
  -h, --help     help for greet
  -t, --toggle   Help message for toggle
```

```shell:引数を指定
> go run main.go 田中太郎
Hello 田中太郎!!!
```

```shell:引数の指定なし
> go run main.go
Hello anonymous!!!
```

## 作成手順

### 下準備

下準備として[Cobra Generator](https://github.com/spf13/cobra-cli/blob/main/README.md)というコマンド一発でアプリケーションの雛形を生成してくれるツールを使用して、プログラムの雛形を作っていきます。

とりあえず、コマンドラインツールを構成するルートディレクトリを作成します

```shell
> mkdir <プロジェクト名>
> cd <プロジェクト名>
```

作成したプロジェクト直下で`go mod init`でモジュールの初期化を行います

```shell
> go mod init <モジュール名>
```

cobra-cliコマンドを使ってアプリケーションの雛形を作成する

```shell
> go install github.com/spf13/cobra-cli@latest
```

cobra-cliがインストールできたので早速雛形を生成していきます

```shell
> cobra-cli init
```

以下のような構成で雛形が作成できます。

```shell
.
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go
```

この時点で`go run main.go`をすると以下のように出力されます。

```shell
> 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.
```

とりあえず下準備としては以上です。

### 構成を必要最小限にする

次に不要なファイルや余分なプログラムを削除していきます。

初めに、LICENSEとかは気にしていないので、LICENSEファイルは削除します。

cobra-cliのaddサブコマンドを使用すれば、簡単にサブコマンドを追加(cmd配下にファイルが生成されるらしいです。)できるのですが、今回は単一のコマンドにしたため、サブコマンドは特に追加しません。
つまり、`cmd/root.go`のみあれば良いです。
また、サブコマンドを追加しないので、root.goのファイル名をコマンド名(greet)と合わせて`greet.go`にリネームしておきます(なんとなく分かりやすいから)。

そしたら以下のような構成になります。

```shell
.
├── cmd
│   └── greet.go
├── go.mod
├── go.sum
└── main.go
```

いい感じ？になりました。
準備は整ったので次にメインロジックを実装していきます。

### 実装

#### main.go

まずは、`main.go`から見ていきましょう。

```go:main.go
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>

*/
package main

import "<gomodで初期化したモジュール名>/cmd"

func main() {
	cmd.Execute()
}
```

エントリーポイントとして使われるだけなので、シンプルな構成になっています。
ほとんど手を加えるところはなさそうです。
とりあえず上のコメント行は削除します。

```diff_go:main.go
- /*
- Copyright © 2022 NAME HERE <EMAIL ADDRESS>
-
- */
package main

import "<gomodで初期化したモジュール名>/cmd"

func main() {
	cmd.Execute()
}
```

#### cmd/greet.go

次に実際にコマンドの振る舞いが書かれていそうな`cmd/greet.go`を見ていきます。

```go:greet.go
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>

*/
package cmd

import (
	"os"

	"github.com/spf13/cobra"
)



// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
	Use:   "demo",
	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.`,
	// Uncomment the following line if your bare application
	// has an action associated with it:
	// Run: func(cmd *cobra.Command, args []string) { },
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

func init() {
	// Here you will define your flags and configuration settings.
	// Cobra supports persistent flags, which, if defined here,
	// will be global for your application.

	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.demo.yaml)")

	// Cobra also supports local flags, which will only run
	// when this action is called directly.
	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
```

比較的にシンプルな構成で分かり易いですね。
`cobra.Command型`の変数`rootCmd`でコマンドの振る舞いを定義して、
`Execute()`関数は`cobra.Command#Execute()`をラップしたシンプルな関数です。
オプションなどの設定は`init()`関数内に実装するっぽい。


ざっと見たところで、実装していきます。
とりあえずコピーライトのコメントを消します。

```diff_go:greet.go
- /*
- Copyright © 2022 NAME HERE <EMAIL ADDRESS>
-
- */
package cmd

import (
	"os"

	"github.com/spf13/cobra"
)
```

次に`rootCmd`変数を変更していきます。

```diff_go:greet.go
- // rootCmd represents the base command when called without any subcommands
- var rootCmd = &cobra.Command{
- 	Use:   "demo",
- 	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.`,
- 	// Uncomment the following line if your bare application
- 	// has an action associated with it:
- 	// Run: func(cmd *cobra.Command, args []string) { },
- }
+ // greet represents the command.
+ var greet = &cobra.Command{
+ 	Use:   "greet",
+ 	Short: "A simple command line tool",
+ 	Long: "greet is a simple command line tool that greets you in English.",
+ 	Run: func(cmd *cobra.Command, args []string) {
+         // コマンドライン引数に何も指定されなかった場合、匿名(anonymous)ユーザーとして挨拶する。
+ 		if len(args) < 1 {
+ 			fmt.Println("Hello anonymous!!!")
+ 			return
+ 		}
+ 		fmt.Printf("Hello %s!!!\n", args[0])
+ 	},
+ }
```

`Run`には`func(cmd *cobra.Command, args []string)`のシグネチャを持つ関数を定義でき、コマンド実行時にはこの関数が呼ばれるため、ここに今回のメインロジックを実装します。
ちなみに、第二引数の`args`はコマンドライン引数で指定された値が入ってきます。

`Execute()`はコメントの修正のみ。

```diff_go:greet.go
- // Execute adds all child commands to the root command and sets flags appropriately.
- func Execute() {
- 	err := greet.Execute()
- // This is called by main.main(). It only needs to happen once to the rootCmd.
+ // Execute executes the command.
+ func Execute() {
+ 	err := greet.Execute()
	if err != nil {
		os.Exit(1)
	}
}
```

また、今回は本当に最小限の`Hello World`的なコマンドにしたいのでオプションはデフォルトのままとします。なので、`init()`は単にコメントの削除だけします。

```diff_go:greet.go
...

func init() {
func init() {
- 	// Here you will define your flags and configuration settings.
- 	// Cobra supports persistent flags, which, if defined here,
- 	// will be global for your application.
-
- 	// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.demo.yaml)")
-
- 	// Cobra also supports local flags, which will only run
- 	// when this action is called directly.
- 	rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ 	greet.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

...
```

全体

```go:greet.go
package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

// greet represents the command.
var greet = &cobra.Command{
	Use:   "greet",
	Short: "A simple command line tool",
	Long:  "greet is a simple command line tool that greets you in English.",
	Run: func(cmd *cobra.Command, args []string) {
		if len(args) < 1 {
			fmt.Println("Hello anonymous!!!")
			return
		}
		fmt.Printf("Hello %s!!!\n", args[0])
	},
}

// Execute executes the command.
func Execute() {
	err := greet.Execute()
	if err != nil {
		os.Exit(1)
	}
}

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

実装は完了です。

## 動作確認

最後に動作を確認していきます。

```shell:Usage
> go run main.go -h
greet is a simple command line tool that greets you in English.

Usage:
  greet [flags]

Flags:
  -h, --help     help for greet
  -t, --toggle   Help message for toggle
```

```shell:引数を指定
> go run main.go 田中太郎
Hello 田中太郎!!!
```

```shell:引数の指定なし
> go run main.go
Hello anonymous!!!
```

完成です！！

## まとめ

今回は最小の構成でコマンドを作りましたが、cobraを使えば簡単にサブコマンドやオプションの設定ができるため、興味があったら実際に色々触ってみて、自作コマンドを作ってみても良いかもしれませんね。