一种命令行解析的新思路(Go 语言描述)

本文探讨了一种新的命令行解析思路,通过 Go 语言实现,旨在简化复杂的命令行处理。作者分析了现有方法如 flag 和 cobra 库的局限性,并提出启发式命令行解析方法,支持任意多的子命令和参数。Cortana 项目由此诞生,它使用 Btree 实现子命令与函数映射,支持动态匹配和默认值,提供类似 cobra 的功能但使用更简单。此外,Cortana 还支持配置文件和环境变量,确保参数解析的灵活性和可扩展性。
摘要由CSDN通过智能技术生成

一 概述

命令行解析是几乎每个后端程序员都会用到的技术,但相比业务逻辑来说,这些细枝末节显得并不紧要,如果仅仅追求满足简单需求,命令行的处理会比较简单,任何一个后端程序员都可以信手拈来。Go 标准库提供了 flag 库以供大家使用。

然而,当我们稍微想让我们的命令行功能丰富一些,问题开始变得复杂起来,比如,我们要考虑如何处理可选项和必选项,对于可选项,如何设置其默认值,如何处理子命令,以及子命令的子命令,如何处理子命令的参数等等。

目前,Go 语言中使用最广泛功能最强大的命令行解析库是 cobra,但丰富的功能让 cobra 相比标准库的 flag 而言,变得异常复杂,为了减少使用的复杂度,cobra 甚至提供了代码生成的功能,可以自动生成命令行的骨架。然而,自动生成在节省了开发时间的同时,也让代码变得不够直观。

本文通过打破大家对命令行的固有印象,对命令行的概念解构后重新梳理,开发出一种功能强大但使用极为简单的命令行解析方法。这种方法支持任意多的子命令,支持可选和必选参数,对可选参数可提供默认值,支持配置文件,环境变量及命令行参数同时使用,配置文件,环境变量,命令行参数生效优先级依次提高,这种设计可以更符合 12 factor的原则。

二 现有的命令行解析方法

Go 标准库 flag提供了非常简单的命令行解析方法,定义好命令行参数后,只需要调用 flag.Parse方法即可。

// demo.go
var limit int
flag.IntVar(&limit, "limit", 10, "the max number of results")
flag.Parse()
fmt.Println("the limit is", limit)

// 执行结果
$ go run demo.go 
the limit is 10
$ go run demo.go -limit 100
the limit is 100

可以看到, flag 库使用非常简单,定要好命令行参数后,只需要调用 flag.Parse就可以实现参数的解析。在定义命令行参数时,可以指定默认值以及对这个参数的使用说明。

如果要处理子命令,flag 就无能为力了,这时候可以选择自己解析子命令,但更多的是直接使用 cobra 这个库。

这里用 cobra 官方给出的例子,演示一下这个库的使用方法

package main

import (
  "fmt"
  "strings"

  "github.com/spf13/cobra"
)

func main() {
  var echoTimes int

  var cmdPrint = &cobra.Command{
    Use:   "print [string to print]",
    Short: "Print anything to the screen",
    Long: `print is for printing anything back to the screen.
For many years people have printed back to the screen.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Print: " + strings.Join(args, " "))
    },
  }

  var cmdEcho = &cobra.Command{
    Use:   "echo [string to echo]",
    Short: "Echo anything to the screen",
    Long: `echo is for echoing anything back.
Echo works a lot like print, except it has a child command.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      fmt.Println("Echo: " + strings.Join(args, " "))
    },
  }

  var cmdTimes = &cobra.Command{
    Use:   "times [string to echo]",
    Short: "Echo anything to the screen more times",
    Long: `echo things multiple times back to the user by providing
a count and a string.`,
    Args: cobra.MinimumNArgs(1),
    Run: func(cmd *cobra.Command, args []string) {
      for i := 0; i < echoTimes; i++ {
        fmt.Println("Echo: " + strings.Join(args, " "))
      }
    },
  }

  cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")

  var rootCmd = &cobra.Command{Use: "app"}
  rootCmd.AddCommand(cmdPrint, cmdEcho)
  cmdEcho.AddCommand(cmdTimes)
  rootCmd.Execute()
}

可以看到子命令的加入让代码变得稍微复杂,但逻辑仍然是清晰的,并且子命令和跟命令遵循相同的定义模板,子命令还可以定义自己子命令。

$ go run cobra.go echo times hello --times 3
Echo: hello
Echo: hello
Echo: hello

cobra 功能强大,逻辑清晰,因此得到大家广泛的认可,然而,这里却有两个问题让我无法满意,虽然问题不大,但时时萦怀于心,让人郁郁。

1 参数定义跟命令逻辑分离

从上面 --times的定义可以看到,参数的定义跟命令逻辑的定义(即这里的 Run)是分离的,当我们有大量子命令的时候,我们更倾向把命令的定义放到不同的文件甚至目录,这就会出现命令的定义是分散的,而所有命令的参数定义却集中在一起的情况。

当然,这个问题用 cobra 也很好解决,只要把参数定义从 main函数移动到 init函数,并将 init 函数分散到跟子命令的定义一起即可。比如子命令 times 定义在 times.go文件中&#

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值