If you are not a medium member, you can still read this story here
Part 4: Link
Part 5: Link
Command-line applications (CLI) are lightweight, fast, and extremely useful for automation tasks, system management, and interacting with APIs. Go is a great language for building CLI tools due to its simplicity, strong standard library, and cross-platform compilation.
In this part, Let’s build a simple CLI application in Go that performs a few basic tasks: greeting users, printing the current date, and performing simple calculations. We will start with Go’s standard library and the use some third-party packages.
Prerequisites
- Go installed on your system
- Familiarity with basic Go programming concepts (functions, packages, etc.)
Table of Contents:
- Introduction to Command Line Applications
- Setting Up the Project
- Go’s Standard Flag Package
- Limitations of the Flag Library
- The Cobra Library
- Creating the Root Command
- Adding Subcommands and Flags
- Organizing the CLI Application
- Cross Compilation for Multiple Platforms
- Packaging and Distributing Go CLI Applications
Setting Up the Project
Create a new directory for your project:
mkdir go-cli-app
cd go-cli-appInitialize a Go module to manage dependencies:
go mod init go-cli-appGo’s Standard Flag Package
The Go standard library provides a flag package to parse command-line arguments. Let's start with a simple example that greets the user.
In your main.go add the following code,
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Define flags
name := flag.String("name", "World", "a name to say hello to")
help := flag.Bool("help", false, "display help")
// Parse flags
flag.Parse()
// Display help if requested
if *help {
fmt.Println("Usage: go run main.go [--name your_name] [--help]")
os.Exit(0)
}
// Print greeting
fmt.Printf("Hello, %s!\n", *name)
}Flags are a way to pass options to a program via the command line.
Here, two flags are defined:
--name: Allows the user to specify a name (like--name John). If no name is provided, it defaults to"World".--help: A boolean flag that can be used to display usage information. It defaults tofalseand can be triggered by running the program with--help.
flag.Parse() function processes the flags passed by the user from the command line. After this, the values of the flags are available for use.
If the --help flag is set (meaning the user provided --help in the command line), the program prints usage information (a help message) and then exits. This is not mandatory but its a really good practice to give have some help manual for your application, that prints what the application does, what are the other flags it accepts etc.
Most shell commands likecd, lsetc. have a built in help flag, which can be accessed by passing in-hlikels -h or cd -h.
Finally, we print the name. Now if you run this program using,
go run main.go --name BenThis will output:
Hello, Ben!Flag Library Has Limitations
The Go standard flag package is a simple and lightweight library for parsing command-line arguments. However, it comes with several limitations, especially when building more complex command-line interfaces (CLIs).
The flag package does not support subcommands natively. If you need to implement commands like git commit, git push, etc., with subcommands, you have to manually handle this logic.
It also lacks formatting and customization options. For this reason, we often use a more feature-rich third-party library like Cobra that can provide several more options and customization.
The Cobra Library
Cobra is a powerful library for creating modern CLI applications. It solves most of the shortcomings of the flag package and provides a number of additional features. Cobra is widely used in the Go community and has extensive documentation and examples. Popular tools like Kubernetes' CLI (kubectl) are built with Cobra, so the community and support for Cobra are robust.
urfave/cli is also another good option for building CLI applications. However, it is a bit more basic and is not suitable for large applications.
Installing Cobra
First, install cobra using Go's package manager. You'll also need to initialize your Go module if you haven't already.
go get -u github.com/spf13/cobraBasic Structure of a Cobra Application
A Cobra-based application consists of:
- Root Command: The entry point of the application.
- Subcommands: Specific commands that can be executed (like
git initorgit commit). - Flags: Options that modify the behavior of commands (like
git commit -m).
Cobra applications typically have a cmd/root.go file and a main.go file. You can create these manually or use cobra-cli, a generator tool that can scaffold the application for you.
Install the cobra-cli tool and initialize your CLI application using cobra-cli:
go install github.com/spf13/cobra-cli@latest
cobra-cli init mycliappThis will generate the basic structure of your CLI project:
mycliapp/
├── cmd/
│ └── root.go
├── main.gocmd/root.go: The root command file where the base command logic is defined.
Creating the Root Command
Open cmd/root.go and define the root command:
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "mycli",
Short: "MyCLI is a simple CLI application",
Long: `MyCLI is a demonstration of building CLI applications using Cobra.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Welcome to MyCLI!")
},
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}In main.go, add the following:
package main
import "github.com/yourusername/mycli/cmd"
func main() {
cmd.Execute()
}Now if your run the program using go run main.go, you should see the output,
go run main.go
// Output
Welcome to MyCLI!Adding Subcommands
Next, let’s add a subcommand to the CLI application. We’ll add a greet subcommand.
Create a new file called greet.go in the cmd directory:
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var greetCmd = &cobra.Command{
Use: "greet",
Short: "Print a greeting message",
Long: `The greet command prints a greeting message to the console.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Hello, welcome to MyCLI!")
},
}
func init() {
rootCmd.AddCommand(greetCmd)
}In Cobra, commands form a hierarchy. The root command (which in this case is mycli) can have subcommands like greet, and these subcommands can have further subcommands. For example, you could add other subcommands under greet if needed. This structure makes it easy to organize complex CLI applications with nested commands.
The subcommand is added to the root command using the AddCommand function in the init() function.
This means that when the CLI application runs, it will recognize greet as a valid subcommand, and associate it with the root command. The init() function in Go is executed automatically when the package is initialized, making it a convenient place to register subcommands.
You can continue adding multiple subcommands to your application, each with their own specific logic, and Cobra will handle parsing and execution based on what the user types.
Now, run the application with the new greet subcommand:
go run main.go greet
// Output
Hello, welcome to MyCLI!Adding Flags
Let’s add a name flag to the greet command to make it more dynamic.
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
var name string
var greetCmd = &cobra.Command{
Use: "greet",
Short: "Print a greeting message",
Long: `The greet command prints a greeting message to the console.`,
Run: func(cmd *cobra.Command, args []string) {
if name != "" {
fmt.Printf("Hello, %s! Welcome to MyCLI!\n", name)
} else {
fmt.Println("Hello, welcome to MyCLI!")
}
},
}
func init() {
greetCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the person to greet")
rootCmd.AddCommand(greetCmd)
}Now, run the application with the --name flag:
go run main.go greet --name John
// Output
Hello, John! Welcome to MyCLI!Using Persistent Flags
Persistent flags apply to all commands, not just a specific subcommand. Let’s add a global flag for verbosity.
In cmd/root.go, add a persistent flag:
var verbose bool
func init() {
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
}Modify the Run function in greet.go to check for the verbose flag:
Run: func(cmd *cobra.Command, args []string) {
if verbose {
fmt.Println("Verbose output enabled.")
}
if name != "" {
fmt.Printf("Hello, %s! Welcome to MyCLI!\n", name)
} else {
fmt.Println("Hello, welcome to MyCLI!")
}
}Now, run the command with the verbose flag:
go run main.go greet --name John --verbose
// Output
Verbose output enabled.
Hello, John! Welcome to MyCLI!Organizing the CLI Application
As your CLI application grows, organizing the commands into separate files or packages becomes important for maintaining code readability and scalability. Here’s an example of a good directory structure for a larger Cobra-based CLI app:
mycli/
├── cmd/
│ ├── greet.go
│ └── root.go
├── main.go
└── go.modAdvanced Features
- Auto-Completion: Cobra supports bash, zsh, fish, and PowerShell completion. To add auto-completion for your CLI, you can use:
rootCmd.GenBashCompletion(os.Stdout)- Help and Usage: Cobra automatically generates help and usage instructions. You can customize these messages by modifying the
Short,Long, andExamplefields in the&cobra.Commandstruct.
Packaging and Distributing Go CLI Applications with Cobra (and Cross Compilation)
Once you’ve built your CLI application using Go and Cobra, the next step is packaging it for distribution. You’ll want to make your application available to others as a downloadable binary that works on different operating systems (Linux, macOS, Windows) and CPU architectures. Go makes this process straightforward with its excellent cross-compilation capabilities.
If you followed the previous tutorial, you should have a functioning CLI project with main.go invoking the cmd.Execute() method.
Build the Binary
To create an executable for your system, run:
go build -o mycliThis will produce a binary named mycli in your current directory (or mycli.exe on Windows). You can then execute your CLI application using:
./mycli greetInstall Locally
If you want to install the binary to a directory in your PATH (such as /usr/local/bin/ on Linux or macOS), you can run:
sudo mv mycli /usr/local/bin/Now, you can run mycli from anywhere in your terminal.
Cross Compilation for Multiple Platforms
Cross-compilation allows you to build binaries for different platforms (OS and CPU architectures) from your development machine.
Go’s build system natively supports cross-compilation using environment variables like GOOS and GOARCH, which define the target operating system and architecture, respectively.
Example: Cross Compile for Linux, macOS, and Windows
To build binaries for Linux, macOS, and Windows from a single machine (e.g., your macOS development machine), you can run the following commands:
- Linux Binary (64-bit)
GOOS=linux GOARCH=amd64 go build -o mycli-linux- macOS Binary (Intel)
GOOS=darwin GOARCH=amd64 go build -o mycli-macos- macOS Binary (Apple M1)
GOOS=darwin GOARCH=arm64 go build -o mycli-macos-m1- Windows Binary (64-bit)
GOOS=windows GOARCH=amd64 go build -o mycli-windows.exeAfter running these commands, you’ll have three separate binaries in your current directory (mycli-linux, mycli-macos, mycli-windows.exe) that can be distributed for the respective platforms.
Handling CGO (Optional)
If you use any C dependencies in your Go application (which is uncommon for CLI tools but possible), you’ll need to disable CGO for cross-compilation:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o mycli-linuxDisabling CGO ensures that your binary doesn’t depend on platform-specific C libraries and is fully statically linked, making it portable across different environments.
Packaging for Distribution
Now that you have binaries for different platforms, you’ll want to package them for easier distribution.
Option 1: Manual Distribution
You can distribute the binaries manually by uploading them to a platform like GitHub, GitLab, or your own website. If you’re using GitHub, you can attach the binaries to a release.
Create a GitHub Release:
- Push your project to a GitHub repository.
- Go to the “Releases” section.
- Create a new release and attach the binaries for Linux, macOS, and Windows to the release.Add Installation Instructions: In your release notes, add instructions on how to download and install the binary:
Add Installation Instructions:
In your release notes, add instructions on how to download and install the binary:
# For Linux
curl -L -o mycli-linux https://github.com/yourusername/mycli/releases/download/v0.1.0/mycli-linux
chmod +x mycli-linux
sudo mv mycli-linux /usr/local/bin/mycliOption 2: Creating Installable Packages
To make installation even easier, you can package your binary into installable packages for various systems:
Debian/Ubuntu Package (.deb): For Debian-based Linux systems, you can use tools like dpkg or fpm to create a .deb package.
RPM Package (RedHat, CentOS): You can create .rpm packages for RedHat-based systems:
Homebrew Formula (macOS): If you’re targeting macOS, you can distribute your binary via a Homebrew formula:
- Fork the homebrew-core repo or create your own tap.
- Write a formula that downloads the binary and installs it.
class Mycli < Formula
desc "MyCLI is a sample CLI application"
homepage "https://github.com/yourusername/mycli"
url "https://github.com/yourusername/mycli/releases/download/v0.1.0/mycli-macos"
version "0.1.0"
sha256 "yourbinaryhashhere"
def install
bin.install "mycli-macos" => "mycli"
end
endUsers can then install your app with:
brew tap yourusername/mycli
brew install mycliScoop (Windows): Windows users can install your CLI tool through the Scoop package manager. Like Homebrew, you’ll need to create a custom bucket or add your package to an existing one.
Option 3: Docker Distribution
For users who may not want to install your binary natively, distributing via Docker is an option. This can be useful for CLI tools that require complex environments.
Create a Dockerfile:
FROM golang:alpine
COPY mycli-linux /usr/local/bin/mycli
ENTRYPOINT ["mycli"]Build and Push to Docker Hub:
docker build -t yourusername/mycli .
docker push yourusername/mycliRun the CLI tool using Docker:
docker run --rm yourusername/mycli greetAutomating the Build Process
You can automate the process of building for multiple platforms using tools like goreleaser or a CI/CD pipeline (GitHub Actions, GitLab CI, etc.). We will explore this in much more detail in a later part.
For now, just to give a sneak peak of goreleaser ,
Goreleaser is a tool designed to automate the release process for Go applications. It can cross-compile, package, and create GitHub releases with minimal configuration.
Install Goreleaser:
brew install goreleaserCreate a .goreleaser.yml configuration file:
project_name: mycli
build:
binary: mycli
goos:
- linux
- windows
- darwin
goarch:
- amd64
- arm64
archive:
format: tar.gz
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
release:
github:
owner: yourusername
name: mycliRun Goreleaser:
goreleaser release --rm-distThis will build, archive, and publish the binaries to your GitHub release automatically.
Wrap-Up
This part guided you through building a simple command-line interface (CLI) application using Go and the Cobra library, covering the essentials of setting up commands and flags for user interaction.
Knowing how to build CLI interfaces and cross-compilation is really crucial to build good and maintainable application and also to use web frameworks in Go.
Until next time!
“Walk slowly and you’ll go far.”
― Hector Garcia, Ikigai: The Japanese Secret to a Long and Happy Life




