The framework I am using to build this blog is Hugo. Under the hood, it uses Cobra to build the command line interface. Besides Hugo, Cobra is also used by many other popular projects such as kubectl, github cli, etc. I was curious about how it works so I decided to learn by building a simple command line app with it.
The app I am going to build is a regex tool that uses openAI’s API to generate a regex expression based on a list of strings or the semantic meaning of the given strings(e.g. email address). As this app needs to offer several options to the user, it is a nice fit for Cobra.
The structure of Cobra
Cobra as it said in offical docs is a bunch of concepts including commands, arguments, and flags. Typically the app works like APPNAME COMMAND ARG --FLAG
The simplest code snippet to create a command is like this:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
}
var awesomeCmd = &cobra.Command{
Use: "awesomeapp",
Short: "Just awesome",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Just awesome")
},
}
func main() {
rootCmd.AddCommand(awesomeCmd)
rootCmd.Execute()
}
Flags
Flags are used to pass options to the command. Under the hood, Cobra uses pflag which follows the POSIX/GNU standard as an improved replacement for Go’s flag package.
Several types of flags are supported by Cobra including int, bool, string, etc. one important question is how the flag work together with your command execution component, or how to save and retrive a flag.
As in my case I need to pass just the short name of the flag without setting a value. Initially I think it could be a string flag like this:
cmd.Flags().StringP("string", "s", "", "string to be processed")
However I quickly found that it’s a bit tricky to get value of the flag, because in this way I can only get the flag value by using cmd.Flags().Lookup("string").Value.String()
. It’s a bit verbose and not intuitive. Then I checked the source code of flags and found there are options StringVarP which can be used to bind the flag to a variable. So I changed the code to this:
var str string
cmd.Flags().StringVarP(&str, "string", "s", "", "string to be processed")
Now I can get the value of the flag by using str
directly.
However the next quetion comes, how I can make the flag optional and don’t mix it with the arguments. For the first one I can give the flag a default value through its input parameter. But for the second one I just can’t find a nice way to tell cobra the flag value is omitted.
After some research I found that I went to the wrong direction. I should not use a string flag but a bool flag. The code snippet is like this:
var boolFlag bool
cmd.Flags().BoolVarP(&boolFlag, "bool", "b", false, "bool flag")
so in this way I don’t need to pass a value to the flag, just use --bool
to set the flag to true. If I don’t pass the flag, it will be false by default. that’s what I need.
Logic codes
After solving the structure and flags, the next step is to write the major logic, in my case it is to call the openAI API to generate the regex expression. This is traightforward, the only trick is define the flag types to match them to different prompts. Because I use boolFlag to hold each option, I don’t want to make it too verbose by declaring a bunch of variables. So I create a struct to hold all the flags and pass it to the function that calls the API. The code snippet is like this:
type ModeFlag struct {
pattern bool
context bool
semantic bool
}
Then the flags are defined like this:
rgxCmd.Flags().BoolVarP(&modelFlag.pattern, "pattern", "p", false, "Find common patterns of the given strings separated by empty space")
rgxCmd.Flags().BoolVarP(&modelFlag.context, "context", "c", false, "Math the first input string from the second input string")
rgxCmd.Flags().BoolVarP(&modelFlag.semantic, "semantic", "s", false, "Generate a regular expression based on semantic meaning, e.g. 'email address'")
On the other hand, the function that calls the API is like this:
func generateRegex(modeFlag *ModeFlag, inputStrings []string, apiKey string) (string, error) {
I understand there must be some clever ways to make the code more elegant, but this works just fine for me.
Package and Install
With go mod system it is very easy to package and install the app. Just run go install
under your root folder and the binary will be installed to $GOPATH/bin
. Then you can run the app in command line with the name you specified in your mod.
The beautifule thing about using github to host your code is you don’t need to deploy your binary to any package repository like npm or pypi. When you push your code to github. you can immediately install this app by using go install github.com/yourname/yourapp
.
In this way you create a distrubtable app with just a few lines of code. Isn’t it cool?
Wrap up
The good thing of cobra is it saves you time on dealing with trivial stuff like parsing the command line arguments and flags. You can focus on the logic of your app. However it also worth thinking and structuring your app before you start. e.g. how do you manage the flags if there are many. Also for large project how this works with your configrations.
Unfortunately cobra doesn’t offer a sophistiated guide on how to build apps in each scenario. but thankfully it has very detailed comments which you can read also on go doc page.
The app I mentioned is already uploaded to github. I personaly found it useful to produce the regex expressions right at hand. You can check it out here: https://github.com/niuguy/gorx welcome to try it out and give me feedbacks.