Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of the time, the built-in flags like Int, String, and Bool are plenty. But eventually, you'll hit a wall where you need something more complex. For example, I often find myself wanting to pass a list of tags or categories to a CLI tool—something like --tags=go,cli,tutorial. The standard flag package doesn't have a StringSlice type out of the box, so we have to build our own.

Defining the stringSlice type

To create a custom flag, we first need a type that knows how to handle the input. I'll start by defining a stringSlice as a slice of strings. This keeps the data organized and makes it easy to iterate over later in the program.

type stringSlice []string

Now, for this type to be compatible with the flag package, it must satisfy the flag.Value interface. This interface is dead simple: it just requires a String() method (to show the default value) and a Set(string) method (to parse the incoming command-line argument).

Implementing the Value interface

The String() method is mostly for the "help" menu. I'll just join the current slice elements with commas so the user sees a clean representation of the current state.

func (s *stringSlice) String() string {
	return strings.Join(*s, ",")
}

The Set() method is where the actual work happens. This is where we take the raw string from the CLI and turn it into our slice. I'll use strings.Split to handle the comma-separated input.

func (s *stringSlice) Set(value string) error {
	*s = strings.Split(value, ",")
	return nil
}

Wiring it up (and the mistake I always make)

Now I want to actually use this in main(). Here is where I usually trip up. My instinct is to look for a helper function like flag.StringSlice(...), but those helpers only exist for the primitive types.

func main() {
	var tags stringSlice
    
    // This is where I'd instinctively try this:
    // flag.StringSlice(&tags, "go", "Tags to filter by") // ERROR: This doesn't exist!
    
    flag.Parse()
}

I forget that for any custom type implementing flag.Value, you have to use the generic flag.Var function. It's a bit less "sugar-coated" than the others, but it's the only way to hook in a custom type.

Let's fix that and put it all together:

package main

import (
	"flag"
	"fmt"
	"strings"
)

type stringSlice []string

func (s *stringSlice) String() string {
	return strings.Join(*s, ",")
}

func (s *stringSlice) Set(value string) error {
	*s = strings.Split(value, ",")
	return nil
}

func main() {
	var tags stringSlice
	flag.Var(&tags, "tags", "Comma-separated list of tags")
	flag.Parse()

	fmt.Printf("Filtering by tags: %v\n", tags)
}

Handling multiple flag declarations

One interesting detail about flag.Var is that if a user provides the flag multiple times (e.g., --tags=go --tags=cli), the Set method is called once for each occurrence.

In my current implementation, *s = strings.Split(value, ",") overwrites the slice every time. If you actually want to append the values instead of replacing them, you'd change that line to *s = append(*s, strings.Split(value, ",")...). Depending on your tool's UX, you might prefer one over the other. For this example, I'll stick with the replacement logic as it's more intuitive for comma-separated lists.




📋 Practical Task

Building a Coordinate Pair Flag

Your goal is to create a custom flag type called Coordinate that allows a user to pass a single flag representing an X and Y coordinate (e.g., --pos=10,20).

  • Define a Coordinate struct with X and Y as integers.
  • Implement the flag.Value interface for *Coordinate.
  • In the Set(string) method, split the input string by the comma and convert the resulting strings to integers using strconv.Atoi.
  • Return a helpful error if the input doesn't contain exactly two integers.
  • Use flag.Var in main() to bind this type to the flag -pos.
  • Print the final X and Y values to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.