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

cli

package
v0.21.0 Latest Latest
Warning

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

Go to latest
Published: Oct 21, 2026 License: MIT Imports: 5 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,会破坏「输出流 由调用方给」的约定)

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

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

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

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

func main() {
	app := &cli.App{
		Name: "mytool",
		Commands: []cli.Command{
			{Name: "build", Summary: "构建产物", Run: runBuild},
			{Name: "serve", Summary: "启动服务", Run: runServe},
		},
	}
	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
)

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

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
}

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 为空(未给子命令):往 stderr 打印提示与用法,返回 ExitUsage(2)
  • 未知子命令:往 stderr 打印错误与用法,返回 ExitUsage(2)
  • 子命令名匹配:调用该子命令的 Run,args 去掉子命令名后透传,返回值 原样返回(0→0、1→1、2→2)
  • 命令表里有空名子命令(宿主装配错误):往 stderr 打印错误,返回 ExitError(1)——不是用户输入问题,不落到 2

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

func (*App) Usage

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

Usage 把用法文本写到 w。

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

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

type Command

type Command struct {
	// Name 子命令名,对应 args[0];不可为空(空名视为宿主装配错误)。
	Name string
	// Summary 一行摘要,用于自动生成用法文本;可为空(此时只列名字)。
	Summary string
	// Run 执行体。args 是去掉子命令名后的剩余参数,stdout/stderr 由宿主
	// 传入。返回值即退出码,原样透传给 App.Run 的调用方。
	Run func(ctx context.Context, args []string, stdout, stderr io.Writer) int
}

Command 单个 CLI 子命令。

Jump to

Keyboard shortcuts

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