feature image

2025年2月15日 | ブログ記事

alecthomas/kongのCLI開発体験がだいぶ良い

こんにちは、24Mの@Rasです。
個人でtraPにブログを出すのは1年半ぶりになってしまいました。
作ったものでサッとブログが書けそうだったのでモチベがあるうちに書いています。

さて、GoでCLIを開発する際は、フレームワークとしてspf13/cobraurfave/cliを使うのがメジャーだと思います。
今回は、そのどちらでもなくalecthomas/kong(以下Kong)を使ってCLIを開発したのでその紹介と感想です。

Kong?

https://github.com/alecthomas/kong

GitHub - alecthomas/kong: Kong is a command-line parser for Go
Kong is a command-line parser for Go. Contribute to alecthomas/kong development by creating an account on GitHub.

Kongは、あらゆる複雑なコマンドライン構造をサポートすることを目指したコマンドラインパーサーです。

Kong aims to support arbitrarily complex command-line structures with as little developer effort as possible.

Gorrila Orgに影響を受けているのか、ゴリラみたいな名前をしています。

日本語の紹介記事は少ないですが、以下のスライドは機能が一通りまとめられていて参考になります(Kongを知るきっかけとなった記事です)。

alecthomas/kong はいいぞ / kamakura.go#7 - Speaker Deck

Kongでは、1つの構造体の中に各コマンド(サブコマンド)やそれらのコマンドで用いる引数やフラグなどを定義する形で記述します。

// https://github.com/alecthomas/kong?#introduction

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 <path>":
  case "ls":
  default:
    panic(ctx.Command())
  }
}

他のフレームワークとの比較

Cobraやurfave/cliはフレームワークが用意した構造体に設定を記述して手続き型にコマンドを紐づけていくのに対し、Kongは1つの構造体の中でコマンドの構造を宣言的に記述し、各フィールドの構造体タグで設定を記述する形になります。

Cobraで先ほどと同じCLIを作ろうとすると、こんな感じです(ChatGPTに書かせた)。

Cobraの場合
package main

import (
    "fmt"
    "os"

    "github.com/spf13/cobra"
)

var (
    force     bool
    recursive bool
)

func main() {
    var rootCmd = &cobra.Command{Use: "cli"}

    var rmCmd = &cobra.Command{
        Use:   "rm [path]",
        Short: "Remove files",
        Run: func(cmd *cobra.Command, args []string) {
            // ...
        },
    }

    rmCmd.Flags().BoolVar(&force, "force", false, "Force removal")
    rmCmd.Flags().BoolVar(&recursive, "recursive", false, "Recursively remove files")

    var lsCmd = &cobra.Command{
        Use:   "ls [path]",
        Short: "List paths",
        Run: func(cmd *cobra.Command, args []string) {
            // ...
        },
    }

    rootCmd.AddCommand(rmCmd)
    rootCmd.AddCommand(lsCmd)

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

先ほどのスライドにも書かれていますが、Kongはコマンドラインパーサーの部分に特化していて、パースさえKongに任せれば他はKongに依存せずにGoのコードを書けるのも嬉しいポイントです。

パース以外は自分で自由に書くこともできますが、Kongは以下のようにRun()を実装することでコマンドを実行する方法もサポートしています。
しかし、(ここも偉いところで)Run()の引数は開発者側が決めることができ、ここでもKongが登場することはないため、制約に縛られない書き方ができます。

Run()を使ったコード
type Context struct {
     Debug bool
}

type RmCmd struct {
     Force     bool `help:"Force removal."`
     Recursive bool `help:"Recursively remove files."`

     Paths []string `arg:"" name:"path" help:"Paths to remove." type:"path"`
}

func (r *RmCmd) Run(ctx *Context) error {
     fmt.Println("rm", r.Paths)
     return nil
}

var cli struct {
     Debug bool `help:"Enable debug mode."`

     Rm RmCmd `cmd:"" help:"Remove files."`
}

func main() {
     ctx := kong.Parse(&cli)
     // Call the Run() method of the selected parsed command.
     err := ctx.Run(&Context{Debug: cli.Debug})
     ctx.FatalIfErrorf(err)
}

また、個人的には以下を構造体タグで表現できる点をかなり評価しています。

Cobraでこれをやろうとすると、spf13/pflagやspf13/viperといったいわゆる「spf13スタック」に乗っかる必要があり、依存は多いし記述も複雑で正直なところかなり大変です。
デフォルト値を構造体タグで記述できるのは探せばcreasty/defaultsなどがあるのですが、これら2つを1つのコマンドラインパーサーで実現できるKongはとても魅力的です。

Kongで何を作ったのか

部内SNS「traQ」のメッセージとシェルの標準入出力を繋げるツール「trapipe」を作りました。
コード量もそこまで多くないので読もうと思えばサッと読めると思います。

https://github.com/ras0q/trapipe

GitHub - ras0q/trapipe
Contribute to ras0q/trapipe development by creating an account on GitHub.

trapipe receive(traQ → Shell)とtrapipe send(Shell → traQ)の2つのコマンドがあり、これらを駆使することでtraQのメッセージをシェルに渡したり、シェルの出力をtraQに投げたりすることができます。

trapipe receive -t "{{ .Message.ChannelID }} {{ .Message.PlainText }}" |
    while read -r channel_id mention args; do
        if [ "$mention" = "@BOT_AWESOME" ]; then
            my-awesome-cli $args | trapipe send --channel-id "$channel_id"
        fi
    done
traQで「@BOT_AWESOME arg1 arg2」と投稿されるとそれに応じてコマンドを実行するスクリプト

これまでtraQ Botを作るときには、連携用のライブラリを入れてtraQ特化のBotを作る必要があったのですが、 trapipe に連携部分の役割を委譲することで普通のCLI開発と同じようにBotを作ることができます。

以下のようにコマンドの出力を返すだけの単純なBotであれば、Dockerfileに20行程度記述すればすぐに動かすことができます。

@BOT_figletの構成コード
FROM alpine:latest

RUN apk add --no-cache ca-certificates figlet

COPY --from=ghcr.io/ras0q/trapipe /bin/trapipe /bin/trapipe

ARG BOT_NAME="@BOT_figlet"
ARG COMMAND="figlet"
COPY --chmod=755 <<EOF /entrypoint.sh
#!/bin/sh -eu
trapipe receive -t "{{ .Message.ChannelID }} {{ .Message.PlainText }}" |
  while read -r channel_id mention args; do
    if [ "\$mention" = "$BOT_NAME" ]; then
      echo "\\`\\`\\`plaintext"
      $COMMAND \$args
      echo "\\`\\`\\`"
    fi | trapipe send --channel-id "\$channel_id"
  done
EOF

ENTRYPOINT ["/entrypoint.sh"]
figlet をデプロイ先のシェルで実行し、実行結果をtraQに返すBot

trapipeではKong用の構造体を以下のように定義して使っています。
trapipe receiveに対応する構造体にdefault:"1"のタグを付けることでサブコマンドなしで実行した際にtrapipe receiveを呼ぶことができます。
特にFormatterやLinterではfoofmt run ./でもfoofmt ./でも実行したいという需要がありがちなので、KongはこれらのCLIを作る際にも有効な選択肢になり得ます。

var cli struct {
    AccessToken string `help:"BOT Access Token" env:"TRAQ_BOT_ACCESS_TOKEN" required:""`
    WSOrigin    string `help:"traQ Websocket Origin" default:"wss://q.trap.jp" env:"TRAQ_WS_ORIGIN"`

    Receive commands.Receive `cmd:"" default:"1" help:"Receive messages from traQ server (default)"`
    Send    commands.Send    `cmd:"" help:"Send a message to traQ server"`
}
cmd,env,help,default,requiredなど様々なタグが使える

サブコマンドの実行は先述のRun()を使う方法を採用しており、commandsパッケージにそれぞれの実装を分離して書いています。

package commands

type Send struct {
    ChannelID string `help:"Channel ID to send a message" required:""`
}

func (c *Send) Run(ctx *Context) error {
    data, err := io.ReadAll(os.Stdin)
    if err != nil {
        return err
    }

    postMessage(data)

    return nil
}

だいぶ好みのスタイルで書くことができ、今後のCLI開発の強力な選択肢になりそうです。

ぜひKong(と trapipe )を使ってみてください。

GitHub - alecthomas/kong: Kong is a command-line parser for Go
Kong is a command-line parser for Go. Contribute to alecthomas/kong development by creating an account on GitHub.
Ras icon
この記事を書いた人
Ras

20B。アライグマです。

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2024年9月20日
2024年 1-Monthonを開催しました!!
Synori icon Synori
2024年9月17日
1か月でゲームを作った #BlueLINE
Komichi icon Komichi
2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2024年6月21日
ハッカソン参加記 4班"Slide Center"
Alt--er icon Alt--er
2024年3月15日
個人開発として2週間でWebサービスを作ってみた話 〜「LABEL」の紹介〜
Natsuki icon Natsuki
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記