CLI命令行实用程序开发实战——Agenda

程序完整代码请看github,该程序为个人完成。
Go Online平台的分享链接

Cobra包

  • 输入以下指令安装systext项目
    git clone https://github.com/golang/text
    git clone https://github.com/golang/sys
    
  • 输入go install github.com/spf13/cobra/cobra安装cobra包,在此之前请确认已经安装spf13包。此时,若安装成功,在bin文件夹下有conbra.exe可执行文件。
  • $GOPATH/src路径下输入..\bin\cobra.exe init agenda指令会调用cobra包生成子样例文件夹agenda,在该目录下执行go run main.go指令,输出以下内容。CLI命令行实用程序开发实战——Agenda_第1张图片
  • 此时输入指令..\..\bin cobra.exe add test指令,创建main.go的子程序test.go,更改test.goinit添加新参数user即输入代码testCmd.Flags().StringP("user", "u", "", "test")后改动变量var testCmd中的匿名函数Run,添加代码parameter, _ := cmd.Flags().GetString("user")获取标签信息并输出,输出结果如下,其中可以通过-u Simon指定参数user值。CLI命令行实用程序开发实战——Agenda_第2张图片

Agenda

  • Agenda的目录路径如下
    agenda
    │   main.go
    │   agenda.log
    |   LICENSE  
    │
    └───service
    │   │   Data_process.go
    │   │   Date.go
    │   │   Log.go
    │   │   Meeting.go
    │   │   Service.go
    │   │   User.go
    │   │
    │   └───data
    │       │   User.txt
    │       │   Metting.txt
    │   
    └───cmd
        │   agenda.go
        │   root.go
    

前期准备

  • 数据存储
    • 根据Agenda指令,需要记录用户User和会议Meeting信息,其中分别保存在data/User.txt/data/Meeting.txt
  • Data_process.go
    • 由于需要在用户注册时判断用户名是否存在以及用户登陆时用户信息是否正确,程序执行前,需要先从上述文件中读取相应信息。以Meeting信息为例。
    • 信息的储存与读取都是按照json格式操作,因此需要对Meeting成员进行encode、decode处理。其中分别调用json.Marshaljson.Unmarshal函数
    func meeting_decode(json_info []byte) Meeting{
    	var res Meeting
    	err := json.Unmarshal(json_info, &res)
    	if err != nil {
    		defer log.Println("[Error] Get meeting error!")
    		defer fmt.Println("[Error] Get meeting error!")
    	}
    	return res
    }
    
    func meeting_encode(meeting Meeting)[]byte{
    	json_info, err := json.Marshal(meeting)
    	if err != nil{
    		defer log.Println("[Error] Write meeting error!")
    		defer fmt.Println("[Error] Write meeting error!")
    	}
    	return json_info
    }
    
    • 使用bufio.NewReader函数打开相应文件并调用ReadString('\n')函数每次读取一行数据或调用WriteString函数每次写入一行数据并写入'\n'数据,并进行相应的编码、解码操作。以read_meetings为例,返回读取的所有Meeting信息。
    func read_meetings() []Meeting{
    	file, err := os.Open("./service/data/Meeting.txt")
    	if err != nil {
    		panic(err)
    	}
    	defer file.Close()
    	var res []Meeting
    	meetings := bufio.NewReader(file)
    	for {
    		meeting_json, err := meetings.ReadString('\n')
    		if err != nil || io.EOF == err {
    			break
    		}
    		res = append(res, meeting_decode([]byte(meeting_json)))
    	}
    	return res
    }
    
  • User.go
    • 根据需求,用户注册时需要记录用户名、密码、邮箱、电话以及参与的会议记录即使用以下结构体保存用户名信息。
      type User struct{
      	User_name string
      	User_password string
      	User_email string
      	User_phone int
      	User_meeting []Meeting 
      }
      
    • 按照要求,使用create_user函数创建用户,其中需要确认传入的参数是否符合需求,包括user_phone是否满足11位要求,user_email是否满足对应的规则。调用regexp库使用正则表达式处理
    func isPhone(phone int) bool{
    	res, _ := regexp.MatchString("^1[0-9]{10}$", strconv.Itoa(phone))
    	if !res{
    		defer log.Println("[Error] Phone's format is not correct!")
    		defer fmt.Println("[Error] Phone's format is not correct!")
    	}
    	return res
    }
    
    func isEmail(email string) bool{
    	res, _ := regexp.MatchString("^([a-z0-9_\\.-]+)@([\\da-z\\.-]+)\\.([a-z\\.]{2,6})$", email)
    	if !res{
    		defer log.Println("[Error] Email's format is not correct!")
    		defer fmt.Println("[Error] Email's format is not correct!")
    	}
    	return res
    }
    
    • 还需要确认用户名是否重复,通过遍历从User.txt中读取的users信息查重即可。
    for _,user := range users{
    	if user.User_name == name {
    		defer log.Println("[Error] User's name has exist!")
    		defer fmt.Println("[Error] User's name has exist!")
    		return false
    	}
    }
    
    • 当且仅当上述满足要求时,创建对应user对象并添加到users
    user := User{name, password, email, phone, empty}
    users = append(users, user)
    
  • Date.go
    • 会议创建时需要判断会议的时间是否有效,其中根据月份等要求判断是否满足一定逻辑。
    func isValid(date Date) bool{
    	if date.Year < 1000 || date.Year > 9999 || date.Month > 12 || date.Month < 1 || date.Hour > 24 || date.Hour < 0 || date.Minute > 60 || date.Minute < 0 || date.Day < 1{
    		return false
    	}
    	switch date.Day {
    	case 1, 3, 5, 7, 8, 10, 12:
    		if date.Day > 31 {
    			return false
    		}
    	case 2:
    		if date.Year % 4 == 0{
    			if date.Day > 29{
    				return false
    			}
    		}else{
    			if date.Day > 28{
    				return false
    			}
    		}
    	default:
    		if date.Day > 30{
    			return false
    		}
    	}
    	return true
    }
    
    • 会议创建时要判断成员之间是否存在时间冲突,根据要求,创建compare_date函数判断日期大小,具体代码此处不贴出。
    • 从终端读取string需要进一步转化为data对象,其中包含DateToStringStringToDate,此处设定的规则为yyyy-mm-dd-hh:mm对应date对象中的year month day hour minute
    func DateToString(date Date) string{
    	res := strconv.Itoa(date.Year) + "-" + strconv.Itoa(date.Month) + "-" + strconv.Itoa(date.Day) + "-" + strconv.Itoa(date.Hour) + "-" + strconv.Itoa(date.Minute)
    	return res
    }
    
  • Meeting.go
    • 创建会议时需要记录会议发起者、参加者以及会议时间,对应的Meeting结构体如下
    type Meeting struct{
       Participators []string //just record user's name
       Sponsor,Title string
       Start_time, End_time Date
    }	
    
    • 调用crete_meeting函数创建会议时,需要调用check_meeting判断对应参数是否出错,其中包含调用isValid、compare_date确认日期是否满足需求、判断Title是否唯一、判断用户名是否有重复以及用户参加的会议是否有冲突或用户是否存在或重复,其中部分条件检测代码如下:
    func check_meeting(res Meeting, participators []string, sponsor, title string, start_time, end_time Date) bool{
       if !isValid(start_time) {
       	defer log.Println("[Error] Error start time!")
       	defer fmt.Println("[Error] Error start time!")
       	return false
       }
       if !isValid(end_time) {
       	defer log.Println("[Error] Error end time!")
       	defer fmt.Println("[Error] Error end time!")
       	return false
       }
       if compare_date(start_time, end_time) { // check date
       	defer log.Println("[Error] start time should earily than end time!")
       	defer fmt.Println("[Error] start time should earily than end time!")
       	return false
       }
       for _, meeting := range meetings{ // check title
       	if meeting.Title == title {
       		defer log.Println("[Error] Meeting's title has exist!")
       		defer fmt.Println("[Error] Meeting's title has exist!")
       		return false
       	}
       }
       for _, par := range participators{
       	user_exist := false
       	for _, user := range users{
       		if user.User_name == par{ //check user existence
       			user_exist = true
       			for _, user_meeting := range user.User_meeting{ // check whether user meetings overlap 
       				if isOverlap(res, user_meeting){
       					defer log.Println("[Error] Some users'meetings may overlap!")
       					defer fmt.Println("[Error] Some users'meetings may overlap!")
       					return false
       				}
       			}
       		}
       	}
       	if !user_exist{
       		defer log.Println("[Error] Some users may not exist")
       		defer fmt.Println("[Error] Some users may not exist")
       		return false
       	} 
       }
       for i := 0; i < len(participators); i++{ // check whether users duplicate
       	for j := i + 1; j < len(participators); j++{
       		if participators[i] == participators[j]{
       			defer log.Println("[Error] Some users may not duplicate")
       			defer fmt.Println("[Error] Some users may not duplicate")
       			return false
       		}
       	}
       }
       return true
    }
    func isOverlap(meeting1, meeting2 Meeting) bool{ // false mean not overlap, input is one person's two meetings
       if compare_date(meeting1.Start_time, meeting2.End_time) || compare_date(meeting2.Start_time, meeting1.End_time){
       	return false
       }else{
       	return true
       }
    }
    
    • 删除会议有两种情况,直接删除会议或删除某个成员,但无论那种情况下删除会议前都需要通过会议信息删除对应成员的会议记录,其中调用delete_user_meeting删除掉该成员的该条会议信息。
    • 首先需要找到用户名对应的users下标并调用append函数删除用户名参加的会议Title对应的会议。
    func delete_user_meeting(meeting Meeting, user_name string) bool{//delete meeting from users
       user_idx := -1
       for index, user_tmp := range users{
       	if user_tmp.User_name == user_name{
       		user_idx = index
       		break
       	}
       }
       if user_idx == -1{
       	defer log.Println("[Error] User does not partipant current meeting!")
       	defer fmt.Println("[Error] User does not partipant current meeting!")
       	return false
       }
       for index, meeting_tmp := range users[user_idx].User_meeting{ 
       	if meeting_tmp.Title == meeting.Title {
       		users[user_idx].User_meeting = append(users[user_idx].User_meeting[:index], users[user_idx].User_meeting[index + 1 :]...)
       		break
       	}
       }
       return true
    }
    
    • 以删除会议为例,删除整个会议前需要对会议的参加者和发起者都调用delete_user_meeting函数后再调用append函数从meetings中删除对应会议对象。而删除会议特定成员只需要找到对应的成员是否存在会议中并调用相关函数即可
    func delete_meeting(meeting_idx int) bool{
       meetings[meeting_idx].Participators = append(meetings[meeting_idx].Participators, meetings[meeting_idx].Sponsor)
       for _, user := range meetings[meeting_idx].Participators { //update all influenced user info
       	delete_user_meeting(meetings[meeting_idx], user)
       }
       meetings = append(meetings[:meeting_idx], meetings[meeting_idx + 1 : ]...)
       defer log.Println("[Success] Delete meeting successful!")
       defer fmt.Println("[Success] Delete meeting successful!")
       return true
    }
    
  • Log.go
    • 根据题目要求,将程序的输出保存到日志中,对应的```Log.go``中代码如下:
    type LogFile struct{
    	file string
    }
    
    
    func (Log *LogFile) Write(p []byte) (int, error) {
        f, err := os.OpenFile(Log.file, os.O_CREATE|os.O_APPEND, 0666)
        defer f.Close()
    
        if err != nil {
            return -1, err
        }
        return f.Write(p)
    }
    
    • 由于程序运行过程中也需要提示用户操作步骤是否出错或成功,因此输出包含fmtlog同时执行,如
    log.Println("[Error] Some info do not fit the standard!")
    fmt.Println("[Error] Some info do not fit the standard!")
    
  • Service.go
    • 上述代码准备完成后,配置UI代码Service.go,此处包含对Agenda的初始化以及提供agenda.go使用的交互函数如Login_in()、Create_user(),以大写字母开头表示public允许函数在包外调用,此处需要注意的是,由于agenda.goData_process.go不在同级目录下,因此如果要在包外执行程序,对应的txt文件路径要有相应的变化。
    • 用户选择注册操作时,会调用相应的Create_user函数,其中直接调用create_user即可,而当注册成功后会默认进入Login_in()函数,需要验证用户名与密码信息是否存在与匹配。
    for index, user := range users{
    	if user.User_name == name && user.User_password == password{
    		user_index = index
    		break 
    	}
    }
    
    • 如果匹配,则进入process函数即用户交互界面,其中提供了5个功能,分别为退出、创建会议、删除会议、删除会议成员、查询当前用户会议记录。每个功能对应不同函数,并分别调用上述所讲的对应函数进行操作。当运行结束后,则写入信息。
    func process(){
    	var choice string
    	for {
    		fmt.Println("--q 	input q to quit agenda")
    		fmt.Println("--cm 	create a new meeting")
    		fmt.Println("--mr	remove a meeting")
    		fmt.Println("--pr	remove a particopator from a meeting")
    		fmt.Println("--qm	query current user's meeting")
    		fmt.Scanln(&choice)
    		if(choice == "q"){
    			break
    		}else if(choice == "cm"){
    			cm_cmd()
    		}else if(choice == "mr"){
    			mr_cmd()
    		}else if(choice == "pr"){
    			pr_cmd()
    		}else if(choice == "qm"){
    			qm_cmd()
    		}
    	}
    	write_users()
    	write_meetings()
    }
    
    • cm_cmd函数为例,其中需要创建会议,因此需要用户输入会议信息后调用create_meeting函数即可。
    func cm_cmd(){
    	var participators []string
    	//sponsor is default to be current user
    	var par, par_number, title, start_time, end_time string
    	fmt.Println("Input your meeting's participators number: ")
    	fmt.Scanln(&par_number)
    	par_num, _ := strconv.Atoi(par_number)
    	for i := 0; i < par_num ; i++{
    		fmt.Println("Input number ", i, "participator's name")
    		fmt.Scanln(&par)
    		participators = append(participators, par)
    	}
    	fmt.Println("Input your meeting's title: ")
    	fmt.Scanln(&title)
    	fmt.Println("Input your meeting's start time: ")
    	fmt.Scanln(&start_time)
    	fmt.Println("Input your meeting's end time: ")
    	fmt.Scanln(&end_time)
    	create_meeting(participators, users[user_index].User_name, title, StringToDate(start_time), StringToDate(end_time))
    }
    
  • Agenda.go
    • 此时调用Cobra包生成相应的文件便于执行程序,按照上述做法创建Agenda后修改相应的匿名函数Init,具体为通过命令行获取相应的初始化信息如用户登陆/注册前需要用到的信息而非直接进入process函数进行用户交互。
    • 其中Init函数如下:
    agendaCmd.Flags().StringP("user_name", "u", "", "user's name")
    agendaCmd.Flags().StringP("user_password", "p", "", "user's password")
    agendaCmd.Flags().StringP("user_email", "e", "", "user's email")
    agendaCmd.Flags().IntP("user_telephone", "t", 0, "user's telephone number")
    agendaCmd.Flags().BoolP("register", "r", false, "register a new user and login in")
    agendaCmd.Flags().BoolP("login_in", "l", false, "login in with current user")
    
    • Run对应添加的代码如下:
    Run: func(cmd *cobra.Command, args []string) {
    	fmt.Println("agenda called")
    	user_name, _ := cmd.Flags().GetString("user_name")
    	user_password, _ := cmd.Flags().GetString("user_password")
    	user_email, _ := cmd.Flags().GetString("user_email")
    	user_phone, _ := cmd.Flags().GetInt("user_telephone")
    	register, _ := cmd.Flags().GetBool("register")
    	login, _ := cmd.Flags().GetBool("login_in")
    	if(register && login){
    		log.Println("[Error] Do not set -l and -r true together!")
    		fmt.Println("[Error] Do not set -l and -r true together!")
    		return
    	}
    	if(register){
    		service.Init() 
    		if service.Create_user(user_name, user_password, user_email, user_phone){
    			service.Login_in(user_name, user_password)
    		}else{
    			log.Println("[Error] Some info do not fit the standard!")
    			fmt.Println("[Error] Some info do not fit the standard!")
    		}
    	}
    	if(login){
    		service.Init() 
    		service.Login_in(user_name, user_password)
    	}
    },
    

测试

  • agenda目录下运行go run main.go agenda -h指令获取对应命令行提示信息,如下:CLI命令行实用程序开发实战——Agenda_第3张图片
  • 此时输入添加相应的指令如-r true -u d -p 123456 -t 11111111111 -e 111111@qq,com输入必要信息以此注册用户,其中需要注意-l -r指令默认为false需要指定为true且不能两个同时为true否则会报错。若用户名不存在则创建成功,输出以下内容CLI命令行实用程序开发实战——Agenda_第4张图片
  • 若输入信息不全或格式不正确,则输出以下内容:
    在这里插入图片描述
  • 若用户名存在,则输出以下内容:
    -
  • 此时尝试登陆指令,由于之前注册登陆过使用-l true -u a -p 123456进入相应账号。
    CLI命令行实用程序开发实战——Agenda_第5张图片
  • 输入指令cm可以创建会议,根据相应指示输入参数,如下
    CLI命令行实用程序开发实战——Agenda_第6张图片
  • 当输入的用户不存在或参数格式不正确或参数重复或时间冲突等情况下,会报相应错误,此处不细讲,具体的可以自己运行程序尝试。
  • 此时调用qm可以查询当前用户的会议信息,如下CLI命令行实用程序开发实战——Agenda_第7张图片
  • 调用pr移除特定Title的会议成员,如下:
    CLI命令行实用程序开发实战——Agenda_第8张图片
  • 调用mr则移除整个对应Title会议,此时再次输入qm指令输出为空,操作如下:
    CLI命令行实用程序开发实战——Agenda_第9张图片
  • 观察User.txt文件,发现其中users信息以json格式保存:在这里插入图片描述
  • 同理,Meeting.txt如下在这里插入图片描述
  • 观察输出日志agenda.log,其中记录了操作过程中的相关信息,如下
    CLI命令行实用程序开发实战——Agenda_第10张图片
  • 然而程序运行需要在go online上运行,而相关配置与visual studio code不同,相关包的安装使用有问题,因此只能将文件压缩到main.go中并修改了相应的UI界面,直接输入指令go run main.go进入程序并按照相似指令操作即可,即
    CLI命令行实用程序开发实战——Agenda_第11张图片
  • 其中保存数据的txt文件正常运行无误,Agenda执行完毕。
    在这里插入图片描述
    在这里插入图片描述

你可能感兴趣的:(服务计算,实验报告)