服务计算:CLI 命令行实用程序开发实战 - Agenda

服务计算:CLI 命令行实用程序开发实战 - Agenda

github地址
Go Online地址

1、概述

命令行实用程序并不是都象 cat、more、grep 是简单命令。go 项目管理程序,类似 java 项目管理 maven、Nodejs 项目管理程序 npm、git 命令行客户端、 docker 与 kubernetes 容器管理工具等等都是采用了较复杂的命令行。即一个实用程序同时支持多个子命令,每个子命令有各自独立的参数,命令之间可能存在共享的代码或逻辑,同时随着产品的发展,这些命令可能发生功能变化、添加新命令等。因此,符合 OCP 原则 的设计是至关重要的编程需求。

任务目标

  1. 熟悉 go 命令行工具管理项目
  2. 综合使用 go 的函数、数据结构与接口,编写一个简单命令行应用 agenda
  3. 使用面向对象的思想设计程序,使得程序具有良好的结构命令,并能方便修改、扩展新的命令,不会影响其他命令的代码
  4. 项目部署在 Github 上,合适多人协作,特别是代码归并
  5. 支持日志(原则上不使用debug调试程序)
2、GO命令

GO命令 的官方说明并不一定是最新版本。最新说明请使用命令 go help 获取。 关于GO命令

必须了解的环境变量:GOROOT,GOPATH
项目目录与 gopath

2.1 go 命令的格式

使用:

go command [arguments]

版本(go 1.8)的命令有:

build       compile packages and dependencies
clean       remove object files
doc         show documentation for package or symbol
env         print Go environment information
bug         start a bug report
fix         run go tool fix on packages
fmt         run gofmt on package sources
generate    generate Go files by processing source
get         download and install packages and dependencies
install     compile and install packages and dependencies
list        list packages
run         compile and run Go program
test        test packages
tool        run specified go tool
version     print Go version
vet         run go tool vet on packages
2.2 go 命令分类
  1. 环境显示: version、env
  2. 构建流水线: clean、build、test、run、(publish/git)、get、install
  3. 包管理: list, get, install
  4. 杂项:fmt,vet,doc,tools …

具体命令格式与参数使用 go help [topic]

3、准备知识或资源

3.1 Golang 知识整理

这里推荐 time-track 的个人博客,它的学习轨迹与课程要求基本一致。以下是他语言学习的笔记,可用于语言快速浏览与参考:

以上仅代表作者观点,部分内容是不准确的,请用批判的态度看待网上博客。 切记:

  • GO 不是面向对象(OOP) 的。 所谓方法只是一种语法糖,它是特定类型上定义的操作(operation)
  • 指针是没有 nil 的,这可以避免一些尴尬。 p.Xv.x (p 指针, v 值) 在语义上是无区别的,但实现上是有区别的 p.x 是实现 c 语言 p->x 的语法糖
  • zero 值好重要

3.2 JSON 序列化与反序列化

参考:JSON and Go
json 包是内置支持的,文档位置:https://go-zh.org/pkg/encoding/json/

3.3 复杂命令行的处理

不要轻易“发明轮子”。为了实现 POSIX/GNU-风格参数处理,–flags,包括命令完成等支持,程序员们开发了无数第三方包,这些包可以在 godoc 找到。

  • pflag 包: https://godoc.org/github.com/spf13/pflag
  • cobra 包: https://godoc.org/github.com/spf13/cobra
  • goptions 包: https://godoc.org/github.com/voxelbrain/goptions
  • docker command 包:https://godoc.org/github.com/docker/cli/cli/command

go dead project 非常有用

这里我们选择 cobar 这个工具。

tip: 安装 cobra

  • 使用命令 go get -v github.com/spf13/cobra/cobra 下载过程中,会出提示如下错误
Fetching https://golang.org/x/sys/unix?go-get=1
https fetch failed: Get https://golang.org/x/sys/unix?go-get=1: dial tcp 216.239.37.1:443: i/o timeout

这是熟悉的错误,请在 $GOPATH/src/golang.org/x 目录下用 git clone 下载 systext 项目,然后使用 go install github.com/spf13/cobra/cobra, 安装后在 $GOBIN 下出现了 cobra 可执行程序。

Cobra 的简单使用

创建一个处理命令 agenda register -uTestUseragenda register --user=TestUser 的小程序。

简要步骤如下:

cobra init
cobra add register

需要的文件就产生了。 你需要阅读 main.gomain() ; root.goExecute(); 最后修改 register.go, init() 添加:

registerCmd.Flags().StringP("user", "u", "Anonymous", "Help message for username")

Run 匿名回调函数中添加:

username, _ := cmd.Flags().GetString("user")
fmt.Println("register called by " + username)

测试命令:

$ go run main.go register --user=TestUser
register called by TestUser

参考文档:

4、agenda 开发项目
4.1 需求描述
  • 业务需求:见后面附件
  • 功能需求: 设计一组命令完成 agenda 的管理,例如:
    • agenda help :列出命令说明
    • agenda register -uUserName –password pass –email=a@xxx.com :注册用户
    • agenda help register :列出 register 命令的描述
    • agenda cm … : 创建一个会议
    • 原则上一个命令对应一个业务功能
  • 持久化要求:
    • 使用 json 存储 User 和 Meeting 实体
    • 当前用户信息存储在 CurrentUserName.txt 中
  • 项目目录
    • cmd :存放命令实现代码
    • entity :存放 User 和 Meeting 对象读写与处理逻辑
    • 其他目录 : 自由添加
4.2 实现

由于潘老师降低了要求,只要求能实现两个指令就可以了。

我实现了一下三个指令:

  • 注册用户 register
  • 登录 login
  • 登出 logout
(一)总体架构

​ 正如程序结构所示:
在这里插入图片描述

(二)实现逻辑
  • cmd文件夹

    里面的go程序(除了root.go)都是根命令的子命令,定义了命令的逻辑和参数

    拿login.go举例

    /*
    Copyright © 2019 NAME HERE <EMAIL ADDRESS>
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    package cmd
    
    import (
    	"fmt"
    	"agenda/entity"
    	"github.com/spf13/cobra"
    )
    
    // loginCmd represents the login command
    var loginCmd = &cobra.Command{
    	Use:   "login",
    	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("login called")
    		_username,_ := cmd.Flags().GetString("user")
    		_password,_ := cmd.Flags().GetString("password")
    		users := entity.ReadUsers()
    		for _,user := range(users) {
    			if _username == user.Username && _password == user.Password {//匹配
    				entity.SetCurrentUserName(_username)
    				fmt.Println("log in succeed!")
    				fmt.Println("Welcome!"+_username)
    				return 
    			} else if _username == user.Username && _password != user.Password {
    				fmt.Println("Wrong Password!")
    				return
    			}
    		}
    		fmt.Println("No such user, please input a registered username and password")
    		return
    	},
    }
    
    func init() {
    	loginCmd.Flags().StringP("user","u","","log in")
    	loginCmd.Flags().StringP("password","p","","log in")
    	rootCmd.AddCommand(loginCmd)
    
    	// Here you will define your flags and configuration settings.
    
    	// Cobra supports Persistent Flags which will work for this command
    	// and all subcommands, e.g.:
    	// loginCmd.PersistentFlags().String("foo", "", "A help for foo")
    
    	// Cobra supports local flags which will only run when this command
    	// is called directly, e.g.:
    	// loginCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    }
    
    

    用cobra进行初始化之后主要修改loginCmd的Run方法以及init方法实现登录的逻辑。

  • entity文件夹

    负责与数据库的交互以及存储数据

    CurrentUser.txt存储了登录了的用户的Username

    Users.txt存储了已经注册的所有用户的信息(json格式)

    data.go负责交互的方法

    package entity
    
    import (
    	"encoding/json"
    	"fmt"
    	"io/ioutil"
    	"os"
    )
    
    type User struct{
    	Username	string
    	Password	string
    	Email		string
    	Phone 		string
    }
    
    func checkerr(err error){
    	if err != nil {
    		fmt.Println(err)
    	}
    }
    
    func GetCurrentUserName()(username string){
    	dir,err := os.Getwd()
    	checkerr(err)
    	b,err := ioutil.ReadFile(dir+"/entity/currentUser.txt")
    	checkerr(err)
    	username = string(b)
    	return username
    }
    
    func SetCurrentUserName(username string){
    	dir,err := os.Getwd()
    	checkerr(err)
    	b := []byte(username)
    	err = ioutil.WriteFile(dir+"/entity/currentUser.txt",b,0777)
    	checkerr(err)
    }
    
    func ReadUsers()(user []User){
    	dir,err := os.Getwd()
    	checkerr(err)
    	b,err := ioutil.ReadFile(dir+"/entity/Users.txt")
    	var users []User
    	json.Unmarshal(b,&users)
    	return users
    }
    
    func WriteUsers(users []User){
    	dir,err := os.Getwd()
    	checkerr(err)
    	data,err := json.Marshal(users)
    	checkerr(err)
    	b := []byte(data)
    	err = ioutil.WriteFile(dir+"/entity/Users.txt",b,0777)
    	checkerr(err)
    }
    
(三)测试
  • 查看命令的基本信息go run main.go

在这里插入图片描述

在这里插入图片描述

  • 注册用户 go run main.go register -u xxx -p xxx -e xxx -t xxx

    参数都是可选类型的

在这里插入图片描述

​ 注册完Users.txt会更新

在这里插入图片描述

  • 登录 go run main.go login -u xxx -p xxx

    如果Users.txt里面没有用户信息则会报错

在这里插入图片描述
​ 需要输入正确的用户名和密码

在这里插入图片描述

​ 登陆完会发现CurrentUser.txt出现了该用户的名字。

在这里插入图片描述

  • 登出go run main.go logout

    成功登出

在这里插入图片描述

​ 这时CurrentUser.txt会清空
在这里插入图片描述

5、全部代码
  • login.go

    /*
    Copyright © 2019 NAME HERE <EMAIL ADDRESS>
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    package cmd
    
    import (
    	"fmt"
    	"agenda/entity"
    	"github.com/spf13/cobra"
    )
    
    // loginCmd represents the login command
    var loginCmd = &cobra.Command{
    	Use:   "login",
    	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("login called")
    		_username,_ := cmd.Flags().GetString("user")
    		_password,_ := cmd.Flags().GetString("password")
    		users := entity.ReadUsers()
    		for _,user := range(users) {
    			if _username == user.Username && _password == user.Password {//匹配
    				entity.SetCurrentUserName(_username)
    				fmt.Println("log in succeed!")
    				fmt.Println("Welcome!"+_username)
    				return 
    			} else if _username == user.Username && _password != user.Password {
    				fmt.Println("Wrong Password!")
    				return
    			}
    		}
    		fmt.Println("No such user, please input a registered username and password")
    		return
    	},
    }
    
    func init() {
    	loginCmd.Flags().StringP("user","u","","log in")
    	loginCmd.Flags().StringP("password","p","","log in")
    	rootCmd.AddCommand(loginCmd)
    
    	// Here you will define your flags and configuration settings.
    
    	// Cobra supports Persistent Flags which will work for this command
    	// and all subcommands, e.g.:
    	// loginCmd.PersistentFlags().String("foo", "", "A help for foo")
    
    	// Cobra supports local flags which will only run when this command
    	// is called directly, e.g.:
    	// loginCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    }
    
    
  • logout.go

/*
Copyright © 2019 NAME HERE <EMAIL ADDRESS>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

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

// logoutCmd represents the logout command
var logoutCmd = &cobra.Command{
	Use:   "logout",
	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("log out called")
		entity.SetCurrentUserName("")
		fmt.Println("log out succeed!")
	},
}

func init() {
	rootCmd.AddCommand(logoutCmd)

	// Here you will define your flags and configuration settings.

	// Cobra supports Persistent Flags which will work for this command
	// and all subcommands, e.g.:
	// logoutCmd.PersistentFlags().String("foo", "", "A help for foo")

	// Cobra supports local flags which will only run when this command
	// is called directly, e.g.:
	// logoutCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

  • register.go

    /*
    Copyright © 2019 NAME HERE <EMAIL ADDRESS>
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    package cmd
    
    import (
    	"fmt"
    	"agenda/entity"
    	"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")
    		_username,_ := cmd.Flags().GetString("user")
    		_password,_ := cmd.Flags().GetString("password")
    		_email,_ := cmd.Flags().GetString("email")
    		_phone,_ := cmd.Flags().GetString("phone")
    		fmt.Println("username:"+_username+" password:"+_password+" email:"+_email+" phone:"+_phone)
    		if _username == "default user" {
    			fmt.Println("username can not be empty")
    			return 
    		}
    		users := entity.ReadUsers()
    		for i := 0; i < len(users); i++ {
    			if users[i].Username == _username {
    				fmt.Println("this username has been registered")
    				return
    			}
    		}
    		newUser := entity.User{_username,_password,_email,_phone}
    		users = append(users,newUser)
    		entity.WriteUsers(users)
    	},
    }
    
    func init() {
    	registerCmd.Flags().StringP("user","u","default user","Help message for username")
    	registerCmd.Flags().StringP("password","p","123456","password of user")
    	registerCmd.Flags().StringP("email","e","123456@163.com","email of user")
    	registerCmd.Flags().StringP("phone","t","12345647897","phone number of user")//telephone
    	rootCmd.AddCommand(registerCmd)
    
    	// Here you will define your flags and configuration settings.
    
    	// Cobra supports Persistent Flags which will work for this command
    	// and all subcommands, e.g.:
    	// registerCmd.PersistentFlags().String("foo", "", "A help for foo")
    
    	// Cobra supports local flags which will only run when this command
    	// is called directly, e.g.:
    	// registerCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    }
    
    
  • root.go

    /*
    Copyright © 2019 NAME HERE <EMAIL ADDRESS>
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    package cmd
    
    import (
      "fmt"
      "os"
      "github.com/spf13/cobra"
    
      homedir "github.com/mitchellh/go-homedir"
      "github.com/spf13/viper"
    
    )
    
    
    var cfgFile string
    
    
    // rootCmd represents the base command when called without any subcommands
    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.`,
      // Uncomment the following line if your bare application
      // has an action associated with it:
      //	Run: func(cmd *cobra.Command, args []string) { },
    }
    
    // Execute adds all child commands to the root command and sets flags appropriately.
    // This is called by main.main(). It only needs to happen once to the rootCmd.
    func Execute() {
      if err := rootCmd.Execute(); err != nil {
        fmt.Println(err)
        os.Exit(1)
      }
    }
    
    func init() {
      cobra.OnInitialize(initConfig)
    
      // Here you will define your flags and configuration settings.
      // Cobra supports persistent flags, which, if defined here,
      // will be global for your application.
    
      rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cmd.yaml)")
    
    
      // Cobra also supports local flags, which will only run
      // when this action is called directly.
      rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    }
    
    
    // initConfig reads in config file and ENV variables if set.
    func initConfig() {
      if cfgFile != "" {
        // Use config file from the flag.
        viper.SetConfigFile(cfgFile)
      } else {
        // Find home directory.
        home, err := homedir.Dir()
        if err != nil {
          fmt.Println(err)
          os.Exit(1)
        }
    
        // Search config in home directory with name ".cmd" (without extension).
        viper.AddConfigPath(home)
        viper.SetConfigName(".agenda")
      }
    
      viper.AutomaticEnv() // read in environment variables that match
    
      // If a config file is found, read it in.
      if err := viper.ReadInConfig(); err == nil {
        fmt.Println("Using config file:", viper.ConfigFileUsed())
      }
    }
    
    
    
  • data.go

package entity

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
)

type User struct{
	Username	string
	Password	string
	Email		string
	Phone 		string
}

func checkerr(err error){
	if err != nil {
		fmt.Println(err)
	}
}

func GetCurrentUserName()(username string){
	dir,err := os.Getwd()
	checkerr(err)
	b,err := ioutil.ReadFile(dir+"/entity/currentUser.txt")
	checkerr(err)
	username = string(b)
	return username
}

func SetCurrentUserName(username string){
	dir,err := os.Getwd()
	checkerr(err)
	b := []byte(username)
	err = ioutil.WriteFile(dir+"/entity/currentUser.txt",b,0777)
	checkerr(err)
}

func ReadUsers()(user []User){
	dir,err := os.Getwd()
	checkerr(err)
	b,err := ioutil.ReadFile(dir+"/entity/Users.txt")
	var users []User
	json.Unmarshal(b,&users)
	return users
}

func WriteUsers(users []User){
	dir,err := os.Getwd()
	checkerr(err)
	data,err := json.Marshal(users)
	checkerr(err)
	b := []byte(data)
	err = ioutil.WriteFile(dir+"/entity/Users.txt",b,0777)
	checkerr(err)
}
  • main.go

    /*
    Copyright © 2019 NAME HERE <EMAIL ADDRESS>
    
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at
    
        http://www.apache.org/licenses/LICENSE-2.0
    
    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
    */
    package main
    
    import "agenda/cmd"
    
    func main() {
      cmd.Execute()
    }
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值