作业网址:CLI 命令行实用程序开发实战 - Agenda
代码传送门:https://github.com/zz9629/go/tree/master/agenda
命令行实用程序并不是都象 cat、more、grep 是简单命令。go 项目管理程序,类似 java 项目管理 maven、Nodejs 项目管理程序 npm、git 命令行客户端、 docker 与 kubernetes 容器管理工具等等都是采用了较复杂的命令行。即一个实用程序同时支持多个子命令,每个子命令有各自独立的参数,命令之间可能存在共享的代码或逻辑,同时随着产品的发展,这些命令可能发生功能变化、添加新命令等。因此,符合 OCP 原则 的设计是至关重要的编程需求。
熟悉 go 命令行工具管理项目
综合使用 go 的函数、数据结构与接口,编写一个简单命令行应用 agenda
使用面向对象的思想设计程序,使得程序具有良好的结构命令,并能方便修改、扩展新的命令,不会影响其他命令的代码
项目部署在 Github 上,合适多人协作,特别是代码归并
支持日志(原则上不使用debug调试程序)
官方文档 推荐
golang命令行库cobra的使用 中文翻译
Cobra既是一个用来创建强大的现代CLI命令行的golang库,也是一个生成程序应用和命令行文件的程序。此命令行工具基于cobra开发.
直接执行以下命令,可能安装不成功:(因为cobra用到的一些依赖包被墙了)
go get -v github.com/spf13/cobra/cobra
所以可以首先安装其依赖包: 在$GOPATH/src/golang.org/x目录下(如果没有,则自行创建)用git clone下载sys和text项目。或者直接去网站下载,这个比较快:
然后手动解压到指定目录:
最后再到$GOPATH/bin目录下获得可执行文件。还是之前的命令:
go get -v github.com/spf13/cobra/cobra
bin文件夹下成功生成exe可执行文件:
然后输入cobra即可,如下:
新版cobra需要带–pkg-name参数
新建一个工作路径为$GOPATH/src/zz
进入此目录,使用命令
λ cobra init agenda --pkg-name=zz/agenda
除了生成应用程序框架,还可以通过 cobra add 命令生成子命令的代码文件。
在agenda的目录下,添加子命令register,就是
λ cobra add register
这条命令生成了 agenda程序中 register 子命令的代码,当然了,还没有什么具体的功能,后面会具体介绍写法。
到现在为止,已经给agenda程序添加了两个command,分别为rootCmd
(cobra init 命令默认生成)和registerCmd
下面介绍cobra的工作原理,部分通过刚刚创建的子命令registerCmd
介绍。
main.go文件如下:
package main
import "zz/agenda/cmd"
func main() {
cmd.Execute()
root.go根命令,是整个应用命令的入口,通过在main函数中调用rootCmd.Execute()启动,内部实现中监听了所有命令。root.go代码:
var rootCmd = &cobra.Command{
Use: "agenda",
Short: "A brief description of your application",
Long: `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.`,
Run: func(cmd *cobra.Command, args []string) {
// Do Stuff Here
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
cobra 中有个重要的概念,分别是 commands、arguments 和 flags。其中 commands 代表行为,arguments 就是命令行参数(或者称为位置参数),flags 代表对行为的改变(也就是我们常说的命令行选项)。执行命令行程序时的一般格式为:
[appName] [command] [arguments] --[flag]
比如下面的例子:
# server是 commands,port 是 flag
hugo server --port=1313
# clone 是 commands,URL 是 arguments,brae 是 flag
git clone URL --bare
如果是一个简单的程序(功能单一的程序),使用 commands 的方式可能会很啰嗦,但是像 git、docker 等应用,把这些本就很复杂的功能划分为子命令的形式,会方便使用。
在本程序中,如果要使用子命令register来创建一个用户时,比如输入:
# agenda是程序名,register是commands,-u/-p/-e/-t均是flag,(u的配置为name)
$ agenda register -u=name -p=pass [email protected] -t=8644274
ps:对于命令行参数,有多种输入方式,比如输入用户名的命令行选项-u, 可以是 -uname
或者 -u name
或者 -u=name
,三种方式都可以。
$ [appName] -h
或者$ [appName] [command] -h
时会显示此程序和此命令的字符串;选项(flags)用来控制 Command 的具体行为。根据选项的作用范围,可以把选项分为两类:
对于 persistent 类型的选项,既可以设置给该 Command,又可以设置给该 Command 的子 Command。对于一些全局性的选项,比较适合设置为 persistent 类型,比如控制输出的 verbose 选项:
var Verbose bool
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
local 类型的选项只能设置给指定的 Command,比如下面定义的 source 选项:
var Source string
rootCmd.Flags().StringVarP(&Source, "source", "s", "", "Source directory to read from")
又比如在子命令register.go中,命令register需要新建用户的信息,使用flag进行控制不同的参数。
var registerUser models.User
func init() {
rootCmd.AddCommand(registerCmd)
registerCmd.Flags().StringVarP(®isterUser.Username, "username", "u", "", "The User's Username")
registerCmd.Flags().StringVarP(®isterUser.Password, "password", "p", "", "The User's Password")
registerCmd.Flags().StringVarP(®isterUser.Email, "email", "e", "", "The User's Email")
registerCmd.Flags().StringVarP(®isterUser.Telephone, "telephone", "P", "", "The User's telephone")
}
那么用户在使用的时候,可以输入如 $ agenda register -u[name] -p[password] -e[email] -P[tel]
创建新用户。
该选项不能指定给 rootCmd 之外的其它 Command。
默认情况下的选项都是可选的,但一些用例要求用户必须设置某些选项,这种情况 cobra 也是支持的,通过 Command 的 MarkFlagRequired 方法标记该选项即可:
var Name Region
rootCmd.Flags().StringVarP(&Region, "region", "r", "", "AWS region (required)")
rootCmd.MarkFlagRequired("region")
此项目中需要用到。
在register命令中,我们需要name和password非空,否则无法创建账户,那么添加两行代码。前面的函数最后的具体代码如下:
PS:注意后两行的参数是位置参数username,不是数据结构registerUSer.Username。
首先我们来搞清楚命令行参数(arguments)
与命令行选项(flags/options)
的区别。以常见的 ls 命令来说,其命令行的格式为:
ls [OPTION]... [FILE]…
其中的 OPTION 对应本文中介绍的 flags,以 - 或 – 开头;而 FILE 则被称为参数(arguments)或位置参数。一般的规则是参数在所有选项的后面,上面的 … 表示可以指定多个选项和多个参数。
cobra 默认提供了一些验证方法:
比如要让 Command cmdTimes 至少有一个位置参数,可以这样初始化它:
var cmdTimes = &cobra.Command{
Use: …
Short: …
Long: …
Args: cobra.MinimumNArgs(1),
Run: …
}
不过这个功能本项目没有用到。
这个模块就是具体代码,比如在register中,我们需要对已有的用户名和新用户名进行一些比较等等步骤。
cobra 会自动添加 --help(-h)选项,所以我们可以不必添加该选项而直接使用:
cobra 同时还自动添加了 help 子命,默认效果和使用 --help 选项相同。如果为 help 命令传递其它命令作为参数,则会显示对应命令的帮助信息,下面的命令输出 register子命令的帮助信息:
本次实验,我选择实现的命令式register 、login、logout、userquery、ru ,接下来将分别介绍 。
用户注册
用户登录
用户登出
用户查询
用户删除
agenda
├── cmd //各个命令
| ├── login.go //登陆
| ├── logout.go //登出
| ├── register.go //注册一个用户
| ├── root.go //主函数
| ├── ru.go //删除某个用户
| └── userquery.go //列出所有用户
├── docs
| ├── commandIntro.md //命令说明文档
| ├── entity.md //entity说明文档
| └── models.md //model说明文档
├── entity
| └── userInfoOp.go //存放 User 对象读写与处理逻辑
├── LICENSE
├── log
| └── logFile.txt //log 包记录命令执行情况
├── main.go
├── models
| ├── logger.go //日志处理函数
| └── user.go //user数据结构
├── README.md
└── storage //user实体存储
├── curUser.txt
└── users.json
directory: 6 file: 19
以register为例,简单过一下项目的流程。
这个指令比较复杂,我们首先要读取新用户信息,接着遍历所有用户的名字,检查是否与新用户信息冲突。若不冲突则可以注册,否则则返回失败。
agenda/entity
包 ,这个包存放 User (和 Meeting) 对象读写与处理逻辑。agenda/entity/userInfoOp.go
这个代码文件中。(如果有会议的处理,也会单股用meetingInfoOp.go代码文件,这样是增加文件的内聚性和减小各个文件间的耦合性)entity
包会读取文件storage/user.json
,这个json文件相当于用户的数据库,所有用户的信息都保存在里面。entity
函数会返回所有用户信息。接着我们就可以在这个数组中遍历所有的用户名,来查询冲突。agenda
├── cmd
| ├── register.go //注册一个用户
| └── root.go
├── entity //存放对象读写与处理逻辑
| └── userInfoOp.go //User对象
├── main.go
├── models
| └── user.go //user数据结构
└── storage //数据实体存储
└── users.json //所有user信息
users.json
保存所有用户的信息,register.go
是通过entity包
读写用户集来访问这个文件的,user.json和register.go两个文件并没有直接的接口。创建register命令后,cmd命令中的register.go初始结构如下:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
// registerCmd represents the register command
var registerCmd = &cobra.Command{
Use: "register",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. 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.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("register called")
},
}
func init() {
rootCmd.AddCommand(registerCmd)
}
在这个命令中,添加代码之后如下:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"zz/agenda/entity"
"zz/agenda/models"
)
var registerUser models.User
// registerCmd represents the register command
var registerCmd = &cobra.Command{
Use: "register",
Short: "This command can register user",
Long: `You can use agenda register to sign up one user`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda register]")
users := entity.ReadUserInfoFromFile()
for _, user := range users {
if user.Username == registerUser.Username {
models.Logger.Println(registerUser.Username, "has been registered!")
fmt.Println(registerUser.Username, "has been registered!")
os.Exit(0)
}
}
users = append(users, registerUser)
entity.WriteUserInfoToFile(users)
models.Logger.Println("Register", registerUser.Username, "successfully!")
fmt.Println("Register", registerUser.Username, "successfully!")
},
}
func init() {
rootCmd.AddCommand(registerCmd)
registerCmd.Flags().StringVarP(®isterUser.Username, "username", "u", "", "The User's Username")
registerCmd.Flags().StringVarP(®isterUser.Password, "password", "p", "", "The User's Password")
registerCmd.Flags().StringVarP(®isterUser.Email, "email", "e", "", "The User's Email")
registerCmd.Flags().StringVarP(®isterUser.Telephone, "telephone", "P", "", "The User's telephone")
registerCmd.MarkFlagRequired("username")
registerCmd.MarkFlagRequired("password")
}
下面介绍一下这个具体代码的补充。
首先这个函数需要一些用户的参数,然后创建一个用户类型存储这些信息,方便之后的比较。
首先是用户的数据结构,因此定义这样一个数据结构,在agenda/models/user.go
中。因此在register.go
中可以直接使用这个数据结构,我们需要做的仅仅是import
这个包即可。
ps:本来完整的项目需要实现会议,那么会议的数据结构meeting.go
也是可以直接打包在models文件夹中的。
有了用户的数据结构之后,就可以在register.go中使用了。
首先定义一个新的用户registerUser
,类型为models.User
即
var registerUser models.User
现在需要把命令行中的参数传入registerUser
中。
原理在flag中已经解释过了,这里不再赘述。因为用户名和密码是必须的,而邮箱和电话号码是选填,因此在后两行需要添加MarkFlagRequired
函数。否则的话,可能出现用户名和密码为空的情况。(以防万一,也可以在run模块中对用户名密码的内容进行判断。不过我觉得MarkFlagRequired
已经足够了)
func init() {
rootCmd.AddCommand(registerCmd)
registerCmd.Flags().StringVarP(®isterUser.Username, "username", "u", "", "The User's Username")
registerCmd.Flags().StringVarP(®isterUser.Password, "password", "p", "", "The User's Password")
registerCmd.Flags().StringVarP(®isterUser.Email, "email", "e", "", "The User's Email")
registerCmd.Flags().StringVarP(®isterUser.Telephone, "telephone", "P", "", "The User's telephone")
registerCmd.MarkFlagRequired("username")
registerCmd.MarkFlagRequired("password")
}
现在已经有了新用户对象,接下来就是注册用户的操作了。我们需要数据库(所有用户信息)中所有用户的信息,然后判断是否可以加入这个新用户。
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda register]")//日志前缀
users := entity.ReadUserInfoFromFile() //从数据库中读取用户集
for _, user := range users { //遍历每一个用户
if user.Username == registerUser.Username {
models.Logger.Println(registerUser.Username, "has been registered!")
fmt.Println(registerUser.Username, "has been registered!")
os.Exit(0)
}
}
users = append(users, registerUser) //添加用户
entity.WriteUserInfoToFile(users) //将新的用户集写入数据库
models.Logger.Println("Register", registerUser.Username, "successfully!") //日志记录
fmt.Println("Register", registerUser.Username, "successfully!")
}, //终端输出
用户集的获取与写入均用到了agenda/entity/userInfoOp.go
中的read,write函数,这个文件封装了user对象的读写。
日志的路径处理用到了agenda/models/logger.go
这些放在后面讲。
其他指令和register差不多,先添加指令,然后在cmd/[command].go指令代码中修改代码即可,指令如下:
λ cobra add login
λ cobra add logout
λ cobra add userquery
λ cobra add ru
如图:
最后在相应的cmd/[command].go指令代码中修改代码即可。
entity
包实现的。package cmd
import (
"os"
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var loginUser models.User
var loginCmd = &cobra.Command{
Use: "login",
Short: "This command can login user",
Long: `You can use agenda login to login one user.`,
Run: func(cmd *cobra.Command, args []string) {
users := entity.ReadUserInfoFromFile()
models.Logger.SetPrefix("[agenda login]")
// 判断是否已经登陆过
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true {
// 已经登陆
fmt.Println(user.Username + " has already in")
os.Exit(0)
}
for _, userInfo := range users {
if userInfo.Username == loginUser.Username && userInfo.Password == loginUser.Password {
entity.SaveCurUserInfo(userInfo)
models.Logger.Println("Login", loginUser.Username, "successfully!")
fmt.Println("Login successfully")
os.Exit(0)
} else {
models.Logger.Println("Login", loginUser.Username, "error!")
fmt.Println("Username or Password error, please check your input")
os.Exit(0)
}
}
models.Logger.Println("Login", loginUser.Username, "no such an user!")
fmt.Println("No such an user")
},
}
func init() {
rootCmd.AddCommand(loginCmd)
loginCmd.Flags().StringVarP(&loginUser.Username, "username", "u", "", "The User's Username")
loginCmd.Flags().StringVarP(&loginUser.Password, "password", "p", "", "The User's Password")
loginCmd.MarkFlagRequired("username")
loginCmd.MarkFlagRequired("password")
}
logout是最简单的一部分,先判断当前是否有人登陆,如果有的话,清空登陆信息即可,均用到了entity
包的函数。
package cmd
import (
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var logoutCmd = &cobra.Command{
Use: "logout",
Short: "This command can logout user",
Long: `You can use agenda logout to logout user.`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda logout]")
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true { //有用户登陆
entity.ClearCurUserInfo() //清空登陆信息
fmt.Println(user.Username, "log out")
models.Logger.Println(user.Username, "log out")
} else {
fmt.Println("No user login")//没人登录,这个就不计入日志了
}
},
}
func init() {
rootCmd.AddCommand(logoutCmd)
}
这个命令实现的逻辑和上面相同。如果当前有人登陆,直接列出所有用户的信息即可。
其中调用entity
包查看当前是否有人登陆,以及获得所有用户信息。
package cmd
import (
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var userqueryCmd = &cobra.Command{
Use: "userquery",
Short: "This command can query all user information only for logged in users",
Long: `You can use agenda userquery to get all user information only for logged in users.`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda userquery]")
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true {
models.Logger.Println("UserQuery", user.Username, "query all users infomation!")
users := entity.ReadUserInfoFromFile()
fmt.Println("Name\tPhone\t\tEmail")
for _, userInfo := range users {
fmt.Printf("%-8s%-16s%s\n", userInfo.Username, userInfo.Telephone, userInfo.Email)
}
} else {
fmt.Println("Please login")
}
},
}
func init() {
rootCmd.AddCommand(userqueryCmd)
}
这个命令只针对当前登陆的用户。会清除当前登陆信息,相当于logout;然后删除当前用户。
package cmd
import (
"fmt"
"zz/agenda/models"
"zz/agenda/entity"
"github.com/spf13/cobra"
)
var ruCmd = &cobra.Command{
Use: "ru",
Short: "This command is used to clear the account for the user who has logged in.",
Long: `You can use agenda ru to Clear your account information [use with caution]`,
Run: func(cmd *cobra.Command, args []string) {
models.Logger.SetPrefix("[agenda remove user]")
isLoggedIn, user := entity.IsLoggedIn()
if isLoggedIn == true {
// delete login info
entity.ClearCurUserInfo()
// delete user info
entity.RemoveUser(user.Username)
models.Logger.Println(user.Username, "clear account")
fmt.Println("Remove user ["+ user.Username + "] successfully")
} else {
fmt.Println("Please login first")
}
},
}
func init() {
rootCmd.AddCommand(ruCmd)
}
ReadUserInfoFromFile()
/**
* @arguments: nil
* @return: []models.User
*/
此函数用于从users.json文件中读取所有用户信息。 通过利用文件读操作,包括os、bufio、json-iterator/go等库的使用,我们遍历整个用户信息文件,获取所有用户的models.Meeting切片然后返回。
WriteUserInfoToFile()
/**
* @arguments: []models.User
* @return: nil
*/
此函数用于将当前列表中的更新后的所有用户信息重新写入users.json文件中。 通过利用文件写操作,包括os、bufio、json-iterator/go等库的使用,我们可以将所有用户信息编码为json格式的字符串并存储到users.json文件中。
SaveCurUserInfo()
/**
* @arguments: loginUser models.User
* @return: nil
*/
此函数用于将当前登陆的用户信息存储到curUser.txt文件中,方便登陆用户信息的存储。
ClearCurUserInfo
/**
* @arguments: nil
* @return: nil
*/
当登陆用户登出的时候,我们利用os库Truncate函数来将登录用户信息从curUser.txt文件中删除。
IsLoggedIn
/**
* @arguments: nil
* @return: bool, models.User
*/
此函数判断当前是否已经已经有用户登录,并且返回登录用户信息。 我们可以利用此函数来加一些限定,因为未登录的用户不能进行cm、mtcancel等操作。
IsUser
/**
* @arguments: name string
* @return: bool
*/
此函数用于判端当前用户名是否为已注册的用户,调用ReadUserInfoFromFile并加以判断即可。可以用于在创建、删除会议时判断用户是否存在;或者注册用户时判断该用户名是否已经被注册等。
RemoveUser
/**
* @arguments: name string
* @return: nil
*/
此函数用于移除用处,主要是方便ru操作。调用ReadUserInfoFromFile获取用户信息,加以处理后再调用WriteUserInfoToFile更新用户信息即可。
agenda
目录中go install
之后,可以在任何目录下启动该agendaλ agenda
λ agenda register -u=zz -p=zz -e=[email protected] -t=8763526
λ agenda register -u=aa -p=aa -e=[email protected] -t=83243526
λ agenda register -u=bb -p=bb -e=[email protected] -t=8312352
现在查看agenda/storage/users.json
:
成功写入新用户信息。
λ agenda login -uzz -pzz
λ agenda userquery
λ agenda logout
λ agenda userquery
λ agenda ru
现在查看agenda/storage/users.json
:
已经成功删除zz用户。
测试结束。
Golang : cobra 包简介