使用Cobra建立一个CLI会计应用程序

229 阅读8分钟

当开发者不在他们的IDE文本编辑器中时,他们通常在终端中。
作为一个开发者,你很有可能为你的项目使用过命令行界面(CLI)。

大多数开发者工具在命令行上运行的主要原因是:易于配置。
CLI应用程序允许一定程度的自由,这在图形用户界面(GUI)应用程序中是不容易找到的。

Cobra是一个用于构建CLI应用程序的Go库。它相当流行,在很多流行的开发者工具中使用,如Github CLI、Hugo等等。

在本教程中,我们将通过建立一个简单的会计CLI应用程序来学习Cobra,该程序可以向用户收费,将信息存储在JSON文件中,记录收据,并跟踪用户的总余额。

安装Cobra

有两种方法可以创建Cobra应用程序。

  1. 安装Cobra生成器
  2. 手动添加Cobra到Go应用程序

在本教程中,我们将安装Cobra Generator。这提供了一种简单的方法来生成命令,为应用程序注入活力。

首先,运行下面的命令来安装Cobra Generator。

go get github.com/spf13/cobra/cobra

这将把Cobra安装在GOPATH 目录中,然后生成Cobra应用程序。

了解Cobra CLI命令和标志

在开始构建我们的应用程序之前,我们必须了解CLI
应用程序的主要组成部分。

当使用Git克隆一个项目时,我们通常会运行以下内容。

git clone <url.to.project>

这包括

  • git ,应用程序名称
  • clone, 命令
  • url.to.project ,传递给命令的参数和我们要git 的项目。clone

一个CLI应用程序通常包括应用程序的名称、命令、标志和参数。

考虑一下这个例子。

npm install --save-dev nodemon

这里,npm 是正在运行的应用程序,install 是命令。--save-dev 是传递给install 命令的标志,而nodemon 是传递给命令的参数。

Cobra允许我们非常容易地创建命令并向其添加标志。对于我们的应用程序,我们将创建两个命令:creditdebit **。**通过使用各种标志,我们可以指定交易的用户、交易的金额和交易的说明等项目。

创建Cobra应用程序

要创建一个新的Cobra应用程序,请运行以下命令。

cobra init --pkg-name github.com/<username>/accountant accountant

该命令创建了一个新的文件夹,accountant ,并创建了一个main.go 文件,一个LICENSE 文件,以及一个cmd 文件夹和一个root.go 文件。

注意,这个命令并没有创建一个go.mod 文件,所以我们必须自己初始化go 模块。

go mod init github.com/<username>/accountant
go mod tidy

我们现在可以像运行任何正常的Go程序一样运行这个程序。

go run .

然而,我们也可以通过以下方式构建应用程序。

go build .

并通过以下方式运行该应用程序。

./accountant

Cobra应用程序的入口点

我们的Cobra应用程序的入口点是main.go ,保持主包的精简是很重要的,这样我们就可以把应用程序的不同方面分开。看一下Cobra生成的main.go 文件,我们发现主函数只有一个功能:执行根命令。

cmd.Execute()

根命令,cmd/root.go, ,包含以下内容。

  • rootCmd 结构,它是一个类型的cobraCommand
  • Execute 函数,它被调用于main.go
  • init 函数,它初始化配置并设置根标志。
  • initConfig 函数,它初始化任何设定的配置

目前,运行的应用程序包含一堆Cobra生成的文本。让我们通过将cmd\root.go 修改为以下内容来改变这种情况,这样我们就可以解释我们的应用程序是用来干什么的。

package cmd
import (
  "github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
  Use:   "accountant",
  Short: "An application that helps manage accounts of users",
  Long: `
This is a CLI that enables users to manage their accounts.
You would be able to add credit transactions and debit transactions to various users.
  `,
  // 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() {
  cobra.CheckErr(rootCmd.Execute())
}

运行该应用程序现在应该给出以下响应。

This is a CLI that enables users to manage their accounts.
You would be able to add credit transactions and debit transactions to various users.

Usage:
  accountant [command]

这里,我们删除了Cobra生成的initinitConfig 函数。这是因为我们不需要这个应用程序的任何环境变量,而且root命令也没有什么作用。相反,该应用程序的所有功能都是由特定的命令来完成的。

在Cobra中创建命令

我们的应用程序应该能够处理两个主要功能:借记和贷记给用户。因此,我们必须创建两个命令:debitcredit

运行下面的程序来生成这些命令。

cobra add credit
cobra add debit

这将创建两个新文件:debit.gocredit.go/cmd 目录中。

检查新创建的文件后,在init 函数中添加以下内容。

rootCmd.AddCommand(debitCmd)

这行代码将新创建的命令添加到根命令中;现在,应用程序已经知道了新的命令。

要运行debitCmd 命令,我们必须通过go build . 构建应用程序,并像这样运行应用程序。

./accountant debit

添加一个JSON存储层

对于这个应用程序,我们将使用一个非常简单的存储层。在这种情况下,我们将在一个JSON文件中存储我们的数据,并通过一个go 模块来访问它们。

在根目录下,创建一个database 文件夹,然后创建一个db.go 文件和一个db.json 文件。

将以下内容添加到db.go ,以便与数据库进行交互。

package database
import (
  "encoding/json"
  "os"
  "strings"
)
type User struct {
  Username     string        `json:"username"`
  Balance      int64         `json:"balance"`
  Transactions []Transaction `json:"transactions"`
}
type Transaction struct {
  Amount    int64  `json:"amount"`
  Type      string `json:"string"`
  Narration string `json:"narration"`
}
func getUsers() ([]User, error) {
  data, err := os.ReadFile("database/db.json")
  var users []User
  if err == nil {
    json.Unmarshal(data, &users)
  }
  return users, err
}
func updateDB(data []User) {
  bytes, err := json.Marshal(data)
  if err == nil {
    os.WriteFile("database/db.json", bytes, 0644)
  } else {
    panic(err)
  }
}
func FindUser(username string) (*User, error) {
  users, err := getUsers()
  if err == nil {
    for index := 0; index < len(users); index++ {
      user := users[index]
      if strings.EqualFold(user.Username, username) {
        return &user, nil
      }
    }
  }
  return nil, err
}
func FindOrCreateUser(username string) (*User, error) {
  user, err := FindUser(username)
  if user == nil {
    var newUser User
    newUser.Username = strings.ToLower(username)
    newUser.Balance = 0
    newUser.Transactions = []Transaction{}
    users, err := getUsers()
    if err == nil {
      users = append(users, newUser)
      updateDB(users)
    }
    return &newUser, err
  }
  return user, err
}
func UpdateUser(user *User) {
  // Update the json with this modified user information
  users, err := getUsers()
  if err == nil {
    for index := 0; index < len(users); index++ {
      if strings.EqualFold(users[index].Username, user.Username) {
        // Update the user details
        users[index] = *user
      }
    }
    // update database
    updateDB(users)
  }
}

这里,我们定义了两个结构:UserTransactionUser 结构定义了如何存储和访问用户的信息,如:username,balancetransactionsTransaction 结构存储交易,包括amount,typenarration

我们还有两个向数据库写入的函数。getUsers 加载数据库文件并返回存储的用户数据,而updateDB 将更新的数据写入数据库。

这些函数对这个包来说是私有的,需要公共函数来与它们进行交互。

FindUser 在数据库中找到一个有用户名的用户并返回该用户。如果没有找到用户,则返回nilFindOrCreateUser 检查是否有一个具有用户名的用户并返回;如果没有用户,则用该用户名创建一个新的用户并返回。

UpdateUser 接收用户数据并更新数据库中的相应条目。

这三个函数被导出,在对用户进行贷记和借记时在命令中使用。

用Cobra实现信用交易

用以下内容修改credit 命令,为该命令创建一个适当的描述,并在长描述中增加一个使用部分。

// cmd/credit.go
var creditCmd = &cobra.Command{
  Use:   "credit",
  Short: "Create a credit transaction",
  Long: `
This command creates a credit transaction for a particular user.
Usage: accountant credit <username> --amount=<amount> --narration=<narration>.`,
  Run: func(cmd *cobra.Command, args []string) {
  },
}

然后,当用户试图获得该命令的帮助时,长描述会出现。

Long Description Appears In Terminal When Users Need Help

接下来,我们必须为credit 命令添加必要的标志:amountnarration

creditCmd 的定义后添加以下内容。

var creditNarration string
var creditAmount int64

func init() {
  rootCmd.AddCommand(creditCmd)
  creditCmd.Flags().StringVarP(&creditNarration, "narration", "n", "", "Narration for this credit transaction")
  creditCmd.Flags().Int64VarP(&creditAmount, "amount", "a", 0, "Amount to be credited")
  creditCmd.MarkFlagRequired("narration")
  creditCmd.MarkFlagRequired("amount")
}

init 方法中,我们通过rootCmd.AddCommandcreditCmd 命令附加到root 命令。

接下来,我们必须使用StringVarP 方法创建一个字符串标志,narration 。这个方法接收五个参数。

  • 一个指向存储标志值的变量的指针
  • 标志的名称
  • 标志的短名称
  • 该标志的默认值
  • 当用户通过--help 标志请求帮助时,将提供一条帮助信息。

另外,我们必须通过Int64VarP 方法创建一个新的标志,amount 。这个方法类似于StringVarP ,但是创建了一个64位的整数标志。

之后,我们必须根据需要设置这两个标志。通过这样做,只要在没有这些标志的情况下调用该命令,Cobra就会输出一个错误,说明这些标志是必需的。

完成了信贷命令,我们使用数据库函数来创建交易,并将其添加到用户中。

要做到这一点,请修改run 函数,使其看起来像下面这样。

var creditCmd = &cobra.Command{
  ...
  Run: func(cmd *cobra.Command, args []string) {
    if len(args) < 1 {
      log.Fatal("Username not specified")
    }
    username := args[0]
    user, err := database.FindOrCreateUser(username)
    if err != nil {
      log.Fatal(err)
    }
    user.Balance = user.Balance + creditAmount
    creditTransaction := database.Transaction{Amount: creditAmount, Type: "credit", Narration: creditNarration}
    user.Transactions = append(user.Transactions, creditTransaction)
    database.UpdateUser(user)
    fmt.Println("Transaction created successfully")
  },
}

run 函数是该命令最重要的部分,因为它处理了该命令的主要动作。

因此,我们希望该命令有如下签名。

./accountant credit <username> --amount=<amount> --narration<narration>

这里发送给命令的参数是username ,更确切地说,是args 数组中的第一个项目。这就保证了至少有一个参数传给了命令。

在得到用户名后,我们可以使用数据库包中的FindOrCreateUser 方法来得到该用户名的相应用户信息。

如果该操作成功,我们就会增加该用户的余额,并添加一个新的交易,其中包括金额和说明。然后,我们用新的用户数据更新数据库。

把所有东西放在一起,信贷命令应该是这样的。

package cmd
import (
  "fmt"
  "log"
  "github.com/jameesjohn/accountant/database"
  "github.com/spf13/cobra"
)
// creditCmd represents the credit command
var creditCmd = &cobra.Command{
  Use:   "credit",
  Short: "Create a credit transaction",
  Long: `
This command creates a credit transaction for a particular user.
Usage: accountant credit <username> --amount=<amount> --narration=<narration>.`,
  Run: func(cmd *cobra.Command, args []string) {
    if len(args) < 1 {
      log.Fatal("Username not specified")
    }
    username := args[0]
    user, err := database.FindOrCreateUser(username)
    if err != nil {
      log.Fatal(err)
    }
    user.Balance = user.Balance + creditAmount
    creditTransaction := database.Transaction{Amount: creditAmount, Type: "credit", Narration: creditNarration}
    user.Transactions = append(user.Transactions, creditTransaction)
    database.UpdateUser(user)
    fmt.Println("Transaction created successfully")
  },
}
var creditNarration string
var creditAmount int64
func init() {
  rootCmd.AddCommand(creditCmd)
  creditCmd.Flags().StringVarP(&creditNarration, "narration", "n", "", "Narration for this credit transaction")
  creditCmd.Flags().Int64VarP(&creditAmount, "amount", "a", 0, "Amount to be credited")
  creditCmd.MarkFlagRequired("narration")
  creditCmd.MarkFlagRequired("amount")
}

这样,我们就成功地实现了credit 命令。

用Cobra实现借记交易

debit 命令看起来与credit 命令类似。唯一不同的是run 功能。Debit 减少用户的余额,而credit 增加用户的余额。

debit 命令应该看起来像下面这样。

./accountant debit <username> --amount=<amount> --narration=<narration>

run 函数与debit 的不同之处在于检查用户的余额是否大于借出的金额;我们不希望在我们的数据库中出现负余额。

要做到这一点,请修改debit.go ,使其看起来像下面这样。

package cmd
import (
  "fmt"
  "log"
  "github.com/jameesjohn/accountant/database"
  "github.com/spf13/cobra"
)
// debitCmd represents the debit command
var debitCmd = &cobra.Command{
  Use:   "debit",
  Short: "Create a debit transaction",
  Long: `
This command creates a debit transaction for a particular user.
Usage: accountant debit <username> --amount=<amount> --narration=<narration>.`,
  Run: func(cmd *cobra.Command, args []string) {
    if len(args) < 1 {
      log.Fatal("Username not specified")
    }
    username := args[0]
    user, err := database.FindOrCreateUser(username)
    if err != nil {
      log.Fatal(err)
    }
    if user.Balance > debitAmount {
      user.Balance = user.Balance - debitAmount
      debitTransaction := database.Transaction{Amount: debitAmount, Type: "debit", Narration: debitNarration}
      user.Transactions = append(user.Transactions, debitTransaction)
      database.UpdateUser(user)
      fmt.Println("Transaction created successfully")
    } else {
      fmt.Println("Insufficient funds!")
    }
  },
}

var debitNarration string
var debitAmount int64

func init() {
  rootCmd.AddCommand(debitCmd)
  debitCmd.Flags().StringVarP(&debitNarration, "narration", "n", "", "Narration for this debit transaction")
  debitCmd.Flags().Int64VarP(&debitAmount, "amount", "a", 0, "Amount to be debited")
  debitCmd.MarkFlagRequired("narration")
  debitCmd.MarkFlagRequired("amount")
}

如果用户有足够的余额进行交易,我们用借记金额减少他们的余额,创建一个新的借记交易,并将交易添加到用户身上。最后,我们用更新的用户更新数据库。

如果用户没有足够的余额,我们就输出一个错误信息,说明他们的余额不足。

我们现在可以使用accountant ,向用户扣款。

./accountant debit henry --amount=40 --narration="Paid James"

现在可以通过运行go build 来构建应用程序。

总结

我们刚刚学会了如何使用Cobra来构建CLI应用程序!考虑到Cobra为我们做的大量工作,我们不难理解为什么流行的开源应用程序和工具会在他们的CLI应用程序中使用它。

这个项目可以在这里找到

The postUsing Cobra to build a CLI accounting appappeared first onLogRocket Blog.