1. Home
  2. Golang
  3. GuideToBecomingGoDeveloper
  4. BuildingCLIs
  5. Urfave CLI - Golang learning step 3-1

Urfave CLI - Golang learning step 3-1

  • 公開日
  • カテゴリ:BuildingCLIs
  • タグ:Golang,roadmap.sh,学習メモ
Urfave CLI - Golang learning step 3-1

roadmap.sh > Go > Building CLIs > Urfave CLI の学習を進めていきます。

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

contents

  1. 開発環境
  2. 参考 URL
  3. Urfave CLI
  4. 特徴
  5. インストール
  6. CLI アプリケーションの実装
    1. 1, 最小構成のアプリケーション
    2. 2. Action - アクションの実装
    3. 3. Args - 引数の追加
    4. 4. Flags - フラグ
    5. 5. Commands - コマンド
    6. 6. Subcommands - サブコマンド

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

参考 URL

Urfave CLI

Urfave CLI は、Go でコマンドラインインターフェース (CLI) アプリケーションを作成するための人気ライブラリです。軽量で直感的な設計が特徴で、小規模から大規模な CLI アプリケーションの開発に適しています。

特徴

  1. コマンドとサブコマンドの管理
    • 複数のコマンドやサブコマンドを簡単に定義・管理できます。コマンド間で引数やオプションを柔軟に設定可能。
  2. 使いやすい API
    • シンプルな API により、初心者でも簡単に CLI を構築できる。
  3. カスタマイズ性
    • ヘルプメッセージやエラーハンドリングなど、アプリケーションの挙動を柔軟にカスタマイズ可能。
  4. 軽量で高速
    • 必要最低限の依存関係で動作するため、Go のエコシステムに馴染みやすい設計。

インストール

Urfave CLI をインストールするには、以下のコマンドを実行します。

go get github.com/urfave/cli/v2

CLI アプリケーションの実装

1, 最小構成のアプリケーション

以下の最小限のコードでコマンドを実行できます。

package main

import (
  "os"

  "github.com/urfave/cli/v2"
)

func main() {
  (&cli.App{}).Run(os.Args)
}

上記プログラムをコンパイルし、実行します。動作しますが、何もアクションを実装していないため、ヘルプメッセージが表示されます。

コンパイル:

# hello という名前でコンパイル
go build -o hello

実行:

./hello

2. Action - アクションの実装

次に、ここにメッセージを出力するアクションを実装し、hello world を出力できるようにします。

package main

import (
  "fmt"
  "log"
  "os"

  "github.com/urfave/cli/v2"
)

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "挨拶します",
    Action: func(*cli.Context) error {
      fmt.Println("hello world")
      return nil
    },
  }

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

コンパイルし、実行すると、hello world と表示されます。

# コンパイル
go build -o hello

# 実行
./hello
# => hello world

3. Args - 引数の追加

コマンド実行時に引数を渡して、それをメッセージに表示するようにします。

アクションの関数に引数 Context に任意の変数名(ここではctx)を設定すると、ctx.Args() で引数を受け取れます。

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "挨拶します",
    Action: func(ctx *cli.Context) error {
      fmt.Printf("Hello %s!!\n", ctx.Args().Get(0))
      return nil
    },
  }

  if err := app.Run(os.Args); err != nil {
    log.Fatal(err)
  }
}
# コンパイル
go build -o hello

# 実行
./hello rito     
# => Hello rito!!

4. Flags - フラグ

引数では、シンプルにコマンドの後ろに値を入力しました。次にフラグを渡して渡す値をわかりやすくします。

フラグとは、コマンドの --lang--name のような形式のオプションを指します。一般的に 「フラグ」(flags) または 「オプション引数」(optional arguments) と呼ばれます。

以下は、--lang--name のフラグを追加しています。

func main() {
  app := &cli.App{
    Name:  "hello",
    Usage: "挨拶します",
    Flags: []cli.Flag{
      // 1 つ目のフラグ name を追加
      &cli.StringFlag{
        Name:  "name", // name フラグ
        Value: "you",  // デフォルト値は "you"
        Usage: "相手の名前",
      },
      // 2 つ目のフラグ lang を追加
      &cli.StringFlag{
        Name:  "lang",    // lang フラグ
        Value: "english", // デフォルト値は "english"
        Usage: "挨拶の言語",
      },
    },
    Action: func(ctx *cli.Context) error {
      name := ctx.String("name") // name フラグの値を取得

      var hello string
      lang := ctx.String("lang") // lang フラグの値を取得
      switch lang {
      case "japanese":
        hello = "こんにちは"
      case "korean":
        hello = "안녕하세요"
      case "spanish":
        hello = "Hola"
      default:
        hello = "Hello"
      }
      fmt.Printf("%s %s!!\n", hello, name)
      return nil
    },
  }

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

フラグにはデフォルト値を設定できます。Value がそれに当たります。つまり、オプション引数を指定しない場合は、デフォルト値として Value に指定した値が設定されます。

# オプション引数を指定した場合:
./hello --lang korean --name rito 
# => 안녕하세요 rito!!

# オプション引数を指定しない場合:
./hello
# => Hello you!!

5. Commands - コマンド

Commands(コマンド)を使うと、複数のアクションを設定できます。これまで hello アクションを実装しましたが、ここに thank アクションも定義することで、両方どちらも使えるようになります。

func main() {
  app := &cli.App{

    Commands: []*cli.Command{
      {
        Name:  "hello",
        Usage: "挨拶します",
        Flags: []cli.Flag{
          &cli.StringFlag{
            Name:  "name", // name フラグ
            Value: "you",  // デフォルト値は "you"
            Usage: "相手の名前",
          },
          &cli.StringFlag{
            Name:  "lang",    // lang フラグ
            Value: "english", // デフォルト値は "english"
            Usage: "挨拶の言語",
          },
        },
        Action: func(ctx *cli.Context) error {
          name := ctx.String("name") // Name フラグの値を取得

          var hello string
          lang := ctx.String("lang") // lang フラグの値を取得
          switch lang {
          case "japanese":
            hello = "こんにちは"
          case "korean":
            hello = "안녕하세요"
          case "spanish":
            hello = "Hola"
          default:
            hello = "Hello"
          }
          fmt.Printf("%s %s!!\n", hello, name)
          return nil
        },
      },
      {
        Name:  "thank",
        Usage: "感謝します",
        Flags: []cli.Flag{
          &cli.StringFlag{
            Name:  "name", // name フラグ
            Value: "you",  // デフォルト値は "you"
            Usage: "相手の名前",
          },
          &cli.StringFlag{
            Name:  "lang",    // lang フラグ
            Value: "english", // デフォルト値は "english"
            Usage: "挨拶の言語",
          },
        },
        Action: func(ctx *cli.Context) error {
          name := ctx.String("name") // Name フラグの値を取得

          var thank string
          lang := ctx.String("lang") // lang フラグの値を取得
          switch lang {
          case "japanese":
            thank = "ありがとう"
          case "korean":
            thank = "고마워요"
          case "spanish":
            thank = "Gracias"
          default:
            thank = "Thank you"
          }
          fmt.Printf("%s %s!!\n", thank, name)
          return nil
        },
      },
    },
  }

  if err := app.Run(os.Args); err != nil {
    log.Fatal(err)
  }
}
# コンパイル
go build -o greet

# hello アクションを実行
./greet hello --name rito
# => Hello rito!!

# thank アクションを実行
./greet thank --name rito
# => Thank you rito!!

ヘルプを見てみると、2 つのアクションが使えるようになっていることがわかります。

% ./greet help             
NAME:
   greet - A new cli application

USAGE:
   greet [global options] command [command options]

COMMANDS:
   hello    挨拶します
   thank    感謝します
   help, h  Shows a list of commands or help for one command

6. Subcommands - サブコマンド

サブコマンドは以下のような場合に使用します。

  1. 機能のグループ化が必要な場合

例:ユーザー管理

app user create
app user list
app user delete
  1. 関連する操作をまとめる場合

例:データベース操作

app db migrate
app db rollback
app db seed
  1. リソース単位で操作を分類する場合

例:Kubernetes ライクな構造

app pod start
app pod stop
app service create
app service delete

このような階層構造により、コマンドが整理され、ヘルプも見やすくなります。

これまでの実装に、wake(寝ている人を起こす)アクションをサブコマンドを使って「優しく起こす」と「強めに起こす」の 2 パターンで実装してみます。

func main() {
  app := &cli.App{

    Commands: []*cli.Command{
      {
        Name:  "hello",
        .
        .
      },
      {
        Name:  "thank",
        .
        .
      },
      {
        Name:    "wake",
        Usage:   "寝ている人を起こします",
        Aliases: []string{"w"},
        Subcommands: []*cli.Command{
          {
            Name:    "gentle",
            Aliases: []string{"g"},
            Usage:   "優しく起こします",
            Flags: []cli.Flag{
              &cli.StringFlag{
                Name:    "name",
                Aliases: []string{"n"},
                Value:   "you",
                Usage:   "起こす人の名前",
              },
              &cli.StringFlag{
                Name:    "time",
                Aliases: []string{"t"},
                Value:   "",
                Usage:   "現在時刻(デフォルトは現在時刻)",
              },
            },
            Action: func(ctx *cli.Context) error {
              name := ctx.String("name")
              timeStr := ctx.String("time")

              currentTime := time.Now()
              if timeStr != "" {
                parsedTime, err := time.Parse("15:04", timeStr)
                if err == nil {
                  currentTime = parsedTime
                }
              }

              var message string
              hour := currentTime.Hour()
              switch {
              case hour < 5:
                message = fmt.Sprintf("(小声で)%sさん、まだ深夜ですが...必要があって起こしました。申し訳ありません。", name)
              case hour < 12:
                message = fmt.Sprintf("おはようございます、%sさん。素敵な朝をお迎えください。朝食の用意ができています。", name)
              default:
                message = fmt.Sprintf("%sさん、お昼を過ぎていますよ。ゆっくり起きましょう。", name)
              }

              fmt.Println(message)
              return nil
            },
          },
          {
            Name:    "strong",
            Aliases: []string{"s"},
            Usage:   "強めに起こします",
            Flags: []cli.Flag{
              &cli.StringFlag{
                Name:    "name",
                Aliases: []string{"n"},
                Value:   "you",
                Usage:   "起こす人の名前",
              },
              &cli.StringFlag{
                Name:    "time",
                Aliases: []string{"t"},
                Value:   "",
                Usage:   "現在時刻(デフォルトは現在時刻)",
              },
            },
            Action: func(ctx *cli.Context) error {
              name := ctx.String("name")
              timeStr := ctx.String("time")

              currentTime := time.Now()
              if timeStr != "" {
                parsedTime, err := time.Parse("15:04", timeStr)
                if err == nil {
                  currentTime = parsedTime
                }
              }

              var message string
              hour := currentTime.Hour()
              switch {
              case hour < 5:
                message = fmt.Sprintf("%sさん!!緊急事態です!!直ちに起きてください!!", name)
              case hour < 12:
                message = fmt.Sprintf("%sさん!もう朝ですよ!!遅刻しますよ!!急いで起きてください!!", name)
              default:
                message = fmt.Sprintf("%sさん!!こんな時間まで寝てるんですか!?早く起きてください!!", name)
              }

              fmt.Println(message)
              return nil
            },
          },
        },
        Action: func(ctx *cli.Context) error {
          fmt.Println("'gentle' または 'strong' サブコマンドを指定してください。")
          fmt.Println("使用例:")
          fmt.Println("  wake gentle --name 田中 --time 07:30")
          fmt.Println("  wake strong --name 田中")
          return nil
        },
      },
    },
  }

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

ヘルプは以下のようになります。

% ./greet help
NAME:
   greet - A new cli application

USAGE:
   greet [global options] command [command options]

COMMANDS:
   hello    挨拶します
   thank    感謝します
   wake, w  寝ている人を起こします
   help, h  Shows a list of commands or help for one command
% ./greet wake help
NAME:
   greet wake - 寝ている人を起こします

USAGE:
   greet wake command [command options]

COMMANDS:
   gentle, g  優しく起こします
   strong, s  強めに起こします
   help, h    Shows a list of commands or help for one command

OPTIONS:
   --name value, -n value  起こす人の名前 (default: "you")
   --time value, -t value  現在時刻(デフォルトは現在時刻)
   --help, -h              show help

実際に実行してみると以下のような結果になります。

# 優しく起こす
 % ./greet wake gentle --name 田中 --time 07:30
おはようございます、田中さん。素敵な朝をお迎えください。朝食の用意ができています。

# 強めに起こす
% ./greet wake strong --name 田中
田中さん!!こんな時間まで寝てるんですか!?早く起きてください!!

また、サブコマンドを指定しないときの表示も最下部のアクションで実装しています。

# サブコマンドを指定せず実行
% ./greet wake       
'gentle' または 'strong' サブコマンドを指定してください。
使用例:
  wake gentle --name 田中 --time 07:30
  wake strong --name 田中

まとめ

  • Urfave CLI は、Go 言語で CLI アプリケーションを構築するための軽量で人気のライブラリ
  • コマンドとサブコマンドを簡単に定義・管理できる高い柔軟性
  • 軽量で必要最低限の依存関係による高速な動作
  • シンプルな API による使いやすさと直感的な設計
  • ヘルプメッセージやエラーハンドリングを自由にカスタマイズ可能
  • 小規模から大規模なプロジェクトまで幅広く対応できる汎用性


[Next] Step 3-2: Cobra

[Prev] Step 2-13: Marshalling & Unmarshalling JSON

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級