mcpField
G
golang-cli-cobra-viper

golang-cli-cobra-viper

github.com/bobmatnyc開発78430 回インストール15 週間前に更新
リポジトリ

golang-cli-cobra-viper について

Go でコマンドラインアプリを作るための開発スキルです。Cobra によるコマンド構成と Viper による設定管理を、補完やテストと合わせて扱います。

主な機能

  1. 01Cobra のサブコマンドとフラグ
  2. 02Viper による複数ソースの設定
  3. 03Cobra と Viper の連携
  4. 04シェル補完の生成
  5. 05CLI のテストとエラー処理

活用シーン

  1. 01複数コマンドを持つ Go CLI の構築
  2. 02フラグ、環境変数、設定ファイルの統合
  3. 03開発ツールへのシェル補完の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill golang-cli-cobra-viper

インストール後、Claude Code で/golang-cli-cobra-viperを入力して読み込まれたか確認します。

同梱ファイルmetadata.json

詳細

Cobra と Viper による Go CLI 開発

概要

Cobra と Viper は、本番品質の Go CLI を構築するための業界標準ライブラリです。Cobra はコマンド構造と引数解析を提供し、Viper は明確な優先順位ルールに基づいて複数のソースから設定を管理します。

主な機能:

  • 🎯 Cobra コマンド:サブコマンドに対応した POSIX 準拠の CLI(app verb noun --flag)
  • ⚙️ Viper 設定:フラグ、環境変数、設定ファイルからの統一された設定
  • 🔄 連携:Cobra と Viper をシームレスに接続するパターン
  • 🐚 シェル補完:bash、zsh、fish、PowerShell 向けに自動生成される補完機能
  • ✅ 本番対応:kubectl、docker、gh、hugo で実績のあるライブラリです

利用例:Kubernetes(kubectl)、Docker CLI、GitHub CLI(gh)、Hugo、Helm、および 100 以上の主要プロジェクト

このスキルを使用するタイミング

次のような場合に、このスキルを有効にします。

  • サブコマンドを備えた複数コマンドの CLI ツールを構築する場合
  • 開発者向けツール、プロジェクトジェネレーター、またはスキャフォールディングユーティリティを作成する場合
  • サービスやインフラストラクチャ向けの管理用 CLI を実装する場合
  • 柔軟な設定(フラグ > 環境変数 > 設定ファイル > デフォルト値)が必要な場合
  • 頻繁に使う CLI にシェル補完を追加する場合
  • DevOps 自動化ツールやデプロイスクリプトを構築する場合

Cobra フレームワーク

コマンド構造のパターン

Cobra は、git や kubectl で広く使われるようになった APPNAME VERB NOUN --FLAG パターンに従います。

// cmd/root.go
package cmd

import (
    "fmt"
    "os"

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

var cfgFile string

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "A powerful CLI tool for developers",
    Long: `MyApp is a CLI tool that demonstrates best practices
for building production-quality command-line applications.

Complete documentation is available at https://myapp.example.com`,
}

func Execute() {
    if err := rootCmd.Execute(); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}

func init() {
    cobra.OnInitialize(initConfig)

    // Persistent flags (available to all subcommands)
    rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.myapp.yaml)")
    rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")

    // Bind persistent flags to viper
    viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
    viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
}

func initConfig() {
    if cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        home, err := os.UserHomeDir()
        if err != nil {
            fmt.Fprintln(os.Stderr, err)
            os.Exit(1)
        }

        viper.AddConfigPath(home)
        viper.AddConfigPath(".")
        viper.SetConfigType("yaml")
        viper.SetConfigName(".myapp")
    }

    viper.SetEnvPrefix("MYAPP")
    viper.AutomaticEnv()

    if err := viper.ReadInConfig(); err == nil {
        if viper.GetBool("verbose") {
            fmt.Println("Using config file:", viper.ConfigFileUsed())
        }
    }
}
引数を持つサブコマンド
// cmd/deploy.go
package cmd

import (
    "fmt"

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

var deployCmd = &cobra.Command{
    Use:   "deploy [environment]",
    Short: "Deploy application to specified environment",
    Long: `Deploy the application to the specified environment.
Supports: dev, staging, production`,
    Args: cobra.ExactArgs(1),
    ValidArgs: []string{"dev", "staging", "production"},
    PreRunE: func(cmd *cobra.Command, args []string) error {
        // Validation logic runs before RunE
        env := args[0]
        if env == "production" && !viper.GetBool("force") {
            return fmt.Errorf("production deploys require --force flag")
        }
        return nil
    },
    RunE: func(cmd *cobra.Command, args []string) error {
        env := args[0]
        region := viper.GetString("region")
        force := viper.GetBool("force")

        fmt.Printf("Deploying to %s in region %s (force=%v)\n", env, region, force)

        // Actual deployment logic
        return deploy(env, region, force)
    },
    PostRunE: func(cmd *cobra.Command, args []string) error {
        // Cleanup or notifications
        fmt.Println("Deployment complete")
        return nil
    },
}

func init() {
    rootCmd.AddCommand(deployCmd)

    // Local flags (only for this command)
    deployCmd.Flags().StringP("region", "r", "us-east-1", "AWS region")
    deployCmd.Flags().BoolP("force", "f", false, "Force deployment without confirmation")

    // Bind flags to viper
    viper.BindPFlag("region", deployCmd.Flags().Lookup("region"))
    viper.BindPFlag("force", deployCmd.Flags().Lookup("force"))
}

func deploy(env, region string, force bool) error {
    // Implementation
    return nil
}
永続フラグとローカルフラグ
// Persistent flags: Available to command and all subcommands
rootCmd.PersistentFlags().String("config", "", "config file path")
rootCmd.PersistentFlags().Bool("verbose", false, "verbose output")

// Local flags: Only available to this specific command
deployCmd.Flags().String("region", "us-east-1", "deployment region")
deployCmd.Flags().Bool("force", false, "force deployment")

// Required flags
deployCmd.MarkFlagRequired("region")

// Flag dependencies
deployCmd.MarkFlagsRequiredTogether("username", "password")
deployCmd.MarkFlagsMutuallyExclusive("json", "yaml")
PreRun/PostRun フック

Cobra は、セットアップとクリーンアップ用の実行フックを提供します。

var serverCmd = &cobra.Command{
    Use:   "server",
    Short: "Start API server",

    // Execution order (all optional):
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        // Runs before PreRunE, inherited by subcommands
        return setupLogging()
    },
    PreRunE: func(cmd *cobra.Command, args []string) error {
        // Validation and setup before RunE
        return validateConfig()
    },
    RunE: func(cmd *cobra.Command, args []string) error {
        // Main command logic
        return startServer()
    },
    PostRunE: func(cmd *cobra.Command, args []string) error {
        // Cleanup after RunE
        return cleanup()
    },
    PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
        // Runs after PostRunE, inherited by subcommands
        return flushLogs()
    },
}

重要:Run、PreRun、PostRun の代わりに、エラーを返すバージョンである RunE、PreRunE、PostRunE を使用してください。

Viper による設定管理

設定の優先順位

Viper では、次の厳密な優先順位に従います(高い順)。

  1. 明示的な設定(viper.Set("key", value))
  2. コマンドラインフラグ(viper.BindPFlag でバインド)
  3. 環境変数(MYAPP_KEY=value)
  4. 設定ファイル(~/.myapp.yaml、./config.yaml)
  5. キー/値ストア(etcd、Consul — オプション)
  6. デフォルト値(viper.SetDefault("key", value))
func initConfig() {
    // 1. Set defaults (lowest priority)
    viper.SetDefault("port", 8080)
    viper.SetDefault("database.host", "localhost")
    viper.SetDefault("database.port", 5432)

    // 2. Config file locations (checked in order)
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath("/etc/myapp/")
    viper.AddConfigPath("$HOME/.myapp")
    viper.AddConfigPath(".")

    // 3. Environment variables (prefix + automatic mapping)
    viper.SetEnvPrefix("MYAPP")
    viper.AutomaticEnv() // MYAPP_PORT, MYAPP_DATABASE_HOST, etc.

    // 4. Read config file (optional)
    if err := viper.ReadInConfig(); err != nil {
        if _, ok := err.(viper.ConfigFileNotFoundError); ok {
            // Config file not found - use defaults and env vars
        } else {
            // Config file found but error reading it
            return err
        }
    }

    // 5. Flags will be bound in init() functions (highest priority)
}
環境変数のマッピング

Viper は、接頭辞とドット表記を使用して環境変数を自動的にマッピングします。

viper.SetEnvPrefix("MYAPP") // Prefix for env vars
viper.AutomaticEnv()        // Enable automatic mapping

// Config key → Environment variable
// "port"              → MYAPP_PORT
// "database.host"     → MYAPP_DATABASE_HOST
// "database.port"     → MYAPP_DATABASE_PORT
// "aws.s3.region"     → MYAPP_AWS_S3_REGION

標準的でない環境変数名を使用する場合は、手動でマッピングします。

viper.BindEnv("database.host", "DB_HOST")          // Custom env var name
viper.BindEnv("database.password", "DB_PASSWORD")  // Different naming convention
設定ファイルの形式

Viper は、YAML、JSON、TOML、HCL、INI、envfile、Java properties など、複数の形式に対応しています。

config.yaml:

port: 8080
log_level: info

database:
  host: localhost
  port: 5432
  user: postgres
  ssl_mode: require

aws:
  region: us-east-1
  s3:
    bucket: my-app-bucket

設定値へのアクセス:

port := viper.GetInt("port")                    // 8080
dbHost := viper.GetString("database.host")      // "localhost"
s3Bucket := viper.GetString("aws.s3.bucket")    // "my-app-bucket"

// Type-safe access
if viper.IsSet("database.ssl_mode") {
    sslMode := viper.GetString("database.ssl_mode")
}

// Unmarshal into struct
type Config struct {
    Port     int    `mapstructure:"port"`
    LogLevel string `mapstructure:"log_level"`
    Database struct {
        Host    string `mapstructure:"host"`
        Port    int    `mapstructure:"port"`
        User    string `mapstructure:"user"`
        SSLMode string `mapstructure:"ssl_mode"`
    } `mapstructure:"database"`
}

var config Config
if err := viper.Unmarshal(&config); err != nil {
    return err
}

Cobra と Viper の連携

重要な連携パターン

Cobra と Viper を連携させる鍵は、フラグを Viper のキーにバインドすることです。

// cmd/root.go
func init() {
    cobra.OnInitialize(initConfig) // Load config before command execution

    // Define flags
    rootCmd.PersistentFlags().String("config", "", "config file")
    rootCmd.PersistentFlags().String("log-level", "info", "log level")
    rootCmd.PersistentFlags().Int("port", 8080, "server port")

    // Bind flags to Viper (critical step!)
    viper.BindPFlag("config", rootCmd.PersistentFlags().Lookup("config"))
    viper.BindPFlag("log_level", rootCmd.PersistentFlags().Lookup("log-level"))
    viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port"))
}

func initConfig() {
    // This runs BEFORE command execution via cobra.OnInitialize
    if cfgFile := viper.GetString("config"); cfgFile != "" {
        viper.SetConfigFile(cfgFile)
    } else {
        viper.AddConfigPath("$HOME/.myapp")
        viper.AddConfigPath(".")
        viper.SetConfigName("config")
    }

    viper.SetEnvPrefix("MYAPP")
    viper.AutomaticEnv()

    viper.ReadInConfig() // Ignore errors - config file is optional
}

フラグのバインド方法:

// Strategy 1: Bind each flag individually (explicit)
viper.BindPFlag("log_level", rootCmd.Flags().Lookup("log-level"))

// Strategy 2: Bind all flags automatically (convenient)
viper.BindPFlags(rootCmd.Flags())

// Strategy 3: Hybrid approach (recommended)
// - Bind persistent flags globally
// - Bind local flags in each command's init()
rootCmd.PersistentFlags().String("config", "", "config file")
viper.BindPFlags(rootCmd.PersistentFlags())

deployCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("deploy.region", deployCmd.Flags().Lookup("region"))
設定読み込みのための PersistentPreRun

PersistentPreRunE を使用して、設定を読み込んで検証します。

var rootCmd = &cobra.Command{
    Use:   "myapp",
    Short: "My application",
    PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
        // Runs before ALL commands (inherited by subcommands)

        // 1. Validate required config
        if !viper.IsSet("api_key") {
            return fmt.Errorf("API key not configured (set MYAPP_API_KEY or add to config file)")
        }

        // 2. Setup logging based on config
        logLevel := viper.GetString("log_level")
        if err := setupLogging(logLevel); err != nil {
            return fmt.Errorf("invalid log level: %w", err)
        }

        // 3. Initialize clients/services
        apiKey := viper.GetString("api_key")
        if err := initAPIClient(apiKey); err != nil {
            return fmt.Errorf("failed to initialize API client: %w", err)
        }

        return nil
    },
}

シェル補完

Cobra は、bash、zsh、fish、PowerShell 向けのシェル補完スクリプトを生成します。

補完コマンドの追加
// cmd/completion.go
package cmd

import (
    "os"

    "github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
    Use:   "completion [bash|zsh|fish|powershell]",
    Short: "Generate shell completion script",
    Long: `Generate shell completion script for myapp.

To load completions:

Bash:
  $ source <(myapp completion bash)
  # To load automatically, add to ~/.bashrc:
  $ echo 'source <(myapp completion bash)' >> ~/.bashrc

Zsh:
  $ source <(myapp completion zsh)
  # To load automatically, add to ~/.zshrc:
  $ echo 'source <(myapp completion zsh)' >> ~/.zshrc

Fish:
  $ myapp completion fish | source
  # To load automatically:
  $ myapp completion fish > ~/.config/fish/completions/myapp.fish

PowerShell:
  PS> myapp completion powershell | Out-String | Invoke-Expression
  # To load automatically, add to PowerShell profile.
`,
    DisableFlagsInUseLine: true,
    ValidArgs:             []string{"bash", "zsh", "fish", "powershell"},
    Args:                  cobra.ExactValidArgs(1),
    RunE: func(cmd *cobra.Command, args []string) error {
        switch args[0] {
        case "bash":
            return cmd.Root().GenBashCompletion(os.Stdout)
        case "zsh":
            return cmd.Root().GenZshCompletion(os.Stdout)
        case "fish":
            return cmd.Root().GenFishCompletion(os.Stdout, true)
        case "powershell":
            return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
        }
        return nil
    },
}

func init() {
    rootCmd.AddCommand(completionCmd)
}
カスタム補完関数

コマンド引数に対して動的な補完を提供します。

var deployCmd = &cobra.Command{
    Use:   "deploy [environment]",
    Short: "Deploy to environment",
    Args:  cobra.ExactArgs(1),
    ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
        // Return available environments
        envs := []string{"dev", "staging", "production"}
        return envs, cobra.ShellCompDirectiveNoFileComp
    },
    RunE: func(cmd *cobra.Command, args []string) error {
        return deploy(args[0])
    },
}

// Custom flag completion
deployCmd.RegisterFlagCompletionFunc("region", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    regions := []string{"us-east-1", "us-west-2", "eu-west-1"}
    return regions, cobra.ShellCompDirectiveNoFileComp
})

CLI のベストプラクティス

ユーザーフレンドリーなエラーメッセージ
// ❌ BAD: Technical jargon
return fmt.Errorf("db connection failed: EOF")

// ✅ GOOD: Actionable error message
return fmt.Errorf("cannot connect to database at %s:%d\nPlease check:\n  - Database is running\n  - Credentials are correct (MYAPP_DB_PASSWORD)\n  - Network connectivity", host, port)

// ✅ GOOD: Suggest remediation
if !viper.IsSet("api_key") {
    return fmt.Errorf("API key not configured\nSet environment variable: export MYAPP_API_KEY=your-key\nOr add to config file: ~/.myapp.yaml")
}
進捗インジケーター
import "github.com/briandowns/spinner"

func deploy(env string) error {
    s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
    s.Suffix = " Deploying to " + env + "..."
    s.Start()
    defer s.Stop()

    // Deployment logic
    if err := performDeployment(env); err != nil {
        s.Stop()
        return err
    }

    s.Stop()
    fmt.Println("✓ Deployment successful")
    return nil
}
出力形式
import (
    "encoding/json"
    "github.com/olekukonko/tablewriter"
)

func displayResults(items []Item, format string) error {
    switch format {
    case "json":
        enc := json.NewEncoder(os.Stdout)
        enc.SetIndent("", "  ")
        return enc.Encode(items)

    case "table":
        table := tablewriter.NewWriter(os.Stdout)
        table.SetHeader([]string{"ID", "Name", "Status"})
        for _, item := range items {
            table.Append([]string{item.ID, item.Name, item.Status})
        }
        table.Render()
        return nil

    default:
        return fmt.Errorf("unknown format: %s (use json or table)", format)
    }
}
ログとユーザー向け出力
import (
    "log"
    "os"
)

var (
    // User-facing output (stdout)
    out = os.Stdout

    // Logging and errors (stderr)
    logger = log.New(os.Stderr, "[myapp] ", log.LstdFlags)
)

func RunCommand() error {
    // User output: stdout
    fmt.Fprintln(out, "Processing files...")

    // Debug/verbose logging: stderr
    if viper.GetBool("verbose") {
        logger.Println("Reading config from", viper.ConfigFileUsed())
    }

    // Errors: stderr
    if err := process(); err != nil {
        fmt.Fprintf(os.Stderr, "Error: %v\n", err)
        return err
    }

    // Success message: stdout
    fmt.Fprintln(out, "✓ Complete")
    return nil
}

CLI アプリケーションのテスト

コマンド実行のテスト
// cmd/deploy_test.go
package cmd

import (
    "bytes"
    "testing"

    "github.com/spf13/cobra"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func executeCommand(root *cobra.Command, args ...string) (output string, err error) {
    buf := new(bytes.Buffer)
    root.SetOut(buf)
    root.SetErr(buf)
    root.SetArgs(args)

    err = root.Execute()
    return buf.String(), err
}

func TestDeployCommand(t *testing.T) {
    tests := []struct {
        name    string
        args    []string
        wantErr bool
        wantOut string
    }{
        {
            name:    "deploy to dev",
            args:    []string{"deploy", "dev"},
            wantErr: false,
            wantOut: "Deploying to dev",
        },
        {
            name:    "deploy to production without force",
            args:    []string{"deploy", "production"},
            wantErr: true,
            wantOut: "production deploys require --force flag",
        },
        {
            name:    "deploy to production with force",
            args:    []string{"deploy", "production", "--force"},
            wantErr: false,
            wantOut: "Deploying to production",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            output, err := executeCommand(rootCmd, tt.args...)

            if tt.wantErr {
                require.Error(t, err)
            } else {
                require.NoError(t, err)
            }

            assert.Contains(t, output, tt.wantOut)
        })
    }
}
Viper 設定を使ったテスト
func TestCommandWithConfig(t *testing.T) {
    // Reset viper state before each test
    viper.Reset()

    // Set test configuration
    viper.Set("region", "eu-west-1")
    viper.Set("api_key", "test-key-123")

    output, err := executeCommand(rootCmd, "deploy", "staging")

    require.NoError(t, err)
    assert.Contains(t, output, "eu-west-1")
}
出力の取得
func TestOutputFormat(t *testing.T) {
    // Capture stdout
    oldStdout := os.Stdout
    r, w, _ := os.Pipe()
    os.Stdout = w
    defer func() { os.Stdout = oldStdout }()

    // Execute command
    err := listCmd.RunE(listCmd, []string{})
    require.NoError(t, err)

    // Read output
    w.Close()
    var buf bytes.Buffer
    io.Copy(&buf, r)
    output := buf.String()

    assert.Contains(t, output, "ID")
    assert.Contains(t, output, "Name")
}

判断基準

Cobra を使用するタイミング

Cobra を使用する場合:

  • ✅ サブコマンドを備えた複数コマンドの CLI を構築する(例:git clone、docker run)
  • ✅ POSIX 準拠のフラグ解析が必要(--flag、-f)
  • ✅ 組み込みのヘルプ生成(--help)が必要
  • ✅ シェル補完への対応が必要
  • ✅ 他の開発者が使うプロフェッショナルな CLI を構築する

Cobra を使用しない場合:

  • ❌ 単一コマンドのシンプルなスクリプト(flag パッケージを使用)
  • ❌ フラグが 1~2 個だけの内部専用ツール
  • ❌ プロトタイプや使い捨てのスクリプト
Viper を使う場面

次の場合は Viper を使います:

  • ✅ 複数のソース(フラグ、環境変数、ファイル)から設定を取得する必要がある
  • ✅ 明確な設定の優先順位ルールが必要
  • ✅ 複数の設定ファイル形式(YAML、JSON、TOML)に対応する
  • ✅ プレフィックス付きの環境変数マッピングが必要
  • ✅ 設定のライブ再読み込みが必要(設定ファイルの変更を監視する)

次の場合は Viper を使いません:

  • ❌ コマンドラインフラグのみを使う(Cobra だけで十分です)
  • ❌ 設定値がハードコードされている
  • ❌ 設定のないシンプルなスクリプト
シェル補完を追加する場面

次の場合はシェル補完を追加します:

  • ✅ CLI を開発者が頻繁に(毎日/毎時間)使う
  • ✅ サブコマンドや複雑なフラグの組み合わせが多い
  • ✅ 引数に既知の有効な値がある(例:環境、リージョン)
  • ✅ プロフェッショナル向けの開発者ツールを構築する

次の場合はシェル補完を省略します:

  • ❌ CLI の使用頻度が低い(月に1回以下)
  • ❌ オプションが少ないシンプルなコマンド
  • ❌ 社内専用ツール
永続フラグを使う場面

次の場合は永続フラグを使います:

  • ✅ フラグがすべてのサブコマンドに適用される(例:--verbose、--config)
  • ✅ コマンド間で共通の設定を共有する
  • ✅ グローバルな動作を変更する(例:--dry-run、--output)

次の場合はローカルフラグを使います:

  • ✅ フラグが特定のコマンドにのみ関係する
  • ✅ コマンド固有のパラメーター(例:deploy コマンドの --region)

アンチパターン

❌ PreRunE/RunE でエラーを処理しない

誤り:

var deployCmd = &cobra.Command{
    Use: "deploy",
    Run: func(cmd *cobra.Command, args []string) {
        deploy(args[0]) // Ignores error!
    },
}

正しい例:

var deployCmd = &cobra.Command{
    Use: "deploy",
    RunE: func(cmd *cobra.Command, args []string) error {
        return deploy(args[0]) // Proper error handling
    },
}
❌ 明確な優先順位を定めずに設定ソースを混在させる

誤り:

// Confusing: Which takes precedence?
config.Port = viper.GetInt("port")
if os.Getenv("PORT") != "" {
    config.Port = atoi(os.Getenv("PORT"))
}
if *flagPort != 0 {
    config.Port = *flagPort
}

正しい例:

// Clear: Viper handles precedence automatically
viper.BindPFlag("port", rootCmd.Flags().Lookup("port"))
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
viper.SetDefault("port", 8080)

config.Port = viper.GetInt("port") // Respects: flag > env > config > default
❌ フラグを Viper にバインドし忘れる

誤り:

rootCmd.Flags().String("region", "us-east-1", "AWS region")
// Flag not bound to Viper - won't respect precedence!

func deploy() {
    region := viper.GetString("region") // Always returns config file value
}

正しい例:

rootCmd.Flags().String("region", "us-east-1", "AWS region")
viper.BindPFlag("region", rootCmd.Flags().Lookup("region")) // Bind it!

func deploy() {
    region := viper.GetString("region") // Respects flag > env > config
}
❌ CLI コマンドをテストしない

誤り:

// No tests for CLI commands - bugs slip through

正しい例:

func TestDeployCommand(t *testing.T) {
    output, err := executeCommand(rootCmd, "deploy", "staging", "--region", "eu-west-1")
    require.NoError(t, err)
    assert.Contains(t, output, "Deploying to staging")
    assert.Contains(t, output, "eu-west-1")
}
❌ エラーメッセージが不適切

誤り:

return fmt.Errorf("connection failed") // Unhelpful

正しい例:

return fmt.Errorf("cannot connect to database at %s:%d\nCheck:\n  - Database is running\n  - Credentials (MYAPP_DB_PASSWORD)\n  - Firewall rules", host, port)
❌ RunE ではなく Run を使う

誤り:

var rootCmd = &cobra.Command{
    Use: "myapp",
    Run: func(cmd *cobra.Command, args []string) {
        if err := execute(); err != nil {
            fmt.Println(err) // Error not propagated
        }
    },
}

正しい例:

var rootCmd = &cobra.Command{
    Use: "myapp",
    RunE: func(cmd *cobra.Command, args []string) error {
        return execute() // Cobra handles error display and exit code
    },
}

本番環境向けの例

本番環境に対応した最小限の CLI 構成:

myapp/
├── cmd/
│   ├── root.go          # Root command + config loading
│   ├── deploy.go        # Deploy subcommand
│   ├── status.go        # Status subcommand
│   └── completion.go    # Shell completion
├── main.go              # Entry point
├── config.yaml          # Example config file
└── go.mod

main.go:

package main

import "myapp/cmd"

func main() {
    cmd.Execute()
}

cmd/root.go:「コマンド構成パターン」セクションを参照してください

ビルドとインストール:

# Development
go run main.go deploy staging --region us-west-2

# Production build
go build -o myapp

# Install globally
go install

# Enable shell completion
myapp completion bash > /etc/bash_completion.d/myapp

リソース

公式ドキュメント:

学習リソース:

  • 「Building CLI Apps in Go with Cobra & Viper」(2025年11月)- 包括的なチュートリアル
  • 「The Cobra & Viper Journey」- CLI 開発の学習パス
  • Cobra Generator - 新しい CLI のひな型生成ツール

本番環境での使用例:

  • kubectl - Kubernetes CLI
  • docker - Docker CLI
  • gh - GitHub CLI
  • hugo - 静的サイトジェネレーター

関連スキル:

  • golang-testing-strategies - CLI コマンドを包括的にテストする
  • golang-http-servers - 設定を使って API サーバーを構築する
  • golang-concurrency-patterns - CLI ツールでの非同期処理

成功の基準

次の条件を満たせば、Cobra + Viper を習得したと言えます:

  • ✅ コマンドが POSIX の規約(VERB NOUN --FLAG)に従っている
  • ✅ 設定の優先順位が明確である:フラグ > 環境変数 > 設定 > デフォルト値
  • ✅ すべてのフラグが Viper にバインドされ、設定に統一的にアクセスできる
  • ✅ 主要なすべてのシェル向けにシェル補完が生成されている
  • ✅ エラーメッセージが次の行動につながり、ユーザーフレンドリーである
  • ✅ CLI コマンドに包括的なテストがある
  • ✅ ヘルプテキストが自動生成され、正確である
  • ✅ グローバルなセットアップ/検証に PersistentPreRunE を使っている
  • ✅ 関心事が分離されている:ユーザー向け出力(stdout)とログ出力(stderr)
  • ✅ 設定ファイルは任意であり、フラグ/環境変数だけでも CLI が動作する

コメント