You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.1 KiB

draft title aliases series author cover description tags date
false Handling flags in Genesis
Handling flags in Genesis
genesis-development
Nick Dumas Using Cobra to accept a huge amount of inputs
genesis
golang
procedural-generation
2018-04-08

Genesis

Genesis is a project Ive spent a great deal of time thinking about and working on for a while with little progress. Im recycling my old Github blog post because it still highlights the overall design plan. Ive since altered the project to use Golang instead of CPython. The change is inspired by a desire/need for improved performance, in my view Golang is the perfect tool to accomplish this goal and is the natural next step in my progression as a developer.

Config files, CLI flags, and repeatability

With the decision to switch to Golang some necessary design choices had to be made. Due to the interactive and 'multi-phase' design of Genesis, it naturally lends itself to a single binary with an abundance of subcommands, such as genesis render, genesis generate terrain and so on.

After some research, an extremely appealing option for building the command-line interface came up: spf13's cobra. This library is used by a lot of pretty big projects, including Hugo ( used to build the site you're reading right now ).

Due to the complex nature involved in each step of the world generation process, and considering one of the design goals is repeatability, I required a powerful yet flexible and reliable option for consuming and referencing configuration data. A user should be able to use interactive mode to iteratively discover parameters that produce results they desire and be able to save those values. Once again, spf13 comes to the rescue with viper. viper allows you to pull config values from quite a few different sources ranging from local files to environment variables to remote stores such as etcd.

The most complex requirement is a composition of the previous two; once a user has found a set of parameters that approximate what theyre looking for, they need to be able to interactively ( via command-line or other user interfaces yet to be designed and developed ) modify or override parameters to allow a fine-tuning of each phase of the generation process. Fortunately, the author of these libraries had the foresight to understand the need for these libraries.

BindPFlags

This composition is then exposed via the BindPFlags method. Given the correct arrangement of cobra flags, viper can now source 'config' values from the aforementioned sources and command-line flags, with flags taking priority over all values except explicit Set() calls written directly into the Golang source code.

Thus, I had my solution. viper will read any configuration files that are present, and when prompted to present the value for a parameter (a pretend example would be something like mountain-tallness), it would check config files, environment variables, and then command-line flags providing the value given last in the sequence of options.

Unfortunately, I was stymied by a number of different issues, not least of which was somewhat unspecified documentation in the README for viper. I opened a Github issue on this in August of 2017 and for a variety of personal reasons lost track of this issue and failed to check for updates. Fortunately, Tyler Butters responded to it relatively quickly and even though I didn't come back to the issue until April of 2018, I responded to further questions on his pull request almost instantly.

I'm going to break down my misunderstandings and what might be considered shortcomings in the libraries and documentation before wrapping up with my solutions at the end of the post.

My first misunderstanding was not quite realizing that once viper has consumed flags from a given command, those values are then within the viper data store available to all commands and other components of the application. In short, PersistentFlags are not necessary once viper has been bound. This being true is a huge boon to the design of my parameters and commands; so long as my parameter names remain unique across the project, I can bind once in each commands init() and never have to touch any cobra value APIs using it for nothing more than dealing with posix flags etc etc. The rest of the problems I had require a little more elaboration.

Naming Confusion

The next issue, I would argue, is a design...oversight in viper. vipers BindPFlags is troublingly named; in the context of cobra, PFlags can be misconstrued as PersistentFlags which are values that propagate downward from a given command to all its children. This could be useful for setting parameters such as an output directory, a desired file format for renders/output files and so on. PersistentFlag values would allow you to avoid repeating yourself when creating deeply nested command hierarchies.

What BindPFlags actually means is "bind" to PFlags, a juiced up, POSIX compliant replacement for the Golang standard library's flag toolkit. Realizing this took me quite a while. I cant be too upset though because BindPFlags accepts a *pflag.Flagset, so it might be assumed that this would be obvious. Either way, it really disrupted my understanding of the process and left me believing that BindPFlags was able and willing to look for PersistentFlag values.

In this commit you can see where I set up my flags; originally these were PersistentFlags because I wanted these values to propagate downwards through subcommands. Thanks to the use of viper as the application's source-of-truth, PersistentFlags aren't strictly necessary.

Order of Operations

The last issue is more firmly in the realm of 'my own fault'; cobra offers a range of initialization and pre/post command hooks that allow you to perform setup/teardown of resources and configurations during the lifecycle of a command being executed.

My failing here is rather specific. cobra by default recommends using the init() function of each command file to perform your flag setup. On line 62, you can see my invocation of BindPFlags. The code I inserted to test whether viper was successfully pulling these values was also included in the same init() method. After some discussion with Tyler B, I had to re-read every single line of code and eventually realize that when init() is called cobra hasn't actually parsed any command line values!

In addition to the change from PersistentFlag to Flag values, I moved my debug code inside of the cobra command hooks and found that configuration file values were being read correctly (as they always had been) and when an identically named command-line flag was passed, viper presented the overriding value correctly.

Summary

This series of misunderstanding and error in logic roadblocked my work on genesis for far longer than I'm proud to admit; efficient, effective, and sane configuration/parameterization is a key non-neogitable feature of this project. Any attempts to move forward with hacked-in or 'magic number' style parameters would be brittle and have to be dismantled (presumably painfully) at some point in the future. Thanks to Tyler, I was able to break through my improper grasp of the tools I was using and reach a point where I can approach implementing the more 'tangible' portions of the project such as generating terrain maps, accomplishing everything from rendering them to even starting to reason out something like a graphical interface.