cobra-viper について
cobra-viperは、CobraとViperを使ったGo製コマンドラインアプリの構築・保守向けスキルです。コマンド構成、設定管理、エラー処理、テストを扱い、CLIのルーティングとビジネスロジックの分離を重視します。
主な機能
- 01ファクトリー関数によるコマンドツリー構築
- 02RunEによるエラーの返却
- 03Viperによる設定ソースの統合
- 04型付き構造体への設定デコード
- 05コンテキスト伝達とテスト可能な出力
活用シーン
- 01Go CLIのコマンド構成の設計
- 02Cobraコマンドへの設定ファイルや環境変数の接続
- 03コマンド出力とエラー処理のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spf13/ go-skills --skill cobra-viperインストール後、Claude Code で/cobra-viperを入力して読み込まれたか確認します。
同梱ファイル.claude-plugin/plugin.json
詳細
Go CLI アーキテクチャ:Cobra と Viper
Cobra と Viper を使用して、堅牢で設定駆動型のコマンドラインインターフェイスを構築するための、慣用的なパターンとベストプラクティスです。
使用するタイミング
- Go で新しい CLI アプリケーションを作成するとき
- 既存の Cobra アプリケーションにコマンド、サブコマンド、またはフラグを追加するとき
- 設定ファイル、環境変数、またはフラグの管理に Viper を統合するとき
- Cobra や Viper を使用する CLI コードをレビューまたはリファクタリングするとき
- CLI ツールのコマンド構造や設定スキーマを設計するとき
- CLI コマンドをテストするとき
基本理念
コマンド優先のアーキテクチャ
アプリケーションのバイナリをコマンドのルーターとして扱います。CLI フレームワーク(Cobra)が担当するのは、フラグ、引数、ルーティングだけにします。コアのビジネスロジックは CLI 層を完全に意識しないようにし、テストや再利用を容易にします。
統一された設定
設定は環境に対応し、統一されている必要があります。Viper を単一の情報源として、デフォルト値、設定ファイル、環境変数、コマンドラインフラグを一貫した状態にまとめてから、アプリケーションロジックに渡します。
コマンドは宣言せずに構築する
パッケージレベルの var 宣言ではなく、ファクトリー関数(NewRootCmd())を使ってコマンドツリーを構築します。グローバル変数があると、コマンドがテストできなくなり(テスト実行間で状態が漏れます)、ライブラリとしても利用できません。適切に構築された CLI で唯一のグローバルな処理は、main.go からファクトリーを呼び出すことです。
CLI パッケージの構成
アンチパターン: すべてのコマンドとコアロジックを internal/ ディレクトリツリーの奥深くに隠したり、すべてを main.go に詰め込んだりすることです。
見つけやすく、フラットな構成
コマンドのルーティングとビジネスロジックは、標準的で論理的な名前のパッケージに配置します。cmd/ パッケージが CLI の表層を担当し、その他のトップレベルパッケージがドメインロジックを担当します。
mycli/
├── main.go # Minimal entry point: strictly calls cmd.Execute()
├── cmd/ # The Cobra routing layer
│ ├── root.go # Root command factory, global flags, Viper setup
│ ├── serve.go # The 'serve' subcommand
│ └── build.go # The 'build' subcommand
├── engine/ # Core business logic (name based on your domain)
│ ├── server.go
│ └── compiler.go
├── go.mod
└── go.sum
main.go は意図的に最小限にします。
package main
import (
"os"
"github.com/spf13/myapp/cmd"
)
func main() {
if err := cmd.Execute(); err != nil {
os.Exit(1)
}
}
ファクトリーパターン(cmd/root.go)
各コマンドファイルはコンストラクターをエクスポートします。ルートファクトリーは独自の Viper インスタンスを作成し、すべてを接続します。パッケージレベルの状態は使いません。
func Execute() error {
return NewRootCmd().Execute()
}
func NewRootCmd() *cobra.Command {
v := viper.New()
rootCmd := &cobra.Command{
Use: "mycli",
Short: "A brief description",
SilenceUsage: true,
SilenceErrors: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig(v, cmd)
},
}
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().String("log-level", "info", "log level")
rootCmd.AddCommand(NewServeCmd(v))
rootCmd.AddCommand(NewBuildCmd(v))
return rootCmd
}
グローバル変数を使ってもよい場合: プロセス内でテストされることも、組み込まれることもない、小規模で用途が単一のツールです。テストを書いた時点で、ファクトリーに切り替えてください。
コマンドと実行処理を分離する
cmd/ パッケージ内のファイルが行うことは、次の 3 つだけです。
- Cobra コマンド、そのエイリアス、ヘルプテキストを定義します。
- そのコマンドに固有の Viper フラグと設定をバインドします。
- 解析済みの設定とコマンドのコンテキストを渡して、コアロジックパッケージ(例:
engine)内の関数を呼び出します。
コアロジック(engine パッケージ)は、github.com/spf13/cobra または github.com/spf13/viper を一切インポートしないでください。
Cobra のベストプラクティス
以下のフラグ解析、コンテキストの受け渡し、コマンドツリーに関する内容は、Go のリリースによって変わりません。Go 1.26 や Go 1.27 で、Cobra のフラグ解析、cmd.Context() のコードへの受け渡し、コマンドの構築や追加の方法が変わることはありません。このスキルにおけるバージョンごとのガイダンスは、エラーの検査と設定のデコードにのみ関係し、該当箇所で説明します。
1. ネイティブなエラー処理には RunE を使う
Run は避けてください。コマンドが失敗した場合は、RunE を使って実行チェーンの上位にエラーを返します。これにより、defer 文を迂回する log.Fatal の呼び出しが各所に散在するのではなく、ルートコマンドでエラーを適切かつ一貫して処理できます。
// Idiomatic: Returning errors to be handled by the executor
func NewServeCmd(v *viper.Viper) *cobra.Command {
cmd := &cobra.Command{
Use: "serve",
Short: "Starts the primary application server",
RunE: func(cmd *cobra.Command, args []string) error {
var cfg engine.Config
if err := v.Unmarshal(&cfg); err != nil {
return fmt.Errorf("decoding config: %w", err)
}
return engine.Serve(cmd.Context(), cfg)
},
}
cmd.Flags().String("addr", ":8080", "listen address")
return cmd
}
2. アプリケーションエラーでは使用方法の表示を抑制する
デフォルトでは、エラーが返されるたびに Cobra はヘルプテキスト全体を表示します。エラーが構文エラーではなく、ネットワークタイムアウトのような実行時エラーの場合、これは混乱を招きます。ルートで SilenceUsage: true と SilenceErrors: true を設定し(上記を参照)、main.go でエラーを一度だけ出力してください。
3. コンテキストを考慮したコマンド
現代の Go では、キャンセルやタイムアウトに context.Context が広く使われています。Cobra コマンドのコンテキストを、そのままビジネスロジックに渡してください。
RunE: func(cmd *cobra.Command, args []string) error {
return engine.Process(cmd.Context(), args)
}
そのコンテキストが Ctrl+C に対応するようにするには、main.go でシグナル処理を設定します。
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
err := cmd.NewRootCmd().ExecuteContext(ctx)
4. Args で位置引数を検証する
RunE の中で引数の数を検証してはいけません。Cobra が RunE の実行前に検証し、適切な使用法エラーを表示します。
cmd := &cobra.Command{
Use: "build [target]",
Args: cobra.ExactArgs(1),
// Also: cobra.NoArgs, cobra.MinimumNArgs(n), cobra.MaximumNArgs(n),
// cobra.RangeArgs(min, max), cobra.OnlyValidArgs (with ValidArgs set)
// Combine: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs)
}
5. 共通のセットアップには PersistentPreRunE を使う
ルートコマンドの PersistentPreRunE を使って、フラグの解析後、サブコマンドの実行前にセットアップ(設定の読み込み、ロギングなど)を行います。Cobra はすべてのサブコマンドに対して自動的にこれを実行します。
注意点: サブコマンドも PersistentPreRunE を定義している場合、親のものは置き換えられます。Cobra はデフォルトではこれらを連鎖して実行しません。親の処理を明示的に呼び出すか、cobra.EnableTraverseRunHooks = true を設定してグローバルに連鎖を有効にしてください(Cobra 1.8 以降)。
6. フラグの設計
// Persistent flags — inherited by all subcommands
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "enable verbose output")
// Local flags — only for this command
serveCmd.Flags().String("addr", ":8080", "listen address")
// Required flags — Cobra validates before RunE is called
serveCmd.Flags().String("name", "", "required name")
serveCmd.MarkFlagRequired("name")
// Flag relationships (Cobra 1.8+ for OneRequired)
serveCmd.MarkFlagsMutuallyExclusive("json", "yaml")
serveCmd.MarkFlagsRequiredTogether("user", "password")
serveCmd.MarkFlagsOneRequired("file", "url")
- 横断的な設定(設定ファイル、詳細度、出力形式)には
PersistentFlagsを使います。 - コマンド固有のオプションには
Flagsを使います。 - よく使うオプションには、必ず短いフラグ(
-v、-o)を用意します。
7. ヘルプ出力でコマンドをグループ化する(Cobra 1.6 以降)
サブコマンドが多い CLI では、--help が単なる羅列ではなく、流れのある説明になるようにグループ化します。
rootCmd.AddGroup(&cobra.Group{ID: "core", Title: "Core Commands:"})
rootCmd.AddGroup(&cobra.Group{ID: "admin", Title: "Admin Commands:"})
serveCmd.GroupID = "core"
migrateCmd.GroupID = "admin"
8. シェル補完
Cobra では、シェル補完を簡単に生成できます。フラグと位置引数の両方に動的補完を追加します。
// Flag value completion
serveCmd.RegisterFlagCompletionFunc("output", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json", "yaml", "table"}, cobra.ShellCompDirectiveNoFileComp
})
// Positional argument completion
buildCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return listTargets(), cobra.ShellCompDirectiveNoFileComp
}
myapp completion bash > /etc/bash_completion.d/myapp
myapp completion zsh > "${fpath[1]}/_myapp"
myapp completion fish > ~/.config/fish/completions/myapp.fish
myapp completion powershell | Out-File -Encoding utf8 "$PROFILE\myapp.ps1"
9. fmt.Printf ではなくコマンド経由で出力する
コマンド内では、cmd.OutOrStdout()/cmd.ErrOrStderr()(または cmd.Println)を使って出力します。fmt.Printf を直接使うと SetOut/SetErr を迂回するため、テストで出力を捕捉できなくなります。
Viper の設定パターン
1. グローバルなシングルトンより Viper のインスタンスを優先する
viper.New() を使うと、インスタンスを注入し、テストで分離し、並行して実行できます。パッケージレベルの viper.Get* シングルトンは、小規模なツールで便利に使うためのものです。これは pflag のデフォルトの FlagSet と同様のトレードオフです。テストや複数のコマンドを含む場合は、(上記のファクトリーの例と同様に)*viper.Viper を明示的に渡してください。
2. 型付きの構造体にアンマーシャルする
アンチパターン: ビジネスロジックの奥深くで v.GetString("database.host") を呼び出すことです。これではドメインが Viper に強く結び付けられ、コードベース全体にマジック文字列が散在します。
代わりに、厳密に型付けされた設定構造体を定義し、ルーティング層(cmd/)で Viper の状態をその構造体にアンマーシャルしてから、構造体を下位層に渡します。
type Config struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return fmt.Errorf("unable to decode config: %w", err)
}
マーシャルも行う設定構造体(config dump コマンドや --json 出力モードなど)では、同じフィールドに JSON タグが必要です。また、ゼロ値を扱うオプションは互換性がありません。time.Time やネストされた設定構造体など、構造体型のフィールドには omitempty ではなく omitzero(Go 1.24)を使ってください。omitempty は構造体を空とは見なさないため、未設定の time.Time は "0001-01-01T00:00:00Z" としてマーシャルされますが、omitzero なら省略されます。
type Config struct {
Host string `mapstructure:"host" json:"host"`
Started time.Time `mapstructure:"started" json:"started,omitzero"` // omitempty would emit the zero time
Listen ListenConfig `mapstructure:"listen" json:"listen,omitzero"` // omitempty never omits a struct
}
3. バインドの優先順位
Viper は、次の順序(高い → 低い)で設定ソースをマージします。
- コード内で明示的に呼び出した
Set() - フラグ(
BindPFlag/BindPFlagsでバインド) - 環境変数(
MYCLI_PORT) - 設定ファイル(
~/.mycli.yaml、./.mycli.yaml) - デフォルト値(
v.SetDefault)
各ソースは明示的にバインドする必要があります。フラグを一つずつではなく、フラグセット全体をまとめてバインドしてください。
// In PersistentPreRunE — after flags are parsed, before RunE
if err := v.BindPFlags(cmd.Flags()); err != nil {
return err
}
init() ではなく PersistentPreRunE でバインドすると、2 つのサブコマンドが異なるフラグを同じ Viper キーにバインドし、最後に実行された init() が黙って優先されるという、よくある衝突も避けられます。
4. 環境変数のマッピング
v.SetEnvPrefix("MYAPP") と v.AutomaticEnv() を使用した場合:
| Viper のキー | 環境変数 |
|---|---|
log-level | MYAPP_LOG_LEVEL |
serve.addr | MYAPP_SERVE_ADDR |
db.password | MYAPP_DB_PASSWORD |
ドットやハイフンを含むネストされたキーを環境変数名に正しくマッピングするには、置換関数が必要です:
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
重大な落とし穴 — AutomaticEnv と Unmarshal の組み合わせ: Unmarshal は、Viper がすでに把握しているキー(デフォルト値、設定ファイル、または明示的なバインドから)だけを走査します。環境変数だけで設定された値は、そのキーが登録されていない限り Unmarshal から見えません。対処法は、設定構造体内のすべてのキーに対して v.SetDefault を呼び出すか、各キーを明示的に v.BindEnv でバインドすることです。これは、Viper を使うアプリケーションで「環境変数が無視される」という最もよくあるバグです。
5. 設定ファイルのセットアップ
func initConfig(v *viper.Viper, cmd *cobra.Command) error {
if cfgFile, _ := cmd.Flags().GetString("config"); cfgFile != "" {
v.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
return err
}
v.AddConfigPath(home)
v.AddConfigPath(".")
v.SetConfigType("yaml")
v.SetConfigName(".myapp")
}
v.SetEnvPrefix("MYAPP")
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_"))
v.AutomaticEnv()
if err := v.ReadInConfig(); err != nil {
// Go 1.26+: generic, type-safe, and faster than errors.As.
if _, ok := errors.AsType[viper.ConfigFileNotFoundError](err); !ok {
return fmt.Errorf("reading config: %w", err)
}
// Not found is fine — defaults, env, and flags still apply
}
return v.BindPFlags(cmd.Flags())
}
errors.AsType[T](err)(Go 1.26 以降)は、一致した値と ok ブール値を返すため、事前にターゲット変数を宣言する必要がなく、ポインター以外を渡して誤用することもありません。Go 1.26 より前では、従来の errors.As を使う形式を使用してください。こちらも引き続き正しく動作し、バージョンを限定した場合の代替手段となります:
var notFound viper.ConfigFileNotFoundError
if !errors.As(err, ¬Found) {
return fmt.Errorf("reading config: %w", err)
}
デフォルトの形式には、読みやすく、ネストにも対応している YAML を使用してください:
# ~/.myapp.yaml
log-level: debug
serve:
addr: ":9090"
port: 9090
db:
host: localhost
port: 5432
name: myapp
6. JSON 設定ファイルのデコードは厳密です(Go 1.27)
Go 1.27 以降、encoding/json は encoding/json/v2 の実装を基盤としており、Viper は .json 設定ファイルを encoding/json でデコードします。新しいデフォルトはより厳密になり、以前は見過ごされていた次のどちらもエラーになりました:
- JSON 文字列内の無効な UTF-8。
- 同じオブジェクト内での名前の重複。
CLI への影響は明確です:以前は解析できていた設定ファイルが、今は失敗することがあります。v.ReadInConfig() はデコードエラーを返し、設定の読み込みは PersistentPreRunE 内で実行されるため、RunE の前にコマンドが停止します。同じファイルを変更していなくても、新しいツールチェーンだけで失敗します:
{
"db": { "host": "localhost", "host": "db.internal" }
}
このオブジェクトは以前から曖昧でした。以前のデコーダーでは最後の host が黙って採用されていました。このファイルがユーザーの .myapp.json であれば、アップグレードによって、正常に動作していたコマンドが起動時に設定エラーで終了するようになります。
回避策は 2 つあります。どちらもコードの一部ではなく、v1 のセマンティクスに限定されます。encoding/json.Options は v2 実装上で v1 の動作を設定し、GOEXPERIMENT=nojsonv2 はプロセス全体で以前の実装に戻します。この環境変数は移行を支援するためのもので、将来のリリースで削除される予定です。そのため、これに依存するより、設定ファイルを修正するか、Options を意図的に使用してください。
v1 の動作はそれ以外では維持されていますが、エラーメッセージのテキストは異なる場合があります。JSON デコードエラーのテキストで判定せず、型で判定してください。
v.Unmarshal 自体はこの切り替えの影響を受けません。encoding/json ではなく mapstructure タグを使ってフィールドをマッピングするため、変わるのはデコードのステップだけです。
バージョンの処理
Cobra にはバージョン表示の仕組みが組み込まれています。独自にサブコマンドを実装するより、こちらを優先してください:
// Set at build time via -ldflags
var (
version = "dev"
commit = "none"
date = "unknown"
)
rootCmd := &cobra.Command{
Use: "myapp",
Version: fmt.Sprintf("%s (commit: %s, built: %s)", version, commit, date),
}
// `myapp --version` now works for free.
// Customize the output with rootCmd.SetVersionTemplate if needed.
go build -ldflags="-X 'github.com/spf13/myapp/cmd.version=1.2.3' \
-X 'github.com/spf13/myapp/cmd.commit=$(git rev-parse --short HEAD)' \
-X 'github.com/spf13/myapp/cmd.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)'"
構造化出力(version --json)が必要な場合に限り、version サブコマンドを追加してください。出力には fmt.Printf ではなく、cmd.OutOrStdout() を使用します。
CLI コマンドのテスト
アンチパターン: バイナリをコンパイルして os/exec を使い、CLI コマンドをテストする方法です。非常に遅く、壊れやすいうえに、テストカバレッジの測定も困難になります。
2 つ目のアンチパターン: パッケージレベルのサブコマンドを直接実行する方法です。cmd.Execute() は常にコマンドツリーのルートから実行されます。また、グローバルコマンドを変更すると、テスト間でフラグの状態が漏れます。コマンドをファクトリーで構築するのは、まさにこのためです。
// Idiomatic: In-memory CLI testing via the factory
func TestServeCommand(t *testing.T) {
tests := []struct {
name string
args []string
want string
wantErr bool
}{
{"default port", []string{"serve"}, "listening on :8080", false},
{"custom port", []string{"serve", "--addr", ":9090"}, "listening on :9090", false},
{"bad flag", []string{"serve", "--bogus"}, "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := new(bytes.Buffer)
root := cmd.NewRootCmd() // fresh tree + fresh Viper every test
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs(tt.args) // full CLI invocation, exactly as a user types it
err := root.ExecuteContext(t.Context())
if (err != nil) != tt.wantErr {
t.Fatalf("Execute() error = %v, wantErr %v", err, tt.wantErr)
}
if !strings.Contains(buf.String(), tt.want) {
t.Errorf("output = %q, want substring %q", buf.String(), tt.want)
}
})
}
}
- 必ずルートから実行し、サブコマンド名を含む
SetArgsを指定してください。フラグの解析やPersistentPreRunEを含め、ユーザーが実際に通るコードパスを実行できます。 - テストケースごとに新しい
NewRootCmd()を作成すれば、状態の共有がなくなり、viper.Reset()も不要になって、テストを並列実行できます。 - グローバルな Viper シングルトンを使わざるを得ない場合は、テスト間で
viper.Reset()を呼び出してください。状態がテストケース間に漏れます。 - 出力のキャプチャには
SetOut/SetErrを使ってください。これが機能するのは、コマンドがcmd.OutOrStdout()経由で出力する場合に限られます。 - 設定の環境変数は
t.Setenvで設定してください。自動的に元に戻され、誤ったt.Parallelの使用も防げます。
よくある間違い
- テストでサブコマンド変数を直接実行する:
serverCmd.Execute()はそのコマンドだけではなく、ルートからツリー全体を実行します。ファクトリーで新しいツリーを構築し、SetArgs経由で実行してください。 Unmarshal後に環境変数のみの値が欠落する:AutomaticEnvはキーを登録しません。設定構造体にあるすべてのキーに対してSetDefaultまたはBindEnvを設定してください(上記の注意点を参照)。- 一度解析できた JSON 設定は今後も解析できると思い込む: Go 1.27 以降、デコーダーは無効な UTF-8 と重複するオブジェクト名を拒否するため、ツールチェーンをアップグレードしただけで、設定ファイルが
ReadInConfigで失敗し始めることがあります(上記の §6 を参照)。 - 設定の読み込み前に Viper にアクセスする: セットアップ(
PersistentPreRunEまたはcobra.OnInitialize)が実行されるまで、値は空です。init()関数やvarブロック内で Viper を読み取らないでください。 BindPFlagsを忘れる: フラグは自動的には Viper から参照できません。コマンドのフラグセットを明示的にバインドしてください。SetEnvKeyReplacerを設定し忘れる: ドットを含むネストされたキー(例:serve.addr)は、置換処理を設定しないとMYAPP_SERVE_ADDRと一致しません。- ビジネスロジックで Cobra/Viper をインポートする:
engineパッケージでは、Cobra や Viper を絶対にインポートしないでください。型付きの設定構造体を渡します。 - コマンド内で
fmt.Printfを使う:SetOutを迂回するため、テストでの出力キャプチャが機能しなくなります。cmd.OutOrStdout()を使ってください。 - サブコマンドをネストしすぎる: 2 階層(
app command subcommand)が通常は適切な深さです。3 階層以上にすると、ユーザーが混乱します。