本教程用最直接的方式教你从零到一做一个可发布的 HelloWorld 命令行工具:选语言、搭框架、解析参数、做交互、打包发布与本地化,带着例子一步步跑通,适合想把小脚本变成可复用产品的人。


先说目标与前提
想象一下,你写了一个小脚本,经常在终端里敲来敲去,如果把它做成一个规范的 CLI(Command Line Interface),别人可以直接安装使用,你也能版本管理、做更新、做国际化、写文档。这个教程的目标是:
- 让你理解 CLI 的基本组成:命令、参数、选项、帮助与版本;
- 给出两种常见实现路径的实战示例:Node.js(快速上手)和 Go(单文件二进制利器);
- 覆盖测试、打包、跨平台发布与简单的本地化策略;
- 用费曼式的讲解方法,拆解每一步的“为什么”和“怎么做”。
把 CLI 想清楚:核心概念
把 CLI 想成一台小机器,输入(命令与参数)进入,机器处理,输出(标准输出、返回码、日志)。关键点在于接口设计和用户体验:
- 命令(command):主要动作词,比如 hello、init、build;
- 子命令(subcommand):类似 git 的 commit、push;
- 选项(flags):短格式 -v,长格式 –verbose;
- 参数(args):位置参数,如文件名、用户名;
- 帮助(help)与版本(version):每个 CLI 必备;
- 返回码(exit code):0 表示成功,非 0 表示错误类型。
为什么选择 Node.js 或 Go?
两者各有优缺点,实践中常见选择:
- Node.js:生态丰富,快速开发,npm 发布简单,适合脚本型工具与交互式 CLI;
- Go:编译为单一二进制,启动快,易于交付给没有 Node 环境的用户,适合系统级工具。
实战一:用 Node.js 做 HelloWorld CLI(一步步)
准备工作
先要有 Node.js(建议 16+)和 npm。目录结构很简单:
hello-cli/
package.json
bin/
hello.js
初始化项目
执行 npm init,把 package.json 填好。关键是 bin 字段,它告诉 npm 如何把脚本链接成可执行命令:
{
"name": "hello-cli",
"version": "0.1.0",
"bin": {
"hello": "./bin/hello.js"
},
"dependencies": {
"commander": "^10.0.0"
}
}
编写入口脚本
用 commander(或 yargs)解析参数:
#!/usr/bin/env node
const { program } = require('commander');
program
.name('hello')
.description('A small HelloWorld CLI')
.version('0.1.0');
program
.command('say ')
.description('Say hello to someone')
.option('-u, --uppercase', 'Uppercase the output')
.action((name, opts) => {
let msg = Hello, ${name}!;
if (opts.uppercase) msg = msg.toUpperCase();
console.log(msg);
});
program.parse(process.argv);
注意文件头的 #!/usr/bin/env node,保证脚本能被系统解释执行。
本地测试与安装
- 在项目根运行
npm link,会在全局把hello命令链接到你的脚本; - 然后可以直接执行
hello say Alice; - 调试时注意权限与换行,Windows 上可能要把换行转换为 CRLF。
添加帮助与例子
好用的 CLI 要有精准的帮助信息:
- 在 command 的 description 中写清用途;
- 示例可以放在 README;
- 尽量把常用选项放在 top-level,复杂子命令拆分文件。
实战二:用 Go 做 HelloWorld CLI(一步步)
为什么用 Cobra
Cobra 是 Go 社区很流行的框架,类似 commander,但更偏向静态编译的二进制工具结构,这里用最简示例:
项目结构
hello-go/
cmd/
root.go
say.go
main.go
go.mod
关键代码片段
// main.go
package main
import "hello-go/cmd"
func main() {
cmd.Execute()
}
// cmd/root.go
package cmd
import (
"github.com/spf13/cobra"
"fmt"
"os"
)
var rootCmd = &cobra.Command{
Use: "hello",
Short: "Hello is a simple CLI",
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
// cmd/say.go
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"strings"
)
var uppercase bool
var sayCmd = &cobra.Command{
Use: "say [name]",
Short: "Say hello to someone",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
msg := fmt.Sprintf("Hello, %s!", args[0])
if uppercase {
msg = strings.ToUpper(msg)
}
fmt.Println(msg)
},
}
func init() {
sayCmd.Flags().BoolVarP(&uppercase, "uppercase", "u", false, "Uppercase the output")
rootCmd.AddCommand(sayCmd)
}
构建与发布
- 编译:
go build -o hello,得到单个可执行文件; - 交叉编译:设置 GOOS/GOARCH(例如 GOOS=windows GOARCH=amd64 go build -o hello.exe);
- 发布工具:goreleaser 可以自动打包多平台二进制并生成 GitHub Release。
测试、日志与返回码
别小看测试,CLI 的行为需要被稳定地断言:
- 写单元测试:针对解析逻辑、子命令行为;
- 做集成测试:用子进程调用可执行文件,断言 stdout、stderr 与 exit code;
- 日志分级:交互信息打印到 stdout,错误信息打印到 stderr;
- 规范返回码:0 成功,1 一般错误,其他按需定义并文档化。
打包、分发与版本管理
根据用户群体选择分发方式:
- Node 发布到 npm:只要 package.json 的 bin 字段正确,用户安装后就能全局使用;
- Go 发布二进制到 GitHub Releases 或在各平台打包为 zip/tar.gz;
- 使用包管理器:Homebrew(macOS)、apt(Debian/Ubuntu 借助仓库)、scoop(Windows)来提升安装体验;
- CI/CD:把测试、构建、签名和发布接入 GitHub Actions 或其他流水线。
简单的本地化(i18n)策略
如果你的 CLI 面向多语种用户,信息提示要本地化,思路并不复杂:
- 抽离文本资源为键值文件(JSON/YAML/PO);
- 检测环境变量 LANG 或提供 –lang 选项;
- 在 Node.js 中用 i18next、在 Go 中用 go-i18n;
- 注意日期、数字和字符编码(UTF-8)。
常见陷阱与调试技巧
- 权限问题:Linux/macOS 上要确保可执行位;
- 换行与编码:脚本头部的 shebang 在 Windows 无效,注意 CRLF;
- 路径问题:相对路径在安装为全局命令后常常错位,使用 __dirname(Node)或 embed(Go 1.16+)管理资源;
- 依赖管理:Node 项目要锁定版本(package-lock.json),Go 用 go.mod;
- 错误可恢复性:尽量给用户可操作的错误提示,而不是堆栈追踪(除非加上 –debug)。
操作速查表(常用命令)
| 操作 | Node | Go |
| 本地测试为全局命令 | npm link | go build -o hello |
| 发布 | npm publish | 上传二进制到 Release(可用 goreleaser) |
| 交叉编译 | 使用 pkg 或 ncc 打包为可执行 | GOOS=linux GOARCH=amd64 go build |
举个小例子:让它可国际化的 Node 实现
只说明思路就好:把提示语写成键值对,运行时根据环境载入对应语言包。一个简单的文件结构:
locales/
en.json
zh.json
bin/hello.js
运行时读取 process.env.LANG 或 –lang 参数,选择合适的语言文本来拼装输出。
费曼式回顾(把复杂说简单)
记住三句话:输入、处理、输出。设计好命令接口就是给用户画一条清晰的输入路径;实现时选择合适的工具(快速原型就 Node,发布交付首选 Go);发布与维护才是长期价值:测试、文档、版本与本地化。
最后一点随笔(边想边写的味道)
我自己做过很多小工具,最常见的坑不是代码,是交付体验:README 写不好、错误提示不友好、安装难——这些会让一个原本有用的工具石沉大海。因此从一开始就把“别人能轻松安装并理解”当成设计目标,会让后续的维护轻快很多。好了,先到这儿,走一步算一步,你试着做个简单版本先,遇到具体问题再回来看这些步骤,反复迭代就行。