GoでCobraを使ってCLIを作成してみた

# 背景・目的
最近、CobraというCLIアプリケーションを作成するライブラリを知りました。
今回、整理し簡単に試してみたいと思います。

# まとめ
下記に特徴を整理します。

|特徴|説明|
|:--|:--|
|Cobraとは|強力な最新の CLI アプリケーションを作成するためのライブラリ<BR>Kubernetes、Hugo、GitHub CLI など、多くの Go プロジェクトで使用されている|
|概要|git や go ツールに似た強力な最新の CLI インターフェースを作成するためのシンプルなインターフェースを提供するライブラリ|
|提供する機能|・簡単なサブコマンドベースの CLI: アプリサーバー、アプリフェッチなど<BR>・完全にPOSIX準拠のフラグ（短いバージョンと長いバージョンを含む）<BR>・ネストされたサブコマンド<BR>・グローバル、ローカル、カスケードフラグ<BR>・インテリジェントな提案<BR>・コマンドとフラグの自動ヘルプ生成<BR>・サブコマンドのヘルプのグループ化<BR>・-h、--help などのヘルプ フラグの自動認識<BR>・アプリケーション用のシェルのオートコンプリートを自動的に生成<BR>・コマンドエイリアスを使用すると、変更しても壊れることはない<BR>・独自のヘルプや使用方法などを定義できる柔軟性<BR>・Twelve-Factor AppのViperとの統合|
|コンセプト|コマンド、引数、フラグの構造に基づいて構築されている|
|ライセンス|Apache 2.0|

# 概要
下記を基に整理します。

https://github.com/spf13/cobra

> Cobra is a library for creating powerful modern CLI applications.
> Cobra is used in many Go projects such as Kubernetes, Hugo, and GitHub CLI to name a few. This list contains a more extensive list of projects using Cobra.

- Cobra は、強力な最新の CLI アプリケーションを作成するためのライブラリ
- Cobra は、Kubernetes、Hugo、GitHub CLI など、多くの Go プロジェクトで使用されている
- 他のCobraを使用するプロジェクトは[こちらのリスト](https://github.com/spf13/cobra/blob/main/site/content/projects_using_cobra.md)を参照

## Overview
> Cobra is a library providing a simple interface to create powerful modern CLI interfaces similar to git & go tools.

- git や go ツールに似た強力な最新の CLI インターフェースを作成するためのシンプルなインターフェースを提供するライブラリ

> Cobra provides:
> - Easy subcommand-based CLIs: app server, app fetch, etc.
> - Fully POSIX-compliant flags (including short & long versions)
> - Nested subcommands
> - Global, local and cascading flags
> - Intelligent suggestions (app srver... did you mean app server?)
> - Automatic help generation for commands and flags
> - Grouping help for subcommands
> - Automatic help flag recognition of -h, --help, etc.
> - Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell)
> - Automatically generated man pages for your application
> - Command aliases so you can change things without breaking them
> - The flexibility to define your own help, usage, etc.
> - Optional seamless integration with viper for 12-factor apps

- Cobraが下記を提供している
    - 簡単なサブコマンドベースの CLI: アプリサーバー、アプリフェッチなど
    - 完全にPOSIX準拠のフラグ（短いバージョンと長いバージョンを含む）
    - ネストされたサブコマンド
    - グローバル、ローカル、カスケードフラグ
    - インテリジェントな提案
    - コマンドとフラグの自動ヘルプ生成
    - サブコマンドのヘルプのグループ化
    - -h、--help などのヘルプ フラグの自動認識
    - アプリケーション用のシェルのオートコンプリートを自動的に生成
    - コマンドエイリアスを使用すると、変更しても壊れることはない
    - 独自のヘルプや使用方法などを定義できる柔軟性
    - Twelve-Factor AppのViperとの統合

## Concepts
> Cobra is built on a structure of commands, arguments & flags.
Commands represent actions, Args are things and Flags are modifiers for those actions.
The best applications read like sentences when used, and as a result, users intuitively know how to interact with them.
The pattern to follow is APPNAME VERB NOUN --ADJECTIVE or APPNAME COMMAND ARG --FLAG.
A few good real world examples may better illustrate this point.
In the following example, 'server' is a command, and 'port' is a flag:

- Cobra は、コマンド、引数、フラグの構造に基づいて構築されている
- コマンドはアクションを表し、引数は物、フラグはそれらのアクションの修飾子
- 最高のアプリケーションは、使用時に文章のように読み取られ、その結果、ユーザーは直感的に操作方法を知ることができる
- 従うべきパターンは、APPNAME VERB NOUN --ADJECTIVE または APPNAME COMMAND ARG --FLAG 
- 例):「server」はコマンド、「port」はフラグ
    ```bash
    hugo server --port=1313
    ```
- 例):このコマンドでは、Git に URL をそのままクローンするように指示している
    ```bash
    git clone URL --bare
    ```

### Commands
> Command is the central point of the application. Each interaction that the application supports will be contained in a Command. A command can have children commands and optionally run an action.

- コマンドはアプリケーションの中心点
- アプリケーションがサポートする各インタラクションはコマンドに含まれる
- コマンドには子コマンドを含めることができ、オプションでアクションを実行できる

### Flags
> A flag is a way to modify the behavior of a command. Cobra supports fully POSIX-compliant flags as well as the Go flag package. A Cobra command can define flags that persist through to children commands and flags that are only available to that command.

- フラグは、コマンドの動作を変更する方法
- Cobra は、Go フラグ パッケージだけでなく、POSIX に完全準拠したフラグもサポートしている
- Cobra コマンドでは、子コマンドにまで存続するフラグと、そのコマンドでのみ使用可能なフラグを定義できる

> Flag functionality is provided by the pflag library, a fork of the flag standard library which maintains the same interface while adding POSIX compliance.

- フラグ機能は、POSIX 準拠を追加しながら同じインターフェースを維持するフラグ標準ライブラリのフォークである pflag ライブラリによって提供される


# 実践
## 前提
下記を前提としています。
- VSCode
- MacOS:Sonoma
- Go:1.23.0

## セットアップ
### プロジェクトの作成
1. ディレクトリを作成します
    ```bash
    % mkdir go-cli
    % 
    ```
1. 作成したディレクトリをVSCodeのワークスペースに追加します
1. プロジェクトを初期化します
    ```bash
    % cd go-cli
    % go mod init go-cli
    go: creating new go.mod: module go-cli
    % cat go.mod 
    module go-cli
    
    go 1.23.0
    % 
    ```
### Cobraのセットアップ
1. Cobraをインストールします
    ```bash
    % go get -u github.com/spf13/cobra@latest
    go: downloading github.com/spf13/cobra v1.8.1
    go: downloading github.com/inconshreveable/mousetrap v1.1.0
    go: downloading github.com/spf13/pflag v1.0.5
    go: added github.com/inconshreveable/mousetrap v1.1.0
    go: added github.com/spf13/cobra v1.8.1
    go: added github.com/spf13/pflag v1.0.5
    % ls -ltr
    total 16
    -rw-r--r--  1 XXXX  XXXX  181 10 21 22:32 go.mod
    -rw-r--r--  1 XXXX  XXXX  896 10 21 22:32 go.sum
    % cat go.mod 
    module go-cli
    
    go 1.23.0
    
    require (
            github.com/inconshreveable/mousetrap v1.1.0 // indirect
            github.com/spf13/cobra v1.8.1 // indirect
            github.com/spf13/pflag v1.0.5 // indirect
    )
    % 
    ```
1. CLIアプリケーションのベースを自動生成するために、CLIもインストールします
    ```bash
    % go install github.com/spf13/cobra-cli@latest
    go: downloading github.com/spf13/cobra-cli v1.3.0
    go: downloading github.com/spf13/cobra v1.3.0
    go: downloading github.com/spf13/viper v1.10.1
    go: downloading github.com/spf13/jwalterweatherman v1.1.0
    go: downloading github.com/magiconair/properties v1.8.5
    go: downloading github.com/mitchellh/mapstructure v1.4.3
    go: downloading github.com/spf13/afero v1.6.0
    go: downloading github.com/fsnotify/fsnotify v1.5.1
    go: downloading github.com/spf13/cast v1.4.1
    go: downloading github.com/subosito/gotenv v1.2.0
    go: downloading gopkg.in/ini.v1 v1.66.2
    go: downloading github.com/hashicorp/hcl v1.0.0
    go: downloading github.com/pelletier/go-toml v1.9.4
    go: downloading gopkg.in/yaml.v2 v2.4.0
    go: downloading golang.org/x/sys v0.0.0-20211210111614-af8b64212486
    go: downloading golang.org/x/text v0.3.7
    %
    ```
1. PATHを確認します
    ```bash
    % go env GOPATH
    /Users/XXXX/go
    % 
    ```

1. `.zshrc`にPATHを追加します
    ```bash
    export PATH="$PATH:$(go env GOPATH)/bin"
    ```
1. 読み込みます
    ```bash
    source ~/.zshrc
    ```
1. 実行できました
    ```go
    % cobra-cli -h   
    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.
    
    Usage:
      cobra-cli [command]
    
    Available Commands:
      add         Add a command to a Cobra Application
      completion  Generate the autocompletion script for the specified shell
      help        Help about any command
      init        Initialize a Cobra Application
    
    Flags:
      -a, --author string    author name for copyright attribution (default "YOUR NAME")
          --config string    config file (default is $HOME/.cobra.yaml)
      -h, --help             help for cobra-cli
      -l, --license string   name of license for the project
          --viper            use Viper for configuration
    
    Use "cobra-cli [command] --help" for more information about a command.
    % 
    ```

## Cobraを使ったCLIアプリケーションの作成
### プロジェクトの初期化
1. 下記のコマンドを実行し、初期化します
    ```bash
    % cobra-cli init
    Your Cobra application is ready at
    /Users/XXXX/XXXX/go-cli
    % 
    ```
1. `LICENSE`、`cmd`ディレクトリ、`main.go`オブジェクトが生成されました
    ```go
    % ls -l
    total 24
    -rw-r--r--  1 XXX  XXX    0 10 21 22:53 LICENSE
    drwxr-x--x  3 XXX  XXX   96 10 21 22:53 cmd
    -rw-r--r--  1 XXX  XXX  181 10 21 22:32 go.mod
    -rw-r--r--  1 XXX  XXX  896 10 21 22:32 go.sum
    -rw-r--r--  1 XXX  XXX  117 10 21 22:53 main.go
    % 
1. `main.go`を確認してみます
    ```go
    % cat main.go 
    /*
    Copyright © 2024 NAME HERE <EMAIL ADDRESS>
    
    */
    package main
    
    import "go-cli/cmd"
    
    func main() {
            cmd.Execute()
    }
    % 
    ```
1. `cmd/root.go`を確認してみます
    ```go
    % cat cmd/root.go 
    /*
    Copyright © 2024 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:   "go-cli",
            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/.go-cli.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")
    }
    % 
    ```

### helloコマンドの追加
1. コマンドを追加します
    ```bash
    % cobra-cli add hello
    hello created at /Users/XXXX/XXXX/go-cli
    % 
    ```
1. `hello`ファイルが追加されました
    ```bash
    % ls -l cmd 
    total 16
    -rw-r--r--  1 XXXX  XXXX  1072 10 21 23:00 hello.go
    -rw-r--r--  1 XXXX  XXXX  1438 10 21 22:53 root.go
    % 
    ```
1. 下記のように編集します
    ```go
    /*
    Copyright © 2024 NAME HERE <EMAIL ADDRESS>
    */
    package cmd
    
    import (
    	"fmt"
    
    	"github.com/spf13/cobra"
    )
    
    var name string
    
    // helloCmd represents the hello command
    var helloCmd = &cobra.Command{
    	Use:   "hello",
    	Short: "Say hello to someone",
    	Long:  `This command allows you to say hello to someone by specifying their name.`,
    	Run: func(cmd *cobra.Command, args []string) {
    		fmt.Printf("Hello, %s!\n", name)
    	},
    }
    
    func init() {
    	rootCmd.AddCommand(helloCmd)
    	helloCmd.Flags().StringVarP(&name, "name", "n", "World", "Name of the person to greet")
    }
    ```

1. 実行します。想定通りの結果です
    ```bash
    % go run main.go hello --name Taro
    Hello, Taro!
    % 
    ```
### ビルド
バイナリファイルを生成し、実行します

1. `go build`コマンドを実行します
    ```bash
    % go build -o hello 
    ```
1. 確認します
    ```bash
    % ls -l            
    total 10680
    -rw-r--r--  1 XXXX  XXXX        0 10 21 22:53 LICENSE
    drwxr-x--x  4 XXXX  XXXX      128 10 21 23:00 cmd
    -rw-r--r--  1 XXXX  XXXX      181 10 21 23:13 go.mod
    -rw-r--r--  1 XXXX  XXXX      896 10 21 22:32 go.sum
    -rwxr-xr-x  1 XXXX  XXXX  5452224 10 21 23:21 hello
    -rw-r--r--  1 XXXX  XXXX      117 10 21 22:53 main.go
    % 
    ```
1. 実行できました
    ```bash
    % ./hello hello --name Jiro
    Hello, Jiro!
    % 
    ```

### フラグをトップレベルコマンドに適用する
`hello --name`の形式で実行できるようにします。
1. hello.goを削除します
    ```bash
    % rm cmd/hello.go
    ```
1. root.goを修正します
    ```go
    /*
    Copyright © 2024 NAME HERE <EMAIL ADDRESS>
    */
    package cmd
    
    import (
    	"fmt"
    	"os"
    
    	"github.com/spf13/cobra"
    )
    
    var name string
    
    // rootCmd represents the base command when called without any subcommands
    var rootCmd = &cobra.Command{
    	Use:   "hello",
    	Short: "Say hello to someone",
    	Long:  `This application allows you to say hello to a person by specifying their name.`,
    	Run: func(cmd *cobra.Command, args []string) {
    		fmt.Printf("Hello, %s!\n", name)
    	},
    }
    
    func Execute() {
    	err := rootCmd.Execute()
    	if err != nil {
    		fmt.Println(err)
    		os.Exit(1)
    	}
    }
    
    func init() {
    	rootCmd.Flags().StringVarP(&name, "name", "n", "World", "Name of the person to greet")
    }
    ```
1. できました
    ```bash
    % ls -l
    total 10680
    -rw-r--r--  1 XXXX  XXXX        0 10 21 22:53 LICENSE
    drwxr-x--x  3 XXXX  XXXX       96 10 21 23:36 cmd
    -rw-r--r--  1 XXXX  XXXX      181 10 21 23:13 go.mod
    -rw-r--r--  1 XXXX  XXXX      896 10 21 22:32 go.sum
    -rwxr-xr-x  1 XXXX  XXXX  5452128 10 21 23:37 hello
    -rw-r--r--  1 XXXX  XXXX      117 10 21 22:53 main.go
    % 
    ```

1. 実行します。想定通りの結果です
    ```bash
    % ./hello --name Jiro
    Hello, Jiro!
    % 
    ```

# 考察
今回、Cobraの基本的な特徴を整理し、簡単な動作確認を行いました。シンプルなCLIツールを作成するところまで進めましたが、今後はサブコマンドを追加したり、より複雑な機能を持つCLIツールの実装を試したいと思います。

# 参考
https://github.com/spf13/cobra