Julien's dev blog

Gist: Go basic CLI package

A basic Go package for handling multiple CLI commands with one executable.

Last updated on: 2025-02-24

Basic package definition

package cli

import "fmt"

type HandlerFunc func(args ...string) (exitcode int)

type Command struct {
	Keyword     string
	Description string
	Do          func(args ...string) (exitcode int)
}

func Run(args []string, commands []*Command, onArgsNone, onNotFound HandlerFunc) (exitcode int) {
	if len(args) == 0 {
		return onArgsNone(args...)
	}
	for _, cmd := range commands {
		if cmd.Keyword == args[0] {
			return cmd.Do(args[1:]...)
		}
	}
	return onNotFound(args...)
}

func PrintAvailableCommands(commands []*Command) {
	fmt.Println("Available commands:")
	for _, cmd := range commands {
		fmt.Printf("- %-20s %s\n", cmd.Keyword, cmd.Description)
	}
}
pkg/cli/command.go

Example usage

package main

import (
	"os"
)

func main() {
	os.Exit(cli.Run(
		os.Args[1:],
		myapp.AllCLICommands,
		myapp.CLICommandHelloWorld.Do,
		myapp.HandleCLICommandNotFound,
	))
}
main.go
package myapp

import (
	"fmt"
)

var AllCLICommands = []*cli.Command{
	CLICommandHelloWorld,
	CLICommandFoo
}

func init() { AllCLICommands = append([]*cli.Command{CLICommandHelp}, AllCLICommands...) }

var CLICommandHelp = &cli.Command{
	Keyword:     "help",
	Description: "Prints helpful information about this executable",
	Do: func(args ...string) (exitcode int) {
		cli.PrintAvailableCommands(AllCLICommands)
		return 0
	},
}

func HandleCLICommandNotFound(args ...string) (exitcode int) {
	fmt.Printf("Command not found: %q\n", args[0])
	_ = CLICommandHelp.Do()
	return 1
}

var CLICommandHelloWorld = &cli.Command{
	Keyword:     "hello",
	Description: "Prints 'Hello World!' to stdout",
	Do: func(args ...string) (exitcode int) {
		fmt.Println("Hello World!")
		return 0
	},
}

var CLICommandFoo = &cli.Command{
	Keyword:     "foo",
	Description: "Prints 'foo' to stdout",
	Do: func(args ...string) (exitcode int) {
		fmt.Println("foo")
		return 0
	},
}
pkg/myapp