Create a command line tool with go and cobra

kgoedert

kgoedert

Posted on November 5, 2019

Create a command line tool with go and cobra

Every now and then, I like to start playing with a new programming language. The one I picked this was go. To start simple, I decided to build a simple command line utility that would do some simple tasks, which I usually do with bash scripts. Reading about how to implement this, I discovered a very cool project called cobra, that would make the task a lot easier. And apparently is used by a lot of big projects.

If my bash scripts are working, why rewrite them? Because with go it is easy to generate an executable. Which means I can distribute them easily in my team, without worrying about their setup.

The development environment

Since I don't want to install go in my machine, and I loved working with VS Code remote containers, I will create one with go. If you don't know what I am talking about, check it out.

I will create a folder called uc (this will be the root of my project), and initialize the container inside it.

Once my container is ready, I need to install cobra.

go get -u github.com/spf13/cobra/cobra

Cobra has a nice utility to help you get started, called cobra generator.

To create the initial structure of the project:

cobra init --pkg-name github.com/kgoedert/uc . 

If you want to create your project somewhere else, other than the current directory, just pass it as a parameter.

cobra init --pkg-name github.com/kgoedert/uc /path/to/some/folder

Which will give me an output like this:

Your Cobra applicaton is ready at
/workspaces/uc

You should get an output close to this one:

uc
├── LICENSE
└── src
    └── github.com
        └── kgoedert
            └── uc
                ├── cmd
                │   └── root.go
                └── main.go

You already have something you can build and run.

Since I am working inside the container, I can build and install my package with:

Ctrl + Shift + P > Go: Install Current Package

Just remember to execute this with the main.go file open and selected.

A binary will be generated on a bin directory. You can open a terminal and run it with

./uc

You should see an output like this:

root@10b479f71c07:/workspaces/uc/bin# ./uc 
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.

subcommand is required

Important: Since I am using visual code inside a container, my GOPATH is different, if I am using the terminal than if I am using the plugin commands from the go extension menu. More informations here https://github.com/Microsoft/vscode-go/wiki/GOPATH-in-the-VS-Code-Go-extension

Adding commands

I will add a command called createFolder, that will simply create an empty folder.

To do that, open a terminal go to the directory that has the main.go file in it, in my case it is uc, and type:

cobra add createFolder

This will allow me to have the command uc createFolder in my project. I will also add a parameter to specify the name of the folder I want to create.

Cobra add a file called createFolder.go in my cmd folder. At this point, my project structure, looks like this:

uc
├── bin
│   └── uc
├── LICENSE
└── src
    └── github.com
        └── kgoedert
            └── uc
                ├── cmd
                │   ├── createFolder.go
                │   └── root.go
                └── main.go

Command implementation

In the createFolder.go, cobra created some templates to get you started. There is a variable to represent your command. What it will do, its name, and some help to your users.

var createFolderCmd = &cobra.Command{
    Use:   "createFolder",
    Short: "Creates a folder",
    Long:  `Creates a folder with a name as parameter`,
    Run: func(cmd *cobra.Command, args []string) {
        createFolder(cmd)
    },
}

func createFolder(cmd *cobra.Command) error {
    name, _ := cmd.Flags().GetString("name")

    if name == "" {
        return errors.New("Your folder needs a name")
    }

    err := os.MkdirAll(name, os.ModePerm)
    if err != nil {
        fmt.Printf("Could not create the directory %v", err)
    }
    fmt.Println("Folder " + name + " created.")

    return nil
}

You can see my implementation is very simple. In the function called init is where you are going to determine to which other command your command is a subcommand to, and add parameters to it.

func init() {
    rootCmd.AddCommand(createFolderCmd)

    createFolderCmd.Flags().StringP("name", "", false, "Name of the folder you want to create.")
}

My createFolder command will be a subcommand to the root command, and will have one parameter, which will be n, for the folder name. So when called it will look like

./uc createFolder -n newFolder

Now, let's add another simple command. Initializing a git repository inside a given folder.

My code on the gitInit.go file, looks like this:

var gitInitCmd = &cobra.Command{
    Use:   "gitInit",
    Short: "Initializes a git repository on a given path",
    Run: func(cmd *cobra.Command, args []string) {
        gitInit(cmd)
    },
}

func gitInit(cmd *cobra.Command) error {
    folder, _ := cmd.Flags().GetString("folder")

    if folder == "" {
        return errors.New("Your need to inform a path to initialize the git repository")
    }

    command := exec.Command("git", "init", folder)
    err := command.Run()
    if err != nil {
        fmt.Printf("Could not create the directory %v", err)
    }
    fmt.Println("Git repository initialized in " + folder)

    return nil
}

func init() {
    rootCmd.AddCommand(gitInitCmd)

    gitInitCmd.Flags().StringP("folder", "f", "", "Path where the git repository will be initialized.")
}

My command will again, be a subcommand of the root command. And will take a -f as a parameter for the name of the folder.

Now, suppose you want to make a command that will aggregate some of the commands you have previously created. One possible solution would be to create a command called for example, all, that would execute the other two. Like this:

var allCmd = &cobra.Command{
    Use:   "all",
    Short: "Executes both commands",
    RunE: func(cmd *cobra.Command, args []string) error {
        createFolder(cmd)
        gitInit(cmd)

        return nil
    },
}

func init() {
    rootCmd.AddCommand(allCmd)

    allCmd.Flags().StringP("folder", "f", "", "Path where the git repository will be initialized.")
    allCmd.Flags().StringP("name", "n", "", "Name of the folder you want to create.")
}

To use your all command, you would:

./uc all -n newProject -f newProject

This would create a folder and initialize a git repository on it.

Although the commands I used were very simple, I hope I was able to show how you can compose your commands to create something very powerful.

You can find the source code here

💖 💪 🙅 🚩
kgoedert
kgoedert

Posted on November 5, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related