命令行工具:用 Go 构建优雅的 CLI 应用

学习如何用 Go 构建专业的命令行工具,包括参数解析、子命令、交互式输入等

命令行工具:用 Go 构建优雅的 CLI 应用

Go 语言非常适合构建命令行工具。事实上,很多知名的 CLI 工具都是用 Go 写的:Docker、Kubernetes、Hugo、Terraform……

Go 的标准库提供了基础的命令行参数解析功能,而社区也涌现出了很多优秀的第三方库(如 cobra、urfave/cli),让构建复杂的 CLI 应用变得简单。

今天我们就来学习如何用 Go 构建专业的命令行工具。

基础:os.Args

最简单的方式是直接使用 os.Args

package main

import (
	"fmt"
	"os"
)

func main() {
	// os.Args[0] 是程序名
	// os.Args[1:] 是命令行参数
	fmt.Println("程序名:", os.Args[0])
	fmt.Println("参数:", os.Args[1:])
	
	if len(os.Args) < 2 {
		fmt.Println("用法: myapp <name>")
		os.Exit(1)
	}
	
	name := os.Args[1]
	fmt.Printf("Hello, %s!\n", name)
}

运行:

$ go run main.go World
程序名: /tmp/go-build.../main
参数: [World]
Hello, World!

flag 包:标准库的参数解析

flag 包提供了基础的命令行参数解析:

package main

import (
	"flag"
	"fmt"
)

func main() {
	// 定义参数
	name := flag.String("name", "World", "要问候的人的名字")
	age := flag.Int("age", 0, "年龄")
	verbose := flag.Bool("verbose", false, "是否显示详细信息")
	
	// 解析参数
	flag.Parse()
	
	// 使用参数
	if *verbose {
		fmt.Printf("详细信息: name=%s, age=%d\n", *name, *age)
	}
	
	fmt.Printf("Hello, %s!", *name)
	if *age > 0 {
		fmt.Printf(" You are %d years old.", *age)
	}
	fmt.Println()
	
	// 获取非标志参数
	args := flag.Args()
	if len(args) > 0 {
		fmt.Println("其他参数:", args)
	}
}

运行:

$ go run main.go -name=Alice -age=30 -verbose
详细信息: name=Alice, age=30
Hello, Alice! You are 30 years old.

$ go run main.go --help
Usage of main:
  -age int
    	年龄
  -name string
    	要问候的人的名字 (default "World")
  -verbose
    	是否显示详细信息

自定义标志类型

package main

import (
	"flag"
	"fmt"
	"strings"
)

// 自定义类型:逗号分隔的字符串列表
type StringList []string

func (s *StringList) String() string {
	return strings.Join(*s, ", ")
}

func (s *StringList) Set(value string) error {
	*s = strings.Split(value, ",")
	return nil
}

func main() {
	var tags StringList
	flag.Var(&tags, "tags", "标签列表(逗号分隔)")
	
	flag.Parse()
	
	fmt.Println("标签:", tags)
}

运行:

$ go run main.go -tags=go,cli,tutorial
标签: [go cli tutorial]

Cobra:专业的 CLI 框架

对于复杂的 CLI 应用,推荐使用 Cobra,它是 Go 社区最流行的 CLI 框架。

安装 Cobra

go get -u github.com/spf13/cobra/cobra

基础示例

package main

import (
	"fmt"
	"os"
	
	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "myapp",
	Short: "MyApp 是一个示例 CLI 工具",
	Long:  `MyApp 是一个用 Go 和 Cobra 构建的示例 CLI 工具。`,
	Run: func(cmd *cobra.Command, args []string) {
		fmt.Println("Hello from MyApp!")
	},
}

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

添加子命令

package main

import (
	"fmt"
	
	"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
	Use:   "myapp",
	Short: "MyApp 是一个示例 CLI 工具",
}

var greetCmd = &cobra.Command{
	Use:   "greet [name]",
	Short: "问候某人",
	Args:  cobra.MaximumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		name := "World"
		if len(args) > 0 {
			name = args[0]
		}
		
		// 获取标志
		times, _ := cmd.Flags().GetInt("times")
		
		for i := 0; i < times; i++ {
			fmt.Printf("Hello, %s!\n", name)
		}
	},
}

func init() {
	// 添加标志
	greetCmd.Flags().IntP("times", "t", 1, "问候次数")
	
	// 注册子命令
	rootCmd.AddCommand(greetCmd)
}

func main() {
	rootCmd.Execute()
}

运行:

$ myapp greet Alice -t 3
Hello, Alice!
Hello, Alice!
Hello, Alice!

$ myapp greet --help
问候某人

Usage:
  myapp greet [name] [flags]

Flags:
  -h, --help       help for greet
  -t, --times int  问候次数 (default 1)

持久化标志

var verbose bool

func init() {
	// 持久化标志在所有子命令中都可用
	rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "详细输出")
}

使用 cobra-cli 生成代码

Cobra 提供了一个代码生成工具:

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

# 初始化项目
cobra-cli init myapp

# 添加命令
cobra-cli add serve
cobra-cli add config

实战:TODO 管理工具

让我们用 Cobra 构建一个完整的 TODO 管理工具:

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"time"
	
	"github.com/spf13/cobra"
)

type Todo struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"created_at"`
}

type TodoList struct {
	Todos  []Todo `json:"todos"`
	NextID int    `json:"next_id"`
}

const dataFile = "todos.json"

func loadTodos() (*TodoList, error) {
	list := &TodoList{NextID: 1}
	
	data, err := os.ReadFile(dataFile)
	if err != nil {
		if os.IsNotExist(err) {
			return list, nil
		}
		return nil, err
	}
	
	err = json.Unmarshal(data, list)
	return list, err
}

func saveTodos(list *TodoList) error {
	data, err := json.MarshalIndent(list, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(dataFile, data, 0644)
}

var rootCmd = &cobra.Command{
	Use:   "todo",
	Short: "一个简单的 TODO 管理工具",
}

var addCmd = &cobra.Command{
	Use:   "add [title]",
	Short: "添加一个新的 TODO",
	Args:  cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		list, err := loadTodos()
		if err != nil {
			fmt.Println("加载失败:", err)
			return
		}
		
		todo := Todo{
			ID:        list.NextID,
			Title:     args[0],
			Done:      false,
			CreatedAt: time.Now(),
		}
		list.Todos = append(list.Todos, todo)
		list.NextID++
		
		err = saveTodos(list)
		if err != nil {
			fmt.Println("保存失败:", err)
			return
		}
		
		fmt.Printf("✓ 添加 TODO #%d: %s\n", todo.ID, todo.Title)
	},
}

var listCmd = &cobra.Command{
	Use:   "list",
	Short: "列出所有 TODO",
	Run: func(cmd *cobra.Command, args []string) {
		list, err := loadTodos()
		if err != nil {
			fmt.Println("加载失败:", err)
			return
		}
		
		if len(list.Todos) == 0 {
			fmt.Println("没有 TODO")
			return
		}
		
		showDone, _ := cmd.Flags().GetBool("all")
		
		for _, todo := range list.Todos {
			if !showDone && todo.Done {
				continue
			}
			
			status := "[ ]"
			if todo.Done {
				status = "[✓]"
			}
			
			fmt.Printf("%s #%d: %s\n", status, todo.ID, todo.Title)
		}
	},
}

var doneCmd = &cobra.Command{
	Use:   "done [id]",
	Short: "标记 TODO 为完成",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		list, err := loadTodos()
		if err != nil {
			fmt.Println("加载失败:", err)
			return
		}
		
		id := 0
		fmt.Sscanf(args[0], "%d", &id)
		
		for i, todo := range list.Todos {
			if todo.ID == id {
				list.Todos[i].Done = true
				err = saveTodos(list)
				if err != nil {
					fmt.Println("保存失败:", err)
					return
				}
				fmt.Printf("✓ TODO #%d 已完成\n", id)
				return
			}
		}
		
		fmt.Printf("✗ TODO #%d 不存在\n", id)
	},
}

var deleteCmd = &cobra.Command{
	Use:   "delete [id]",
	Short: "删除 TODO",
	Args:  cobra.ExactArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		list, err := loadTodos()
		if err != nil {
			fmt.Println("加载失败:", err)
			return
		}
		
		id := 0
		fmt.Sscanf(args[0], "%d", &id)
		
		for i, todo := range list.Todos {
			if todo.ID == id {
				list.Todos = append(list.Todos[:i], list.Todos[i+1:]...)
				err = saveTodos(list)
				if err != nil {
					fmt.Println("保存失败:", err)
					return
				}
				fmt.Printf("✓ TODO #%d 已删除\n", id)
				return
			}
		}
		
		fmt.Printf("✗ TODO #%d 不存在\n", id)
	},
}

func init() {
	listCmd.Flags().BoolP("all", "a", false, "显示所有 TODO(包括已完成的)")
	
	rootCmd.AddCommand(addCmd)
	rootCmd.AddCommand(listCmd)
	rootCmd.AddCommand(doneCmd)
	rootCmd.AddCommand(deleteCmd)
}

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

使用:

$ todo add "学习 Go 语言"
✓ 添加 TODO #1: 学习 Go 语言

$ todo add "写一个 CLI 工具"
✓ 添加 TODO #2: 写一个 CLI 工具

$ todo list
[ ] #1: 学习 Go 语言
[ ] #2: 写一个 CLI 工具

$ todo done 1
✓ TODO #1 已完成

$ todo list
[ ] #2: 写一个 CLI 工具

$ todo list -a
[] #1: 学习 Go 语言
[ ] #2: 写一个 CLI 工具

交互式输入

使用 bufio 读取用户输入:

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	fmt.Print("请输入你的名字: ")
	name, _ := reader.ReadString('\n')
	name = strings.TrimSpace(name)
	
	fmt.Printf("Hello, %s!\n", name)
	
	// 确认操作
	fmt.Print("确定要继续吗?(y/n): ")
	confirm, _ := reader.ReadString('\n')
	confirm = strings.TrimSpace(strings.ToLower(confirm))
	
	if confirm == "y" || confirm == "yes" {
		fmt.Println("继续执行...")
	} else {
		fmt.Println("已取消")
	}
}

彩色输出

使用 fatih/color 库添加颜色:

package main

import (
	"fmt"
	
	"github.com/fatih/color"
)

func main() {
	// 预定义颜色
	color.Red("这是红色")
	color.Green("这是绿色")
	color.Blue("这是蓝色")
	color.Yellow("这是黄色")
	
	// 自定义样式
	bold := color.New(color.Bold)
	bold.Println("这是粗体")
	
	underline := color.New(color.Underline)
	underline.Println("这是下划线")
	
	// 组合样式
	success := color.New(color.FgGreen, color.Bold)
	success.Println("✓ 操作成功")
	
	error := color.New(color.FgRed, color.Bold)
	error.Println("✗ 操作失败")
	
	// 格式化输出
	color.Cyan("用户 %s 的年龄是 %d", "张三", 25)
}

进度条

使用 schollz/progressbar/v3

package main

import (
	"fmt"
	"time"
	
	"github.com/schollz/progressbar/v3"
)

func main() {
	bar := progressbar.Default(100, "处理中")
	
	for i := 0; i < 100; i++ {
		bar.Add(1)
		time.Sleep(50 * time.Millisecond)
	}
	
	fmt.Println("\n完成!")
}

小结

今天我们学习了用 Go 构建命令行工具:

  1. 基础os.Argsflag
  2. Cobra 框架:子命令、标志、参数验证
  3. 实战:TODO 管理工具
  4. 用户体验:交互式输入、彩色输出、进度条

Go 的编译型特性和标准库让构建 CLI 工具变得简单而高效。无论是简单的脚本还是复杂的工具,Go 都能胜任。

练习时间

  1. 实现一个文件统计工具,统计目录下的文件数量和大小
  2. 创建一个密码生成器,支持自定义长度和字符集
  3. 构建一个简单的书签管理工具
  4. 实现一个 HTTP 请求测试工具(类似 curl)

我们下篇见!

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页