Flags 2.0 Making Use of Runner
Let’s walk through a simple command-line tool that greets a user by name. We’ll enhance this tool by incorporating the flag package to personalize the greeting based on user input. Here we will be looking at the NewFlagSet method in the flags package
type Runner interface {
Init([]string) error
Run() error
Name() string
}
type GreetCommand struct {
fs *flag.FlagSet
name string
}
func NewGreetCommand() *GreetCommand {
gc := &GreetCommand{
fs: flag.NewFlagSet("greet", flag.ContinueOnError),
}
gc.fs.StringVar(&gc.name, "name", "World", "name of the person to be greeted") // -name
gc.fs.StringVar(&gc.name, "n", "World", "name of the person to be greeted") // -n
return gc
}
func (g *GreetCommand) Name() string {
return g.fs.Name()
}
func (g *GreetCommand) Init(args []string) error {
return g.fs.Parse(args)
}
func (g *GreetCommand) Run() error {
fmt.Println("Hello", g.name, "!")
return nil
}
func root(args []string) error {
if len(args) < 1 {
return errors.New("you must pass a sub-command")
}
cmds := []Runner{
NewGreetCommand(),
}
subcommand := os.Args[1]
for _, cmd := range cmds {
if cmd.Name() == subcommand {
cmd.Init(os.Args[2:]) // parse the flags
return cmd.Run()
}
}
return fmt.Errorf("unknown subcommand: %s", subcommand)
}
func main() {
if err := root(os.Args[1:]); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
Running the Program with Flags
Let’s see how this code works in action. To greet someone named dre
we can run:
go run main.go greet -name dre
This will output:
Hello dre !
The -name
flag allows us to override the default name (“World”) and personalize the greeting.
Expanding on the Runner Interface for Composable Commands
Let’s delve deeper into its purpose and how it facilitates composability for various command types.
The Power of Interfaces: Defining Common Behavior
The Runner interface defines a contract for different sub-commands within our program. It outlines three essential methods:
-
Init([]string) error
: This method handles initialization tasks for the command, typically involving parsing flags using the flag package. -
Run() error
: This method executes the core functionality of the command. -
Name() string
: This method returns the unique name of the command, used for identification during sub-command selection.
By defining these methods in an interface, we create a blueprint for any sub-command within our application. Any struct implementing this interface automatically becomes a valid sub-command.
Composability in Action: Adding a New Command
Let’s illustrate this concept by introducing a new command: stats. This command might retrieve and display program usage statistics. Here’s a simplified example of the stats command:
type StatsCommand struct {
// ... relevant fields for stats command
}
func (s *StatsCommand) Init([]string) error {
// Handle any initialization specific to stats command (e.g., no flags)
return nil
}
func (s *StatsCommand) Run() error {
// Implement logic to retrieve and display stats
fmt.Println("Program Usage Statistics...")
// ...
return nil
}
func (s *StatsCommand) Name() string {
return "stats"
}
This StatsCommand struct implements the Runner interface, making it a valid sub-command. We can now add it to the list of available commands in the root function:
func root(args []string) error {
// ... existing code
cmds := []Runner{
NewGreetCommand(),
&StatsCommand{}, // Add StatsCommand to the list
}
// ... remaining code
}
With this modification, users can now execute the stats
command alongside the existing greet command.
Benefits of Composable Commands
The Runner interface promotes code reusability and simplifies adding new functionalities. By adhering to this interface, developers can create various sub-commands with unique purposes while maintaining a consistent structure within the program. This modular approach makes the codebase cleaner and easier to maintain as the number of commands grows.
Further Enhancements
The Runner interface can be further extended to include additional methods specific to command management, such as providing help messages or handling errors specific to each command type. Explore these possibilities to create a more robust and flexible command-line application framework in Go.
Did I make a mistake? Please consider sending a pull request .