koluku's blog

コードは毎日の欠片

urfave/cli/v2ではRunContextでContextをもたせることができる

TL;DR

  • urfave/cli/v2ではRunContextでContextをもたせることができる(それだけ)

本題

Goでコマンドラインツールを作る際に皆さんはどんなライブラリを使っていますか?自分はflagだと薄いのでちょっと機能を拡張するときに困り、cobraだとライブラリ自体が大きすぎて取り回しづらいという理由からurfave/cliを好んで使ってる。

割と直感的に書きやすいのが魅力で、それでなおかつ拡張しやすいので巨大なツールを作りたいわけでは無いならだいたいこれでいいんじゃないかなぁと思ってる。

Getting Started - urfave/cli

func main() {
    app := &cli.App{
        Name:  "greet",
        Usage: "say a greeting",
        Action: func(c *cli.Context) error {
            fmt.Println("Greetings")
            return nil
        },
    }

    if err := app.Run(os.Args); err != nil {
        log.Fatal(err)
    }
}

ただ、コマンドラインツールを作る上でどうしてもコマンドラインライブラリより上で制御したいパターンが2つあり、1つがSignalキャッチからの強制終了の伝搬、もう1つがタイムアウトからの強制終了の伝搬がある。

前者はコマンドラインツールを強制的に止めたいときにメモリリークがgoroutineリークなど発生させないように安全に処理したい場合や、graceful shutdownを実装したい時に使う。後者はDBの書き換えなどでいつ終わるかわからない場合に一旦処理を打ち切りたいときに使える。

これらはコマンドラインライブラリの中で制御するよりも優先度が高いので親Contextに持っていきたいが、どうも公式ドキュメントには書いてなさそうだと若干諦めかけていたのですが、godocを見ているとなんだ RunContext っていうのがあるじゃないかと気がついた。

https://pkg.go.dev/github.com/urfave/cli/v2#App.RunContext

cli.Context がcontext.Contextを内包しているのでRunContextで親Contextからもらって、cli.Context.Context を実行関数に渡してやれば伝達がうまくいく。

func main() {
    // Ctrl+C などでシグナルを飛ばすとctx.Done()が走る
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
    defer stop()

    app := &cli.App{
        Name:  "greet",
        Usage: "say a greeting",
        Action: func(c *cli.Context) error {
            // 関数が終わるまで100秒かかるような処理
            if err := veryVeryLongGreet(c.Context); err != nil {
                return err
            }
            return nil
        },
    }

    if err := app.RunContext(ctx, os.Args); err != nil {
        log.Fatal(err)
    }
}

実行時:

$ ./very-very-long-greet
// Ctrl+C
2022/09/23 03:56:27 abort

参考

mattn.kaoriya.net