Help us shape the future of Go! Take the 2026 Go Developer Survey

cli

package
v0.31.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 6 Imported by: 0

Documentation

Overview

Package cli 提供框架无关、可单测的 CLI 子命令分发原语。

设计目标是「命令逻辑能被单元测试直接调用」:分发入口是 App.Run(ctx, args, stdout, stderr) int —— 参数与输出流全部由调用方传入。 本包既不读取进程参数,也不终止进程(不读 os.Args、不调 os.Exit), 因此测试无需起子进程、无需捕获 stdout,命令体可以像普通函数一样断言。

组成:

  • Command:单个子命令(名称 + 一行摘要 + 别名 + 执行体)
  • App:子命令集合的分发器,负责查找子命令、生成用法文本、映射退出码
  • NewFlagSet / ParseFlags:标准库 flag 的薄封装,把解析错误与用法 重定向到调用方给的 stderr(标准库默认写 os.Stderr,会破坏「输出流 由调用方给」的约定)
  • Table:列宽自适应的文本表格(中文按两列算宽度)
  • Report:自检报告(doctor 式清单)的渲染原语

退出码约定写死在包里,调用方不要另立一套:

ExitOK    = 0  成功
ExitError = 1  运行失败
ExitUsage = 2  用法错误(缺少子命令、未知子命令、参数不合法)

未知子命令与参数不合法同归 ExitUsage(2);子命令返回什么,整体就返回 什么(0→0、1→1、2→2)。

三件不必各写一遍的事:

  • 内建帮助:--help / -h / help 打印用法到 stdout,退出码 0。问帮助不是 错误,所以不是 2。宿主自己声明了名为 "help" 的子命令时它照常工作。
  • 内建版本:App.Version 非空时,--version / version 打印版本到 stdout。
  • 默认子命令:App.Default 非空时,无参等于执行它(以 stdio 提供服务的 进程通常没人给它传子命令名,而「没给子命令」在那种场景里等于 「按默认方式跑」)。

用法(main 里只做三件事:拼 args、给流、拿退出码):

func main() {
	app := &cli.App{
		Name:    "mytool",
		Default: "serve",
		Version: "1.0.0",
		Commands: []cli.Command{
			{Name: "build", Summary: "构建产物", Run: runBuild},
			{Name: "serve", Summary: "启动服务", Run: runServe},
			{Name: "doctor", Summary: "自检", Aliases: []string{"--doctor"}, Run: runDoctor},
		},
	}
	os.Exit(app.Run(context.Background(), os.Args[1:], os.Stdout, os.Stderr))
}

命令体(可直接被单测调用,无需起子进程):

func runBuild(ctx context.Context, args []string, stdout, stderr io.Writer) int {
	fs := cli.NewFlagSet("build", stderr)
	release := fs.Bool("release", false, "发布模式")
	rest, code := cli.ParseFlags(fs, args)
	if code != cli.ExitOK {
		return code
	}
	_ = release
	_ = rest
	return cli.ExitOK
}

用法文本由 Command.Summary 自动生成,调用方不需要手写:

  • App.Usage(w) 单独打印(宿主想接自己的 --help 分支时写到 stdout)
  • App.Run 在「缺少子命令」或「未知子命令」时把用法写到 stderr 并返回 ExitUsage(错误路径的诊断信息一律走 stderr,stdout 只留给命令体的 正常输出,便于管道)

Index

Constants

View Source
const (
	// ExitOK 成功。
	ExitOK = 0
	// ExitError 运行失败:子命令执行出错。
	ExitError = 1
	// ExitUsage 用法错误:缺少子命令、未知子命令、参数不合法。
	ExitUsage = 2
)

退出码约定(写死在包里,宿主与子命令共用同一套语义)。

View Source
const (
	// StatusOK 通过。
	StatusOK = "ok"
	// StatusWarn 有问题但还能用——需要人看一眼,不算坏。
	StatusWarn = "warn"
	// StatusFail 坏了。
	StatusFail = "fail"
)

检查项的状态。取值固定,Report 之外的任何判断都按这三个读。

Variables

This section is empty.

Functions

func NewFlagSet

func NewFlagSet(name string, stderr io.Writer) *flag.FlagSet

NewFlagSet 创建绑定到 stderr 的标准库 FlagSet。

两个关键点:

  • flag.ContinueOnError:解析出错不终止进程,而是返回 error,交由调用方 映射成退出码(ExitUsage)——与本包「不调 os.Exit」的约定一致;
  • fs.SetOutput(stderr):把标准库默认写到 os.Stderr 的错误与用法重定向 到调用方给的 stderr,遵守「输出流由调用方给」的约定。

典型用法:

fs := cli.NewFlagSet("build", stderr)
release := fs.Bool("release", false, "发布模式")
rest, code := cli.ParseFlags(fs, args)
if code != cli.ExitOK {
	return code
}

注意:FlagSet 不支持并发 Parse;每个子命令自建一个即可。

func ParseFlags

func ParseFlags(fs *flag.FlagSet, args []string) ([]string, int)

ParseFlags 用 fs 解析 args,返回剩余位置参数与退出码。

  • 解析成功:返回 (位置参数, ExitOK)
  • -h/--help:标准库已把用法写到 fs 的输出流(调用方给的 stderr), 返回 (位置参数, ExitOK)——帮助请求不算错误
  • 解析失败(未定义的 flag、取值类型不匹配等):标准库已把错误信息与 用法写到 fs 的输出流,返回 (nil, ExitUsage)

fs 为 nil 时返回 (nil, ExitUsage)。

Types

type App

type App struct {
	// Name 程序名,用于用法文本与错误信息;为空时退化为 "app"。
	Name string
	// Commands 子命令列表。用法文本按声明顺序展示(作者自己决定顺序),
	// 同名子命令以先声明的为准。
	Commands []Command
	// Default 无参时执行的子命令名。
	//
	// 为空则无参是用法错误(ExitUsage)——这是本包的默认行为,保持不变。
	// 填上之后,`prog` 与 `prog <Default>` 等价。
	//
	// 它解决的是「这个程序不接参数也能干活」的场景:以 stdio 提供服务的
	// 进程通常是被框架拉起来的,没人给它传子命令名,而「没给子命令」在
	// 别的程序里等于用法错误,在这里等于「按默认方式跑」。
	//
	// Default 指向不存在的子命令是宿主装配错误(ExitError),不是用法
	// 错误。
	Default string
	// Version 版本串。填上之后提供内建的 version 子命令与 --version 标志。
	//
	// 为空则不提供:此时 `version` / `--version` 是未知子命令(ExitUsage)。
	// 版本串由宿主自己给(构建期注入),本包不读环境变量也不猜。
	Version string
}

App CLI 应用:子命令集合的分发器。

App 本身不含可变状态,零值之外的结构体在 Run 之后仍可复用;并发调用 Run 是安全的(只读 Commands)。

func (*App) Run

func (a *App) Run(ctx context.Context, args []string, stdout, stderr io.Writer) int

Run 分发 args[0] 对应的子命令,返回其退出码。

行为约定:

  • args 为空(未给子命令):有 Default 则执行默认子命令;否则往 stderr 打印提示与用法,返回 ExitUsage(2)
  • 未知子命令:往 stderr 打印错误与用法,返回 ExitUsage(2)
  • 子命令名匹配:调用该子命令的 Run,args 去掉子命令名后透传,返回值 原样返回(0→0、1→1、2→2)
  • 命令表里有空名子命令(宿主装配错误):往 stderr 打印错误,返回 ExitError(1)——不是用户输入问题,不落到 2
  • `--help` / `-h` / `help`:把用法写到 stdout,返回 ExitOK(0)。 问帮助不是错误,所以不是 2
  • `--version` / `version`(且 App.Version 非空):把版本写到 stdout, 返回 ExitOK(0)

匹配顺序:子命令名 → 别名 → 内建帮助 → 内建版本。名字排在别名前面, 所以宿主自己声明了名为 "help" 的子命令时,它仍然照常工作。

本方法不读进程参数、不终止进程:args 与两个输出流全部由调用方给定, 因此可被单元测试直接调用。stdout/stderr 传 nil 时按丢弃处理,不会 panic。

func (*App) Usage

func (a *App) Usage(w io.Writer)

Usage 把用法文本写到 w。

用法文本包含程序名、各子命令名与其 Summary、以及退出码约定; Summary 为空的子命令只列名字。子命令按声明顺序展示。

默认子命令标注「(默认)」,带别名的标注「(别名:…)」。标注附在摘要 之后——名称列只按 ASCII 名字算宽度,标注不参与列宽计算,否则中文摘要 会把整列的对齐挤歪。

宿主想接自己的 --help 分支时,直接 Usage(os.Stdout) 即可; App.Run 内部的错误路径写的是 stderr。

type Command

type Command struct {
	// Name 子命令名,对应 args[0];不可为空(空名视为宿主装配错误)。
	Name string
	// Summary 一行摘要,用于自动生成用法文本;可为空(此时只列名字)。
	Summary string
	// Aliases 子命令的别名。
	//
	// 别名与名字同等对待:先在所有子命令里精确匹配名字,匹配不到再在
	// 所有子命令里匹配别名。它解决的是「同一个动作有两种叫法」这件事——
	// 比如历史上以 `--doctor` 调用的子命令,改成子命令形式后仍要认旧写法。
	//
	// 别名可以带前导连字符("--doctor"),也可以不带("doc")。为空的子
	// 命令没有别名。
	Aliases []string
	// Run 执行体。args 是去掉子命令名后的剩余参数,stdout/stderr 由宿主
	// 传入。返回值即退出码,原样透传给 App.Run 的调用方。
	Run func(ctx context.Context, args []string, stdout, stderr io.Writer) int
}

Command 单个 CLI 子命令。

type Report added in v0.31.0

type Report struct {
	// contains filtered or unexported fields
}

Report 是自检报告。

它只负责渲染,不负责判断:哪些检查项算失败由调用方通过 Failures()/ Warnings() 自己看,退出码也由调用方定。报告是给人看的,退出码是给脚本 看的,两者的口径不必相同。

func NewReport added in v0.31.0

func NewReport(title string) *Report

NewReport 建一份报告。title 是第一行(通常是「<程序名> doctor」)。

func (*Report) Add added in v0.31.0

func (r *Report) Add(status, name, detail string) *Report

Add 追加一个检查项。

func (*Report) AddConclusion added in v0.31.0

func (r *Report) AddConclusion(line string) *Report

AddConclusion 追加一行结论(写在检查项之后、结果行之前)。

它承载的是「检查项合起来说明什么」——判定、建议、范围一类的文字。

func (*Report) AddFix added in v0.31.0

func (r *Report) AddFix(status, name, detail, fix string) *Report

AddFix 追加一个检查项,并带一条修复建议。

修复建议单独一行,缩进到名称列的起点——它讲的是这一项怎么修,挂在这一 项下面才读得懂。

func (*Report) Failures added in v0.31.0

func (r *Report) Failures() int

Failures 返回失败(StatusFail)的检查项个数。

func (*Report) Header added in v0.31.0

func (r *Report) Header(lines ...string) *Report

Header 追加头部信息行(键值对已由调用方格式化好)。

头部与检查项之间空一行——它们是两类东西:上面是「这是什么环境」,下面 是「查出来什么」。

func (*Report) Items added in v0.31.0

func (r *Report) Items() []ReportItem

Items 返回检查项(副本)。

func (*Report) Render added in v0.31.0

func (r *Report) Render(w io.Writer)

Render 把报告写到 w。

func (*Report) Result added in v0.31.0

func (r *Report) Result(text string) *Report

Result 设置最后一行结果,替换掉自动生成的那一行。

不调用它时,结果行按 Status() 自动生成(healthy / needs attention / failed)。调用方要自己的措辞时用它覆盖——两个程序对「什么算坏了」的 口径本就可能不同(有的把警告也算成「需要处理」,有的只算失败),措辞 跟着口径走。

func (*Report) Status added in v0.31.0

func (r *Report) Status() string

Status 返回整份报告的状态:失败压过警告,两者都没有才是 ok。

func (*Report) String added in v0.31.0

func (r *Report) String() string

String 返回报告的文本。

形状:

<标题>
  <头部行>

  [OK  ] 检查项名   详情
         fix: 怎么修

<结论行>
result: healthy

func (*Report) Warnings added in v0.31.0

func (r *Report) Warnings() int

Warnings 返回警告(StatusWarn)的检查项个数。

type ReportItem added in v0.31.0

type ReportItem struct {
	// Status 状态:StatusOK / StatusWarn / StatusFail。别的值按未知渲染。
	Status string
	// Name 检查项的名称。
	Name string
	// Detail 一句话说明这一项查出了什么。
	Detail string
	// Fix 修复建议。可为空——为空就不写这一行。
	Fix string
}

ReportItem 是一个检查项。

type Table added in v0.31.0

type Table struct {
	// contains filtered or unexported fields
}

Table 是列对齐的文本表格。

列宽自适应:每列取该列所有单元格(含表头)的最大显示宽度,列与列之间 隔两个空格。最后一列不补尾随空格。

单元格里的换行原样写出(表格不折行)——折行会让「一格一行」的读法失效, 要折行请自己把内容拆成多行。

func NewTable added in v0.31.0

func NewTable(headers ...string) *Table

NewTable 建一个带表头的表格。

表头可以不给(传空即可),此时只有数据行。

func (*Table) Append added in v0.31.0

func (t *Table) Append(cells ...string)

Append 追加一行。

单元格少于列数时补空串,多于列数时多出来的部分被丢弃——表格的形状由 表头决定,不能被一行数据撑歪。没有表头时,形状由第一行决定。

func (*Table) Render added in v0.31.0

func (t *Table) Render(w io.Writer)

Render 把表格写到 w。

表头与数据行之间不加分隔线:CLI 表格通常很短,一条横线会让人误以为 下面那段是另一个区段。

没有表头时,形状由第一行决定——有些输出(如工具目录)本来就没有列名, 硬塞一个表头反而改变了它的形状。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL