🧑‍💻

Banner Image: https://unsplash.com/photos/black-red-and-yellow-coated-wires-6ySmw7CwYDk

Software Engineering Essentials: Flags

Published On:  

Tags:   Guide Programming Rant

Licenses:   CC-BY-4.0 | MIT
Filter By License


Software Engineering Essentials is a series of blog posts designed to help you get started with a wide variety of software engineering topics. This post was originally part of my Go-Twitter project, a project-based curriculum designed to take someone from “zero” to “competent” in the world of software engineering.

All writing for this post is licensed under CC-BY-4.0, while all code is licensed under the MIT license.

Flags

Flag Usage

Flags are effectively named arguments, with some extra features. Instead of needing to remember the order of your program’s arguments, you can just specify the name of the flag and what that variable should be set to. Let’s take a look at a simple example:

01-simple-flags/main.go:

package main

import (
	"flag"
	"fmt"
)

func main() {
	greeting := flag.String("greeting", "", "")
	name := flag.String("name", "", "")
	flag.Parse()
	fmt.Printf("%s, %s!\\n", *greeting, *name)
}

Now, if we run this program:

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go
  !

Huh, that looks weird. This is because our greeting and name variables are empty. We never specified the flags. How do we know what the flags are? Flags have built-in help functionality that will display all of the options available. Most programs use -h or --help to display program usage text, including flags and arguments.

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -h
Usage of /tmp/go-build1913891522/b001/exe/main:
  -greeting string

  -name string

So now we can see that this program accepts the -greeting and -name flags. Let’s set some values:

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting="Sup" -name Tom
Sup, Tom!

Flag arguments can be set with some optional grammar to help avoid problems with ambiguity or spaces:

  • flag value: For one-word values
    • Unquoted spaces can change how your command is interpreted, leading to missing values and skipped flags. See the appendix section How Spaces Can Affect Flags for an example.
  • flag "value with spaces or special characters": For multi-word values or values that contain some special characters.
    • If your value has a $ symbol, you may need to use single-quotes (') to avoid the shell interpreting $ as the start of a shell variable.
  • flag="value": This is the belt-and-suspenders of flag declaration.
    • The = just removes space from the declaration and makes it very clear that this particular value belongs to that particular flag. This can be helpful to aid in readability, especially if you’re writing out examples or documentation.

We can also specify the flags in any order:

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -name="General" -greeting="Hello There"
Hello There, General!

Or not specify a value for -name at all!

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting="Hello There"
Hello There, !

We can even abuse the flags to make the program finish the meme:

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting="General Kenobi\\!" -name="You are a bold one"
General Kenobi!, You are a bold one!

Defaults

Let’s re-create the program that inserted profiles into a database from the end of the Command Line Arguments chapter, but with flags instead:

02-profile-saver/main.go:

package main

import (
	"flag"
	"fmt"
)

func main() {
	firstName := flag.String("first-name", "", "")
	lastName := flag.String("last-name", "", "")
	favFruit := flag.String("fruit", "", "")
	favVeg := flag.String("veg", "", "")
	favPasta := flag.String("pasta", "", "")
	favMeal := flag.String("meal", "", "")
	riceOrBeans := flag.String("rice-or-beans", "", "")
	favDesert := flag.String("desert", "", "")
	favDessert := flag.String("dessert", "", "")
	flag.Parse()

	fmt.Printf(`Profile Name: %s %s
	Rice or Beans: %s
	Favorites:
		Fruit: %s
		Vegetable: %s
		Pasta: %s
		Desert: %s
		Dessert: %s
		Meal: %s
`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)
}

Right now if we run this program with no flags set, we get this results:

go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go
Profile Name:
        Rice or Beans:
        Favorites:
                Fruit:
                Vegetable:
                Pasta:
                Desert:
                Dessert:
                Meal:

Lots of blank spaces here. We don’t want to commit empty values to the database, it can introduce ambiguity. Did this person not have a favorite pasta or did we just forget to add the data? Being explicit in programming has several benefits, including clarity-of-intent, so let’s add some defaults.

Default values can provide the usual options or placeholder data. Flags make this easy. For now, let’s just use UNKNOWN as the profile placeholder. With the stanadrd flag package, setting defaults is easy. Let’s take a look at the GoDocs for flag.String, available here:

func String

func String(name string, value string, usage string) *string

String defines a string flag with specified name, default value, and usage string. The return value is the address of a string variable that stores the value of the flag.

So the second function argument allows us to specify a default value for that particular flag. Let’s do that now:

02-profile-saver/main.go:

package main

import (
	"flag"
	"fmt"
)

func main() {
	firstName := flag.String("first-name", "UNKNOWN", "")
	lastName := flag.String("last-name", "UNKNOWN", "")
	favFruit := flag.String("fruit", "UNKNOWN", "")
	favVeg := flag.String("veg", "UNKNOWN", "")
	favPasta := flag.String("pasta", "UNKNOWN", "")
	favMeal := flag.String("meal", "UNKNOWN", "")
	riceOrBeans := flag.String("rice-or-beans", "UNKNOWN", "")
	favDesert := flag.String("desert", "UNKNOWN", "")
	favDessert := flag.String("dessert", "UNKNOWN", "")
	flag.Parse()

	fmt.Printf(`Profile Name: %s %s
	Rice or Beans: %s
	Favorites:
		Fruit: %s
		Vegetable: %s
		Pasta: %s
		Desert: %s
		Dessert: %s
		Meal: %s
`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)
}

Now when we run this program without any flags set:

go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go
Profile Name: UNKNOWN UNKNOWN
        Rice or Beans: UNKNOWN
        Favorites:
                Fruit: UNKNOWN
                Vegetable: UNKNOWN
                Pasta: UNKNOWN
                Desert: UNKNOWN
                Dessert: UNKNOWN
                Meal: UNKNOWN

There we go, that at least looks more complete.

Now let’s actually use those flags to set some values… but wait, what were the flags again? Let’s use -h to see what options we have:

go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -h
Usage of /tmp/go-build261536590/b001/exe/main:
  -desert string
         (default "UNKNOWN")
  -dessert string
         (default "UNKNOWN")
  -first-name string
         (default "UNKNOWN")
  -fruit string
         (default "UNKNOWN")
  -last-name string
         (default "UNKNOWN")
  -meal string
         (default "UNKNOWN")
  -pasta string
         (default "UNKNOWN")
  -rice-or-beans string
         (default "UNKNOWN")
  -veg string
         (default "UNKNOWN")

Ah, got it. Now let’s actually use these:

go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -first-name Bob -last-name Sacamano -fruit Apple -veg "Green Beans" -pasta Manicotti -meal Steak -desert Sahara -dessert Sundae -ri
ce-or-beans Rice
Profile Name: Bob Sacamano
        Rice or Beans: Rice
        Favorites:
                Fruit: Apple
                Vegetable: Green Beans
                Pasta: Manicotti
                Desert: Sahara
                Dessert: Sundae
                Meal: Steak

So now we’re able to print out all of the profile information using flags. Its still a lot of flags to remember, but at least we have the default help text and the order of options doesn’t matter.

Help Text

As we saw above, -h is pretty helpful to understand which flags are available, what type is expected, and what the defaults are, but we can add some more context through help text. As always, reading the documentation can help us find the right place to add help text to our flags. For now, let’s just add one piece of help text and see how it compares to the rest of the flags:

02-profile-saver/main.go:

package main

import (
	"flag"
	"fmt"
)

func main() {
	firstName := flag.String("first-name", "UNKNOWN", "the first name of the user profile")
	lastName := flag.String("last-name", "UNKNOWN", "")
	favFruit := flag.String("fruit", "UNKNOWN", "")
	favVeg := flag.String("veg", "UNKNOWN", "")
	favPasta := flag.String("pasta", "UNKNOWN", "")
	favMeal := flag.String("meal", "UNKNOWN", "")
	riceOrBeans := flag.String("rice-or-beans", "UNKNOWN", "")
	favDesert := flag.String("desert", "UNKNOWN", "")
	favDessert := flag.String("dessert", "UNKNOWN", "")
	flag.Parse()

	fmt.Printf(`Profile Name: %s %s
	Rice or Beans: %s
	Favorites:
		Fruit: %s
		Vegetable: %s
		Pasta: %s
		Desert: %s
		Dessert: %s
		Meal: %s
`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)
}

Now let’s check out the help text:

go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -h
Usage of /tmp/go-build1166983742/b001/exe/main:
  -desert string
         (default "UNKNOWN")
  -dessert string
         (default "UNKNOWN")
  -first-name string
        the first name of the user profile (default "UNKNOWN")
  -fruit string
         (default "UNKNOWN")
  -last-name string
         (default "UNKNOWN")
  -meal string
         (default "UNKNOWN")
  -pasta string
         (default "UNKNOWN")
  -rice-or-beans string
         (default "UNKNOWN")
  -veg string
         (default "UNKNOWN")

Neat! Now let’s add the rest:

02-profile-saver/main.go:

package main

import (
	"flag"
	"fmt"
)

func main() {
	firstName := flag.String("first-name", "UNKNOWN", "the first name of the user profile")
	lastName := flag.String("last-name", "UNKNOWN", "the last name of the user profile")
	favFruit := flag.String("fruit", "UNKNOWN", "the user's favorite fruit")
	favVeg := flag.String("veg", "UNKNOWN", "the user's favorite vegetable")
	favPasta := flag.String("pasta", "UNKNOWN", "the user's favorite type of pasta")
	favMeal := flag.String("meal", "UNKNOWN", "the user's all-time favorite meal")
	riceOrBeans := flag.String("rice-or-beans", "UNKNOWN", "whether the user prefers a side of rice or beans")
	favDesert := flag.String("desert", "UNKNOWN", "the user's favorite desert (usually hot and sandy)")
	favDessert := flag.String("dessert", "UNKNOWN", "the user's favorite dessert (usually a sweet after-meal treat)")
	flag.Parse()

	fmt.Printf(`Profile Name: %s %s
	Rice or Beans: %s
	Favorites:
		Fruit: %s
		Vegetable: %s
		Pasta: %s
		Desert: %s
		Dessert: %s
		Meal: %s
`, *firstName, *lastName, *riceOrBeans, *favFruit, *favVeg, *favPasta, *favDesert, *favDessert, *favMeal)
}

And if we check out help text:

go run example-code/02-command-line-arguments-and-flags/02-flags/02-profile-saver/main.go -h
Usage of /tmp/go-build2677080627/b001/exe/main:
  -desert string
        the user's favorite desert (usually hot and sandy) (default "UNKNOWN")
  -dessert string
        the user's favorite dessert (usually a sweet after-meal treat) (default "UNKNOWN")
  -first-name string
        the first name of the user profile (default "UNKNOWN")
  -fruit string
        the user's favorite fruit (default "UNKNOWN")
  -last-name string
        the last name of the user profile (default "UNKNOWN")
  -meal string
        the user's all-time favorite meal (default "UNKNOWN")
  -pasta string
        the user's favorite type of pasta (default "UNKNOWN")
  -rice-or-beans string
        whether the user prefers a side of rice or beans (default "UNKNOWN")
  -veg string
        the user's favorite vegetable (default "UNKNOWN")

So much better. Now our program has clear flagged arguments, defaults set, and some help text to explain what each flag is intended for. However, this could be cleaned up a bit more. To clean up the help text, let’s dig into constants.

Constants

Constants work very similarly to variables, but with one critical difference: They cannot be changed at runtime. Constants are set in your source code and compiled-in, after that, the value cannot be changed. Constants are helpful to document and organize variables that control large pieces of your application or that are used in a variety of places, but the values won’t change.

Let’s dig in with an example. Let’s say you were making a program that talks to a website to get information. The functions you write to talk to the website are all going to use the same address over and over and over again. Instead of typing https://www.example.com over and over again, you can put this address in a constant declaration near the top of your file, then just reference it using the constant name:

package main

import (
	"fmt"
)

const (
       website = "<https://www.example.com>"
)

func main() {
       fmt.Println(website)
}

So constants work kinda like variables, but let’s see what happens when we try to change the value in our main function:

package main

import (
	"fmt"
)

const (
	website = "<https://www.example.com>"
)

func main() {
	fmt.Println(website)
	website = "A new value"
	fmt.Println(website)
}

When we run it, we get this:

main.go:13:2: cannot assign to website (untyped string constant "<https://www.example.com>")

The constants must remain constant. Trying to change them results in the program refusing to build.

So, constants can be useful to help avoid re-typing, but they can also help generally clean up your code. Right now in our profile-saver program, we have a lot of loose help text that we can clean up by moving those help strings from the flag.String functions into a constant declaration. So we can turn this:

func main() {
	firstName := flag.String("first-name", "UNKNOWN", "the first name of the user profile")
	lastName := flag.String("last-name", "UNKNOWN", "the last name of the user profile")
	favFruit := flag.String("fruit", "UNKNOWN", "the user's favorite fruit")
	favVeg := flag.String("veg", "UNKNOWN", "the user's favorite vegetable")
	favPasta := flag.String("pasta", "UNKNOWN", "the user's favorite type of pasta")
	favMeal := flag.String("meal", "UNKNOWN", "the user's all-time favorite meal")
	riceOrBeans := flag.String("rice-or-beans", "UNKNOWN", "whether the user prefers a side of rice or beans")
	favDesert := flag.String("desert", "UNKNOWN", "the user's favorite desert (usually hot and sandy)")
	favDessert := flag.String("dessert", "UNKNOWN", "the user's favorite dessert (usually a sweet after-meal treat)")
...

Into this:

const (
	firstNameHelp   = "the first name of the user profile"
	lastNameHelp    = "the last name of the user profile"
	fruitHelp       = "the user's favorite fruit"
	vegHelp         = "the user's favorite vegetable"
	pastaHelp       = "the user's favorite type of pasta"
	mealHelp        = "the user's all-time favorite meal"
	riceOrBeansHelp = "whether the user prefers a side of rice or beans"
	desertHelp      = "the user's favorite desert (usually hot and sandy)"
	dessertHelp     = "the user's favorite dessert (usually a sweet after-meal treat)"
)

func main() {
	firstName := flag.String("first-name", "UNKNOWN", firstNameHelp)
	lastName := flag.String("last-name", "UNKNOWN", lastNameHelp)
	favFruit := flag.String("fruit", "UNKNOWN", fruitHelp)
	favVeg := flag.String("veg", "UNKNOWN", vegHelp)
	favPasta := flag.String("pasta", "UNKNOWN", pastaHelp)
	favMeal := flag.String("meal", "UNKNOWN", mealHelp)
	riceOrBeans := flag.String("rice-or-beans", "UNKNOWN", riceOrBeansHelp)
	favDesert := flag.String("desert", "UNKNOWN", desertHelp)
	favDessert := flag.String("dessert", "UNKNOWN", dessertHelp)
...

There’s still a lot of text, but its a bit more organized and easier to work with. This is one of the nicest bonus features of constants: They make great organizational tools. If you have certain aspects of your program that can be tuned by yourself or another developer, it may be a good idea to put them in a const declaration at the top of the file so they’re easy to access and conveniently grouped.

Its worth keeping in mind that not everything can be a constant. Due to the way the Go compiler works, complex expressions cannot be pre-evaluated. Said another way: Constants are baked into your application when you run go build. According to the Go language spec, the only valid constant types are:

  • Runes (characters)
  • Strings
  • Booleans (true or false)
  • Numeric types (ints, floats, etc…)

If you want the same style of organization for types that don’t work as constants, you can use a var declaration instead:

var (
	aMap = map[int]string{
		1: "one",
		2: "two",
		3: "three",
	}
)

Appendix and References

References

Appendix

How Spaces Can Affect Flags

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting Hello There -name General
Hello, !

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting "Hello There" -name General
Hello There, General!

go run example-code/02-command-line-arguments-and-flags/02-flags/01-simple-flags/main.go -greeting="Hello There" -name General
Hello There, General!