Slide 1

Slide 1 text

alecthomas/kong はいいぞ 2024.12.13 kamakura.go #7 @fujiwara

Slide 2

Slide 2 text

自己紹介 @fujiwara (X, GitHub, Bluesky) 面白法人カヤック SREチーム ISUCON 優勝4回 / 運営5回 github.com/kayac/ecspresso github.com/fujiwara/lambroll

Slide 3

Slide 3 text

CLI flag parser、何を使ってますか? ChatGPT-4o にお勧めを聞いてみたらこんな感じ flag (標準) spf13/pflag spf13/cobra urfave/cli alecthomas/kong 特に異存はないけど今日は alecthomas/kong をお勧めする話をします

Slide 4

Slide 4 text

https://github.com/alecthomas/kong

Slide 5

Slide 5 text

package main import "github.com/alecthomas/kong" var CLI struct { Rm struct { Force bool `help:"Force removal."` Recursive bool `help:"Recursively remove files."` Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"` } `cmd:"" help:"Remove files."` Ls struct { Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"` } `cmd:"" help:"List paths."` } func main() { ctx := kong.Parse(&CLI) switch ctx.Command() { case "rm ": case "ls": default: panic(ctx.Command()) } }

Slide 6

Slide 6 text

サブコマンドと引数を構造体に定義 var CLI struct { Rm struct { Force bool `help:"Force removal."` Recursive bool `help:"Recursively remove files."` Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"` } `cmd:"" help:"Remove files."` Ls struct { Paths []string `arg:"" optional:"" name:"path" help:"Paths to list." type:"path"` } `cmd:"" help:"List paths."` } rm コマンドには --force , --recursive 、 paths (複数可)が引数 command rm --force --recursive foo bar ls コマンドには paths (複数可)が引数 command ls foo bar baz

Slide 7

Slide 7 text

定義した構造体を kong.Parse に渡して解析 func main() { ctx := kong.Parse(&CLI) // このctxはcontext.Context**ではない**ので注意 switch ctx.Command() { case "rm ": case "ls": default: panic(ctx.Command()) } } 基本はこれだけ コマンドラインから渡された引数( os.Args )を解析して構造体に入れてくれる その後のコードは好きに書いていい

Slide 8

Slide 8 text

kong のいいところ 宣言的。サブコマンドまで含めて一目でオプションが把握できる map ( Foo map[string]int → --foo="x=1;y=2;z=3" ) slice ( Foo []string → --foo=x --foo=y ) struct tag でいろいろ設定できる required:"" (必須) default:"value" (デフォルト値の設定) short:"x" (短縮形 -x を生成) negatable:"" (bool型の否定形を生成 --limit → --no-limit ) enum:"a,b,c" (a,b,cいずれかの値を要求) env:"FOO" (環境変数FOOを読む) などなど コードの書き方を強要されない CLIフレームワークではない、パーサー

Slide 9

Slide 9 text

応用例 例えば Lambda 関数を Go で書く場合、設定値は環境変数から読む func main() { lambda.Start(handler) } func handler(ctx context.Context) error { // ダメな例 s3Bucket := os.Getenv("BUCKET") if s3Bucket == "" { return errors.New("BUCKET is required") } // ... } 環境変数がないときにエラーになるのは「関数の実行時」 デプロイ直後には発覚しない、めったに通らないところにこれが埋まっていると…?

Slide 10

Slide 10 text

環境変数を読むのは kong に任せる var CLI struct { Bucket string `env:"BUCKET" required:"" help:"S3 BUCKET"` LogLevel string `env:"LOG_LEVEL" default:"info" help:"Log level"` } func main() { ctx := kong.Parse(&CLI) // ここで CLI に値が入っている // 必須がなければエラー、デフォルト値も埋まっている lambda.Start(handler) } func handler(ctx context.Context) error { // ここでは CLI の値を使うだけ } こうしておけば、関数の初期化時に確実にエラーになる デフォルト値も設定できる

Slide 11

Slide 11 text

更に応用 fujiwara/lamblocal と組み合わせる var CLI struct { Bucket string `env:"BUCKET" required:"" help:"S3 BUCKET"` LogLevel string `env:"LOG_LEVEL" default:"info" help:"Log level"` } func main() { ctx := kong.Parse(&CLI) lamblocal.Run(handler) } func handler(ctx context.Context, in Payload) (Response, error) { // ... } lamblocal: CLIでもLambdaでも動くコマンドを作れる(入出力はSTDIN/STDOUT) $ echo '{"key":"foobar"}' | go run main.go --s3-bucket foo {"result":"ok"}

Slide 12

Slide 12 text

まとめ alctehomas/kong はパーサー特化、便利なのでお勧めです 最近の fujiwara-ware はだいたい kong を使っています (ecspresso, lambroll は alecthomas/kingpin から移行)