まえおき
最近、Androidアプリを開発するのが全然おもしろくなくなってきたので、Dartを触り始めた。(※ Flutterではない)
で、Dartでコマンドラインツール作ろうと思ったけど、コマンドラインツールといえば、とりあえずGoの cobra/viper 知っとかないと・・・
ということでGoを5年ぶりくらいに触り始めた。
何を作った?
appetize.io っていうWebベースのエミュレータのAPIを便利に使うコマンドラインツールを作ってみた。 brew tapでインストールできるようにリポジトリを適当に作った ので、たぶん
brew tap YusukeIwaki/appetize brew install appetize
で使えると思われる。
※ 完全に余談だけど、今回はとくに「自分自身が "使いたい!" と思う形でリリースする」ということにこだわった。
「必須だけどユーザによって使う値はだいたい決まっている、けどコマンドラインパラメータでオーバーライドもできる」が瞬殺だった
会社の同僚がQiitaに書いていて、そのまんまなんだけども。
APIトークンとかって、毎回毎回 --token=tok_79ad08a09dca80dc9a
みたいなのを打つのはあまりに面倒。
~/.appetize.yml
にapi_token: tok_9ads8ad9a7dsa
みたいな設定値があればそれを使う- 環境変数で
APPETIZE_API_TOKEN=tok_7987adaca9d7ca8c7
みたいな変数があれば、それを使う - コマンドラインパラメータで
--token=toka9sd7sa9d87a
みたいなパラメータが指定されていれば、それを使う
で、同時指定の場合は、3 > 2 >1 の優先順位で動いてくれるのが望ましい。
これ、viperとcobraの典型的なユースケースで、わりと瞬殺で実装できてしまう。
// initConfig reads in config file and ENV variables if set. func initConfig() { // Find home directory. home, err := homedir.Dir() if err != nil { fmt.Println(err) os.Exit(1) } // Search config in home directory with name ".appetize" (without extension). viper.AddConfigPath(home) viper.SetConfigName(".appetize") viper.BindEnv("api_token", "APPETIZE_API_TOKEN") // If a config file is found, read it in. if err := viper.ReadInConfig(); err == nil { //fmt.Println("Using config file:", viper.ConfigFileUsed()) } } func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().String("token", "", "Appetize api token") viper.BindPFlag("api_token", rootCmd.PersistentFlags().Lookup("token")) }
これだけだ。なんて便利。
「上書き保存用のフォームオブジェクト」ってどうやってつくるの?
App |- PublicKey |- Timeout |- Disabled
みたいなのがサーバー側でモデルとしてあって、JSON APIで書き換えるときに詰まった。
1つの属性だけ書き換えたい時にどうするか?
たとえば、 ./appdemo update --timeout 100
って打った時に、勝手にdisabled=falseを上書きするような実装だと困る。
コマンドラインパラメータの処理
cmd.Flags().GetBool("Disabled")
だと、
- ユーザが指定せずデフォルトパラメータとして falseが返っている
- ユーザが意図して
--disabled false
を指定している
がわからない。
調べてみると、cobraのリポジトリにissueが既に上がっていて、
Question: See if a flag was set by the user · Issue #23 · spf13/cobra · GitHub
Changed
を使えばわかるよ!って書いてあった。
if cmd.Flags().Changed("Disabled") { attributes.Disabled = cmd.Flags().GetBool("Disabled") }
ってすればいいらしい。
パラメータの構造体の定義
if cmd.Flags().Changed("Disabled") {
のなかでいきなりJSONを組み立てるのとかはさすがに筋が良くないので、多くの場合は一旦それ専用の構造体をつくることになる。
ここでも、「ユーザが意図してfalseを設定したのか、デフォルト値としてのfalseなのか・・・」というのを迷うことになった。
type AttributesForUpdate struct { PublicKey string Timeout int Disabled bool }
愚直に↑みたいなフォームを作ると、Disabled = false のときの処理に困ることになる。(ちなみに、これは正直Goに限った問題ではなくて、だいたいどの言語でも同じようなことは言える)
var attributes AttributesForUpdate attributes.Timeout = 100 fmt.Println(attributes.Disabled) // => false
これは、調べてみるといくつかのパターンはあって、みんなそれなりに苦しんでいるっぽい?
nilもいけるように、ポインタ型にしちゃう?
proposal: spec: option types · Issue #7054 · golang/go · GitHub
type AttributesForUpdate struct { PublicKey *string Timeout *int Disabled *bool }
*int
がなかなかに鬼畜、ってissueでコメントしてる人がいる。
Optional型を作っちゃう
GoogleのCloud PlatformのSDKがこの方式を使っている
- google-cloud-go/storage.go at 645ff4ff85c1ea74e5b89916b9064c67282bf85b · GoogleCloudPlatform/google-cloud-go · GitHub
- optional - GoDoc
nilでToBoolするとpanicする、ってなかなか激しいぞ・・・
type AttributesForUpdate struct { PublicKey optional.String Timeout optional.Int Disabled optional.Bool }
JSONのMarshalとかをやらないなら、アリかもしれない。
ぜんぶstring型にしちゃおう
さすがにこれをやってる例は見なかった。
type AttributesForUpdate struct { PublicKey string Timeout string Disabled string }
if attributes.Disabled == "true" { params.Add("disabled", true) } else if attributes.Disabled == "false" { params.Add("disabled", false") }
ヤバイw
いっそのことstructじゃなくて、 map[string]interface{} にする
var attributes map[string]interface{} if cmd.Flags().Changed("Disabled") { attributes["Disbaled"] = cmd.Flags().GetBool("Disabled") }
うん、タイポしててもわからないね・・・。
これはさすがに却下。
ということで、とりあえずGoogleがやってるように、Optional型つくる、かなぁー・・・
まとめ
cobra/viperは素晴らしい