いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-394 (flag.FlagSetを使ってサブコマンドを実装)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。

Goを使っていると、CLIで動作するちょっとしたツールをよく作ります。

その際にコマンドラインから引数を貰うのに、通常は flag パッケージを使ってちょちょいとフラグ制御したりもしますね。

んで、たまにもう少しちゃんとしたコマンドを作るときは、サブコマンドを使えるようにしたいときがあります。

サブコマンドというのは、git pull とか git switch みたいに、2つ目のコマンドを用意するということです。

spf13/cobra のような素晴らしいライブラリも沢山ありますが、私個人は標準パッケージでなんとかするのが結構好きです。

flag パッケージには、FlagSet という構造体が存在するので、それを使うとサブコマンドも作れます。

使い方もとてもシンプルなのでよく利用する構造体です。FlagSet を使うとユニットテストもし易いです。

サンプル

package main

import (
    "flag"
    "log"
    "os"
    "runtime/debug"
)

type subcommand string

const (
    subcmd1 subcommand = "cmd1"
    subcmd2 subcommand = "cmd2"
    subver  subcommand = "version"
    subhelp subcommand = "help"
)

// cmd1 subcommand
var (
    cmd1     = flag.NewFlagSet("cmd1", flag.ExitOnError)
    cmd1OptC = cmd1.Int("c", 0, "option c")
)

// cmd2 subcommand
var (
    cmd2     = flag.NewFlagSet("cmd2", flag.ExitOnError)
    cmd2OptF = cmd2.String("f", "", "option f")
)

func init() {
    log.SetFlags(0)
}

func main() {
    if len(os.Args) < 2 {
        help()
        os.Exit(1)
    }

    var (
        subcmd = subcommand(os.Args[1])
        args   = os.Args[2:]
    )

    switch subcmd {
    case subcmd1:
        cmd1.Parse(args)
        runCmd1()
    case subcmd2:
        cmd2.Parse(args)
        runCmd2()
    case subver:
        version()
        return
    case subhelp:
        help()
        return
    default:
        help()
        os.Exit(1)
    }
}

func runCmd1() {
    log.Printf("option c is %v", *cmd1OptC)
}

func runCmd2() {
    log.Printf("option f is %v", *cmd2OptF)
}

func version() {
    info, ok := debug.ReadBuildInfo()
    if !ok {
        log.Println("error: call debug.ReadBuildInfo()")
        os.Exit(1)
    }

    for _, s := range info.Settings {
        if s.Key == "vcs.revision" {
            log.Printf("version: vX.Y.Z (%s)", s.Value)
            return
        }
    }
}

func help() {
    log.Println("Usage: app <cmd1|cmd2|version|help>")
    cmd1.Usage()
    cmd2.Usage()
}

以下のようなタスクファイルを用意。

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - go build -o app
      - ./app
      - ./app help
      - ./app version
      - ./app cmd1 -c 100
      - ./app cmd2 -f helloworld
      - ./app cmd1 -f helloworld
    ignore_error: true

実行すると以下のようになります。

$ task
task: [default] go build -o app
task: [default] ./app
Usage: app <cmd1|cmd2|version|help>
Usage of cmd1:
  -c int
        option c
Usage of cmd2:
  -f string
        option f
task: [default] ./app help
Usage: app <cmd1|cmd2|version|help>
Usage of cmd1:
  -c int
        option c
Usage of cmd2:
  -f string
        option f
task: [default] ./app version
version: vX.Y.Z (fe9c8eb719af1f17bcd9e2e7174c791e7ecd74c0)
task: [default] ./app cmd1 -c 100
option c is 100
task: [default] ./app cmd2 -f helloworld
option f is helloworld
task: [default] ./app cmd1 -f helloworld
flag provided but not defined: -f
Usage of cmd1:
  -c int
        option c

参考情報

github.com

gobyexample.com

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。