Please purchase the course to watch this video.

Full Course
Many command line applications enhance usability and organization by using subcommands, which group related features under distinct command structures—examples include Docker, Git, and GitHub CLI. Subcommands clarify how to interact with CLI tools by separating actions (like adding, subtracting, or managing resources) into logical, discoverable operations. Building subcommands from scratch in Go, without third-party libraries such as Cobra, helps deepen understanding of core command line argument handling—primarily by parsing arguments, validating input, and routing execution using basic constructs like switch statements and handler functions. For illustration, a basic calculator CLI can implement subcommands for addition and subtraction, with each command acting as a focused, testable subroutine, and robust error handling for invalid input. This approach not only leads to more user-friendly and extendable tools but also provides a solid foundation for implementing similar features in other programming languages. Future lessons further enhance functionality by introducing command line flags and custom abstractions for even more powerful and maintainable CLIs.
Many popular CLI applications such as the GitHub CLI, Docker and DigitalOcean's command line tool make use of a feature called subcommands, which allow you to group related functionality under a common command structure. This enables commands such as:
gh repo
command with the GitHub CLI, which allows you to manage all of your repositoriesdocker run
command, which when used with the docker CLI allows you to run a docker containerdoctl compute
, which is a DigitalOcean command allowing you to manage your compute resources
Each of these commands trigger a behaviour within their respective CLI application, allowing you to share a common base command to perform other actions. For example, as you can see with the doctl compute
command, we have a bunch of actions we can take underneath this, such as:
- Managing a virtual machine or a droplet
- Accessing a droplet using SSH
- Commands allowing us to manage SSH keys on our DigitalOcean account
Subcommands We Already Use
Throughout this course we've already interacted with subcommands ourselves, such as with the go toolchain:
go build
subcommand, which will build our applicationgo run
, which runs our application for us
We've also looked at subcommands when it comes to git:
git init
command, which allows us to initialise an empty git repositorygit status
command, which will show us any of the tracked or untracked changes we currently have
Why Use Subcommands?
Subcommands are a really powerful feature to add to any command line application, as they help to organise all of the commands available to your CLI tool into clear, focused areas of functionality, instead of cramming every option into a single root command. This not only allows us to group related features when it comes to our CLI application, but it also makes our tools:
- Easier to understand
- Easier to use
- Easier to extend
Building Subcommands from Scratch
In this lesson we're going to spend some time adding in subcommands to a CLI application in order for us to understand how we can do so. When it comes to Go, the most popular way of adding subcommands to an application is to make use of the Cobra package, which is a powerful library to create modern CLI applications.
Cobra makes it really easy to add subcommand-based CLIs, as well as adding additional features such as:
- POSIX compliant flags
- Nested subcommands
- Global and local cascading flags
We'll be taking a look at Cobra in the next module when it comes to working with third-party packages, and we'll also be using it quite a lot, through building out many other applications when we look at some more advanced features.
However, for this lesson we're actually going to take a step back and build subcommands ourselves from scratch using the Go standard library. By doing so we'll better understand how we can build them without any abstractions on top. This will also give us a good understanding of how we can implement subcommands when it comes to any programming language, which in my opinion is a good foundation to have.
Our Project: A Calculator CLI
In order to add subcommands, we're going to go ahead and add them to a simple CLI application that I currently have, called calc. Calc is going to be a very basic command line application to perform calculations, and in our case we want to add two CLI commands to it:
- The
add
command, so that we can add two numbers together - The
subtract
command, so that we can subtract two numbers from each other
Expected Results:
calc add 1 2 # Should produce the result of 3
calc subtract 7 2 # Should produce the result of 5
Implementation
Starting Point
Here I have a very simple application set up already, which just has an empty main function:
package main
func main() {
// Empty for now
}
Accessing Command Line Arguments
In order to be able to access our subcommands, we're going to need to pull out the values from the os.Args
variable, which we've taken a look at a few times throughout this course. If you'll remember, the os.Args
provides a list of command line arguments, starting with the program name.
So in order for us to be able to access the subcommand, we're going to want to pull out the first, or pull out the value at index number one:
package main
import (
"log"
"os"
)
func main() {
// Guard clause to ensure we have enough arguments
if len(os.Args) < 2 {
log.SetFlags(0) // Remove date/time from log output
log.Fatal("missing subcommand")
}
// Get the subcommand (first argument after program name)
subcommand := os.Args[1]
}
Implementing Subcommand Handling
Next, we can go ahead and begin implementing some handling of our subcommand. To do so with Go, the most simplest approach we can take is to use a simple switch statement, switching on the actual subcommand itself:
func main() {
if len(os.Args) < 2 {
log.SetFlags(0)
log.Fatal("missing subcommand")
}
subcommand := os.Args[1]
switch subcommand {
case "add":
addCmd(os.Args[2:]) // Pass remaining arguments
case "subtract":
subtractCmd(os.Args[2:]) // Pass remaining arguments
default:
log.Fatal("invalid command")
}
}
Rather than adding the logic directly in the switch case, let's instead make this a little easier for us to reason with and easier for us to test if we wanted to add in test cases by defining this inside of its own function.
Implementing the Add Command
Now we can begin implementing the actual add command itself. We're going to need to obtain the rest of the command line arguments that we pass in. For example, in our case, we want to call calc add 1 2
in order to add the two numbers together:
import (
"fmt"
"log"
"os"
"strconv"
)
func addCmd(args []string) {
// Validate we have exactly 2 arguments
if len(args) != 2 {
log.Fatal("incorrect arguments for add command")
}
// Parse first number
num1, err := strconv.ParseFloat(args[0], 64)
if err != nil {
log.Fatal("invalid number")
}
// Parse second number
num2, err := strconv.ParseFloat(args[1], 64)
if err != nil {
log.Fatal("invalid number")
}
// Perform addition and print result
sum := num1 + num2
fmt.Println(sum)
}
Note: In this case, I'm choosing to use a float just because it's more versatile than using an integer, but we're also going to make use of this in the next lesson when we add in CLI flags to perform rounding.
When it comes to subcommands, especially when working with Cobra, this is actually a very common way of implementing the subcommands themselves. And in the next lesson, when we start adding in flags, we'll take a look at why this works.
Testing the Add Command
Now let's test our implementation:
go build
./calc add 1 2 # Should output: 3
When I didn't pass a command initially, we just got an empty output. But now with our default case, if we run an invalid command:
./calc badcommand # Should output: invalid command
Implementing the Subtract Command
With the add command working, let's go ahead and quickly implement the subtract command, which is going to be very much the same thing:
func subtractCmd(args []string) {
// Validate we have exactly 2 arguments
if len(args) != 2 {
log.Fatal("incorrect arguments for subtract command")
}
// Parse first number
num1, err := strconv.ParseFloat(args[0], 64)
if err != nil {
log.Fatal("invalid number")
}
// Parse second number
num2, err := strconv.ParseFloat(args[1], 64)
if err != nil {
log.Fatal("invalid number")
}
// Perform subtraction and print result
result := num1 - num2
fmt.Println(result)
}
Testing the Complete Implementation
Let's test both commands:
go build
./calc add 1 2 # Should output: 3
./calc subtract 10 5 # Should output: 5
Complete Code Example
Here's our complete implementation:
package main
import (
"fmt"
"log"
"os"
"strconv"
)
func main() {
log.SetFlags(0) // Remove date/time from log output
if len(os.Args) < 2 {
log.Fatal("missing subcommand")
}
subcommand := os.Args[1]
switch subcommand {
case "add":
addCmd(os.Args[2:])
case "subtract":
subtractCmd(os.Args[2:])
default:
log.Fatal("invalid command")
}
}
func addCmd(args []string) {
if len(args) != 2 {
log.Fatal("incorrect arguments for add command")
}
num1, err := strconv.ParseFloat(args[0], 64)
if err != nil {
log.Fatal("invalid number")
}
num2, err := strconv.ParseFloat(args[1], 64)
if err != nil {
log.Fatal("invalid number")
}
sum := num1 + num2
fmt.Println(sum)
}
func subtractCmd(args []string) {
if len(args) != 2 {
log.Fatal("incorrect arguments for subtract command")
}
num1, err := strconv.ParseFloat(args[0], 64)
if err != nil {
log.Fatal("invalid number")
}
num2, err := strconv.ParseFloat(args[1], 64)
if err != nil {
log.Fatal("invalid number")
}
result := num1 - num2
fmt.Println(result)
}
Key Concepts
With that we've managed to add two subcommands into our calculator CLI tool. One for adding two numbers together and one for subtracting.
The basic premise when it comes to subcommands: You can think of each subcommand as being a sub program in your application code. We're basically handling our code as if we were just implementing a CLI to perform addition or subtraction.
What's Next?
In the next lesson, we're going to build on top of this project and start adding in some more features:
- Command line flags - We're going to add flags for the entire command, as well as adding flags for each individual subcommand, which is a very common pattern when it comes to CLI applications
- Custom abstractions - In the lesson after that, we're going to begin adding in our own abstraction, similar to how Cobra works, but much more simplified, where we will add in the ability to easily add subcommands, as well as printing out help messages based on the commands we add
📝 Homework Assignment
Before we move on to the next lesson, implement two other commands:
1. Multiply Command
calc multiply 4 5 # Should return 20 (num1 * num2)
2. Division Command
calc divide 10 2 # Should return 5 (num1 / num2)
Important: For the division command, you'll want to add in a simple guard in order to prevent dividing by zero, which will cause your application to crash.
Once you've managed to add those two commands, then move on to the next lesson, where we're going to take a look at how we can add flag groups to each subcommand using the flag set.
Add Multiply Command
- Create a
multiplyCmd()
function that multiplies two numbers - Add
"multiply"
case to the switch statement in main - Command should work like:
calc multiply 4 5
→ outputs20
Divide Subcommand
- Create a
divideCmd()
function that divides first number by second number - Add
"divide"
case to the switch statement in main - Add guard clause to prevent division by zero errors
- Command should work like:
calc divide 10 2
→ outputs5
Test your implementation
- Build and test both commands with various inputs
- Verify division by zero handling works correctly
- Ensure existing add/subtract commands still work