Go

Go のいけてる CLI フレームワーク kingpin の隠された実力のご紹介

はじめに

  • Go の CLI フレームワーク kingpin について便利な機能が一通り揃っているうえに記法がスマートでいけているというはなしと、README ではちゃんとアピールされていない隠された実力についてのご紹介記事です。

kingpin の基本機能

フラグの指定

// 型安全なフラグ指定
debug := app.Flag("debug", "Enable debug mode.").Bool()
// ショートフラグも対応
debug := app.Flag("debug", "Enable debug mode.").Short('d').Bool()
// デフォルト値
serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP()
// 必須フラグ
serverIP = app.Flag("server", "Server address.").Required().IP()

引数の指定

// 型安全な引数指定
arg1 = app.Arg("arg1", "first argument").String()
// 必須フラグ
arg1 = app.Arg("arg1", "first argument").Required().String()
// デフォルト値
arg2 = app.Arg("arg2", "first argument").Default("ARG2").String()

コマンド対応

package main

import (
    "os"
    "strings"
    "gopkg.in/alecthomas/kingpin.v2"
)

var (
    // アプリケーションの定義
    app      = kingpin.New("chat", "A command-line chat application.")
    // トップレベルのフラグ指定
    debug    = app.Flag("debug", "Enable debug mode.").Bool()
    serverIP = app.Flag("server", "Server address.").Default("127.0.0.1").IP()

    // コマンドの定義
    register     = app.Command("register", "Register a new user.")
    // コマンドのオプションの定義
    registerNick = register.Arg("nick", "Nickname for user.").Required().String()
    registerName = register.Arg("name", "Name of user.").Required().String()

    // 二つ目のコマンドの定義
    post        = app.Command("post", "Post a message to a channel.")
    // 二つ目のコマンドのオプションの定義
    postImage   = post.Flag("image", "Image to post.").File()
    postChannel = post.Arg("channel", "Channel to post to.").Required().String()
    postText    = post.Arg("text", "Text to post.").Strings()
)

func main() {
    // コマンドライン引数をパースして Flag や Arg に値を入れる
    switch kingpin.MustParse(app.Parse(os.Args[1:])) {
    case register.FullCommand():
        // register コマンドの実行
        println(*registerNick)

    case post.FullCommand():
        // post コマンドの実行
        if *postImage != nil {
        }
        text := strings.Join(*postText, " ")
        println("Post:", text)
    }
}
  • サブコマンドを定義したい場合はコマンドを定義した変数から Command を実行する
sub = post.Command("sub", "help")

kingpin の隠された実力

  • 前述のコマンドのコード例を見ると、コマンドやフラグや引数が増えた時に変数定義が長大になって可読性悪くなんないのかなーとか、コマンド増えたらそれだけ switch 文が長くなるのかなー面倒だなーとかのネガティブな印象を受ける。
  • しかし kingpin はコマンドのコールバックを Action 関数で定義することで他の CLI フレームワークのようにコマンド単位で処理を分割することが可能で、可読性の良いコードを書くことができる。
package main

import (
    "os"
    "strings"
    "gopkg.in/alecthomas/kingpin.v2"
)

func main() {
    // アプリケーションの定義
    app := kingpin.New("chat", "A command-line chat application.")
    // register コマンドの定義
    register(app)
    // post コマンドの定義
    post(app)
    // コマンドライン引数のパースとコールバックの実行
    kingpin.MustParse(app.Parse(os.Args[1:]))
}

func register(app *kingpin.Application) {
    // コマンドの定義
    cmd := app.Command("register", "Register a new user.")
    // 引数の定義. 変更前は変数名が重複しないように "registerNick" のように接頭語を入れていたがローカル変数なのでそれも不要になった
    nick := cmd.Arg("nick", "Nickname for user.").Required().String()
    name := cmd.Arg("name", "Name of user.").Required().String()
    // コールバックの定義
    cmd.Action(func(c *kingpin.ParseContext) error {
        // 引数を使って処理を実行
        // *kingpin.ParseContext にはコマンド定義や引数定義も含まれているが簡便に取り出す手段はたぶん提供されていないので先に引数を定義してローカル変数に入れてから参照している
        println(*nick, *name)
        return nil
    })
}

func post(app *kingpin.Application) {
    // コマンドの定義
    cmd := app.Command("post", "Post a message to a channel.")
    // フラグの定義
    image := cmd.Flag("image", "Image to post.").File()
    // 引数の定義
    channel := cmd.Arg("channel", "Channel to post to.").Required().String()
    text := cmd.Arg("text", "Text to post.").Strings()
    // コールバックの定義
    cmd.Action(func (c *kingpin.ParseContext) error {
        text := strings.Join(*text, " ")
        println("Post:", text)
        println("Image:", *image)
        println("Channel:", *channel)
        return nil
    })
}