86
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

逆引き cobra & viper

Posted at

はじめに

  • Go 言語の CLI フレームワークである cobra と設定ファイル導入支援ライブラリ viper を組み合わせて利用すると CLI ツール作成が大変楽になる。
  • 本家のドキュメントは充実したものではあるが簡単な Example があると嬉しいと感じたので、cobra と viper を連携させて利用する方法について逆引き形式でまとめてみた。

cobra で hello world

  • cobra で "hello world" を出力するだけの最小構成のコマンドを実装する。
0/main.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
)

func main() {
	rootCmd := &cobra.Command{
		Use: "app",
		Run: func(c *cobra.Command, args []string) {
			fmt.Println("hello world")
		},
	}
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

実行結果

$ go run 0/main.go
hello world

cobra でフラグを使う

  • フラグで文字列を受け取って参照する例。
  • 同じような方法で整数や真偽値なども扱うことができる。
1/main.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
)

var (
	name1 string
)

func main() {
	rootCmd := &cobra.Command{
		Use: "app",
		Run: func(c *cobra.Command, args []string) {
			// セットされた値を変数から取得する
			fmt.Println("name1:", name1)

			// フラグ名で値を取得する
			if name2, err := c.PersistentFlags().GetString("name2"); err == nil {
				fmt.Println("name2:", name2)
			}
		},
	}

	// フラグの値を変数にセットする場合
	// 第1引数: 変数のポインタ
	// 第2引数: フラグ名
	// 第3引数: デフォルト値
	// 第4引数: 説明
	rootCmd.PersistentFlags().StringVar(&name1, "name1", "name1", "your name1")

	// フラグの値をフラグ名で参照する場合
	// 第1引数: フラグ名
	// 第2引数: 短縮フラグ名(末尾が "P" の関数では短縮フラグを指定できる)
	// 第3引数: デフォルト値
	// 第4引数: 説明
	rootCmd.PersistentFlags().StringP("name2", "n", "name2", "your name2")

	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

実行結果

# デフォルト値の表示
$ go run 1/main.go
name1: name1
name2: name2

# name1, name2 共にフラグで指定する
$ go run 1/main.go --name1 foo --name2 bar
name1: foo
name2: bar

# name2 のみ短縮形で指定する
$ go run 1/main.go -n bar
name1: name1
name2: bar

cobra でサブコマンドとフラグを使う

  • サブコマンド定義とコマンド共通フラグ、サブコマンド専用フラグの定義をする例。
  • ルートコマンドの *cobra.Command が参照できるように定義されていれば容易にファイルを分割することができる。
2/main.go
package main

func main() {
	// コマンドの実行
	execute()
}
2/root.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"os"
)

// ルートコマンドの定義
var rootCmd = &cobra.Command{
	Use: "app",
	Run: func(c *cobra.Command, args []string) {
		fmt.Println("debug:", debug)
	},
}

// 共通フラグ用の変数
var debug bool

// ファイル読み込みのタイミングでフラグを定義する
func init() {
	// コマンド共通のフラグを定義
	rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug enable flag")
}

func execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
2/sub.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"log"
)

// サブコマンドの定義
var subCmd = &cobra.Command{
	Use: "sub",
	Run: func(c *cobra.Command, args []string) {
		if name, err := c.PersistentFlags().GetString("name"); err == nil {
			// 共通フラグ debug をサブコマンドからも利用できる
			if debug {
				log.Println("name:", name)
			} else {
				fmt.Println("name:", name)
			}
		}
	},
}

func init() {
	// サブコマンドのフラグ定義
	subCmd.PersistentFlags().String("name", "john", "sub command string flag test")
	// サブコマンドをルートコマンドに登録
	rootCmd.AddCommand(subCmd)
}

実行結果

# ルートコマンドの実行. debug はデフォルト値.
$ go run 2/*
debug: false

# default の指定
$ go run 2/* -d
debug: true

# サブコマンドの実行. name, debug はデフォルト値
$ go run 2/* sub
name: john

# サブコマンドの実行. name を指定.
$ go run 2/* sub --name name1
name: name1

# サブコマンドの実行. name, default を指定.
$ go run 2/* sub --name name1 -d
2018/02/20 10:44:53 name: name1

cobra と viper で設定ファイルを読み込む

  • cobra のフラグで設定ファイル名を指定して、viper で設定ファイルを読み込む。
3/main.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"os"
)

// 読み込む設定の型
type Config struct {
	ApplicationName string
	Debug           bool
}

// 読み込む設定ファイル名
var configFile string

// 読み込んだ設定ファイルの構造体
var config Config

func main() {
	rootCmd := &cobra.Command{
		Use: "app",
		Run: func(c *cobra.Command, args []string) {
			// 受け取った設定ファイル名と設定の内容を表示
			fmt.Printf("configFile: %s\nconfig: %#v", configFile, config)
		},
	}

	// 設定ファイル名をフラグで受け取る
	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "config.toml", "config file name")

	// cobra.Command 実行前の初期化処理を定義する。
	// rootCmd.Execute > コマンドライン引数の処理 > cobra.OnInitialize > rootCmd.Run という順に実行されるので、
	// フラグでうけとった設定ファイル名を使って設定ファイルを読み込み、コマンド実行時に設定ファイルの内容を利用することができる。
	cobra.OnInitialize(func() {

		// 設定ファイル名を viper に定義する
		viper.SetConfigFile(configFile)

		// 設定ファイルを読み込む
		if err := viper.ReadInConfig(); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		// 設定ファイルの内容を構造体にコピーする
		if err := viper.Unmarshal(&config); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	})

	// コマンド実行
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
3/config.yaml
ApplicationName: "APP_YAML"
Debug: true
3/config.toml
ApplicationName = "APP_TOML"
Debug = true

実行結果

# ファイル名を指定して設定ファイルを読み込む.
$ go run 3/main.go --config 3/config.yaml
configFile: 3/config.yaml
config: main.Config{ApplicationName:"APP_YAML", Debug:true}

# 対応しているファイル形式であればファイル名の拡張子からファイル種別を推測して読み込む.
# `JSON, TOML, YAML, HCL, or Java properties formats` に対応している.
# `viper.SetConfigType` でファイル形式を明示的に指定することもできる.
$ go run 3/main.go --config 3/config.toml
configFile: 3/config.toml
config: main.Config{ApplicationName:"APP_TOML", Debug:true}

viper で環境変数を使って設定ファイルの値を上書きする

  • viper.AutomaticEnv() を利用することで設定ファイルの項目名と同名の環境変数があれば値を上書きすることができる。
4/main.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"os"
)

// 設定型
type Config struct {
	ApplicationName string
	Debug           bool
}

// 設定ファイル名
var configFile string

// 設定項目を上書きする環境変数のプレフィックス
var envPrefix string

// 設定構造体
var config Config

func main() {
	rootCmd := &cobra.Command{
		Use: "app",
		Run: func(c *cobra.Command, args []string) {
			// 読み込んだ設定の内容を表示
			fmt.Printf("config: %#v\n", config)
		},
	}

	// 設定ファイル名をフラグで受け取る
	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "config.toml", "config file name")
	// 設定項目を上書きする環境変数のプレフィックスをフラグで受け取る
	rootCmd.PersistentFlags().StringVar(&envPrefix, "env-prefix", "", "env prefix, enabled if defined")

	// cobra.Command 実行前の初期化処理を定義する
	cobra.OnInitialize(func() {

		// 設定ファイル名を viper に定義する
		viper.SetConfigFile(configFile)

		// 設定項目を上書きする環境変数のプレフィックスの指定
		// 対象項目が "Debug" で指定したプレフィックスが "app" なら、環境変数に "APP_DEBUG" を設定しておくことで値を上書きできる
		viper.SetEnvPrefix(envPrefix)

		// 環境変数を自動で読み込んで項目に対応する値が存在すれば上書きする
		viper.AutomaticEnv()

		// 設定ファイルを読み込む
		if err := viper.ReadInConfig(); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		// 設定ファイルの内容を構造体にコピーする
		if err := viper.Unmarshal(&config); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	})

	// コマンド実行
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

実行結果

# 素の設定ファイルを表示する
$ go run 4/main.go -c 4/config.toml
config: main.Config{ApplicationName:"APP_TOML", Debug:true}

# "Debug" 項目を "DEBUG" 環境変数で上書きする
$ DEBUG=false go run 4/main.go -c 4/config.toml
config: main.Config{ApplicationName:"APP_TOML", Debug:false}

# 複数の項目を環境変数で上書きする.
$ DEBUG=false APPLICATIONNAME=APP_ENV go run 4/main.go -c 4/config.toml
config: main.Config{ApplicationName:"APP_ENV", Debug:false}

# 値を上書きする環境変数のプレフィックスを app とする.
# Debug を上書きする環境変数は APP_DEBUG となるので DEBUG では上書きできない.
DEBUG=false go run 4/main.go -c 4/config.toml --env-prefix app
config: main.Config{ApplicationName:"APP_TOML", Debug:true}

# env-prefix を app とすることで APP_DEBUG で Debug を上書きできる.
$ APP_DEBUG=false go run 4/main.go -c 4/config.toml --env-prefix app
config: main.Config{ApplicationName:"APP_TOML", Debug:false}

cobra と viper で読み込んだ設定ファイルの値をフラグの値で上書きする

  • viper.BindPFlag() で cobra のフラグを指定することで、設定ファイルの値をフラグで上書きできるようになる。
5/main.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"os"
)

// 設定型
type Config struct {
	ApplicationName string
	Debug           bool
}

// 設定ファイル名
var configFile string

// 設定構造体
var config Config

func main() {
	rootCmd := &cobra.Command{
		Use: "app",
		Run: func(c *cobra.Command, args []string) {
			fmt.Printf("config: %#v\n", config)
		},
	}

	rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "config.toml", "config file name")

	// 設定ファイルの ApplicationName 項目をフラグで上書きする
	rootCmd.PersistentFlags().String("name", "", "application name")
	viper.BindPFlag("ApplicationName", rootCmd.PersistentFlags().Lookup("name"))

	cobra.OnInitialize(func() {
		viper.SetConfigFile(configFile)
		viper.AutomaticEnv()
		if err := viper.ReadInConfig(); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		if err := viper.Unmarshal(&config); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	})

	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

実行結果

# 素の実行結果
$ go run 5/main.go -c 5/config.toml
config: main.Config{ApplicationName:"APP_TOML", Debug:true}

# フラグから取得した値で設定の値を上書きする
$ go run 5/main.go -c 5/config.toml --name APP_FLAG
config: main.Config{ApplicationName:"APP_FLAG", Debug:true}

# 環境変数で設定を上書きできていることの確認
$ APPLICATIONNAME=APP_ENV go run 5/main.go -c 5/config.toml
config: main.Config{ApplicationName:"APP_ENV", Debug:true}

# 環境変数とフラグによる上書きが同時に行われた場合はフラグの方が優先される
$ APPLICATIONNAME=APP_ENV go run 5/main.go -c 5/config.toml --name APP_FLAG
config: main.Config{ApplicationName:"APP_FLAG", Debug:true}

cobra と viper で設定ファイルの各項目に対応したフラグを定義する

  • 設定ファイルの型が基本型のみである場合に自動で対応するフラグを定義する。
6/main.go
package main

import (
	"fmt"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"os"
	"reflect"
)

// 設定型
type Config struct {
	B   bool
	S   string
	I   int
	I8  int8
	I16 int16
	I32 int32
	I64 int64
	U   uint
	U8  uint8
	U16 uint16
	U32 uint32
	U64 uint64
	F32 float32
	F64 float64
}

// 設定ファイル名
var configFile string

// 設定構造体
var config Config

func main() {
	rootCmd := &cobra.Command{
		Use: "app",
		Run: func(c *cobra.Command, args []string) {
			fmt.Printf("config: %#v\n", config)
		},
	}

	flags := rootCmd.PersistentFlags()

	// 設定ファイル
	flags.StringVarP(&configFile, "config", "c", "config.toml", "config file name")

	// 設定ファイルの各項目ごとにフラグを定義する
	rt := reflect.TypeOf(Config{})
	for i := 0; i < rt.NumField(); i++ {
		sf := rt.Field(i)
		name := sf.Name
		desc := fmt.Sprintf("overwrite %s value from config", name)
		switch sf.Type.Kind() {
		case reflect.Bool:
			flags.Bool(name, false, desc)
		case reflect.String:
			flags.String(name, "", desc)
		case reflect.Int:
			flags.Int(name, 0, desc)
		case reflect.Int8:
			flags.Int8(name, 0, desc)
		case reflect.Int16:
			flags.Int16(name, 0, desc)
		case reflect.Int32:
			flags.Int32(name, 0, desc)
		case reflect.Int64:
			flags.Int64(name, 0, desc)
		case reflect.Uint:
			flags.Uint(name, 0, desc)
		case reflect.Uint8:
			flags.Uint8(name, 0, desc)
		case reflect.Uint16:
			flags.Uint16(name, 0, desc)
		case reflect.Uint32:
			flags.Uint32(name, 0, desc)
		case reflect.Uint64:
			flags.Uint64(name, 0, desc)
		case reflect.Float32:
			flags.Float32(name, 0, desc)
		case reflect.Float64:
			flags.Float64(name, 0, desc)
		}
	}

	//flags.Uint64("U8", 0, "")

	// 設定ファイルの各項目をフラグで上書きする
	viper.BindPFlags(rootCmd.PersistentFlags())

	cobra.OnInitialize(func() {
		viper.SetConfigFile(configFile)
		viper.AutomaticEnv()
		if err := viper.ReadInConfig(); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
		if err := viper.Unmarshal(&config); err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	})

	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
6/config.toml
B = true
S = "string1"
I = 1
I8 = 10
I16 = 100
I32 = 1000
I64 = 10000
U = 2
U8 = 20
U16 = 200
U32 = 2000
U64 = 20000
F32 = 0.1
F64 = 0.01

実行結果

# 素の実行結果.
$ go run 6/main.go --config 6/config.toml
config: main.Config{B:true, S:"string1", I:1, I8:10, I16:100, I32:1000, I64:10000, U:0x2, U8:0x14, U16:0xc8, U32:0x7d0, U64:0x4e20, F32:0.1, F64:0.01}

# 設定項目に対応したフラグの値を指定する
$ go run 6/main.go --config 6/config.toml --B false --S string2 --I 3 --I8 33 --I16 333 --I32 3333 --I64 33333 --U 4 --U8 44 --U16 444 --U32 4444 --U64 44444 --F32 0.5 --F64 0.55
config: main.Config{B:true, S:"string2", I:3, I8:33, I16:333, I32:3333, I64:33333, U:0x4, U8:0x2c, U16:0x1bc, U32:0x115c, U64:0xad9c, F32:0.5, F64:0.55}

おわりに

  • 実コードは cobra-viper-example にまとめた。
  • とりあえず自分のユースケースで出てきたものだけ記載してみたのでもっと他にもあればコメントでもなんでも歓迎します。
86
53
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
86
53

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?