I recently came across a site on Twitter called “Command Line Interface Guidelines” that brings together philosophies and guidelines to help people write better command line programs. The guidelines are based on traditional Unix programming principles, but updated to “keep up with the times” in the modern context. I haven’t really done a systematic review of how to write command-line interactive programs before, so in this article, we’ll combine the clig guide (which may not be comprehensive) with a guide to writing CLI programs in Go for your reference.

1. Introduction to Command Line Programs

A Command Line Interface (CLI) program is a type of software that allows users to interact with a computer system using text commands and arguments. Developers writing CLI programs are often used for automated scripting, data processing, system administration, and other tasks that require low-level control and flexibility. Command line programs are also a favorite of Linux/Unix administrators as well as back-end developers.

The results of the official Q2 Go user survey in 2022 show (below) that the CLI category ranks second in the category of programs developed using Go, with 60% of the votes.

2022 Q2 Go Official User Survey Results

This is possible thanks to the many facilities that the Go language offers for CLI development, such as:

  • The simplicity and expressiveness of the Go syntax;
  • Go has a powerful standard library with built-in concurrency support;
  • Go has almost the best cross-platform compatibility and fast compilation speed;
  • Go also has a rich ecosystem of third-party packages and tools.

All these make it easy for developers to create powerful and user-friendly CLI programs using Go.

Easy is easy, but to write great CLI programs in Go, we also need to follow some principles and get some best practices and conventions for developing Go CLI programs. These principles and conventions cover topics such as interactive interface design, error handling, documentation, testing, and distribution. In addition, with the help of some popular Go CLI development libraries and frameworks such as: cobra, Kingpin and Goreleaser, we can get our CLI programs done well and fast. By the end of this article, you will have learned how to create an easy-to-use, reliable, and maintainable Go CLI application, and you will have gained some insight into CLI development best practices and conventions.

2. Setting up a Go development environment

Before we can start writing Go CLI programs, we need to make sure we have the necessary Go tools and dependencies installed and configured on our system. In this section, we will show you how to install Go and set up your workspace, how to use go mod for dependency management, and how to use go build and go install to compile and install your program.

2.1. Installing Go

To install Go on your system, you can follow the official installation instructions for the operating system you are using. You can also use a package manager such as homebrew (for macOS), chocolatey (for Windows), or snap/apt (for Linux) to install Go more easily.

Once you have installed Go, you can verify that it is working by running the following command in the terminal

1
$go version

If the installation is successful, the command go version should print out the version of Go you have installed. For example:

1
go version go1.20 darwin/amd64

2.2. Set up your workspace

Go used to have a convention of organizing your code and dependencies in a workspace directory (\$GOPATH). The default workspace directory is located at $HOME/go, but you can change its path by setting the GOPATH environment variable. The workspace directory contains three subdirectories: src, pkg, and bin. src contains your source code files and directories. pkg contains the compiled packages imported by your code. bin contains the executable binaries generated by your code.

With the introduction of the Go module in Go 1.11, this requirement to organize code and find dependencies under \$GOPATH was completely removed. In this article, I’m still following my custom of placing my code examples under $HOME/go/src.

In order to create a new project directory for our CLI program, we can run the following command in the terminal:

1
2
$mkdir -p $HOME/go/src/github.com/your-username/your-li-program
$cd $HOME/go/src/github.com/your-username/your-cli-program

Note that our project directory names use the github URL format. This is a common practice in Go projects, as it makes it easier to import and manage dependencies using go get. this requirement for project directory names has been removed since go module became the build standard, but many Gophers still retain this practice.

2.3. Using go mod for dependency management

After version 1.11 Go recommends that developers use mods to manage package dependencies. A module is a collection of related packages that share a common version number and import path prefix. A module is defined by a file called go.mod, which specifies the name, version, and dependencies of the module.

To create a new module for our CLI application, we can run the following command in our project directory.

1
$go mod init github.com/your-username/your-cli-program

This will create a file called go.mod with the following contents.

1
2
3
module github.com/your-username/your-cli-program

go 1.20

The first line specifies the name of our module, which matches the name of our project directory. The second line specifies the minimum version of Go required to build our module.

In order to add dependencies to our module, we can use the go get command, plus the import path and optional version tag of the package we want to use. For example, if we wanted to use cobra as our CLI framework, we could run the following command:

1
$go get github.com/spf13/cobra@v1.3.0

go get will download cobra from github and add it as a dependency in our go.mod file. It will also create or update a file called go.sum to record the checksums of all downloaded mods for subsequent validation.

We can also use other commands like go list, go mod tidy, go mod graph, etc. to check and manage our dependencies more easily.

2.4. Use go build and go install to compile and install your programs

Go has two commands that allow you to compile and install your programs: go build and go install. both commands take one or more package names or import paths as arguments and produce executable binaries from them.

The main difference between them is where they store the generated binaries.

  • go build stores them in the current working directory.
  • go install stores them in \$GOPATH/bin or \$GOBIN (if set).

For example, if we want to compile the main package of the CLI program (which should be located at github.com/your-username/your-cli-program/cmd/your-cli-program) into an executable binary file called your-cli-program, we can run the following command:

1
$go build github.com/your-username/your-cli-program/cmd/your-cli-program

or.

1
$go install github.com/your-username/your-cli-program/cmd/your-cli-program@latest

3. Designing the user interface

One of the most important aspects of writing a good CLI program is designing a user-friendly interface. A good command line user interface should be consistent, intuitive and expressive. In this section, I explain how to name and select command structures for command-line programs, how to use flags, arguments, subcommand and options as input arguments, how to use cobra or Kingpin, etc. to parse and validate user input, and how to follow POSIX conventions and the GNU extended CLI syntax.

3.1. Command line program naming and command structure selection

The name of your CLI program should be short, easy to remember, descriptive and easy to type. It should avoid conflicts with existing commands or keywords in the target platform. For example, if you are writing a program that converts images between formats, you can name it imgconv, imago, picto, etc., but not image, convert, or format.

The command structure of your CLI program should reflect the main functional features you want to provide to the user. You may choose to use one of the following command structure patterns:

  • A single command with multiple flags and arguments (e.g. curl, tar, grep, etc.)
  • A single command with multiple subcommands (e.g., git, docker, kubectl, etc.)
  • Multiple commands with a common prefix (e.g.: aws s3, gcloud compute, az vm, etc.)

The choice of command structure mode depends on the complexity of your program and the scope of its use, in general:

  • If your program has only one main function or operation mode, you can use a single command with multiple flags and arguments.
  • If your program has multiple related but different functions or operation modes, you can use a single command with multiple subcommands.
  • If your program has multiple unrelated or separate functions or operation modes, you can use multiple commands with a common prefix.

For example, if you are writing a program that performs various operations on files (e.g., copy, move, delete), you can choose any of the following command structure modes:

  • single command with multiple flags and arguments (e.g., fileop -c src dst -m src dst -d src)
  • single command with multiple subcommands (e.g., fileop copy src dst, fileop move src dst, fileop delete src)

3.2. Using flags, arguments, subcommands and options

Flags (flags) are input arguments that begin with one or more (usually 2) underscores (-) that modify the behavior or output of a CLI program. For example:

1
$curl -s -o output.txt https://example.com

In this example:

  • “-s” is a flag to silence curl, i.e., not to output the execution log to the console;
  • “-o” is another flag that specifies the name of the output file
  • “output.txt” is a argument that provides a value for the “-o” flag.

argument(s) are input arguments that do not begin with an underscore (-) and provide additional information or data to your CLI program. For example:

1
$tar xvf archive.tar.gz

Let’s see in this example:

  • x is a argument specifying the extraction mode
  • v is a argument specifying the level of detail (verbose) of the output content
  • f is another argument specifying the file mode to be used, i.e. outputting the result to a file or reading data from a compressed file
  • archive.tar.gz is a argument that provides the file name.

Subcommands are input arguments that act as auxiliary commands under the main command. They usually have their own set of flags and arguments. For example, the following example:

1
$git commit -m "Initial commit"

Let’s see in this example:

  • git is the primary command
  • commit is a subcommand that creates a new commit from the staged changes (commit)
  • “-m” is a flag for the commit subcommand, which specifies the commit information
  • “Initial commit” is an argument to the commit subcommand that provides a value for the “-m” flag.

option is an input argument which can combine flags and arguments into one argument using an equal sign (=). Example.

1
$docker run --name=my-container ubuntu:latest

Let’s see in this example “-name=my-container” is an option that sets the name of the container to my-container. the first part of the option “-name” is a flag, and the next part “my-container” is an argument.

3.3. Parsing and validating user input using cobra packages, etc

Parsing and validating user input by hand would be tedious and error-prone. Fortunately, there are many libraries and frameworks that can help you parse and validate user input in Go. One of the most popular is cobra.

cobra is a Go package that provides a simple interface to create powerful CLI programs. It supports subcommands, flags, arguments, options, environment variables and configuration files. It also integrates well with other libraries such as: viper (for configuration management), pflag (for POSIX/GNU style flags) and Docopt (for generating documentation).

Another package that is less popular but provides a declarative approach to creating elegant CLI programs is Kingpin, which supports flags, arguments, options, environment variables and configuration files. It also features automatic help generation, command completion, error handling, and type conversion.

Both cobra and Kingpin have extensive documentation and examples on their official websites, so you can choose either one depending on your preference and needs.

3.4. CLI syntax following POSIX conventions and GNU extensions

POSIX (Portable Operating System Interface) is a set of standards that define how software should interact with the operating system. One of these standards defines the syntax and semantics of CLI programs. GNU (GNU’s Not Unix) is a project aimed at creating a UNIX-compatible free software operating system. A sub-project under GNU is GNU Coreutils, which provides many common CLI programs such as ls, cp, mv, etc.

Both POSIX and GNU have established some conventions and extensions for CLI syntax, which are adopted by many CLI programs. Some of the main elements of these conventions and extensions are listed below:

  • single-letter flag starts with a single underscore (-) and can be combined (e.g. -a -b -c or -abc )
  • long flag starts with two underscores (-), but cannot be combined (e.g. -all, -backup, -color )
  • Options use an equal sign (=) to separate the flag name from the argument value (e.g. -name=my-container )
  • Arguments follow flags or options without any separator (example: curl -o output.txt https://example.com )
  • Subcommands follow the main command without any separators (e.g.: git commit -m "Initial commit" )
  • A double dash (-) indicates the end of a flag or option and the beginning of an argument. (For example: rm — -f means to delete the file “-f”, where “-f” is no longer a flag due to the double dash)

Following these conventions and extensions can make your CLI programs more consistent, intuitive, and compatible with other CLI programs. However, they are not mandatory and you are not obliged to follow them completely if you have a good reason to do so. For example, some CLI programs use slashes (/) instead of underscores (-) to indicate flags (e.g. robocopy /S /E src dst).

4. Handling Errors and Signals

An important part of writing a good CLI program is handling errors and signals gracefully.

An error is a situation where your program is unable to perform its intended function due to some internal or external factor. Signals are events sent to your program by the operating system or other processes to notify it of some change or request. In this section, I’ll explain how to use the log, fmt, and errors packages for log output and error handling, how to use the os.Exit and defer statements for graceful termination, how to use the os.Signal and context packages for interrupt and cancel operations, and how to follow the exit status code conventions for CLI programs.

4.1. logging and error handling with the log, fmt, and errors packages

The Go standard library has three packages log, fmt, and errors to help you with logging and error handling. the log package provides a simple interface to write formatted messages to standard output or files. the fmt package provides various functions for formatting strings and values. the errors package provides functions for creating and manipulating error values.

To use the log package, you need to import it in your code at

1
import "log"

You can then use functions such as log.Println, log.Printf, log.Fatal, and log.Fatalf to output information of varying severity. For example:

1
2
3
4
log.Println("Starting the program...") // Printing Timestamped Messages
log.Printf("Processing file %s...\n", filename) // Print a formatted message with a time stamp
log.Fatal("Cannot open file: ", err) //Print a timestamped error message and exit the program
log.Fatalf("Invalid input: %v\n", input) // Print a time-stamped formatting error message and exit the program.

To use the errors package, you also need to import it in your code.

1
import "errors"

You can then use errors.New, errors.Unwrap, errors.Is, and other functions to create and manipulate error values. For example:

1
2
3
err := errors.New("Something went wrong") // Create an error value with a message
cause := errors.Unwrap(err) // Returns the root cause of the error value (or nil if none)。
match := errors.Is(err, io.EOF) // Returns true if an error value matches another error value (otherwise returns false)。

4.2. Graceful termination of CLI programs using os.Exit and defer statements

Go has two functions to help you gracefully terminate CLI programs: os.Exit and defer. The os.Exit function exits the program immediately with an exit status code. defer statement, on the other hand, executes a function call before the current function exits, and it is often used to perform cleanup closing actions such as closing files or releasing resources.

To use the os.Exit function, you need to import the os package in your code.

1
import "os"

You can then use the os.Exit function, which has an integer argument representing the exit status code. For example

1
2
os.Exit(0) // Exit the program with a successful code
os.Exit(1) // Exit the program with a failure code

To use the defer statement, you need to write it before the function call you want to execute subsequently. For example

1
2
3
4
5
6
7
file, err := os.Open(filename) // Opens a file for reading.
if err != nil {
    log.Fatal(err) // Exit the program when an error occurs
}
defer file.Close() // Closes the file at the end of the function.

// Do some processing on the files...

4.3. Use the os.signal and context packages to implement interrupt and cancel operations

Go has two packages that can help you implement interrupt and cancel long-running or blocking operations: os.signal and the context package. os.signal provides a way to receive signals from the operating system or other processes. the context package provides a way to pass cancel signals and deadlines across API boundaries.

To use os.signal, you need to import it in your code first.

1
2
3
4
import (
  "os"
  "os/signal"
)

You can then use the signal.Notify function to register a receive channel(sig) for the signal of interest (such as the os.Interrupt signal below). For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
sig := make(chan os.Signal, 1) // Create a signal channel with buffering.
signal.Notify(sig, os.Interrupt) // Register sig to receive interrupt signals (e.g. Ctrl-C).

// Do something...

select {
case <-sig: // Wait for a signal from the sig channel
    fmt.Println("Interrupted by user")
    os.Exit(1) // Exit the program with a failure code.
default: //If no signal is received then execute
    fmt.Println("Successfully completed")
    os.Exit(0) // Exit the program with a success code.
}

To use the context package, you need to import it in your code.

1
import "context"

You can then use its functions such as context.Background, context.WithCancel, context.WithTimeout, etc. to create and manage Context, which is an object that carries cancellation signals and deadlines that can cross API boundaries. For example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ctx := context.Background() // Create an empty background context (never cancel).
ctx, cancel := context.WithCancel(ctx) // Creating a new context can be cancelled by calling the cancel function.
defer cancel() // Execute ctx's cancel action before the end of the function

// Pass ctx to some function that accepts it as an argument ......

select {
case <-ctx.Done(): // Wait for a cancellation signal from ctx
    fmt.Println("Canceled by parent")
    return ctx.Err() // Return an error value from ctx
default: // Execute if no cancellation signal is received
    fmt.Println("Successfully completed")
    return nil // Does not return an error value
}

4.4. Exit status code conventions for CLI programs

An exit status code is an integer that indicates whether a CLI program has successfully completed execution. a CLI program returns the exit status value by calling os.Exit or by returning from main. Other CLI programs or scripts can examine these exit status codes and perform different processing actions depending on the status code value.

There are a number of industry conventions and extensions for exit status codes, and these conventions are widely adopted by many CLI programs. Some of the major conventions and extensions are as follows:

  • An exit status code of zero indicates successful program execution (e.g., os.Exit(0) )
  • A non-zero exit status code indicates a failure (e.g., os.Exit(1) ).
  • Different non-zero exit status codes may indicate different types or causes of failure (e.g., os.Exit(2) for usage error, os.Exit(3) for permission error, etc.).
  • An exit status code greater than 125 may indicate termination by an external signal (e.g., os.Exit(130) for interrupted by a signal).

Following these conventions and extensions can make your CLI programs behave more consistently, reliably, and be compatible with other CLI programs. However, they are not mandatory, and you can use any exit status code that makes sense for your program. For example, some CLI programs use exit status codes higher than 200 to indicate custom or application-specific errors (e.g., os.Exit(255) for unknown errors).

5. Writing Documentation

Another important part of writing a good CLI program is writing clear and concise documentation that explains what your program does and how to use it. Documentation can take various forms, such as README files, usage information, help flags, etc. In this section, we will show you how to write a README file for your program, how to write a useful usage and help flag for your program, etc.

5.1. write a clear and concise README file for your CLI program

A README file is a text file that provides basic information about your program, such as its name, description, usage, installation, dependencies, license, and contact details. It is usually what users or developers will see when they first use your program on a source code repository or package manager.

If you are going to write a good README file for a Go CLI program, you should follow some best practices, such as:

  • Use a descriptive, eye-catching title that reflects the purpose and function of your program.
  • Provide a short introduction that explains what your program does and why it is useful or unique.
  • Include a USAGE section that explains how to invoke your program with different flags, arguments, subcommands, and options. You can use code blocks or screenshots to illustrate these examples.
  • Includes an install section that explains how to download and install your program on different platforms. You can use go install, go get, goreleaser, or other tools to simplify this process.
  • Specify the distribution license for your program and provide a link to the full license. You can use the SPDX identifier to indicate the license type.
  • Provide contact information for users or developers who want to report issues, request new features, contribute code, or ask questions. You can use github issue, pr, discussion, email, or other channels for this purpose.

The following is an example README file for a Go CLI application for reference:

README file for the Go CLI program

5.2. Write useful usage and help flags for your CLI programs

The usage message is a short text that summarizes how to use your program and its available flags, arguments, subcommands, and options. It is usually displayed when your program runs with no arguments or invalid input.

The help flag is a special flag (usually -h or -help) that triggers the display of usage information and some additional information about your program.

In order to write useful usage information and help flags for your Go CLI programs, you should follow some guidelines, such as

  • Use a consistent and concise syntax to describe flags, arguments, subcommands, and options. You can use square brackets “ ” for optional elements, angle brackets “< >” for required elements, ellipses “…” for repeated element, use pipe “|” for alternative, use underscore “-” for flag, use equal sign “=” for flag value, etc.
  • Descriptive names should be used for flags, arguments, subcommands, and options to reflect their meaning and function. Avoid using single-letter names unless they are very common or intuitive (e.g., -v indicates verbose mode by convention).
  • Provide a short, clear description of each flag, argument, subcommand, and option, explaining what they do and how they affect the behavior of your program. You can use parentheses “( )” for additional details or examples.
  • Use headings or indents to group related flags, arguments, subcommands, and options together. You can also use blank lines or horizontal lines (-) to separate different parts of the usage.
  • Arrange the flags in alphabetical order by name in each group. Arrange arguments in each group in order of importance or logical order. Arrange subcommands in each group by frequency of use.

The git usage is a good example of this.

1
2
3
4
5
6
$git
usage: git [--version] [--help] [-C <path>] [-c <name>=<value>]
           [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
           [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]
           [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
           <command> [<args>]

6. Testing and releasing your CLI program

The final part of writing a great CLI program is testing and releasing your program. Testing ensures that your program works as expected and meets quality standards. Publishing makes your program available and accessible to users.

In this section, I will explain how to unit test your code using testing, testify/assert, mock packages, how to use go test, coverage, benchmark tools to run tests and measure program performance and how to build cross-platform binaries using the goreleaser package.

6.1. Unit testing your code with testing, testify’s assert and mock packages

Unit testing is a technique for verifying the correctness and functionality of individual units of code, such as functions, methods, or types. Unit testing can help you find errors early, improve code quality and maintainability, and facilitate refactoring and debugging.

To write unit tests for your Go CLI application, you should follow some best practices:

1
2
3
4
5
6
7
8
func TestSum(t *testing.T) {
    t.Run("positive numbers", func(t *testing.T) {
        // test sum with positive numbers
    })
    t.Run("negative numbers", func(t *testing.T) {
        // test sum with negative numbers
    })
}
  • Use table-driven tests to run multiple test cases, such as the following example:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    func TestSum(t *testing.T) {
        tests := []struct{
            name string
            a int
            b int
            want int
        }{
            {"positive numbers", 1, 2, 3},
            {"negative numbers", -1, -2, -3},
            {"zero", 0, 0 ,0},
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                got := Sum(tt.a , tt.b)
                if got != tt.want {
                    t.Errorf("Sum(%d , %d) = %d; want %d", tt.a , tt.b , got , tt.want)
                }
            })
        }
    }
    
  • Use external packages such as testify/assert or mock to simplify your assertions or dependencies on externals. For example:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    import (
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/mock"
    )
    
    type Calculator interface {
        Sum(a int , b int) int
    }
    
    type MockCalculator struct {
        mock.Mock
    }
    
    func (m *MockCalculator) Sum(a int , b int) int {
        args := m.Called(a , b)
        return args.Int(0)
    }
    

6.2. Use Go’s testing, coverage, and performance benchmarking tools to run tests and measure performance

Go provides a set of tools to run tests and measure the performance of your code. You can use these tools to make sure your code works as expected, detect errors or bugs, and optimize your code for speed and efficiency.

To use the go test, coverage, and benchmark tools to run tests and measure the performance of your Go CLI programs, you should follow a few steps, such as.

  • Write the test file ending with _test.go in the same package as the code being tested. For example, sum_test.go is used to test sum.go.
  • Use the go test command to run all the tests in a package or a particular test file. You can also use flags such as -v for displaying verbose output, -run for filtering test cases by name, -cover for displaying code coverage, and so on. For example: go test -v -cover ./…
  • Use the go tool cover command to generate an HTML report of code coverage and highlight lines of code. You can also use flags like -func to display code coverage for functions, -html to open coverage result reports in the browser, and so on. For example: go tool cover -html=coverage.out
  • Write a performance benchmark function starting with Benchmark followed by the name of the function or method being tested. Use argument b of type *testing.B to control the number of iterations, and use methods such as b.N, b.ReportAllocs, etc. to control the output of the report results. For example
1
2
3
4
func BenchmarkSum(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Sum(1 , 2)
    }
  • Use the go test -bench command to run all the performance benchmarks in a package or a particular benchmark file. You can also use flags like -benchmem to display memory allocation statistics, -cpuprofile or -memprofile to generate CPU or memory profile files, etc. For example: go test -bench . -benchmem ./…
  • Use tools like pprof or benchstat to analyze and compare CPU or memory profile files or benchmarking results. For example.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Generate CPU profile
go test -cpuprofile cpu.out ./...

# Analyze CPU profile using pprof
go tool pprof cpu.out

# Generate two sets of benchmark results
go test -bench . ./... > old.txt
go test -bench . ./... > new.txt

# Compare benchmark results using benchstat
benchstat old.txt new.txt

6.3. Building cross-platform binaries with the goreleaser package

Building cross-platform binaries means compiling your code into executable files that can run on different operating systems and architectures, such as Windows, Linux, Mac OS, ARM, etc. This can help you distribute your program to more people and make it easier for users to install and run your program without any dependencies or configuration.

To build cross-platform binaries for your Go CLI programs, you can use external packages such as goreleaser etc. , which automate the process of building, packaging and distributing your programs. Here are some steps to build a program using the goreleaser package.

  • Install goreleaser using the go get or go install commands. e.g. go install github.com/goreleaser/goreleaser@latest
  • Create a configuration file (usually .goreleaser.yml) that specifies how to build and package your program. You can customize various options, such as binary name, version, master file, output format, target platform, compression, checksum, signature, etc. For example.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# .goreleaser.yml
project_name: mycli
builds:
  - main: ./cmd/mycli/main.go
    binary: mycli
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - arm64
archives:
  - format: zip
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    files:
      - LICENSE.txt
      - README.md
checksum:
  name_template: "{{ .ProjectName }}_checksums.txt"
  algorithm: sha256

Run the goreleaser command to build and package your program according to the configuration file. You can also use -snapshot for testing, -release-notes for generating release notes from commit messages, -rm-dist for removing previous builds, and so on. For example: goreleaser -snapshot -rm-dist.

Check the output folder (usually dist) for generated binaries and other files. You can also upload them to the source code repository or package manager using the publish feature of goreleaser.

7. Highlights of the clig.dev guide

With the above system descriptions, you should now be able to design and implement a CLI program using Go. However, this article does not cover all the main points of the clig.dev guide, so before we end this article, let’s review the main points of the clig.dev guide and you can experience them again.

As mentioned earlier, the cli guide on clig.dev is an open source guide to help you write better command line programs that takes traditional UNIX principles and updates them for modern situations.

Some of the benefits of following the cli guidelines are:

  • You can create CLI programs that are easy to use, understand, and remember.
  • You can design CLI programs that work well with other programs and follow common conventions.
  • You can avoid common pitfalls and errors that frustrate users and developers.
  • You can learn from the experience and wisdom of other CLI designers and users.

The following are some of the key points of the guide:

  • Philosophy

    This section explains the core principles behind good CLI design, such as human-centered design, composability, discoverability, and conversationalism. For example, human-centered design means that CLI programs should be easy to use and understand for humans, not just machines. Composability means that CLI programs should collaborate well with other programs by following common conventions and standards.

  • Arguments and flags

    This section describes how to use positional arguments and flags in your CLI programs. It also explains how to handle default values, mandatory arguments, boolean flags, multiple values, etc. For example, you should use positional arguments for the primary object or action of a command, and flags for modified or optional arguments. You should also use flags of both long and short forms (such as -v or -verbose) and follow common naming patterns (such as -help or -version).

  • Configuration

    This section describes how to use configuration files and environment variables to store persistent settings for your CLI programs. It also explains how to handle prioritization, validation, documentation, etc. of configuration options. For example, you should use configuration files to handle settings that are rarely changed by users or that are specific to a particular project or environment. For settings specific to an environment or session (such as credentials or paths), you should also use environment variables.

  • Output

    This section describes how to format and present the output of your CLI program. It also explains how to handle output verbose levels, progress indicators, colors, tables, etc. For example, you should use standard output (stdout) for normal output, so that the output can be piped to other programs or files. You should also use standard error (stderr) to handle errors or warnings that are not part of the normal output stream.

  • Errors

    This section describes how to handle errors gracefully in your CLI programs. It also explains how to use exit status codes, error messages, stack traces, and more. For example, you should use exit codes that indicate the type of error (e.g., 0 for success, 1 for general error). You should also use clear and concise error messages that explain what went wrong and how to fix it.

  • Subcommands

    This section describes how to use subcommands in CLI programs when they have multiple operations or modes of operation. It also explains how to build subcommands in layers, organize help text, and handle common subcommands (such as help or version). For example, you should use subcommands when your program has different functions that require different arguments or flags (such as git clone or git commit). You should also provide a default subcommand, or a list of available subcommands if none are given.

    There are many examples of well-designed CLI tools in the industry that follow cli guidelines, and you can gain insight into these guidelines by using them. The following are some examples of such CLI tools:

    • httpie: A command-line HTTP client with an intuitive UI, support for JSON, syntax highlighting, wget-like downloads, plugins, and more. For example, Httpie uses a clear and concise syntax for HTTP requests, supports multiple output formats and colors, handles errors gracefully and provides useful documentation.
    • git: A distributed version control system that lets you manage your source code and collaborate with other developers. For example, Git uses subcommands for different operations (such as git clone or git commit), follows common flags (such as -v or -verbose), provides useful feedback and suggestions (such as git status or git help), and supports configuration files and environment variables.
    • npm: A JavaScript package manager that lets you install and manage dependencies for your projects. For example, NPM uses a simple command structure (npm [args]), provides a concise initial help message with more detailed options (npm help npm), supports tab completion and sensible defaults, and allows you to customize settings via a configuration file (.npmrc).

8. Summary

In this article, we systematically explained how to write Go CLI programs that follow the command line interface guidelines.

You learned how to set up a Go environment, design a command line interface, handle errors and signals, write documentation, and test and distribute programs using various tools and packages. You also saw some examples of code and configuration files. By following these guidelines and best practices, you can create a CLI program that is user-friendly, robust, and reliable.

We conclude by reviewing the main points of the clig.dev guidelines in the hope that you will gain a deeper understanding of what they mean.

9. Ref

  • https://tonybai.com/2023/03/25/the-guide-of-developing-cli-program-in-go/