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
- func NewFlagSet(name string, stderr io.Writer) *flag.FlagSet
- func ParseFlags(fs *flag.FlagSet, args []string) ([]string, int)
- type App
- type Command
- type Report
- func (r *Report) Add(status, name, detail string) *Report
- func (r *Report) AddConclusion(line string) *Report
- func (r *Report) AddFix(status, name, detail, fix string) *Report
- func (r *Report) Failures() int
- func (r *Report) Header(lines ...string) *Report
- func (r *Report) Items() []ReportItem
- func (r *Report) Render(w io.Writer)
- func (r *Report) Result(text string) *Report
- func (r *Report) Status() string
- func (r *Report) String() string
- func (r *Report) Warnings() int
- type ReportItem
- type Table
Constants ¶
const ( // ExitOK 成功。 ExitOK = 0 // ExitError 运行失败:子命令执行出错。 ExitError = 1 // ExitUsage 用法错误:缺少子命令、未知子命令、参数不合法。 ExitUsage = 2 )
退出码约定(写死在包里,宿主与子命令共用同一套语义)。
const ( // StatusOK 通过。 StatusOK = "ok" // StatusWarn 有问题但还能用——需要人看一眼,不算坏。 StatusWarn = "warn" // StatusFail 坏了。 StatusFail = "fail" )
检查项的状态。取值固定,Report 之外的任何判断都按这三个读。
Variables ¶
This section is empty.
Functions ¶
func NewFlagSet ¶
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 ¶
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 ¶
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。
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 (*Report) AddConclusion ¶ added in v0.31.0
AddConclusion 追加一行结论(写在检查项之后、结果行之前)。
它承载的是「检查项合起来说明什么」——判定、建议、范围一类的文字。
func (*Report) AddFix ¶ added in v0.31.0
AddFix 追加一个检查项,并带一条修复建议。
修复建议单独一行,缩进到名称列的起点——它讲的是这一项怎么修,挂在这一 项下面才读得懂。
func (*Report) Header ¶ added in v0.31.0
Header 追加头部信息行(键值对已由调用方格式化好)。
头部与检查项之间空一行——它们是两类东西:上面是「这是什么环境」,下面 是「查出来什么」。
func (*Report) Result ¶ added in v0.31.0
Result 设置最后一行结果,替换掉自动生成的那一行。
不调用它时,结果行按 Status() 自动生成(healthy / needs attention / failed)。调用方要自己的措辞时用它覆盖——两个程序对「什么算坏了」的 口径本就可能不同(有的把警告也算成「需要处理」,有的只算失败),措辞 跟着口径走。
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 是列对齐的文本表格。
列宽自适应:每列取该列所有单元格(含表头)的最大显示宽度,列与列之间 隔两个空格。最后一列不补尾随空格。
单元格里的换行原样写出(表格不折行)——折行会让「一格一行」的读法失效, 要折行请自己把内容拆成多行。