使用urfave/cli实现命令行备忘录

使用urfave/cli实现命令行备忘录

🍕前言

本周抽空了解了一下go的工具包:github.com/urfave/cli还是挺有意思的;

于是就写了一个基于github.com/urfave/cli包的简单命令行备忘录demo;

功能很简单,代码也并不是很复杂,当作入门来看还是很不错的;

当然,学这些工具并不是就是为了去实现一个简单备忘录,在这个学习的过程中能了解到很多新鲜的东西亦能拓宽我们的视野,比如说,知道了黑窗口中各种命令究竟是如何去实现的,为什么go语言适合去制作这种命令行工具,shell脚本如何去实现的,docker,数据库命令,git等等的命令原理,这有助于我们对计算机整体知识和底层的理解;

话不多说,我们进入正题;

🍔urfave/cli入🚪

github.com/urfave/cli包的介绍

github.com/urfave/cli 是一个 Go 语言的命令行应用程序开发包。它提供了一组简单且功能强大的工具,用于构建命令行界面的应用程序,包括解析命令行参数、定义命令和子命令、显示帮助信息等功能。

具体来说,github.com/urfave/cli 包的主要功能包括:

  1. 解析命令行参数: 可以方便地解析命令行中的选项和参数,并将它们映射到相应的数据结构中。
  2. 定义命令和子命令: 可以定义主命令以及其对应的子命令,使得命令行应用程序可以拥有层次结构和多个操作选项。
  3. 显示帮助信息: 自动生成并显示命令行应用程序的帮助信息,包括命令、选项、参数的说明和用法示例等。
  4. 处理命令执行: 可以将函数与特定的命令关联起来,当用户输入相应的命令时,执行相应的函数逻辑。
  5. 处理错误和异常情况: 可以定义自定义的错误处理逻辑,以及处理未知命令和参数错误的方式。
入门程序介绍
package main

import (
	"fmt"
	"log"
	"os"

	"github.com/urfave/cli"
)

func main() {
	// 创建一个新的命令行应用程序
	app := cli.NewApp()

	// 设置应用程序的名称、版本和简要描述
	app.Name = "MyCLI"
	app.Version = "1.0.0"
	app.Usage = "A simple command-line application"

	// 定义一个命令
	app.Commands = []cli.Command{
		{
			Name:    "greet",       //命令触发词:greet
			Aliases: []string{"g"},  //简写:g
			Usage:   "Greet someone",
			Action:  greet,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "name, n",
					Value: "World",
					Usage: "Name of the person to greet",
				},
			},
		},
	}

	// 运行命令行应用程序
	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}

// greet 函数用于执行 greet 命令的逻辑
func greet(c *cli.Context) error {
	name := c.String("name")
	fmt.Printf("Hello, %s!\n", name)
	return nil
}

这段代码意思是:

定义了一个名为 “MyCLI” 的命令行应用程序,它包含一个命令 “greet”,用于向某人打招呼。命令 “greet” 可以通过 greetg 触发,并接受一个名为 “name” 的选项,用于指定要打招呼的人的姓名。如果用户没有提供姓名,则默认使用 “World”。

当用户执行命令 ./MyCLI greet 时,默认会输出 “Hello, World!”。如果用户执行命令 ./MyCLI greet --name Alice,则会输出 “Hello, Alice!”。

其实整个cli项目框架就是:

1.初始化命令行程序,定义版本等等信息;

2.定义各种命令(包括触发关键词,命令报错,命令方法的调用等)

3.写各种命令的详细代码实现;

ok;相信你已经对这个包的基础使用已经有了一部分的了解;

现在就引入备忘录工具的实现;

🍟备忘录具体代码讲解

先看项目的分层:

image-20240417160411245

各文件的含义:

  • commands文件存储命令的具体实现代码
  • memo为可执行文件
  • memo.go为项目入口,也包含了命令说明及调用
  • memos.txt存储备忘录中的数据
memo

按照刚刚讲的初始化会放在:memo.go文件中:确实如此:

package main

import (
	"github.com/urfave/cli"
	"log"
	"os"
	"time"
)

// Memo represents a memo item
type Memo struct {
	ID         int
	Text       string
	CreatedAt  time.Time
	ModifiedAt time.Time
}
var memos []Memo
var lastID int
func main() {
	loadMemos() // 加载备忘录数据
	app := cli.NewApp() //初始化app类型数据
	app.Name = "Memo"
	app.Usage = "A simple command-line memo application"
	app.Version = "1.0.0"
	app.Commands = []cli.Command{
		{
			Name:    "add",
			Aliases: []string{"a"},
			Usage:   "Add a new memo",
			Action:  addMemo,
		},
		{
			Name:    "list",
			Aliases: []string{"l"},
			Usage:   "List all memos",
			Action:  listMemos,
		},
		{
			Name:    "delete",
			Aliases: []string{"d"},
			Usage:   "Delete a memo by ID",
			Action:  deleteMemo,
			Flags: []cli.Flag{
				cli.IntFlag{
					Name:  "id",
					Usage: "ID of the memo to delete",
				},
			},
		},
		{
			Name:    "search",
			Aliases: []string{"s"},
			Usage:   "Search memos by keyword",
			Action:  searchMemos,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "keyword, k",
					Usage: "Keyword to search for in memos",
				},
			},
		},
		{
			Name:    "update",
			Aliases: []string{"u"},
			Usage:   "Update a memo by ID",
			Action:  updateMemo,
			Flags: []cli.Flag{
				cli.IntFlag{
					Name:  "id",
					Usage: "ID of the memo to update",
				},
				cli.StringFlag{
					Name:  "text, t",
					Usage: "New text for the memo",
				},
			},
		},
	}
	err := app.Run(os.Args) //没有启动起来就直接报错;
	if err != nil {
		log.Fatal(err)
	}
}

工具主页:

image-20240417161820722

别看代码这么多,其实并没有多复杂,因为我们要实现备忘录那么必定是要存数据的,因为是学习的小项目,所以数据干脆直接存到txt文件中了,实际上会存到gob后缀或者存入数据库中,为了项目的简化和易用性以及性能,存gob中应该是最好的选择,但是由于gob是二进制文件,当作教学并不是很直观,txt存少量数据也可以;

所以既然要存数据,那么数据肯定是有属性的,这里只写了几个简单数据:id,内容,生成时间,修改时间

type Memo struct {
	ID         int
	Text       string
	CreatedAt  time.Time
	ModifiedAt time.Time
}
var memos []Memo //切片从前到后存储所有的数据结构体;
var lastID int   //给数据排序,为下次加入数据时编号;

初始化:

func main() {
	loadMemos() // 加载备忘录数据
	app := cli.NewApp() //初始化app类型数据
	app.Name = "Memo"
	app.Usage = "A simple command-line memo application"
	app.Version = "1.0.0"
    //...各种命令的定义
    //...
    //...
    err := app.Run(os.Args) //没有启动起来报错;
	if err != nil {
		log.Fatal(err)
	}
}

各种命令的触发和参数定义:

app.Commands = []cli.Command{
		{
			Name:    "add",
			Aliases: []string{"a"},
			Usage:   "Add a new memo",
			Action:  addMemo,
		},
		{
			Name:    "list",
			Aliases: []string{"l"},
			Usage:   "List all memos",
			Action:  listMemos,
		},
		{
			Name:    "delete",
			Aliases: []string{"d"},
			Usage:   "Delete a memo by ID",
			Action:  deleteMemo,
			Flags: []cli.Flag{
				cli.IntFlag{
					Name:  "id",
					Usage: "ID of the memo to delete",
				},
			},
		},
		{
			Name:    "search",
			Aliases: []string{"s"},
			Usage:   "Search memos by keyword",
			Action:  searchMemos,
			Flags: []cli.Flag{
				cli.StringFlag{
					Name:  "keyword, k",
					Usage: "Keyword to search for in memos",
				},
			},
		},
		{
			Name:    "update",
			Aliases: []string{"u"},
			Usage:   "Update a memo by ID",
			Action:  updateMemo,
			Flags: []cli.Flag{
				cli.IntFlag{
					Name:  "id",
					Usage: "ID of the memo to update",
				},
				cli.StringFlag{
					Name:  "text, t",
					Usage: "New text for the memo",
				},
			},
		},
	}

经过上边简单示例说明,相信这个也是可以一看就懂的,接下来讲解每个命令的具体实现:

具体方法实现

commands.go

package main

import (
	"bufio"
	"fmt"
	"github.com/urfave/cli"
	"log"
	"os"
	"strconv"
	"strings"
	"time"
)
func addMemo(c *cli.Context) error {
	text := c.Args().First()
	if text == "" {
		return fmt.Errorf("Please provide memo text")
	}

	lastID++
	now := time.Now()
	memos = append(memos, Memo{ID: lastID, Text: text, CreatedAt: now, ModifiedAt: now})
	saveMemos() // 保存备忘录数据到文本文件
	fmt.Println("Memo added successfully")
	return nil
}
//显示数据列表
func listMemos(c *cli.Context) error {
	if len(memos) == 0 {
		fmt.Println("No memos found")
		return nil
	}

	fmt.Println("ID\tMemo\tCreated At\tModified At")
	for _, memo := range memos {
		fmt.Printf("%d\t%s\t%s\t%s\n", memo.ID, memo.Text, memo.CreatedAt.Format("2006-01-02 15:04:05"), memo.ModifiedAt.Format("2006-01-02 15:04:05"))
	}
	return nil
}
func deleteMemo(c *cli.Context) error {
	id := c.Int("id")
	if id <= 0 {
		return fmt.Errorf("Please provide a valid memo ID")
	}
	found := false
	for i, memo := range memos {
		if memo.ID == id {
			memos = append(memos[:i], memos[i+1:]...)
			found = true
			break
		}
	}
	if found {
		saveMemos() // 保存备忘录数据到文本文件
		fmt.Println("Memo deleted successfully")
	} else {
		fmt.Println("Memo not found")
	}

	return nil
}

func searchMemos(c *cli.Context) error {
	keyword := c.String("keyword")
	if keyword == "" {
		return fmt.Errorf("Please provide a keyword to search for")
	}

	found := false
	for _, memo := range memos {
		if strings.Contains(memo.Text, keyword) {
			fmt.Printf("ID: %d\tMemo: %s\n", memo.ID, memo.Text)
			found = true
		}
	}

	if !found {
		fmt.Println("No memos found matching the keyword")
	}

	return nil
}

func updateMemo(c *cli.Context) error {
	id := c.Int("id")
	if id <= 0 {
		return fmt.Errorf("Please provide a valid memo ID")
	}

	text := c.String("text")
	if text == "" {
		return fmt.Errorf("Please provide new text for the memo")
	}

	found := false
	for i, memo := range memos {
		if memo.ID == id {
			memos[i].Text = text
			memos[i].ModifiedAt = time.Now()
			found = true
			break
		}
	}

	if found {
		saveMemos() // 保存备忘录数据到文本文件
		fmt.Println("Memo updated successfully")
	} else {
		fmt.Println("Memo not found")
	}

	return nil
}

// 加载备忘录数据
func loadMemos() {
	file, err := os.OpenFile("memos.txt", os.O_RDONLY|os.O_CREATE, 0644)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := scanner.Text()
		fields := strings.Split(line, "\t")
		if len(fields) != 4 {
			log.Printf("Invalid memo data: %s", line)
			continue
		}

		id, err := strconv.Atoi(fields[0])
		if err != nil {
			log.Printf("Invalid memo ID: %s", fields[0])
			continue
		}

		createdAt, err := time.Parse("2006-01-02 15:04:05", fields[2])
		if err != nil {
			log.Printf("Invalid created at time: %s", fields[2])
			continue
		}

		modifiedAt, err := time.Parse("2006-01-02 15:04:05", fields[3])
		if err != nil {
			log.Printf("Invalid modified at time: %s", fields[3])
			continue
		}

		memos = append(memos, Memo{
			ID:         id,
			Text:       fields[1],
			CreatedAt:  createdAt,
			ModifiedAt: modifiedAt,
		})
	}

	// 设置 lastID 为最大的备忘录项ID加1
	for _, memo := range memos {
		if memo.ID > lastID {
			lastID = memo.ID
		}
	}
}

// 保存备忘录数据到文本文件
func saveMemos() {
	file, err := os.OpenFile("memos.txt", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) //写入模式
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	writer := bufio.NewWriter(file) //缓存数据,提高写入效率
	for _, memo := range memos {
		line := fmt.Sprintf("%d\t%s\t%s\t%s\n", memo.ID, memo.Text, memo.CreatedAt.Format("2006-01-02 15:04:05"), memo.ModifiedAt.Format("2006-01-02 15:04:05"))
		if _, err := writer.WriteString(line); err != nil {
			log.Println("Error writing memo:", err)
		}
	}

	if err := writer.Flush(); err != nil {
		log.Println("Error flushing writer:", err)
	}
}

方法很多,重复度也很高,并不会全部都讲

add:增加备忘录数据
func addMemo(c *cli.Context) error {
	text := c.Args().First()
	if text == "" {
		return fmt.Errorf("Please provide memo text")
	}

	lastID++
	now := time.Now()
	memos = append(memos, Memo{ID: lastID, Text: text, CreatedAt: now, ModifiedAt: now})
	saveMemos() // 保存备忘录数据到文本文件
	fmt.Println("Memo added successfully")
	return nil
}

判断关键词是add才会调用该方法:

判断text不符合格式会报错,若符合格式则计数id+1,将时间初始化,调用保存方法将结构体存入文件中并返回添加成功;

image-20240417162420819

list:显示备忘录中所有数据;

image-20240417162614789

delete删除备忘录中数据

根据id删除数据

image-20240417162731308

search 查询备忘录中内容

查询的是内容中的数据,当有多条符合可以全部查出;

image-20240417162905832

update 更新指定id的数据;

image-20240417163620230

其他就不说了,项目代码可以自己看具体的实现;

项目地址:命令行备忘录工具

下次再见;

  • 10
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值