CLI Programming with Golang
Command-line interface (CLI) programming is a fundamental skill for software developers, enabling the creation of powerful, efficient, and user-friendly tools and applications. CLI programs provide a text-based interface for users to interact with software, issuing commands and providing input directly from the terminal.
In the world of Golang CLI development, the github.com/peterbourgon/ff
package stands out as a versatile and powerful tool for building command-line applications. Developed by Peter Bourgon, ffcli
provides a flexible and intuitive framework for defining and organizing CLI commands and subcommands, parsing command-line arguments, and handling user input.
Now, let’s develop our CLI application by defining commands, flags, and subcommands using the ffcli.Command
API. Here’s a simple example of a basic CLI application using ffcli
:
package main
import (
"context"
"flag"
"fmt"
"os"
"strconv"
"github.com/peterbourgon/ff/v3/ffcli"
)
func main() {
root := &ffcli.Command{
ShortUsage: "mytool [flags] <operation> <operands...>",
ShortHelp: "A simple CLI tool for basic arithmetic operations",
Exec: run,
}
add := &ffcli.Command{
Name: "add",
ShortHelp: "Perform addition",
Exec: runOperation(addOperands),
}
sub := &ffcli.Command{
Name: "sub",
ShortHelp: "Perform subtraction",
Exec: runOperation(subOperands),
}
mul := &ffcli.Command{
Name: "mul",
ShortHelp: "Perform multiplication",
Exec: runOperation(mulOperands),
}
div := &ffcli.Command{
Name: "div",
ShortHelp: "Perform division",
Exec: runOperation(divOperands),
}
root.Subcommands = []*ffcli.Command{add, sub, mul, div}
if err := root.ParseAndRun(context.Background(), os.Args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string) error {
return flag.ErrHelp
}
func runOperation(operationFunc func([]string) (float64, error)) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
if len(args) < 2 {
return fmt.Errorf("insufficient operands")
}
result, err := operationFunc(args[0:])
if err != nil {
return err
}
fmt.Printf("Result: %.2f\n", result)
return nil
}
}
func addOperands(operands []string) (float64, error) {
result := 0.0
for _, op := range operands {
num, err := strconv.ParseFloat(op, 64)
if err != nil {
return 0, fmt.Errorf("invalid operand: %s", op)
}
result += num
}
return result, nil
}
func subOperands(operands []string) (float64, error) {
result, _ := strconv.ParseFloat(operands[0], 64)
for _, op := range operands[1:] {
num, err := strconv.ParseFloat(op, 64)
if err != nil {
return 0, fmt.Errorf("invalid operand: %s", op)
}
result -= num
}
return result, nil
}
func mulOperands(operands []string) (float64, error) {
result := 1.0
for _, op := range operands {
num, err := strconv.ParseFloat(op, 64)
if err != nil {
return 0, fmt.Errorf("invalid operand: %s", op)
}
result *= num
}
return result, nil
}
func divOperands(operands []string) (float64, error) {
result, _ := strconv.ParseFloat(operands[0], 64)
for _, op := range operands[1:] {
num, err := strconv.ParseFloat(op, 64)
if err != nil {
return 0, fmt.Errorf("invalid operand: %s", op)
}
if num == 0 {
return 0, fmt.Errorf("division by zero")
}
result /= num
}
return result, nil
}
- We define the root command using
ffcli.Command
. - Each arithmetic operation (
add
,sub
,mul
,div
) is defined as a subcommand of the root command. - We use the
Exec
field offfcli.Command
to specify the function to execute for each subcommand. - The
runOperation
function returns a function that executes the specified arithmetic operation. - We parse the command-line arguments and execute the root command using
root.ParseAndRun()
. - The execution logic of each operation is encapsulated in separate functions (
addOperands
,subOperands
,mulOperands
,divOperands
).
Use the go run
command to execute the main.go
file. However, note that the go run
command compiles and runs only a single file, so changes made in other files won't take effect.
go run main.go add 3 1
This command will compile and execute the main.go
file, and then run the add
subcommand. However, when using this method, any changes you make will require recompiling the files. Therefore, if you plan to make frequent changes to your code, it's more convenient to compile once using the go build
command and then use the generated executable file. Or do the next one.
- Build the CLI Application: Navigate to the directory containing your
main.go
file (mycliapp
directory) in your terminal, and run the following command to build the application:
go build -o mycliapp
This command will compile your Go source code and generate an executable binary file named mycliapp
in the same directory.
2. Run the CLI Application: Once the build process is complete, you can run your CLI application by executing the generated binary file:
./mycliapp mul 3 1
This command will execute the mul
subcommand of your CLI application, and you should see the "Hello, world!" message printed to the console.
To understand how ffcli
handles command-line arguments and flags, as well as parsing and validating user input, let's break it down step by step:
- Command-Line Arguments and Flags Handling:
ffcli
allows you to define commands and flags using structures and annotations. You can use theFlag
type to define flags for your command-line interface. Here's a basic example:
package main
import (
"flag"
"fmt"
"os"
"github.com/peterbourgon/ff"
)
func main() {
var (
name string
age int
married bool
)
fs := flag.NewFlagSet("example", flag.ExitOnError)
fs.StringVar(&name, "name", "", "your name")
fs.IntVar(&age, "age", 0, "your age")
fs.BoolVar(&married, "married", false, "are you married")
err := ff.Parse(fs, os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v/n", err)
os.Exit(1)
}
fmt.Printf("Name: %s, Age: %d, Married: %t\n", name, age, married)
}
In this example, ff.Parse
parses the command-line arguments using the provided FlagSet
. If there's an error, it's handled appropriately.
Here are some examples of how you can run the commands defined in the ffcli
example provided earlier:
go build -o myapp
❯ ./myapp --name=okan --married=true --age=30
Name: okan, Age: 30, Married: true
❯ ./myapp --name=okan
Name: okan, Age: 0, Married: false
❯ ./myapp --age=30
Name: , Age: 30, Married: false
- Parsing and Validating User Input:
ffcli
doesn't specifically handle validation, but you can perform validation after parsing the command-line arguments. For example:
if age <= 0 {
fmt.Println("Age must be a positive number")
os.Exit(1)
}
You can add validation checks like this to ensure that the input meets your requirements.
- Configuring Commands and Subcommands:
ffcli
allows you to define commands and subcommands with default values and usage messages. Here's a basic example:
package main
import (
"context"
"flag"
"fmt"
"os"
"github.com/peterbourgon/ff/v3/ffcli"
)
func main() {
rootCmd := &ffcli.Command{
Name: "myapp",
ShortUsage: "myapp [flags] <command>",
ShortHelp: "Example comman-line app",
FlagSet: flag.NewFlagSet("myapp", flag.ExitOnError),
Exec: func(context context.Context, args []string) error { return flag.ErrHelp },
}
greetCmd := &ffcli.Command{
Name: "greet",
ShortUsage: "myapp greet [flags] <name>",
ShortHelp: "Greet someone",
FlagSet: flag.NewFlagSet("greet", flag.ExitOnError),
Exec: func(ctx context.Context, args []string) error {
if len(args) < 1 {
return fmt.Errorf("missing name")
}
name := args[0]
fmt.Printf("Hello, %s\n", name)
return nil
},
}
var s string
greetCmd.FlagSet.StringVar(&s, "saying", "", "write your say")
rootCmd.Subcommands = []*ffcli.Command{greetCmd}
err := rootCmd.ParseAndRun(context.Background(), os.Args[1:])
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
fmt.Println(s)
}
- In this example,
rootCmd
is the root command with a subcommandgreetCmd
. Each command has its ownFlagSet
for handling flags and anExec
function for executing the command logic. TheExec
function receives the command-line arguments after the flags.
Here are some examples of how you can run the commands defined in the ffcli
example provided earlier:
go build -o myapp
❯ ./myapp greet --saying='how are you' okan
Hello, okan
how are you
When you’re building big command-line programs with lots of different tasks to handle, it’s important to organize your code well. One way to do this is by using a modular design strategy. This means breaking your program into smaller, more manageable parts, with each part handling a specific task or group of related tasks. For example, you might have one module for handling file operations, another for handling network requests, and so on. Within this modular structure, you’ll often use the idea of commands and subcommands. Think of commands as the main actions your program can perform, like “open file” or “send message.” Subcommands are like smaller, more specific versions of commands. For instance, under “send message,” you might have subcommands like “send email” or “send text.” This modular approach makes your code easier to understand and maintain, especially as your program grows.
It’s really important to keep things consistent when you’re designing the interface that users interact with. This means making sure that every command and subcommand looks and behaves the same way, so users don’t have to think too hard about how to use them. One way to do this is by making sure that the options and arguments you use with each command are consistent. This helps users understand what to expect and how to use each command effectively. It’s also a good idea to provide clear and helpful messages that explain what each command does and how to use it. Another thing to consider is how you organize the flags (options) that users can use with each command. It’s helpful to group related flags together and give them clear and descriptive names and descriptions. This makes it easier for users to understand what each flag does and how it affects the command’s behavior.
Real-world examples showcase the versatility and effectiveness of ffcli
in various CLI applications:
- etcd: A distributed key-value store used for configuration management and service discovery,
etcd
employsffcli
for its command-line interface. This demonstratesffcli
's robustness in managing complex distributed systems' configuration and operation. - CockroachDB: A distributed SQL database,
CockroachDB
leveragesffcli
to provide a user-friendly command-line interface for database administration tasks. This illustrates howffcli
facilitates interactions with sophisticated database systems, allowing users to perform tasks efficiently. - Terraform Plugin Framework:
Terraform
, a popular infrastructure as code tool, utilizesffcli
in its plugin framework for creating custom providers and modules. This highlightsffcli
's suitability for building extensible CLI applications, enabling seamless integration with existing systems and workflows. - Kubernetes Kubectl Plugin Development:
Kubectl
, the command-line interface for Kubernetes, utilizesffcli
for developing plugins that extend its functionality. This demonstratesffcli
's effectiveness in creating modular and interoperable CLI components within the Kubernetes ecosystem.
ffcli
excels in several use cases compared to other CLI frameworks:
- Complex CLI Hierarchies:
ffcli
offers a straightforward approach for defining hierarchical command structures with nested subcommands, making it ideal for managing complex CLI hierarchies with ease. - Customizable Flag Parsing:
ffcli
provides extensive support for defining and parsing command-line flags, allowing developers to customize flag behavior and validation rules according to their specific requirements. - Plugin and Extension Development:
ffcli
's modular design and flexible architecture make it well-suited for building plugins and extensions for existing CLI tools and frameworks, enabling seamless integration and extensibility. - Cross-Platform Compatibility:
ffcli
is designed to be platform-agnostic, ensuring consistent behavior across different operating systems and environments, making it suitable for developing CLI applications targeting diverse platforms.
The needs of diverse environments and workflows. Its flexibility, extensibility, and ease of use make it a valuable tool for creating sophisticated command-line interfaces that streamline operations and enhance productivity. Whether managing distributed systems, administering databases, or orchestrating cloud infrastructure, ffcli
proves to be a reliable choice for building robust and scalable CLI applications in various domains.