Replacing Bash CLI with Go CLI

CaptainCore is just a collection of bash scripts tied together using a cli called Bash CLI. Bash has it’s limits. I’ve been thinking about replacing the underlying Bash CLI with a more robust solution. In terms of creating CLIs there are plenty of great options. Here are a few I’ve considered or looked at.

Bash is awesome, however it’s not great at handling complex logic.

CaptainCore is a mix of bash, WordPress, PHP and Go. I’m not planning to phase out any of those languages or framworks. They each serve a purpose. Bash is great for reusable scripts that handle the majority of site maintenance. PHP is great at handling complex logic and structured data within the context of WordPress. Lastly, Go is amazing at running commands and streaming real-time results through web sockets.

Switching to Go simplifies CaptainCore.

The main reason I decided to lean more heavily on Go is for simplicity. This allows me to combine 2 separate projects CaptainCore CLI and CaptainCore Dispatch into a single code base. As a result will make packaging up and redistributing way easier. Go, being a complied language, comes with some welcomed performance gains.

Selecting Cobra over other Go libraries.

This was very much a trial and error process. I really like the simplicity of building CLIs with urfave/cli however did not like their strict rule that flags have to be placed before arguments. I know technically you should run commands like captaincore ssh --command="wp core update" <site> however I prefer being able to do captaincore ssh <site> --command="wp core update".

Cobra is a very popular Go CLI library and for good reason. It’s well documented and feature rich. I haven’t done very much Go programming. Before implementing Cobra I went through a few crash courses on learning Go modules and basics of separating a Go application into various packages and importing. I think I got the basics down 😁.

Cobra allows for a well structured CLI, yet can pass off a command with syscall to another app.

Just because the CLI itself is written in Go doesn’t mean we need to re-code everything in Go. With Go’s syscall can just run any script or application. To keep the existing code base working I have 2 Go functions resolveCommand and resolveCommandWP which handle passing off the process to run a bash script and the other to run a WP-CLI command.

func resolveCommandWP(c *cobra.Command, args []string) {
   env := os.Environ()
   command := c.CommandPath()
   command = strings.Replace(command, "captaincore ", "", -1)
   command = strings.Replace(command, " ", "-", -1)
   err = syscall.Exec("/usr/local/bin/wp"+command, []string{"wp", "eval-file", "lib/local-scripts/", command}, env)

Think of Go as just the top layer of the CLI. It’s what you see when interacting with the command line and determines what arguments and flags a command accepts. Under the hood those commands run other arbitrary scripts and WP-CLI commands.

Relying on other scripts makes packaging a bit more complicated.

It would be great if everything, even bash scripts, could be embedded into a single binary. That might be possible with Go’s embed package with some cleaver rebuilding the files back to the local system using a temporary directory. I’ll leave that idea another time to figure out. For now I think it’s reasonable to leverage git to make sure all relating PHP and bash scripts are keep up to date. Maybe even handle that as part of the the initial install and upgrade processes to keep complexity minimal.

Next release of CaptainCore CLI, v0.12, will be powered by Go.

I’m about halfway through implementing the new Cobra CLI for CaptainCore. Things are looking pretty good so far. Still have a bunch more development to get through before the transition will be completed. Longterm this will open up new possibilities and maybe move over more code to Go where it makes sense. Be sure to follow Github if you’d like to see once that’s been completed.