rcli

package module
v0.0.0-...-f39d52c Latest Latest
Warning

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

Go to latest
Published: Jan 28, 2025 License: MIT Imports: 5 Imported by: 0

README

Reflecting CLI

反射CLI是一款声明式Go语言命令行框架,追求接口的简单易用。支持如下功能:

  • 支持解析标准长选项和标准短选项
  • 以函数为中心定义命令,选项和参数
  • 以上下文为中心定义命令,选项和参数
  • 内置命令自动生成帮助页
  • TODO: 内置命令自动生成补全脚本

文档目录

命令声明

rcli 框架支持以函数为中心和以上下文为中心两种命令声明方法。

  • 以函数为中心的声明方式简洁快速,适合随手写的简单应用开发。
  • 以上下文为中心的命令声明能够支撑更加复杂的命令设计,适用于正式工具开发。

以函数为中心

使用 rcli.CmdFn 接口可以快速声明一个命令:

// app.go
package main

import "github.com/godgnidoc/rcli"

func MyCmd(args []string) int {
    // Do something
}

func main() {
    // ./app cmd fn
    rcli.CmdFn(MyCmd)

    rcli.Run()
}

上述代码定义了一个指令 my cmd 使用方法如下:

./app my cmd

rcli.CmdFn 接口会将驼峰命名风格的函数名拆解用作命令关键字,rcli.Run 接口解析命令行参数,找到并执行指定的命令。

命令回调的参数是去掉选项和命令关键字之后的剩余参数。命令回调的返回值被用作进程退出码。

我们也可以多次调用 rcli.CmdFn 接口定义更多指令,指令的关键字前缀可以重复,自然形成命令树。

// app.go
package main

import "github.com/godgnidoc/rcli"

func AddUser(args []string) int {
    // Do something
}

func AddGroup(args []string) int {
    // Do something
}

func main() {
    // ./app add user
    rcli.CmdFn(AddUser)

     // ./app add group
    rcli.CmdFn(AddGroup)

    rcli.Run()
}

注意,CmdFn 接口需要函数的名字作为命令关键字,所以匿名函数不能被用作命令回调。

使用 rcli.MainFn 可以定义一个主命令,当命令行参数不指定任何命令时,回滚到执行主命令。

// app.go
package main

import "github.com/godgnidoc/rcli"

func AddUser(args []string) int {
    // Do something
}

func AddGroup(args []string) int {
    // Do something
}

func ShowStatus(args []stirng) int {
    // Do something
}

func main() {
    // ./app add user
    rcli.CmdFn(AddUser) 
    
    // ./app add group
    rcli.CmdFn(AddGroup) 
    
    // ./app
    // ./app show status
    rcli.MainFn(ShowStatus)

    rcli.Run()
}

使用 MainFn 定义的命令依然可以使用原本的关键字执行。

以上下文为中心

以上下文为中心定义命令的风格适用于较为复杂的场景,比如命令较多,选项或参数也很多的情况。

本节的话题尚未涉及参数和选项的声明,此处仅展示以上下文为中心声明命令的基本方法。

// app.go
package main

import "github.com/godgnidoc/rcli"

type AddUser {
  
}

func (cmd *AddUser) Run(args []string) int {
    // ...
}

type AddGroup {
  
}

func (cmd *AddGroup) Run(args []string) int {
    // ...
}

func main() {
    // ./app add user
    rcli.Cmd(&AddUser{
        // ...
    })

    // ./app add group
    rcli.Cmd(&AddGroup{
        // ...
    })

    rcli.Run()
}

Cmd接口接受一个上下文指针来创建指令,使用上下文类型名生成命令关键字。

命令上下文需要实现一个 Run 方法,参数为剔除选项和具名参数以及命令关键字后的剩余命令行参数。返回值被用作进程退出码。

具名参数

命令可以接受若干按顺序指定的参数,命令回调的形参过于简单,当命令需要处理多个参数时,反复使用下标访问参数列表会严重降低代码的可读性。

以上下文为中心

可以为上下文命令定义具名参数,框架会将成功分析的具名参数存入指定变量,多出来的剩余参数才会被传入命令入口函数。

// app.go
package main

import "github.com/godgnidoc/rcli"

type AddUser {
  UserName *rcli.Arg
  GroupName *rcli.Arg
}

func (cmd *AddUser) Run(args []string) int {
    // ...
    user_name := cmd.UserName.Value()
    if cmd.GroupName.HasValue() {
        group_name := cmd.GroupName.Value()
        // ...
    }
}

type AddGroup {
  GroupName *rcli.Arg
}

func (cmd *AddGroup) Run(args []string) int {
    // ...
    group_name := cmd.GroupName.Value()
}

func main() {
    // ./app add user myuser
    // ./app add user myuser mygroup
    rcli.Cmd(&AddUser{
        UserName: rcli.Argument("user-name"),
        GroupName: rcli.Optional("group-name")
    })

    // ./app add group mygroup
    rcli.Cmd(&AddGroup{
        GroupName: rcli.Argument("group-name"),
    })

    rcli.Run()
}

可以用 *rcli.Arg 类型在命令上下文中定义任意个参数,它们必须都是公开成员。框架会根据这些参数字段的定义顺序解析命令行参数。

命令参数分为可选参数和必须参数,如果命令行没有给足必须参数,则框架会报告错误并提前退出。

可选参数出现后,不能再出现必须参数。出于效率考虑,rcli 不会每次主动检查此错误。用户可以在开发阶段使用 rcli.ValidateApp() 来校验应用定义的正确性。

参数定义除了用于指导命令行解析以外,还参与构建帮助页面。必须参数被显示为 <arg> 形式,可选参数被显示为 <arg> 形式。

剩余参数

框架总是会把剔除命令关键字、选项和具名参数之后剩余的命令行参数传递给命令回调函数。

如果我们确实总是需要用到剩余参数,且希望在帮助页告诉用户可以放心大胆的传入若干参数,可以使用 More 接口定义一个剩余参数。

剩余参数之后不能出现任何参数定义。

// app.go
package main

import "github.com/godgnidoc/rcli"

type Compile struct {
    Source *rcli.Arg
    More *rcli.Arg
}


func (cmd *Compile) Run(args []string) int {
    // ...
    sources := append([]string{cmd.Source.Value()}, args...)
}

func main() {
    // ./app compile firstSource
    // ./app compile firstSource SecondSource ThirdSource
    rcli.Cmd(&Compile{
        Source: rcli.Argument("source"),
        More: rcli.More()
    })

    rcli.Run()
}

如上例子演示了如何使用剩余参数,同时也暴露了一个问题。如果剩余参数和最后一个必须参数实际上是同一组数据,我们要将它们合并再处理就会麻烦。

因此,我们引入和 Some 接口,可以为命令定义要求至少出现一次的剩余参数。

// app.go
package main

import "github.com/godgnidoc/rcli"

type Compile struct {
    Source *rcli.Arg
}


func (cmd *Compile) Run(args []string) int {
    // ...
    for source := range cmd.Source.All() {
        // ...
    }
}

func main() {
    // ./app compile firstSource
    // ./app compile firstSource SecondSource ThirdSource
    rcli.Cmd(&Compile{
        Source: rcli.Some("source"),
    })

    rcli.Run()
}

使用 Some 接口定义的剩余参数相当于合并了一个必须参数和一系列剩余参数。使用 All 可以获取其中存储的全部值。

以函数为中心

以函数为中心的命令也可以使用接口定义具名参数,但我们拿不到这些定义的指针,所以也无从获取存储其中的值。

因此,框架不会将以函数为中心的命令的具名参数从命令行剔除,但是依然会为它们生成帮助文档,以及检查命令行参数是否够用。

// app.go
package main

import "github.com/godgnidoc/rcli"

func Compile(args []string) int {
    // ...
    for source := range args {
        // ...
    }
}

func Compare(args []string) int {
    first := args[0]
    second := args[1]
    // ...
}

func Setup(args []string) int {
    if len(args) > 1 {
        taget := args[0]
        // ...
    }
}

func main() {
    // ./app compile firstSource
    // ./app compile firstSource SecondSource ThirdSource
    rcli.CmdFn(Compile).
        Some("source")

    // ./app compare firstValue SecondValue
    rcli.CmdFn(Compare).
        Arg("first").
        Arg("second")

    // ./app setup
    // ./app setup mytarget
    rcli.CmdFn(Setup).
        Opt("target")

    rcli.Run()
}

选项声明

参数的指定是有顺序要求的,而选项则不要求传入顺序。rcli支持为指令定义选项并绑定到一个变量上。选项有如下属性可以设置:

  • 选项至少要有一个关键字
  • 是否必须,默认可选
  • 是否可重复,默认不可以
  • 选项也可以携带任意个具名参数,但是仅支持必须参数
  • 选项可以有描述文本用于生成帮助页

框架会从命令行分析选项是否被指定,每次指定携带的参数。

以上下文为中心

以上下文为中心的命令,可以在上下文结构中使用 *rcli.Opt 类型声明任意个公开的选项变量并指定具体属性。

框架会在命令被关键字选中后,反射出命令上下文结构中的选项定义。并尝试从命令行参数解析选项和它们携带的参数存入选项变量。

被成功分析的选项会从命令行参数中被移除,若分析到参数缺失,或选项缺失或不合理的选项重复,则框架会报告错误并提前退出。

// app.go
package main

import "github.com/godgnidoc/rcli"

type Compile struct {
    DebugInfo *rcli.Opt
    OutputDir *rcli.Opt
    IncludeDir *rcli.Opt
    Source *rcli.Arg
}

func (cmd *Compile) Run(args []string) int {
    // ...
    debug := cmd.DebugInfo.IsOn()
    includes := cmd.IncludeDir.AllArgs()
    output := cmd.OutputDir.AllArgs()[0]
}

func main() {
    rcli.Cmd(&Compile{
        DebugInfo : rcli.Option("-d", "--debug").
            Doc("Generate debug info"),
        OutputDir: rcli.Option("-o", "--output").
            Arg("path").
            Required().
            Doc("Specify the output path of binary"),
        IncludeDir: rcli.Option("-I", "--include").
            Arg("path").
            Repeatable().
            Doc("Add include path"),
        Source: rcli.Some("source")
    })

    rcli.Run()
}

框架为选项提供了一系列方法用于访问存储其中的数据:

  • IsOn 用于判断此选项是否至少被指定过一次
  • AllArgs 把每次指定时携带的参数收集到一个数组中获取
  • Times 获取当前选项的指定次数
  • ArgGroup 获取第i次指定当前选项时携带的参数列表

以函数为中心

以函数为中心的命令也可以定义选项。我们需要先定义好选项指针,把 Option 接口返回的指针保存起来。之后再使用 Use 接口告诉框架当前命令使用哪些选项。

// app.go
package main

import "github.com/godgnidoc/rcli"

var DebugInfo = rcli.Option("-d", "--debug").
    Doc("Generate debug info")
var OutputDir = rcli.Option("-o", "--output").
    Arg("path").
    Required().
    Doc("Specify the output path of binary")
var IncludeDir = rcli.Option("-I", "--include").
    Arg("path").
    Repeatable().
    Doc("Add include path")

func Compile(args []string) int {
    // ...
    debug := DebugInfo.IsOn()
    includes := IncludeDir.AllArgs()
    output := OutputDir.AllArgs()[0]
}

func main() {
    rcli.Cmd(Compile).
        Use(DebugInfo).
        Use(OutputDir).
        Use(IncludeDir).
        Some("source")

    rcli.Run()
}

聪明的你可能已经注意到了,预先定义的选项变量可以被多个命令通过 Use 接口绑定起来。这样可以复用选项定义。

这是一个非常聪明的想法,但是这对选项的设计和版本管理有更高的要求。请慎重使用。

其它功能

应用校验

应用设计需要遵循一些规则,比如长短选项的语法规则,可选参数之后不能定义必须参数等。

有些规则不能通过编译器静态检查,为了性能考虑,框架也不能每次自动执行运行时检查。

所以,用户可以在开发环境下,调用 rcli.ValidateApp 接口检查应用的设计是否合理。待发布应用时,去掉此调用即可。

应用执行

调用 rcli.Run 接口会开始分析命令行参数,执行指定的命令。待命令执行完毕,框架会直接使用命令回调的返回值做退出码结束进程。

rcli.Run 默认使用 os.Args[1:] 作为命令行参数。我们可以手动传入自定义的命令行参数:

rcli.Run(&[]string{"this", "is", "my", "args"})

帮助命令

rcli 框架内置了一个基于上下文的 Help 命令用于从当前的应用设计生成并显示帮助信息。

Help 命令可以展示应用整体的使用说明。也可以展示某个命令的详细说明。

我们需要手动启用帮助命令:

func main() {
    rcli.Compile(Compile)
    rcli.Cmd(rcli.Help)
    rcli.Run()
}

命令文档

前文已经展示了如何在定义选项时顺便指定帮助文本。我们还可以为命令指定帮助文本。

与选项不同,命令支持设置两份帮助文档,即 BriefDoc 。当帮助页要显示整体说明时,会在命令列表中展示每个命令的 Brief。当帮助页要展示某个命令的详细说明时,会打印 Doc 文本。

我们可以在命令上下文中定义 BriefDoc 变量,框架找到这两个变量后就会在恰当的时机打印它们。

type Compile struct {
    DebugInfo *rcli.Opt
    OutputDir *rcli.Opt
    IncludeDir *rcli.Opt
    Source *rcli.Arg
    Brief string
    Doc string
}

func (cmd *Compile) Run(args []string) int {
    // ...
}

func main() {
    rcli.Cmd(&Compile{
        // ...
        Brief: "Short brief of command",
        Doc: "This is the long doc\n" +
            "Which could have multiple lines"
    })

    rcli.Run()
}

对于基于函数的命令,rcli框架页对应提供了 BriefDoc 接口用于指定说明文本:

func Compile(args []string) int {
    // ...
}

func main() {
    rcli.Cmd(Compile).
        Brief("Short brief of command").
        Doc("This is the long doc\n" +
            "Which could have multiple lines")

    rcli.Run()
}

应用信息

帮助命令会在一些页面展示应用信息,我们可以设置如下变量来定制这些页面上的信息:

func main() {
    rcli.AppName = "myapp"
    rcli.AppUsage = "Just for fun"
}

应用版本

rcli 还提供了另外一个命令 Version 用于打印应用版本信息,同样需要手动启用它:

func main() {
    rcli.Cmd(rcli.Version)

    rcli.Run()
}

Version 命令会打印如下可以定制的信息:

func main() {
    rcli.AppName = "myapp"
    rcli.AppAuthor = "myname <myname@mymail.com>"
    rcli.Version = "1.0.0"
}

Documentation

Index

Constants

This section is empty.

Variables

View Source
var AppAuthor string
View Source
var AppName = "App name here"
View Source
var AppUsage = "Describe usage here when no main command is specified"
View Source
var AppVersion string
View Source
var Complete = &complete{}
View Source
var Help = &help{
	More:  More(),
	Brief: "Show help",
	Doc:   "Show help, specify a command to show detailed help",
}
View Source
var Version = &version{
	Brief: "Show version information",
	Verbose: Opt{
		flags: []string{"-v", "--verbose"},
		doc:   "Show verbose output",
	},
}

Functions

func Cmd

func Cmd(cmd Command)

注册命令,cmd 对象被用作命令上下文来查找命令选项

命令的类型名会按照驼峰被拆解,转换为全小写的单词串用作命令名

!!!注意确保使用指针传参

func Main

func Main(cmd Command)

注册主要命令,cmd 对象被用作命令上下文来查找命令选项

命令的类型名会按照驼峰被拆解,转换为全小写的单词串用作命令名 当命令行参数未指定命令时,会执行主要命令

!!!注意确保使用指针传参

func Run

func Run(optional_args ...[]string)

将 optional_args 用作命令行参数,开始执行命令行应用程序

optional_args 是命令行参数列表,省略则自动使用 os.Args[1:]

func ValidateApp

func ValidateApp()

校验应用定义是否正确

Types

type Arg

type Arg struct {
	// contains filtered or unexported fields
}

func Argument

func Argument(name string) *Arg

创建一个必须的参数

func More

func More() *Arg

创建剩余参数

func Optional

func Optional(name string) *Arg

创建一个可选的参数

func Some

func Some(name string) *Arg

创建必须参数和剩余参数

func (*Arg) HasValue

func (arg *Arg) HasValue() bool

func (*Arg) Value

func (arg *Arg) Value() string

获取存入参数的值,使用空格拼接成字符串

func (*Arg) Values

func (arg *Arg) Values() []string

获取存入参数的值,返回原始字符串数组

type Command

type Command interface {
	// 运行命令,args 是剔除全局选项和命令选项后的参数列表
	Run(args []string) int
}

Command 是一个命令行命令的接口

可以为命令定义 Brief 和 Doc 参数来提供命令的简要和详细描述

可以为命令上下文定义若干 Option 字段来设计指令选项

type FnCommand

type FnCommand struct {
	// contains filtered or unexported fields
}

简易命令由框架提供通用上下文结构

func CmdFn

func CmdFn(fn func([]string) int) *FnCommand

注册命令,fn 为命令的执行函数

func MainFn

func MainFn(fn func([]string) int) *FnCommand

注册主要命令,fn 为命令的执行函数

func (*FnCommand) Arg

func (cmd *FnCommand) Arg(name string) *FnCommand

为命令添加参数

func (*FnCommand) Brief

func (cmd *FnCommand) Brief(brief string) *FnCommand

为命令设置简要描述

func (*FnCommand) Doc

func (cmd *FnCommand) Doc(doc string) *FnCommand

为命令设置详细描述

func (*FnCommand) More

func (cmd *FnCommand) More() *FnCommand

为命令添加可重复参数

func (*FnCommand) Opt

func (cmd *FnCommand) Opt(name string) *FnCommand

为命令添加可选参数

func (*FnCommand) Run

func (cmd *FnCommand) Run(args []string) int

运行命令,args 是剔除全局选项和命令选项后的参数列表

func (*FnCommand) Some

func (cmd *FnCommand) Some(name string) *FnCommand

为命令添加必须剩余参数

func (*FnCommand) Use

func (cmd *FnCommand) Use(opt *Opt) *FnCommand

为命令绑定选项

type Opt

type Opt struct {
	// contains filtered or unexported fields
}

选项定义

作为指令上下文的公开成员变量时可以被框架自动识别

func Option

func Option(flag string, more_flags ...string) *Opt

创建选项对象,至少需要一个标志

可以直接创建在命令上下文中作为公开成员变量

也可以通过命令对象的 Use 方法绑定到命令上下文

func (*Opt) AllArgs

func (opt *Opt) AllArgs() []string

获取选项携带的所有参数

func (*Opt) Arg

func (opt *Opt) Arg(name string) *Opt

设置选项参数

func (*Opt) ArgsAt

func (opt *Opt) ArgsAt(i int) []string

获取选项第 i 次被指定时携带的参数

func (*Opt) Doc

func (opt *Opt) Doc(doc string) *Opt

设置选项描述

func (*Opt) Global

func (opt *Opt) Global() *Opt

设置选项为全局选项

func (*Opt) IsOn

func (opt *Opt) IsOn() bool

选项是否被指定

func (*Opt) Repeatable

func (opt *Opt) Repeatable() *Opt

设置选项可重复

func (*Opt) Required

func (opt *Opt) Required() *Opt

设置选项必须

func (*Opt) Times

func (opt *Opt) Times() int

获取选项被指定的次数

Jump to

Keyboard shortcuts

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