💻

【Go】promptuiでCLIツールを作ってbrewにリリースする

2024/10/09に公開

はじめに

SREとして日常的いろんなコマンドを実行することが多く、ターミナルとの付き合い時間が多いです。
自分がよくいろんなCLIツールを探したり、作業効率化を上がったりすることに楽しんでいます。
せっかくなので、今回は自分でもCLIツールを作ってみようと思いました。
本記事では、Go製のCLIツールを作るために、promptuiを使う予定です。

本記事のスコープ

  • promptui基本の使い方を紹介する
  • AWS各リージョンにあるEC2の情報、起動ステータス、インスタンスタイプ、想定の利用料金を表示するCLIツールを作る
  • 作ったCLIツールをbrewにリリースする
  • GitHub ActionsとGoreleaserを使って、リリースの自動化を行う

また、文字数が長くなるため、詳細なソースコードは割愛します。

promptuiの基本

promptuiは、Go製のコマンドラインツールを作るためのライブラリです。
主な機能は下記のようにとてもシンプルです:

  • Prompt: ユーザーの入力を受け付ける
  • Select: ユーザーの選択肢を受け付ける
  • Confirm: ユーザーの確認を受け付ける

Prompt

公式の例を見てみましょう:

package main

import (
	"errors"
	"fmt"
	"strconv"

	"github.com/manifoldco/promptui"
)

func main() {
	validate := func(input string) error {
		_, err := strconv.ParseFloat(input, 64)
		if err != nil {
			return errors.New("Invalid number")
		}
		return nil
	}

	prompt := promptui.Prompt{
		Label:    "Number",
		Validate: validate,
        // パスワードの場合は、Mask: '*'を指定する
        // Mask: '*',
	}

	result, err := prompt.Run()

	if err != nil {
		fmt.Printf("Prompt failed %v\n", err)
		return
	}

	fmt.Printf("You choose %q\n", result)
}

実行結果

❯ go run main.go
Number: 111
You choose "111"

// 数字以外の入力をした場合
✗ Number: 111aa█
>> Invalid number

主な特徴としては、カスタムのバリデーションを行うことができる点です。
そしてリアルタイムで検証できるためユーザー体験が良いです。

Select

公式の例を見てみましょう:

package main

import (
	"fmt"

	"github.com/manifoldco/promptui"
)

func main() {
	prompt := promptui.Select{
		Label: "Select Day",
		Items: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
			"Saturday", "Sunday"},
	}

	_, result, err := prompt.Run()

	if err != nil {
		fmt.Printf("Prompt failed %v\n", err)
		return
	}

	fmt.Printf("You choose %q\n", result)
}

実行結果

❯ go run main.go
Use the arrow keys to navigate: ↓ ↑ → ← 
? Select Day: 
  ▸ Monday
    Tuesday
    Wednesday
    Thursday
    Friday

// 選択した結果
✔ Sunday
You choose "Monday"

また、template機能も用意されていて、選択する度にリアルタイムで結果をプレイビューするなど複雑な実装も可能です。

package main

import (
	"fmt"
	"strings"

	"github.com/manifoldco/promptui"
)

type sushi struct {
	Name       string
	Ingredient string
	Price      int
}

func main() {
	sushiList := []sushi{
		{Name: "マグロ", Ingredient: "赤身マグロ", Price: 300},
		{Name: "サーモン", Ingredient: "生サーモン", Price: 250},
		{Name: "エビ", Ingredient: "茹でエビ", Price: 200},
		{Name: "イカ", Ingredient: "新鮮なイカ", Price: 180},
		{Name: "ウナギ", Ingredient: "蒲焼きウナギ", Price: 400},
		{Name: "タマゴ", Ingredient: "玉子焼き", Price: 150},
		{Name: "イクラ", Ingredient: "鮭の卵", Price: 350},
	}

	templates := &promptui.SelectTemplates{
		Label:    "{{ . }}?",
		Active:   "\U0001F363 {{ .Name | cyan }} ({{ .Price }}円)",
		Inactive: "  {{ .Name | cyan }} ({{ .Price }}円)",
		Selected: "\U0001F363 {{ .Name | red | cyan }}",
		Details: `
--------- 寿司の詳細 ----------
{{ "名前:" | faint }} {{ .Name }}
{{ "主な具材:" | faint }} {{ .Ingredient }}
{{ "価格:" | faint }} {{ .Price }}円`,
	}

	searcher := func(input string, index int) bool {
		sushi := sushiList[index]
		name := strings.Replace(strings.ToLower(sushi.Name), " ", "", -1)
		input = strings.Replace(strings.ToLower(input), " ", "", -1)

		return strings.Contains(name, input)
	}

	prompt := promptui.Select{
		Label:     "寿司を選んでください",
		Items:     sushiList,
		Templates: templates,
		Size:      4,
		Searcher:  searcher,
	}

	i, _, err := prompt.Run()

	if err != nil {
		fmt.Printf("プロンプトが失敗しました: %v\n", err)
		return
	}

	fmt.Printf("あなたが選んだのは %s です。価格は %d円です。\n", sushiList[i].Name, sushiList[i].Price)
}

実行結果

❯ go run main.go
Use the arrow keys to navigate: ↓ ↑ → ←  and / toggles search
寿司を選んでください?
  🍣 マグロ (300円)
    サーモン (250円)
    エビ (200円)
↓   イカ (180円)

--------- 寿司の詳細 ----------
名前: マグロ
主な具材: 赤身マグロ
価格: 300円

// 選択した結果
🍣 マグロ
あなたが選んだのは マグロ です。価格は 300円です。

Confirm

Confirmは、ユーザーの確認を受け付けるための機能です。
一番シンプルなので、公式の例を見てみましょう:

package main

import (
	"fmt"

	"github.com/manifoldco/promptui"
)

func main() {
	prompt := promptui.Prompt{
		Label:     "Delete Resource",
		IsConfirm: true,
	}

	result, err := prompt.Run()

	if err != nil {
		fmt.Printf("Prompt failed %v\n", err)
		return
	}

	fmt.Printf("You choose %q\n", result)
}

実行結果

❯ go run main.go
? Delete Resource? [y/N] █

// 選択した結果
Delete Resource: y
You choose "y"

自分のCLIツールを作る

今回はec2catというAWS各リージョンにあるEC2の情報、起動ステータス、インスタンスタイプ、想定の利用料金を表示するCLIツールを作ります。
実際に作ったものはこちらです。

Brewにリリースする

せっかく開発したCLIツールをbrewにリリースして、他の人にも使ってもらいましょう。

リポジトリの作成

まずは専用のリポジトリを作成しましょう。
命名規則はhomebrew-${CLIツール名}とします。
今回はhomebrew-ec2catにしました。

CREDITSの作成

CREDITSとは、ツールを配布する際に、そのツールを作成するために使用したライブラリの作者を記載するためのファイルです。
しかし一個ずつ記載するのは大変なので、gocreditsを使って自動生成するのが良いです。

brew install Songmu/tap/gocredits
gocredits . > CREDITS

GitHub Actionsの設定

続いて、リリースの自動化を行います。

goreleaser CLIのインストール

まずはgoreleaser CLIをインストールしましょう。
goreleaserは、Go製のCLIツールをリリースするためのツールです。
GitHub Actionsと合わせて使うことで、リリースの自動化を行うことができます。

brew install goreleaser

release.ymlの設定

続いて、.github/workflows/release.ymlを作成しましょう。

name: release
on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          distribution: goreleaser
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}

.goreleaser.ymlの設定

続いて、リポジトリのルートパスに.goreleaser.ymlを作成しましょう。

version: 2
project_name: ec2cat
env:
  - GO111MODULE=on
before:
  hooks:
    - go mod tidy
builds:
  - main: .
    binary: ec2cat
    ldflags: -s -w -X main.version={{.Version}} -X main.revision={{.ShortCommit}} -X main.date={{.Date}}
archives:
  - format: tar.gz
    name_template: >-
      {{ .ProjectName }}_
      {{- title .Os }}_
      {{- if eq .Arch "amd64" }}x86_64
      {{- else if eq .Arch "386" }}i386
      {{- else }}{{ .Arch }}{{ end }}
    files:
      - LICENSE
      - CREDITS
release:
  prerelease: auto

changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'
      - Merge pull request
      - Merge branch

brews:
  - repository:
      owner: the-exile-110 # GitHubのユーザー名
      name: homebrew-ec2cat # リポジトリ名
      token: "{{ .Env.TAP_GITHUB_TOKEN }}" 
    homepage: 'https://github.com/the-exile-110/homebrew-ec2cat' # リポジトリのホームページ
    description: 'ec2cat is a command line tool to list and filter EC2 instances' # ポジトリの説明
    license: "MIT"

トークンの発行

yaml編集の際に、TAP_GITHUB_TOKENを書いてあるため、実際にトークンを発行してGitHub Actionsのシークレットとして設定します。
こちらから発行できます。

  • Token name: ec2cat
  • Expiration: 今回は30日
  • Repository access: Only select repositories -> homebrew-ec2cat
  • Repository permissions
    • Contents: Read and write
    • Metadata: Read-only

シークレットの設定

発行したトークンをGitHub Actionsのシークレットとして設定します。
https://github.com/<ユーザー名>/<リポジトリ名>/settings/secrets/actions/newにアクセスして、TAP_GITHUB_TOKENを設定します。

リリース・動作確認

全ての設定が完了したので、実際にタグを発行してリリースします。

git tag v0.0.1
git push origin v0.0.1

リリースが成功したら、brewでインストールして動作確認します。

brew tap the-exile-110/ec2cat
brew install ec2cat
❯ ec2cat
✔ gogo
Selected AWS profile: gogo
✓ Retrieving all AWS regions...
Retrieved 17 regions
✓ Checking EC2 instances in each region...
Time taken to check regions: 2.084923s
Found 2 regions with EC2 instances
✔ View all regions
Retrieving EC2 instances from all regions...
+----------------+---------------+---------------------+---------------+---------+---------------------+---------------+-----------------------+------------+
|     REGION     | INSTANCE NAME |     INSTANCE ID     | INSTANCE TYPE |  STATE  |     LAUNCH TIME     | TOTAL RUNTIME | ESTIMATED HOURLY COST | TOTAL COST |
+----------------+---------------+---------------------+---------------+---------+---------------------+---------------+-----------------------+------------+
| us-east-1      | test-2        | i-062a75268f29f648d | t3.micro      | running | 2024-10-06 08:06:42 | 0d 7h 7m      | $0.0104               | $0.0741    |
+                +---------------+---------------------+---------------+---------+---------------------+---------------+-----------------------+------------+
|                | test          | i-0be81ad68d76a071e | t3.small      | running | 2024-10-06 08:02:30 | 0d 7h 11m     | $0.0884               | $0.6359    |
+----------------+---------------+---------------------+---------------+---------+---------------------+---------------+-----------------------+------------+
| ap-northeast-1 | test          | i-08be43355bb37b8fb | t3.micro      | running | 2024-10-06 07:04:01 | 0d 8h 10m     | $0.0136               | $0.1111    |
+----------------+---------------+---------------------+---------------+---------+---------------------+---------------+-----------------------+------------+

Total:
Estimated Total Hourly Cost: $0.1124
Total Cost: $0.8211

おわりに

今回はpromptuiを使ってCLIツールを作ってbrewにリリースするまでの一連の流れを紹介しました。
promptuiは、シンプルながらも柔軟なカスタマイズが可能なので、ぜひ使ってみてください。

Discussion