Derex.dev

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 .