1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. BuildingCLIs
  5. Cobra - Golang learning step 3-2

Cobra - Golang learning step 3-2

  • 公開日
  • カテゴリ:BuildingCLIs
  • タグ:Golang,roadmap.sh,学習メモ
Cobra - Golang learning step 3-2

roadmap.sh > Go > Building CLIs > Cobra の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. 参考 URL
  3. Cobra
  4. インストール
  5. コマンド構造
  6. 1. 初期化
  7. 2. root.go
  8. 1. エントリーポイント作成
  9. コマンドの実装
  10. サブコマンドを持つコマンドの実装
    1. 1. コマンドの初期化
    2. 2. サブコマンドの定義
    3. 3. フラグの設定
    4. 4. コマンドの階層構造の構築
    5. 5. 時刻処理のヘルパー関数
    6. 6. サブコマンドの実装
    7. 使用例
  11. PersistentFlags
  12. アプリケーションのビルド

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

参考 URL

Cobra

Cobra は Go のコマンドラインインターフェース (CLI) アプリケーションを作成するためのライブラリです。以下のような特徴があります。

  1. ネストされたサブコマンドをサポート
  2. グローバルフラグとローカルフラグの管理
  3. インテリジェントなサジェスション機能
  4. 自動的なヘルプの生成
  5. シェル補完の自動生成(bash, zsh, fish, PowerShell)

インストール

go get -u github.com/spf13/cobra/cobra

コマンド構造

Cobra は以下のようなディレクトリ構造でコマンドを定義します。

myapp/
├── cmd/
│   ├── root.go
│   ├── hello.go
│   ├── thank.go
│   └── wake.go
├── go.mod
└── main.go

プロジェクトルート配下に cmd ディレクトリを作成し、そこに root.go とコマンド群を設置、main.go がエントリーポイントとなります。

  • cmd/: コマンドの定義を格納するディレクトリ
    • root.go: メインとなるコマンドの定義
    • hello.go: コマンドの定義
    • thank.go: コマンドの定義
    • wake.go: コマンドの定義
  • main.go: アプリケーションのエントリーポイント

1. 初期化

myapp ディレクトリに移動し、以下のコマンドを実行します。

cd path/to/myapp

go mod init myapp

myapp/ 配下に go.mod が作成されます。

2. root.go

次に、root.go を作成します。root.go は CLI アプリケーションの基盤となるファイルです。

  1. アプリケーションのエントリーポイントとなるルートコマンドの定義
  2. 共通の設定やフラグの管理
  3. 設定ファイルの読み込みと環境変数の管理
  4. エラーハンドリングの基本設定
  5. ロギングなどの共通機能の初期化
package cmd

import (
  "fmt"
  "os"

  "github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
  Use:   "myapp",
  Short: "挨拶と起こし機能を提供するCLIアプリケーション",
  Long: `このアプリケーションは、以下の機能を提供します:

- hello:  指定した言語で挨拶をします(日本語・英語・韓国語・スペイン語)
- thank:  指定した言語で感謝を伝えます(日本語・英語・韓国語・スペイン語)
- wake:   寝ている人を起こします(優しく・強めに)

各コマンドは --help オプションで詳細な使用方法を確認できます。`,
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}
  1. rootCmd
    • Use: コマンドラインで使用する名前
    • Short: myapp --help で表示される1行の説明
    • Long: myapp --help で表示される詳細な説明
    • Run: コマンド実行時に呼び出される関数
  2. Execute 関数
    • main.go から呼び出される関数
    • コマンドの実行とエラーハンドリングを行う
    • エラーが発生した場合は終了コード1でプログラムを終了
  3. init 関数
    • プログラム起動時に自動的に実行される初期化関数
    • フラグの設定などを行う
    • PersistentFlags: すべてのサブコマンドで使用可能なフラグを設定

1. エントリーポイント作成

次に、エントリーポイントである main.go を作成します。

package main

import "myapp/cmd"

func main() {
  cmd.Execute()
}

ここまでが最小構成です。以下のコマンドを実行してみましょう。ヘルプが表示されます。

cd myapp

go run main.go --help
このアプリケーションは、以下の機能を提供します:

- hello:  指定した言語で挨拶をします(日本語・英語・韓国語・スペイン語)
- thank:  指定した言語で感謝を伝えます(日本語・英語・韓国語・スペイン語)
- wake:   寝ている人を起こします(優しく・強めに)

各コマンドは --help オプションで詳細な使用方法を確認できます。

Usage:
  myapp [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  hello       挨拶します
  help        Help about any command
  thank       感謝します
  wake        寝ている人を起こします

Flags:
  -h, --help   help for myapp

コマンドの実装

この CLI アプリケーションの機能を実装します。

hello.go は挨拶機能を提供するコマンドを実装するファイルです。以下で各部分の詳細を解説します。

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

var helloName string // 挨拶する相手の名前を保持
var helloLang string // 挨拶の言語を保持

func init() {
  helloCmd := &cobra.Command{
    Use:   "hello",
    Short: "挨拶します",
    Run:   runHello,
  }

  helloCmd.Flags().StringVarP(&helloName, "name", "n", "you", "相手の名前")
  helloCmd.Flags().StringVarP(&helloLang, "lang", "l", "english", "挨拶の言語")

  rootCmd.AddCommand(helloCmd)
}

func runHello(cmd *cobra.Command, args []string) {
  var hello string
  switch helloLang {
  case "japanese":
    hello = "こんにちは"
  case "korean":
    hello = "안녕하세요"
  case "spanish":
    hello = "Hola"
  default:
    hello = "Hello"
  }
  fmt.Printf("%s %s!!\n", hello, helloName)
}

init 関数では、コマンドの定義とフラグの設定を行います。

  1. コマンドの定義
    • Use: コマンド名(myapp helloで使用)
    • Short: ヘルプでの短い説明
    • Run: 実際の処理を行う関数を指定
  2. フラグの設定
    • StringVarP(): 文字列型のフラグを定義
      • 第1引数: 値を格納する変数のポインタ
      • 第2引数: フラグの長い名前(--name)
      • 第3引数: フラグの短い名前(-n)
      • 第4引数: デフォルト値
      • 第5引数: ヘルプでの説明
  3. ルートコマンドへの追加
    • rootCmd.AddCommand(): 作成したコマンドをルートコマンドに追加

以下のコマンド、引数で実行できます。

# デフォルト(英語)での挨拶
$ go run main.go hello
Hello you!!

# 名前を指定して挨拶
$ go run main.go hello --name Alice
Hello Alice!!

# 短い形式で名前を指定
$ go run main.go hello -n Bob
Hello Bob!!

# 言語を指定して挨拶
$ go run main.go hello --lang japanese --name 太郎
こんにちは 太郎!!

# 短い形式で言語を指定
$ go run main.go hello -l korean -n 김철수
안녕하세요 김철수!!

同じように、thank.go も実装します。

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

var thankName string
var thankLang string

func init() {
  thankCmd := &cobra.Command{
    Use:   "thank",
    Short: "感謝します",
    Run:   runThank,
  }

  thankCmd.Flags().StringVarP(&thankName, "name", "n", "you", "相手の名前")
  thankCmd.Flags().StringVarP(&thankLang, "lang", "l", "english", "挨拶の言語")

  rootCmd.AddCommand(thankCmd)
}

func runThank(cmd *cobra.Command, args []string) {
  var thank string
  switch thankLang {
  case "japanese":
    thank = "ありがとう"
  case "korean":
    thank = "고마워요"
  case "spanish":
    thank = "Gracias"
  default:
    thank = "Thank you"
  }
  fmt.Printf("%s %s!!\n", thank, thankName)
}

サブコマンドを持つコマンドの実装

次は、サブコマンドを持つコマンドの実装例として wake.go を見ていきましょう。wake コマンドは「優しく起こす」と「強めに起こす」という2つのサブコマンドを持ちます。

package cmd

import (
  "fmt"
  "time"

  "github.com/spf13/cobra"
)

var wakeName string
var wakeTime string

func init() {
  wakeCmd := &cobra.Command{
    Use:     "wake",
    Aliases: []string{"w"},
    Short:   "寝ている人を起こします",
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("'gentle' または 'strong' サブコマンドを指定してください。")
      fmt.Println("使用例:")
      fmt.Println("  wake gentle --name 田中 --time 07:30")
      fmt.Println("  wake strong --name 田中")
    },
  }

  gentleCmd := &cobra.Command{
    Use:     "gentle",
    Aliases: []string{"g"},
    Short:   "優しく起こします",
    Run:     runWakeGentle,
  }

  strongCmd := &cobra.Command{
    Use:     "strong",
    Aliases: []string{"s"},
    Short:   "強めに起こします",
    Run:     runWakeStrong,
  }

  gentleCmd.Flags().StringVarP(&wakeName, "name", "n", "you", "起こす人の名前")
  gentleCmd.Flags().StringVarP(&wakeTime, "time", "t", "", "現在時刻(デフォルトは現在時刻)")

  strongCmd.Flags().StringVarP(&wakeName, "name", "n", "you", "起こす人の名前")
  strongCmd.Flags().StringVarP(&wakeTime, "time", "t", "", "現在時刻(デフォルトは現在時刻)")

  wakeCmd.AddCommand(gentleCmd)
  wakeCmd.AddCommand(strongCmd)
  rootCmd.AddCommand(wakeCmd)
}

func getCurrentTime() time.Time {
  currentTime := time.Now()
  if wakeTime != "" {
    parsedTime, err := time.Parse("15:04", wakeTime)
    if err == nil {
      currentTime = parsedTime
    }
  }
  return currentTime
}

func runWakeGentle(cmd *cobra.Command, args []string) {
  currentTime := getCurrentTime()
  hour := currentTime.Hour()

  var message string
  switch {
  case hour < 5:
    message = fmt.Sprintf("(小声で)%sさん、まだ深夜ですが...必要があって起こしました。申し訳ありません。", wakeName)
  case hour < 12:
    message = fmt.Sprintf("おはようございます、%sさん。素敵な朝をお迎えください。朝食の用意ができています。", wakeName)
  default:
    message = fmt.Sprintf("%sさん、お昼を過ぎていますよ。ゆっくり起きましょう。", wakeName)
  }

  fmt.Println(message)
}

func runWakeStrong(cmd *cobra.Command, args []string) {
  currentTime := getCurrentTime()
  hour := currentTime.Hour()

  var message string
  switch {
  case hour < 5:
    message = fmt.Sprintf("%sさん!!緊急事態です!!直ちに起きてください!!", wakeName)
  case hour < 12:
    message = fmt.Sprintf("%sさん!もう朝ですよ!!遅刻しますよ!!急いで起きてください!!", wakeName)
  default:
    message = fmt.Sprintf("%sさん!!こんな時間まで寝てるんですか!?早く起きてください!!", wakeName)
  }

  fmt.Println(message)
}

まず、フラグの値を格納するグローバル変数を定義します。これまでと同様です。

1. コマンドの初期化

func init() {
  wakeCmd := &cobra.Command{
    Use:     "wake",
    Aliases: []string{"w"},
    Short:   "寝ている人を起こします",
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("'gentle' または 'strong' サブコマンドを指定してください。")
      fmt.Println("使用例:")
      fmt.Println("  wake gentle --name 田中 --time 07:30")
      fmt.Println("  wake strong --name 田中")
    },
  }

wake コマンドの定義では、以下の新しい要素が登場します。

  1. Aliases: コマンドの別名を設定できます。この場合 w が別名として設定され、myapp w でも実行可能になります。
  2. Run: サブコマンドが指定されなかった場合に実行される関数です。ここでは使用方法を表示します。

2. サブコマンドの定義

gentle(優しく)と strong(強めに)の2つのサブコマンドを定義します。それぞれに別名(gs)が設定されています。

gentleCmd := &cobra.Command{
    Use:     "gentle",
    Aliases: []string{"g"},
    Short:   "優しく起こします",
    Run:     runWakeGentle,
}

strongCmd := &cobra.Command{
    Use:     "strong",
    Aliases: []string{"s"},
    Short:   "強めに起こします",
    Run:     runWakeStrong,
}

3. フラグの設定

gentleCmd.Flags().StringVarP(&wakeName, "name", "n", "you", "起こす人の名前")
gentleCmd.Flags().StringVarP(&wakeTime, "time", "t", "", "現在時刻(デフォルトは現在時刻)")

strongCmd.Flags().StringVarP(&wakeName, "name", "n", "you", "起こす人の名前")
strongCmd.Flags().StringVarP(&wakeTime, "time", "t", "", "現在時刻(デフォルトは現在時刻)")

各サブコマンドに同じフラグを設定しています。これにより、両方のサブコマンドで --name--time オプションが使用可能になります。

4. コマンドの階層構造の構築

wakeCmd.AddCommand(gentleCmd)
wakeCmd.AddCommand(strongCmd)
rootCmd.AddCommand(wakeCmd)
  1. wakeCmd.AddCommand() で wake コマンドに gentle と strong のサブコマンドを追加
  2. rootCmd.AddCommand() で wake コマンド自体をルートコマンドに追加

これにより、以下のような階層構造が作られます。

myapp
└── wake
    ├── gentle
    └── strong

5. 時刻処理のヘルパー関数

func getCurrentTime() time.Time {
  currentTime := time.Now()
  if wakeTime != "" {
    parsedTime, err := time.Parse("15:04", wakeTime)
    if err == nil {
      currentTime = parsedTime
    }
  }
  return currentTime
}

--time フラグで時刻が指定されていれば、その時刻を、指定がなければ現在時刻を返す関数です。

6. サブコマンドの実装

gentle と strong の各サブコマンドは、時間帯に応じて異なるメッセージを表示します。

func runWakeGentle(cmd *cobra.Command, args []string) {
  currentTime := getCurrentTime()
  hour := currentTime.Hour()

  var message string
  switch {
  case hour < 5:
    message = fmt.Sprintf("(小声で)%sさん、まだ深夜ですが...必要があって起こしました。申し訳ありません。", wakeName)
  case hour < 12:
    message = fmt.Sprintf("おはようございます、%sさん。素敵な朝をお迎えください。朝食の用意ができています。", wakeName)
  default:
    message = fmt.Sprintf("%sさん、お昼を過ぎていますよ。ゆっくり起きましょう。", wakeName)
  }

  fmt.Println(message)
}

runWakeStrong も同様の構造ですが、より強めの表現でメッセージを表示します。

使用例

# 優しく起こす(朝7:30の場合)
$ go run main.go wake gentle --name 田中 --time 07:30
おはようございます、田中さん。素敵な朝をお迎えください。朝食の用意ができています。

# 強めに起こす(深夜の場合)
$ go run main.go wake strong --name 田中 --time 03:00
田中さん!!緊急事態です!!直ちに起きてください!!

# エイリアスを使用
$ go run main.go w g -n 田中
おはようございます、田中さん。素敵な朝をお迎えください。朝食の用意ができています。

PersistentFlags

PersistentFlags は、親コマンドで定義されたフラグを全てのサブコマンドで共有・継承できるようにする機能です。通常の Flags は定義されたコマンドでのみ使用できるのに対し、PersistentFlags は全ての子コマンドでも使用できます。

PersistentFlagsは通常、root.goinit() 関数内で定義します。

func init() {
    // 設定ファイルのパスを指定するフラグ
    rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "設定ファイルのパス")
}

PersistentFlags と通常の Flags の違い

項目PersistentFlagsFlags
定義の適用範囲コマンド自身およびその配下のサブコマンドコマンド自身のみ
用途アプリケーション全体やサブコマンド全体で共有する設定特定のコマンドに固有の設定
定義の目的グローバル設定、環境設定個別機能の制御
# 全てのコマンドでconfigフラグが使える
go run main.go --config settings.yaml hello
go run main.go --config settings.yaml thank
go run main.go --config settings.yaml wake

アプリケーションのビルド

今まで go run main.go で実行していましたが、ビルドを行えば、単独(コンパイル済み)の実行ファイルとして利用できます。

# プロジェクトのルートディレクトリで実行
go build

これにより、以下でコマンドを実行できます。

./myapp hello
Hello you!!

まとめ

  • Cobra は Go で CLI アプリケーションを作成するための強力なライブラリ
  • ネストされたサブコマンドやフラグ管理を簡単に実現できる
  • 自動ヘルプ生成やシェル補完機能を標準で提供
  • cmd/ ディレクトリでコマンドを構造化する設計が推奨される
  • root.go はルートコマンドや共通設定の基盤となるファイル
  • main.go から rootCmd.Execute() を呼び出してアプリケーションを実行
  • Flags はコマンドごとのオプションを、PersistentFlags はサブコマンド全体のオプションを定義
  • 実行ファイルにビルドすることで、CLI アプリケーションを配布可能


[Next] Step 4-1: GORM

[Prev] Step 3-1: Urfave CLI

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級