DEV Community

kgoedert
kgoedert

Posted on • Updated on

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

Top comments (1)

Collapse
 
tchi91 profile image
Fethi TChi

Thanks a lot, it's a really nice article, but the github repo doesn't have source code 🤔