About

I have been messing about with writing a CLI front-end at work a bit, and I decided I wanted to finally dive in on something a bit more flexible than my usual go-to (read: copious lines of once BASH, now POSIX, shell). The problem is that I'm targeting multiple architecture and operating systems and am tired of debugging environmental issues. (`sed` is different, `read` is different, even `echo` of all things behaves differently between Mac and Linux!)

A good colleague of mine, DG, convinced me that Go was the solution a little while ago, but I hadn't gotten around to even breaking in. This is my attempt to break into coding a CLI in Go.

Prerequisite Knowledge

The basic prerequisites apply here: terminals, a bit of BASH and git, what a CLI is, computing in general.

As to Go(lang), I'm reputedly a clueless fool with respect to the language, so you're welcome to be one, too. Perhaps today we'll adjust that for both of us! You should first have a working install of Golang, as I won't cover that. Unlike previous Ruby blogs, I didn't bother over-designing this with gvm – I just installed the latest golang.

On wheels, and the reinvention thereof

I've mentioned Black box'ing as a mental technique before, but it goes farther than just ignoring things that aren't relevant to you. It also means to stand on the shoulders of giants and, particularly where you have something greater to do, letting libraries do the talking instead of reinventing the wheel.

We could figure out how to print to a screen and read input and whatnot in Go – honestly, that's a phenomenal idea – but it's just not what we're going for in the end. Keep the end in mind, even when beginning! We're looking to black box all of that complexity so we can get working on what we care about – our CLI front-end!

In other words, slam "golang cli" into DuckDuckGo or your search engine of choice and begin seeking something that can do all of that for you. I typed "writing go cli", a little bit less search friendly, but it didn't take me long to immediately spot Cobra as a candidate.

Cobra, installation and introspecting

If you don't hang around these parts too often (e.g. working with argparse), then you'll have to take my word from it that Cobra looks phenomenally well-built and cared for; I'll explain. Follow along in your terminal!

A quick `sudo apt install cobra` on my workstation and it's downloaded. I can type `cobra` and immediately see great information (below):

$ cobra
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.

Usage:
  cobra [command]

Available Commands:
  add         Add a command to a Cobra Application
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  init        Initialize a Cobra Application

Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -h, --help             help for cobra
  -l, --license string   name of license for the project
      --viper            use Viper for configuration

Use "cobra [command] --help" for more information about a command.

Hot damn!

You're telling me it'll set up our project, let us fiddle with commands right from the outside, and even do completion for us? Com'on Cobra, say the words "ZShell autocompletion" and my Mac friends are going to be so happy.

$ cobra completion
Generate the autocompletion script for cobra for the specified shell.
See each sub-command's help for details on how to use the generated script.

Usage:
  cobra completion [command]

Available Commands:
  bash        Generate the autocompletion script for bash
  fish        Generate the autocompletion script for fish
  powershell  Generate the autocompletion script for powershell
  zsh         Generate the autocompletion script for zsh

Flags:
  -h, --help   help for completion

Global Flags:
  -a, --author string    author name for copyright attribution (default "YOUR NAME")
      --config string    config file (default is $HOME/.cobra.yaml)
  -l, --license string   name of license for the project
      --viper            use Viper for configuration

Use "cobra completion [command] --help" for more information about a command.

There it is, folks – `zsh`! Honestly, what's left to do!? Oh right, all of the work.

Cobra init, and bootstrapping a Go project

Firing up the engines, we do a `git init my-first-cli` and try `cobra init`.

$ cobra init
Error: Please run `go mod init <MODNAME>` before `cobra init`

Hm. Ok, nevermind – we still have lots of work to do: I have no clue what this means. We could read the manual (`cobra init --help`), but we can also just hack and see what happens. Education shouldn't begin with dry reading (wait – this blog isn't dry, right?).

$ go mod init my-first-go-cli
go: creating new go.mod: module my-first-go-cli
$ ls
go.mod

"Uh, sure". So, firing up the engines, we do a … you get it.

$ cobra init 
Your Cobra application is ready at
/home/nicholas/dev/my-first-go-cli
$ ls
cmd  go.mod  go.sum  LICENSE  main.go

Good enough for me! We have a project of some kind.

I did a bit of googling and the standard way of running Golang things is just `go run main.go` (substituting the file at the end where appropriate, of course). Dry-running all of "our" hard effort (thanks, everyone else!), we see

$ go run main.go
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.

Excellent. All is working according to plan. It doesn't seem to do anything, so let's get busy with that `cobra add` bit.

$ cobra add new-command
newCommand created at /home/nicholas/dev/my-first-go-cli

Sure! So we're learning that Go is Camel cased. Fine by me, and I sure appreciate the gentle override! Sane conventions are important. How about now, brown cow?

$ go run main.go
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.

Usage:
  my-first-go-cli [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  newCommand  A brief description of your command

Flags:
  -h, --help     help for my-first-go-cli
  -t, --toggle   Help message for toggle

Use "my-first-go-cli [command] --help" for more information about a command.
$ go run main.go newCommand
newCommand called

And there it is. So once we actually do the work, this should be a seamless experience. Love it! Also, if you haven't noticed yet, the foobar boilerplate that Cobra adds actually helps us gradually learn a bit more about Cobra. I like it! For example, if you look in the generated files, you can see the "longer description" in a string variable that it speaks of and it's a very natural way of finding out how to change it.

Exploring before proceeding

Okay, so now we know that this is what we want, it's time to slow down and smell the roses a bit. We're going to be working directly with Cobra, so getting acquainted with what it has done so far before proceeding would be very wise.

"'Mango'? No, thanks, I just ate."

Again before things get too complex, what does a `main.go` look like anyway? Let's see.

$ cat main.go
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>

*/
package main

import "my-first-go-cli/cmd"

func main() {
	cmd.Execute()
}

Yep. I mean, it doesn't get much simpler than that. Boilerplate comment, check. Package is main – sure why not. Import our module which we initialized above, the… folder? file? something named 'cmd', check.

`cmd.Execute()` !! </Dalek voice>

What's this 'cmd/' bit?

The next quandary in my mind as no Golang maven: I've no clue why we have a directory named "cmd". Luckily, a quick search of "golang project layout" yields some fairly beefy results. I still don't feel like losing speed on dry documents right now, so I'll stow that one for later. Got a quicker summary for me, internet?

Beautiful.

Taking a quick look inside the `cmd/` folder, the obvious guess was a winner.

Yep, it's a package

yep-thats-a-pigeon.jpg

Examining the root.go

So, let's in turn now look in the `cmd` folder more deeply.

$ ls cmd
newCommand.go root.go

Hm! `root.go`, eh? Well, I guess they can't all be `main.go`, why not. Opening that file will yield a bit more text than is worth dumping on the blog, but I directed my focus immediately to…

// 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() {
	err := rootCmd.Execute()
	if err != nil {
		os.Exit(1)
	}
}

There's our function. Things are starting to connect – `main.go` imports `root.go` by calling its folder name (`cmd`) and then we address this function there. I'm sure there's lots to learn about module privacy and exporting functions and whatnot, but, right now, we don't really care.

So, how coupled are all of the pieces of `newCommand` into the rest of these bits? I tend to use `grep` to answer a question like that (`git grep`, in this case, even).

$ git grep newCommand
cmd/newCommand.go:// newCommandCmd represents the newCommand command
cmd/newCommand.go:var newCommandCmd = &cobra.Command{
cmd/newCommand.go:      Use:   "newCommand",
cmd/newCommand.go:              fmt.Println("newCommand called")
cmd/newCommand.go:      rootCmd.AddCommand(newCommandCmd)
cmd/newCommand.go:      // newCommandCmd.PersistentFlags().String("foo", "", "A help for foo")
cmd/newCommand.go:      // newCommandCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

Hm! Ok, that's unexpected. I mean, I saw that `main.go` had nearly nothing in it, and `root.go` didn't explicitly mention it, and we only have 3 source files, so you can feel free to call me out on this, but it was still pleasantly surprising to confirm how loosely coupled everything was. Somehow, `go.mod` (and it's corresponding hash file `go.sum`) are doing the heavy lifting here – that'd be Cobra at work!

In summary, there's not much to summarize

So, we now know that projects and packages are both pretty simple in Go. For the former, you stick a `main.go` into a folder and mostly call it a day. As things get more complex you add some folders with a `root.go` (assumedly) and suddenly you have packages.

While this may be the case for most programming languages, some can be pickier, and others begin a whole `src/main/java/com/bar/foo/MyApp/AppComponent` Journey to the Center of the Earth (and thanks, Java, I hate it).

One last thing: Compiling

One of the other major requirements for me is having a fully-compiled single front-end program that I can port to other architectures and operating systems.

A quick search showed me some awesome looking official (?) Golang docs that point out how to get a binary: `go build`!

It prints nothing, but on an `ls`, I have a nice shiny `my-first-go-cli` executable staring back at me from the terminal. Hello, friend!

$ ./my-first-go-cli
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.

Usage:
  my-first-go-cli [command]

Available Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command
  newCommand  A brief description of your command

Flags:
  -h, --help     help for my-first-go-cli
  -t, --toggle   Help message for toggle

Use "my-first-go-cli [command] --help" for more information about a command.

We are in business!

But what about cross-compiling?

Let's hit the search again – `golang cross compile`. Yeah, LGTM (looks good to me). Darwin is what Mac is called, for reasons I've been too lazy to look up. For the most part the story is going to be "because it is based off of something else named Darwin" and you'll never get to the bottom of what a "Darwin" truly is, so I sometimes fail to concern myself with these histories.

$ GOOS=darwin  go build
$ ls
cmd  go.mod  go.sum  LICENSE  main.go  my-first-go-cli
$ ./my-first-go-cli
bash: ./my-first-go-cli: cannot execute binary file: Exec format error

Perfect. I don't have a test machine right now, but I assume that it did the right thing, as I expected exactly that kind of error if I'm on the wrong OS.

ARM, though?

Another quick search, `go cross compile arm darwin`, and bam! Magic table, pretty much what I expected: either `arm` or `arm64` in another variable. Turns out it's just the latter.

$ GOOS=darwin GOARCH=arm64 go build
$ ./my-first-go-cli 
bash: ./my-first-go-cli: cannot execute binary file: Exec format error

This has been utterly painless. Throw that in a GitHub Action and call it a day. Easy!

Fin

I'd say it's about time to actually start cracking into what this CLI is going to do! That's a much less worthy topic to blog about, so I'll end things here. Next, I'll probably need to actually learn Go. Exercism is pretty great for picking up languages quickly, so I'll be doing that.

I might blog more about Cobra if there's more interesting bits, but, for now: know of any other great Golang CLI library options I missed in my greedy search algorithm?