Skip to content

常见命令应用的2种形式 ​

  1. 只有一个 主命令 + 参数 + 操作目标(无名参数)
sh
curl -i -X GET http://localhost:3000/api

# 主命令: curl
# 参数: -i -X
# 无名参数(操作目标): http://localhost:3000/api
  1. 除 主命令 入口以外还有 子命令及其参数
sh
git --help
git add --help
git commit --help
# 主命令: git
# 子命令: add/commit
# 主命令+参数: git --help
# 子命令+参数: add --help / commit --help

对比参数解析库优劣 ​

flags ​

一些简单的参数解析可以用这个标准库, 但是如果要支持类似 git add xxx 这样的子命令, 实现比较复杂, 功能较少

urfave/cli ​

spf13/cobra ​

命令需求 ​

fanyi ​

尝试实现一个在终端中直接翻译内容的命令: fanyi fanyi2

这是一个典型的: 无子命令, 仅有不同的参数来控制命令的行为

txt
fanyi hello
# 输出: 你好

# --debug/-d:    表示是否要输出程序运行时的调试信息(bool)
# --provider/-p: 表示使用的接口(string)
# --prompt:      表示自定义使用大模型接口时的提示词(string)
fanyi "hello" -d --provider "deepseek" --prompt "请帮我翻译如下内容,直接输出翻译结果即可"
toml
[provider.baidu]
label="百度翻译"
app_id=""
secret=""

[provider.youdao]
label="有道翻译"
app_id=""
secret=""

[provider.openai]
label="openai"
prompt=""
api_key=""

[provider.deepseek]
label="deepseek"
prompt=""
api_key=""

[provider.openai_compatible]
label="openai(兼容模式)-硅基流动"
prompt=""
api_key=""

textfmt ​

尝试实现一个文本格式化命令, 并且包含 upper lower contains 3个子命令, 并且有 --debug/-d 全局选项

  • upper: 将输入的文本转为大写 --json 是否以json格式输出结果(局部选项)
  • lower: 将输入的文本转为小写 --json 是否以json格式输出结果(局部选项)
  • contains: 判断输入的文本是否包含某个子文本
    • --ignore/-i 是否忽略大小写查找
    • --keyword/-k 要搜索的关键字
sh
textfmt upper --debug "Hello World"
# 输出: HELLO WORLD

textfmt upper -d --json "Hello World"
# 输出: {"original":"Hello World", "formatted": "HELLO WORLD", isDebug:true}

textfmt lower "Hello World" --json
# 输出: {"original":"Hello World", "formatted": "Hello World", isDebug:false}

textfmt contains "Hello World" --keyword="hello"
# 输出: false, 默认情况是不忽略大小写

textfmt contains "Hello World" -k="hello" -i
# 输出: true, 忽略大小写, 就能找到关键字 `hello`

urfave/cli 实现 fanyi ​

初始化项目 ​

sh
# 创建&进入源码目录
mkdir fanyi && cd -

# 初始化 Go 项目模块
go mod init fanyi

# 安装 urfave/cli
go get -u github.com/urfave/cli/v3

# 创建命令入口(实际命令名称)
touch fanyi.go
go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli/v3"
)

func main() {
	// 定义命令名称/描述/执行方法
	cmd := &cli.Command{
		Name:  "fanyi",                          // 命令名称(在 --help 帮助文档中显示的名称)
		Usage: "translate text in command line", // 命令描述
		Action: func(ctx context.Context, cmd *cli.Command) error {
			fmt.Println("hello from fanyi.go")
			return nil
		},
	}

	if err := cmd.Run(context.Background(), os.Args); err != nil {
		log.Fatal(err)
	}
}
sh
# 编译 & 安装 fanyi.go: install 命令会自动编译
go install .

# 查看命令的位置
which fanyi

# 执行 fanyi 命令
fanyi

现在已经基本实现了: 编写源码 -> 编译&安装到系统 -> 执行命令

那么接下来的工作就是: 完善源码 -> 重新编译安装 -> 执行命令测试

定义选项 & 获取选项 ​

go
package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"slices"

	"github.com/urfave/cli/v3"
)

// 定义选项 & 默认值
var (
	provider string = "openai" // 可以这样设置默认值(注意使用这个变量设置 cli.StringFlag#Value 字段)
	prompt   string
	isDebug  bool
)

// 定义允许的翻译提供商方便验证传入的参数值是否有误
var allowProviders = []string{
	"baidu",
	"youdao",
	"openai",
	"deepseek",
	"openai_compatible",
}

func main() {
	flags := []cli.Flag{
		// --provider
		&cli.StringFlag{
			Name:        "provider",                 // 选项名称 --provider
			Aliases:     []string{"p"},              // 选项短别名 -p
			Value:       provider,                   // 选项默认值(使用 provider 变量)
			Usage:       "translation api provider", // 使用说明
			Destination: &provider,                  // 绑定的变量地址
		},

		// --prompt
		&cli.StringFlag{
			Name:        "prompt",
			Value:       "Please translate this text, output only the translation result.",
			Usage:       "translation api provider",
			Destination: &prompt,
		},

		// --debug
		&cli.BoolFlag{
			Name:        "debug",
			Aliases:     []string{"d"},
			Value:       false,
			Usage:       "print debug information",
			Destination: &isDebug,
		},
	}

	// 定义命令名称/描述/执行方法
	cmd := &cli.Command{
		Flags: flags,
		Name:  "fanyi",
		Usage: "translate text in command line",
		Action: func(ctx context.Context, cmd *cli.Command) error {
			// 待翻译的文本验证规格
			if cmd.NArg() <= 0 {
				return fmt.Errorf("missing text to translate")
			}
			text := cmd.Args().Get(0)

			// --provider 参数校验
			if !slices.Contains(allowProviders, provider) {
				return fmt.Errorf("invalid translation api provider: %s", provider)
			}

			fmt.Printf("待翻译的文本是:%s \n使用的API是:%s\nprompt内容是:%s\nisDebug:%v\n", text, provider, prompt, isDebug)

			// 所有参数已经收集完成, 后续具体实现就是:
			// 0.安装 viper, 读取配置文件内容
			// 1.从 ~/.config/fanyi/config.toml 中读取 provider 配置
			// 2.判断是否是大模型接口, 如果是, 则需要 prompt 参数
			// 3.按照读取到的 provider 参数, 创建对应的处理方法(请求不同的接口&输出结果)
			// 然而这些并不是学习 cobra/cli 的重点, 感兴趣的话可以自己实现下
			return nil
		},
	}

	if err := cmd.Run(context.Background(), os.Args); err != nil {
		log.Fatal(err)
	}
}
sh
# 再次安装
go install .

# 执行 fanyi --help
# NAME:
#    fanyi - translate text in command line
#
# USAGE:
#    fanyi [global options]
#
# GLOBAL OPTIONS:
#    --provider string, -p string  translation api provider (default: "openai")
#    --prompt string               translation api provider (default: "Please translate this text, output only the translation result.")
#    --debug, -d                   print debug information
#    --help, -h                    show help

# 执行 fanyi 并设置所有选项
fanyi --debug --provider="deepseek" --prompt="translate this text" hello
# 待翻译的文本是:hello
# 使用的API是:deepseek
# prompt内容是:translate this text
# isDebug:true

# 执行 fanyi 并设置 provider 参数
fanyi --provider="deepseek" hello
# 待翻译的文本是:hello
# 使用的API是:deepseek
# prompt内容是:Please translate this text, output only the translation result.
# isDebug:false

spf13/cobra 实现 fanyi2 ​

初始化项目 ​

[注意]

为了防止和之前的 urfave/cli 实现冲突, 这个项目名字为 fanyi2

sh
# 创建&进入源码目录
mkdir fanyi2 && cd -

# 初始化 Go 项目模块
go mod init fanyi2

# 安装 spf13/cobra
go get -u github.com/spf13/cobra@latest

# 安装 cobra-cli: 用来初始化项目结构
go install github.com/spf13/cobra-cli@latest

# 初始化项目源码
cobra-cli init
sh
.
├── cmd
│   └── root.go   # 根命令(命令入口)
├── go.mod
├── go.sum
├── LICENSE       # 开源协议文件
└── main.go       # 程序入口文件
sh
# 编译 & 安装 fanyi2/main.go: install 命令会自动编译
go install .

# 查看命令的位置
which fanyi2

# 执行 fanyi 命令
fanyi2
# 输出如下(在 cmd/root.go 中定义的)
# A longer description that spans multiple lines and likely contains
# examples and usage of using your application. For example:

# Cobra is a CLI library for Go that empowers applications.
# This application is a tool to generate the needed files
# to quickly create a Cobra application.

定义选项 & 获取选项 ​

go
package cmd

import (
	"fmt"
	"os"
	"slices"
	"strings"

	"github.com/spf13/cobra"
)

// 定义命令选项参数(默认值)
var (
	config   string = "~/.fanyi2.toml"                                                  // 执行命令时要加载的配置文件
	provider string = "deepseek"                                                        // 翻译时需要的接口提供商
	prompt   string = "Please translate this text, output only the translation result." // 翻译时需要的提示词(如果是请求大模型接口)
	isDebug  bool   = false                                                             // 是否输出调试信息
)

// 允许的接口提供商
var allowProviders = []string{
	"baidu",
	"youdao",
	"openai",
	"deepseek",
	"openai_compatible",
}

// 定义命令
var rootCmd = &cobra.Command{
	Use:     "fanyi2",
	Short:   "cli tool for translate text",
	Long:    `fanyi2 is cli tool fro translate text,the translation api can be customized`,
	Example: `fanyi2 "hello world" --provider baidu`,
	RunE:    run, // 执行并返回错误
	// Run: run,     // 仅执行, 不返回错误
}

// init 模块初始化时定义命令的参数
func init() {
	// --config or -c(注意使用的是: StringVarP方法,他有 5 个参数:
	// StringVarP(选项的引用, 选项名, 短选项名, 选项描述)
	rootCmd.Flags().StringVarP(&config, "config", "c", config, "set loaded config file path")

	// --provider or -p
	rootCmd.Flags().StringVarP(&provider, "provider", "p", provider, "set translation api provider")

	// --prompt 没有短选项(注意使用的是 StringVar 方法, 它只有4个参数)
	rootCmd.Flags().StringVar(&prompt, "prompt", prompt, "set translation api prompt")

	// --debug 是否输出调试信息(注:选项值默认是 false, 但是只有选项存在
	// (--debug/-d)就会设置 isDebug 为 true, 值不需要设置), 如:
	// fanyi2 "hello world" --debug 或 fanyi2 "hello world" -d
	rootCmd.Flags().BoolVarP(&isDebug, "debug", "d", isDebug, "print debug infomation")
}

// run 执行命令逻辑
func run(cmd *cobra.Command, args []string) error {
	if len(args) <= 0 {
		return fmt.Errorf("[Error]%s", "No text required for translation was provided.")
	}

	// text: 参数校验
	// text := args[0] // fanyi2 hello world: 只获取到 "hello", 说明如果args不加""只会获取第一个字符串
	text := strings.Join(args, " ")
	if text == "" {
		return fmt.Errorf("[Error]%s", "No text required for translation was provided.")
	}

	// provider: 参数校验
	if !slices.Contains(allowProviders, provider) {
		return fmt.Errorf("[Error]Invalid translation api provider: %s", provider)
	}

	// 具体实现就不实现了, 本质就是根据收集到的参数,
	// 执行请求不同的接口去翻译文本, 这里用输出函数代替
	// 待翻译的文本是: $text
	// 使用的API是: $provider
	// prompt内容是: $prompt
	// 是否输出调试信息: $isDebug
	fmt.Printf("待翻译的文本是:%s \n使用的API是:%s\nprompt内容是:%s\nisDebug:%v\n", text, provider, prompt, isDebug)

	return nil
}

// Execute 命令执行逻辑(暴露给 main.go)
func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
sh
# 重新安装
go install .

# 执行测试
fanyi2 --help
# 输出如下:
# fanyi2 is cli tool fro translate text,the translation api can be customized
# Usage:
#   fanyi2 [flags]
#
# Examples:
# fanyi2 "hello world" --provider baidu
#
# Flags:
#   -c, --config string     set loaded config file path (default "~/.fanyi2.toml")
#   -d, --debug             print debug infomation
#   -h, --help              help for fanyi2
#       --prompt string     set translation api prompt (default "Please translate this text, output only the translation result.")
#   -p, --provider string   set translation api provider (default "deepseek")

fanyi2 -d -p "openai" "hello world"
# 待翻译的文本是:hello world
# 使用的API是:openai
# prompt内容是:Please translate this text, output only the translation result.
# isDebug:true

fanyi2 -d -p "openai" --prompt="custom prompt" hello world
# 待翻译的文本是:hello world
# 使用的API是:openai
# prompt内容是:custom prompt
# isDebug:true

fanyi2 -d -p "openai" --prompt="custom prompt2" hello world
# 待翻译的文本是:hello world
# 使用的API是:openai
# prompt内容是:custom prompt2
# isDebug:true

urfave/cli 实现 textfmt ​

go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/urfave/cli/v3"
)

var (
	isDebug  bool   = false // 是否输出调试信息
	isJSON   bool   = false // 是否以json格式输出
	isIgnore bool   = false // 是否忽略大搜索
	keyword  string         // 搜索关键字
)

// InitJSONFlag 初始化json格式输出标志(可复用所以用函数来初始化): --json
func InitJSONFlag() *cli.BoolFlag {
	return &cli.BoolFlag{
		Name:        "json",
		Usage:       "print result in json format",
		Value:       isJSON,
		Destination: &isJSON,
	}
}

// PrintDebuInfo 输出调试信息
func PrintDebuInfo(printFn func()) {
	if isDebug {
		printFn()
	}
}

func PrintAsJSON(original string, formatted string) {
	// {"original":"Hello World", "formatted": "HELLO WORLD", isDebug:true}
	data := make(map[string]any)
	data["original"] = original
	data["formatted"] = strings.ToUpper(original)
	data["isDebug"] = isDebug
	json, err := json.Marshal(data)
	if err != nil {
		fmt.Println("[Error]json encodeing:", err)
		return
	}
	fmt.Println(string(json))
}

func InitUpperCommand() *cli.Command {
	return &cli.Command{
		Name:    "upper",
		Aliases: []string{"u", "up"},
		Usage:   "format text to upper case",
		Flags: []cli.Flag{
			InitJSONFlag(),
		},
		Action: func(ctx context.Context, cmd *cli.Command) error {
			// textfmt contains -k hello Mock Long String Hello
			// 如果不加双引号, Mock Long String Hello 是多个参数
			// cmd.Args().First() 或 cmd.Args().First() 只会获取第一个参数 Mock
			// 所以应该获取所有参数并拼接字符串
			original := strings.Join(cmd.Args().Slice(), " ")
			formatted := strings.ToUpper(original)

			PrintDebuInfo(func() {
				fmt.Println("[original]", original)
				fmt.Println("[formatted]", formatted)
			})

			if isJSON {
				PrintAsJSON(original, formatted)
			} else {
				fmt.Println(formatted)
			}

			return nil
		},
	}
}

func InitLowerCommand() *cli.Command {
	return &cli.Command{
		Name:    "lower",
		Aliases: []string{"l", "lo"},
		Usage:   "format text to lower case",
		Flags: []cli.Flag{
			InitJSONFlag(),
		},
		Action: func(ctx context.Context, cmd *cli.Command) error {
			original := strings.Join(cmd.Args().Slice(), " ")
			formatted := strings.ToLower(original)

			PrintDebuInfo(func() {
				fmt.Println("[original]", original)
				fmt.Println("[formatted]", formatted)
			})

			if isJSON {
				PrintAsJSON(original, formatted)
			} else {
				fmt.Println(formatted)
			}
			return nil
		},
	}
}

func InitContainsCommand() *cli.Command {
	return &cli.Command{
		Name:    "contains",
		Aliases: []string{"c"},
		Usage:   "search string in text",
		Flags: []cli.Flag{
			// --ignore/-i: 忽略大小写
			&cli.BoolFlag{
				Name:        "ignore",
				Aliases:     []string{"i"},
				Usage:       "ignore case when search",
				Value:       isIgnore,
				Destination: &isIgnore,
			},

			// --keyword/-k: 要搜索的关键字
			&cli.StringFlag{
				Name:        "keyword",
				Aliases:     []string{"k"},
				Usage:       "ignore case when search",
				Value:       "",
				Destination: &keyword,
			},
		},
		Action: func(ctx context.Context, cmd *cli.Command) error {
			original := strings.Join(cmd.Args().Slice(), " ")

			// 验证 --keyword/-k 选项
			if len(keyword) == 0 {
				return fmt.Errorf("[Error]keyword is empty")
			}

			PrintDebuInfo(func() {
				fmt.Println("[original]", original)
				fmt.Println("[keyword]", keyword)
			})

			// 是否忽略大小写选项
			if isIgnore {
				isContainsIgnoreCase := strings.Contains(strings.ToLower(original), strings.ToLower(keyword))
				fmt.Println(isContainsIgnoreCase) // "true" or "false"
			} else {
				isContains := strings.Contains(original, keyword)
				fmt.Println(isContains) // "true" or "false"
			}

			return nil
		},
	}
}

func InitCommand() *cli.Command {
	cmd := &cli.Command{
		Name:  "textfmt",
		Usage: "format text",
		Flags: []cli.Flag{
			// 全局选项: --debug/-d
			&cli.BoolFlag{
				Name:        "debug",
				Aliases:     []string{"d"},
				Usage:       "print debug information",
				Value:       isDebug,
				Destination: &isDebug,
			},
		},
		// 初始化子命令
		Commands: []*cli.Command{
			InitUpperCommand(),
			InitLowerCommand(),
			InitContainsCommand(),
		},
		// Action: 根命令没有 Action, 默认执行: cmd --help
	}
	return cmd
}

func main() {
	cmd := InitCommand()
	if err := cmd.Run(context.Background(), os.Args); err != nil {
		log.Fatal(err)
	}
}
sh
textfmt
# NAME:
#    textfmt - format text
#
# USAGE:
#    textfmt [global options] [command [command options]]
#
# COMMANDS:
#    upper, u, up  format text to upper case
#    lower, l, lo  format text to lower case
#    contains, c   search string in text
#    help, h       Shows a list of commands or help for one command
#
# GLOBAL OPTIONS:
#    --debug, -d  print debug information
#    --help, -h   show help

textfmt up "Hello World" -d
# [original] Hello World
# [formatted] HELLO WORLD
# HELLO WORLD

textfmt up "Hello World" -d --json
# [original] Hello World
# [formatted] HELLO WORLD
# {"formatted":"HELLO WORLD","isDebug":true,"original":"Hello World"}

textfmt contains "Hello World" -k "hello"
# false

textfmt contains "Hello World" -d -k "hello"
# [original] Hello World
# [keyword] hello
# false

textfmt contains "Hello World" -d -i -k "hello"
# [original] Hello World
# [keyword] hello
# true
sh
mkdir textfmt && cd -
go mod init textfmt
go get -u github.com/urfave/cli/v3
touch textfmt.go

go install .
which textfmt

urfave/cli 实现 textfmt2 ​

go
package cmd

import (
	"encoding/json"
	"fmt"
	"os"
	"strings"

	"github.com/spf13/cobra"
)

// 全局选项 & 可复用选项
var (
	isDebug bool = false // --debug/-d 全局选项
	isJSON  bool = false // --json 局部选项
)

// 根命令
var rootCmd = &cobra.Command{
	Use:   "textfmt2",
	Short: "a cli tool for formatting strings",
	Long: `a cli tool for formatting strings:
	upper: format string to upper case
	lower: format string to lower case
	contains: check if string contains a substring`,
	// 根命令没有设置 Run/RunE 默认执行 cmd --help
}

// init 模块初始化钩子函数
func init() {
	// 初始化 --debug 选项
	InitDebugFlag(upperCmd)
	InitDebugFlag(lowerCmd)
	InitDebugFlag(containsCmd)

	// 初始化 --json 选项
	InitIsJSONFlag(upperCmd)
	InitIsJSONFlag(lowerCmd)
}

// Execute 执行命令
func Execute() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

// InitDebugFlag 初始化 --debug 选项(复用)
// 与 urfave/cli 不同的是: 使用子命令时, 默认不会继承全局选项
// 也就是说: 给 rootCmd 设置 debugFlag, 使用 rootCmd upper 时无法直接使用 --debug 选项
func InitDebugFlag(cmd *cobra.Command) {
	cmd.Flags().BoolVarP(&isDebug, "debug", "d", isDebug, "Print debug infomation")
}

// InitIsJSONFlag 初始化 --json 选项(复用)
func InitIsJSONFlag(cmd *cobra.Command) {
	cmd.Flags().BoolVar(&isJSON, "json", isJSON, "Print result as json format")
}

// PrintDebuInfo 输出调试信息(复用)
func PrintDebuInfo(printFn func()) {
	if isDebug {
		printFn()
	}
}

// PrintAsJSON 输出json 格式的信息
func PrintAsJSON(original string, formatted string) {
	// {"original":"Hello World", "formatted": "HELLO WORLD", isDebug:true}
	data := make(map[string]any)
	data["original"] = original
	data["formatted"] = strings.ToUpper(original)
	data["isDebug"] = isDebug
	json, err := json.Marshal(data)
	if err != nil {
		fmt.Println("[Error]json encodeing:", err)
		return
	}
	fmt.Println(string(json))
}
go
package cmd

import (
	"fmt"
	"strings"

	"github.com/spf13/cobra"
)

// upperCmd represents the upper command
var upperCmd = &cobra.Command{
	Use:     "upper",
	Aliases: []string{"u", "up"},
	Short:   "format string to upper case",
	Long: `format string to upper case
	Example: textfm2 upper "hello"
	Output: HELLO`,
	Run: func(cmd *cobra.Command, args []string) {
		// textfmt2 upper -k hello Mock Long String Hello
		// 如果不加双引号, Mock Long String Hello 是多个参数
		// args[0] 只会获取第一个参数 Mock, 所以应该获取所有参数并拼接字符串
		original := strings.Join(args, " ")
		formatted := strings.ToUpper(original)

		PrintDebuInfo(func() {
			fmt.Println("[original]", original)
			fmt.Println("[formatted]", formatted)
		})

		if isJSON {
			PrintAsJSON(original, formatted)
		} else {
			fmt.Println(formatted)
		}
	},
}

func init() {
	rootCmd.AddCommand(upperCmd)
}
go
package cmd

import (
	"fmt"
	"strings"

	"github.com/spf13/cobra"
)

// lowerCmd represents the lower command
var lowerCmd = &cobra.Command{
	Use:     "lower",
	Aliases: []string{"l", "lo"},
	Short:   "format string to lower case",
	Long: `format string to lower case
	Example: textfm2 upper "Hello"
	Output: hello`,
	Run: func(cmd *cobra.Command, args []string) {
		original := strings.Join(args, " ")
		formatted := strings.ToLower(original)

		PrintDebuInfo(func() {
			fmt.Println("[original]", original)
			fmt.Println("[formatted]", formatted)
		})

		if isJSON {
			PrintAsJSON(original, formatted)
		} else {
			fmt.Println(formatted)
		}
	},
}

func init() {
	rootCmd.AddCommand(lowerCmd)
}
go
package cmd

import (
	"fmt"
	"strings"

	"github.com/spf13/cobra"
)

var (
	isIgnore bool   = false // 是否忽略大搜索
	keyword  string         // 搜索关键字
)

// containsCmd represents the contains command
var containsCmd = &cobra.Command{
	Use:     "contains",
	Aliases: []string{"c"},
	Short:   "check if string contains a keyword",
	Long: `check if string contains a keyword|
	Example: textfm2 contains "Hello World" -k="world"
	Output: false

	Example: textfm2 contains "Hello World" -i -k="world"
	Output: true`,
	RunE: func(cmd *cobra.Command, args []string) error {
		original := strings.Join(args, " ")

		// 验证 --keyword/-k 选项
		if len(keyword) == 0 {
			return fmt.Errorf("[Error]keyword is empty")
		}

		PrintDebuInfo(func() {
			fmt.Println("[original]", original)
			fmt.Println("[keyword]", keyword)
		})

		// 是否忽略大小写选项
		if isIgnore {
			isContainsIgnoreCase := strings.Contains(strings.ToLower(original), strings.ToLower(keyword))
			fmt.Println(isContainsIgnoreCase) // "true" or "false"
		} else {
			isContains := strings.Contains(original, keyword)
			fmt.Println(isContains) // "true" or "false"
		}

		return nil
	},
}

func init() {
	containsCmd.Flags().BoolVarP(&isIgnore, "ignore", "i", isIgnore, "Ignore case when search")
	containsCmd.Flags().StringVarP(&keyword, "keyword", "k", keyword, "Keyword to search")
	rootCmd.AddCommand(containsCmd)
}
sh
mkdir textfmt2 && cd -
go mod init textfmt2
go get -u github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest
cobra-cli init

cobra-cli add upper
cobra-cli add lower
cobra-cli add contains
go install .
which textfm2
go
.
├── cmd
│   ├── contains.go # contains 子命令
│   ├── lower.go    # lower 子命令
│   ├── root.go     # 根命令
│   └── upper.go    # upper 子命令
├── go.mod
├── go.sum
└── main.go         # 入口文件(未修改过)
sh
textfmt2
# a cli tool for formatting strings:
# 	upper: format string to upper case
# 	lower: format string to lower case
# 	contains: check if string contains a substring
#
# Usage:
#   textfmt2 [command]
#
# Available Commands:
#   completion  Generate the autocompletion script for the specified shell
#   contains    check if string contains a keyword
#   help        Help about any command
#   lower       format string to lower case
#   upper       format string to upper case
#
# Flags:
#   -h, --help   help for textfmt2
#
# Use "textfmt2 [command] --help" for more information about a command.

textfmt2 up --help
# format string to upper case
# 	Example: textfm2 upper "hello"
# 	Output: HELLO
#
# Usage:
#   textfmt2 upper [flags]
#
# Aliases:
#   upper, u, up
#
# Flags:
#   -d, --debug   Print debug infomation
#   -h, --help    help for upper
#       --json    Print result as json format

textfmt2 up "Hello World" -d
# [original] Hello World
# [formatted] HELLO WORLD
# HELLO WORLD

textfmt2 up "Hello World" -d --json
# [original] Hello World
# [formatted] HELLO WORLD
# {"formatted":"HELLO WORLD","isDebug":true,"original":"Hello World"}

textfmt2 contains "Hello World" -k "hello"
# false

textfmt2 contains "Hello World" -d -i -k "hello"
# [original] Hello World
# [keyword] hello
# true

Released under the MIT License.