2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

cobraで超シンプルなCLIを作ってCLIの作り方の基本を学ぶ

Last updated at Posted at 2025-04-18

はじめに

CLIの作成に関して個人的にブラックボックスすぎたので、表面的なとこに触れておいていざちゃんと作ろうとした時の心理的ハードルをなるべく無くしておきたいと思い、試しにMarkdownで記事を書くときのちょっとしたサポートを目的としたCLIを作ってみることにしました。
実際に作ったのは、ルートコマンド サブコマンド ファイルパスみたいな感じのコマンドで指定したファイルの末尾に何かしらテキストを挿入するだけの超シンプルなCLIです。
例えばこんな感じです。

# コマンド
msc link ファイルパス

# 結果(指定ファイルの末尾(改行含む)
[タイトル](URL/ "")

これからGoでCLIを作ってみたいけど、そもそもCLIってどうやって作るものなのか把握しておきたい方のご参考になれば幸いです。

結果的に作ったもの

msc link [<ファイルパス>]             タイトル付きリンクのテンプレ生成

msc details [<ファイルパス>]          詳細折りたたみのテンプレ生成

msc snip [<言語>] [<ファイルパス>]    コードスニペットを追加

msc table [<ファイルパス>]            テーブルのテンプレ生成

msc all [<ファイルパス>]              全テンプレ生成

msc version                           バージョン表示

msc help                              このヘルプを表示

Githubリポジトリ

実装例

ルートコマンド

package cmd

import (
	"os"

	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "msc <command> [options]",
	Short: "Markdown補助CLI(md-sup-cli)",
	Long: `md-sup-cliはMarkdownの少しだけ面倒な記法のテンプレをファイル末尾に追記するCLIです。
`,
}

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

サブコマンド(一部)

package cmd

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

var detailsCmd = &cobra.Command{
	Use:   "details <file>",
	Short: "折りたたみ詳細をファイル末尾に追記",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		path := args[0]
		snippet := `<details><summary>折りたたみタイトル</summary>

</details>
`
		f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
		if err != nil {
			return err
		}
		defer f.Close()
		fmt.Fprint(f, "\n\n"+snippet)
		return nil
	},
}

func init() {
	rootCmd.AddCommand(detailsCmd)
}

cobraで雛形生成

Goにはcobraという簡単にCLIが作れる便利なライブラリがあるようです。
実際に使ってみます。

go install github.com/spf13/cobra-cli@latest

以下のようにして雛形が生成できます。

cobra-cli init

生成された雛形は以下のようになっていると思います。

.
├── LICENSE
├── cmd
│   └── root.go
├── go.mod
├── go.sum
└── main.go

main.goではcmd/root.goのCLIエントリポイントであるExcute()関数を実行しています。
cmd/root.goにはコマンドの作成ガイドと、実際にそのコマンドをルートコマンドに設定しCLIを起動するExcute()があります。cmd配下で自分の作りたいコマンドを作成していくことになります。

各部分メモ

&cobra.Command {}

コマンドを作るための構造体です。主要そうなものだとUse、Short、Long、Run、RunE、Argsなど様々なCLIコマンドの設定ができるフィールドが実装されています。


RunとRunE

コマンド実行時の処理を設定できます。RunEは返り値にerrorを返せます。エラー返したいならRunE、標準出力だけなどエラー返すほどでもないならRunという感じだと認識していますが、基本RunEを使ったほうが良さそうな雰囲気を感じました。
ここまでで登場したものを扱って実装すると以下のようになります。

var versionCmd = &cobra.Command{
	Use:   "version",
	Short: "バージョンを表示",
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println(cliVersion)
	},
}

これは単純にCLIのバージョンを標準出力するだけのものです。このようにUse(実際のCLIのコマンド名を定義)やShortなど、その他自分の求めている機能が表現できるフィールドを書き、上記のように書いていきます。


CLIエントリポイント

下記のコードはCLI自体の起動のためのエントリポイントです。ここでrootCmdを指定してrootcmdをこのCLIのルートコマンドに指定します。

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

init関数内で以下のようにすることでサブコマンドを設定できます(例:CLIでよく見る
msc snipみたいなのができるようになる)。

ルートコマンド変数.AddCommand(サブコマンド変数)

ファイルを開き実際に書き込む

ファイルを書き込み専用で開き、書き込みたい内容をファイルの末尾に追加できるようにします。pathは書き込みたいファイルパスです。
0644は所有者は読み書きOK、他は読取りだけOKというパーミッション設定です。

f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
  return err
}
defer f.Close()

fは書き込み先のファイルオブジェクトで、fmtパッケージのFprintで指定した書き込み先に文字列を出力できます。Fprintは内部実装でWrite()を呼び出しています。

fmt.Fprint(f, 書き込みたい内容)

デフォルトのコマンドを上書き(-h, --help)

以下のようにすることで、conbraで内部実装されている自動でヘルプを生成する関数が自動生成する内容を上書きできます。

func init() {
	// SetHelpFuncで-hと--helpを上書き
	rootCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
		fmt.Print(helpText)
	})

	rootCmd.AddCommand(helpCmd)
}

GoReleaserも使ってみる

ついでにGoReleaserを使ってビルドして生成したバイナリを配布するのもやってみました。
GoReleaserは主に以下のことをしてくれます。

  • 各OS用にそれぞれビルド
  • ビルドしたバイナリをOS毎に圧縮
  • GitHub Releasesへのアップロード
    など

まずGoReleaserの設定をします。

.goreleaser.yml
project_name: msc

release:
  github:
    owner: minminseo
    name: md-sup-cli

builds:
  - main: ./main.go
    binary: msc
    goos: # ここでOS指定
      - linux
      - darwin
      - windows
    goarch:
      - amd64
      - arm64

archives:
  - format: tar.gz

次にGitHubActionsのワークフローを設定します。
流れとしては、新しいタグがPush(例:git tag v1.0.1git push origin v1.0.1)されたらワークフローを起動→Ubuntu起動→ソースコードダウンロード→Goインストール→GoReleaserインストール→.goreleaser.ymlをもとにバイナリをビルドとアップロードという感じになります。

.github/workflows/release.yml
name: Release

on:
  push:
    tags:
      - "v*.*.*"

jobs:
  release:
    runs-on: ubuntu-latest

    permissions:
      contents: write

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-go@v4
        with:
          go-version: "1.24"

      - uses: goreleaser/goreleaser-action@v5
        with:
          version: latest
          args: release --rm-dist
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

さいごに

まだ特定のファイルに文字を書き込むだけの簡単なものしかできませんが、使い方に慣れてくればもっと便利で面白そうなものが作れそうだなと思いました。
こちらの記事によると、gohugoio/hugox-motemen/ghqなどがGoそのものの学習においても良いそうなのでこの当たりで学んでみたいなと思います。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?