DEV Community

Cover image for Are you still coding go cli handling by hand?
suntong
suntong

Posted on

2 4

Are you still coding go cli handling by hand?

Are you still coding your go cli handling by hand? Time to stop and rethink better ways to do it now.

Take a look at this go command line handling file:

// !!! !!!
// WARNING: Code automatically generated. Editing discouraged.
// !!! !!!
package main
import (
"flag"
"fmt"
"os"
)
////////////////////////////////////////////////////////////////////////////
// Constant and data type/structure definitions
const progname = "OpenSesame" // os.Args[0]
// The Options struct defines the structure to hold the commandline values
type Options struct {
Port string // listening port
Path string // path to serve files from
Help bool // show usage help
}
////////////////////////////////////////////////////////////////////////////
// Global variables definitions
// Opts holds the actual values from the command line parameters
var Opts Options
////////////////////////////////////////////////////////////////////////////
// Commandline definitions
func init() {
// set default values for command line parameters
flag.StringVar(&Opts.Port, "port", ":18888",
"listening port")
flag.StringVar(&Opts.Path, "path", "./",
"path to serve files from")
flag.BoolVar(&Opts.Help, "help", false,
"show usage help")
exists := false
// Now override those default values from environment variables
if len(Opts.Port) == 0 ||
len(os.Getenv("OPENSESAME_PORT")) != 0 {
Opts.Port = os.Getenv("OPENSESAME_PORT")
}
if len(Opts.Path) == 0 ||
len(os.Getenv("OPENSESAME_PATH")) != 0 {
Opts.Path = os.Getenv("OPENSESAME_PATH")
}
if _, exists = os.LookupEnv("OPENSESAME_HELP"); Opts.Help || exists {
Opts.Help = true
}
}
const usageSummary = " -port\tlistening port (OPENSESAME_PORT)\n -path\tpath to serve files from (OPENSESAME_PATH)\n -help\tshow usage help (OPENSESAME_HELP)\n\nDetails:\n\n"
// Usage function shows help on commandline usage
func Usage() {
fmt.Fprintf(os.Stderr,
"\nUsage:\n %s [flags..]\n\nFlags:\n\n",
progname)
fmt.Fprintf(os.Stderr, usageSummary)
flag.PrintDefaults()
fmt.Fprintf(os.Stderr,
"\nWill serve the files from the given path via web server\nof the given port using a one-time random path.\n\nExit and restart will serve from another random path.\n\nThe `-port` / `-path` can be overridden by environment variable(s)\n `OPENSESAME_PORT` / `OPENSESAME_PATH`\n")
os.Exit(0)
}

The help text "path to serve files from" shows up three times:

$ grep -1 'path to serve' OpenSesame_cliDef.go
        Port string // listening port
        Path string // path to serve files from
        Help bool   // show usage help
--
        flag.StringVar(&Opts.Path, "path", "./",
                "path to serve files from")
        flag.BoolVar(&Opts.Help, "help", false,
--

const usageSummary = "  -port\tlistening port (OPENSESAME_PORT)\n  -path\tpath to serve files from (OPENSESAME_PATH)\n  -help\tshow usage help (OPENSESAME_HELP)\n\nDetails:\n\n"
Enter fullscreen mode Exit fullscreen mode

If coding by hand, most probably the first occurrence will be omitted. But that's kind of pity, as the help text is helpful in explaining what exactly the Path string variable is for:

        Path string // path to serve files from
Enter fullscreen mode Exit fullscreen mode

Also, if coding by hand, the second and third occurrences will very easily get out of sync if not be taken care of all the time.

All these violate the DRY (Don't Repeat Yourself) principle. How to avoid it? The answer is this file:

# program name, name for the executable
ProgramName: OpenSesame
PackageName: main
# Name of the structure to hold the values for/from commandline
StructName: Options
# The actual variable that hold the commandline paramter values
StructVar: Opts
# Whether to use the USAGE_SUMMARY in Usage help
UsageSummary: "TRUE"
UsageLead: "\\nUsage:\\n %s [flags..]\\n\\nFlags:\\n\\n"
UsageEnd: "\\nWill serve the files from the given path via web server\\nof the given port using a one-time random path.\\n\\nExit and restart will serve from another random path.\\n\\nThe `-port` / `-path` can be overridden by environment variable(s)\\n `OPENSESAME_PORT` / `OPENSESAME_PATH`\\n"
Options:
- Name: Port
Type: string
Flag: port
Usage: listening port
Value: '":18888"'
- Name: Path
Type: string
Flag: path
Usage: path to serve files from
Value: '"./"'
- Name: Help
Type: bool
Flag: help
Usage: show usage help
Value: false

from which we can see that the help text "path to serve files from" is defined only once but get automatically put into three different proper places.

success

This is the go cli handling code auto generation we are talking about. Look at the above two files, which one is more easier to maintain? With cli handling code auto generation feature out there, don't code it manually next time when you do cli handling again.

What if you already have a manually written cli handling code? Well, converting it to automatically generated is not that hard at all. Here are just two simple steps,

  • introduce config.go, which ports the existing cli handling from existing code into auto-generated config.go file.

  • implemented -d, -c, & -help, which adds more business logic handling for the newly added command line flags/parameters.

Very simple, no sweat at all. All thanks to the auto-generated config.go file.

success

OK, now comes to the question, how is the config.go file auto-generated? Let me answer it with another example, that starts everything from the beginning, with all the following explanation borrowed from here

This section will work through every steps how the program is created.

  1. Initialize an empty project. The _proj.yaml file will later be used to
  2. Initialize project wireframe using the standard Go flag package, by
    1. Edit the _cli.yaml cli arguments definition file
    2. Then do go generate (enabled like this), or directly invoke the _cliGen.sh script
    3. Finally do go build
  3. Provide the initial OpenSesame functionality
  4. Update OpenSesame code to make use of the auto-generated cli wireframe
  5. The auto-generated cli wireframe code are not set in stones, they can later be further amended, just like the real-world cases. For e.g.

This finished a full Initialize -> Make use -> More changes to cli definitions cycle.

success

Make sure to check out the rest of the above quoted wiki, Go command line flag handling code auto generation, from which you can see that using the same .yaml driven file, not only the go standard flag command line handling code can be automatically generated, but viper and cobra as well, and all the way to the full-featured cli code that can set the argument values at three different levels:

So the priority of setting the Host value is, from lowest priority to the highest:

  1. self-config file
  2. environment variable
  3. command line

Three different levels.

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more