Controlling script execution mode via “flaggy” environment variables

What’s script execution mode?

If you like Unix shell scripting just as much as I do, sooner or later you face the need to control script execution modes from the outside. By “execution modes” I mean slight variations in how the script might do its job. The examples are countless, but you’re probably familiar with cases like these:

  • Enable verbosity. Let our script talk a bit more on what it’s up to.
  • Enable debugging. Let our script show actual commands being executed.
  • Disable lengthy build stages. Let our Dockerfile do just the bare minimum.

“Well, what’s the big deal?”, a seasoned Unix lover might ask. “Every fool knows how to assign one-off environment variables per shell command. Place a VAR=xyz before the command and there you are”.

I’d definitely agree that such way of specifying “stringy” values like names and numbers is a right tool for the job: DOMAIN=mysite.com ./Setup looks and behaves unambiguously. With booleans, however, it’s not that straightforward.

Shell and booleans

By design, Unix shell doesn’t natively support boolean values like true or false. Everything on the right side of VAR= is a string. Thus, the script has to interpret the value of VAR to make specific clauses think it’s a boolean. For instance, there’s a number of ways to represent true:

VAR=1
VAR=True
VAR=y
VAR=yes

I’d even expect some of us rightfully include VAR=ja or VAR=oui to this list. What we should also keep in mind, that there’s a symmetric list of “falsy” values like "0", "False" etc.

In other words, there’s a variety of string values one can consider “truthy” or “falsy” based on common sense and personal preference.

Drafting it out

To make our story a bit less theoretical, let’s draft something out. Suppose we have a script called Build which generates a static website. We want it to support the following mode switches via environment variables:

  • VERBOSE= to be a bit more talkative on what the script is up to, default is false;
  • DEBUG= to print extended debug information like commands being executed, default is false;
  • WITH_GIT= to initialize a Git repository right after site generation, default is true.

Implementation requirements

Our upcoming implementation should:

  1. Be simple to implement in plain Unix shell code without 3rd party dependencies.
  2. Be documentable and unambiguous.
  3. Be consistently implementable in other programming languages used by our team (Ruby, Golang, JavaScript, …).

The “natural” way

Considering above mentioned, a traditional implementation of verbosity handling in Bash strict mode code could look like this:

# Detect script verbosity mode. Return 0 if enabled, 1 otherwise.
is_verbose() {
  echo -e "1\ntrue\ny\nyes" | grep -iq "^${VERBOSE:-}$"
}

Looks decently clear so far. Let’s handle the two remaining switches, namely DEBUG= and WITH_GIT=:

print_falsy() {
  echo -e "0\nfalse\nn\nno"
}

print_truthy() {
  echo -e "1\ntrue\ny\nyes"
}

# Detect script debug mode. Return 0 if enabled, 1 otherwise.
is_debug() {
  print_truthy | grep -iq "^${DEBUG:-}$"
}

# Detect script verbosity mode. Return 0 if enabled, 1 otherwise.
is_verbose() {
  print_truthy | grep -iq "^${VERBOSE:-}$"
}

# Detect script Git mode. Return 0 if enabled, 1 otherwise.
with_git() {
  ! print_falsy | grep -iq "^${WITH_GIT:-}$"
}

Having above mechanics in place, the script is now equipped with easy-to-use conditionals which can be used like this:

is_verbose && echo "Cloning theme..."
is_debug && set -x
with_git && init_repo

The “flaggy” way

Even though the “natural” implementation listed above looks like a nice practical shell hack, I can’t stop having doubts on the following aspects of it:

  1. We still have to enumerate truthy and falsy string values, despite the fact they’re wrapped in DRY functions.
  2. There’s still a challenge of selecting a “canonical” truthy/falsy representation for documentation purposes.
  3. with_git() is different from its two siblings due to the fact its default value is true.
  4. Bash implementation took five functions and we’d expect it to be at least equally complicated in other languages.
  5. Considering Item 4, we won’t be able to implement “natural” handling, e.g. in a Dockerfile, as its syntax doesn’t allow functions at all.

So, what if we could, by some mystery, avoid messing with all this true/false value recognition altogether?

Indeed, each of our mode switches has a default, and the user is only interested in changing the default to its opposite in certain cases. So it’s only a matter of agreeing on a distinguishable checkmark (“flag”) value:

$ VERBOSE=✔ ./Build
$ DEBUG=⚑ ./Build

The problem with “✔” and “⚑”, however, is that they’re multibyte Unicode characters which we can’t directly input by keyboard and which can still cause compatibility issues as of this writing. Let’s look for a modest replacement.

Proudly being a Ruby man for years, I got to using the “bang” methods like sort! every now and then. Thus, VERBOSE=! unambiguously sounds like “make it verbose!” to me.

Considering we’ve agreed on "!" as the checkmark (“flag”) value, mode functions become these:

# Detect script debug mode. Return 0 if enabled, 1 otherwise.
is_debug() {
  [ "${DEBUG:-}" = "!" ]
}

# Detect script verbosity mode. Return 0 if enabled, 1 otherwise.
is_verbose() {
  [ "${VERBOSE:-}" = "!" ]
}

# Detect script Git mode. Return 0 if disabled, 1 otherwise.
without_git() {
  [ "${WITHOUT_GIT:-}" = "!" ]
}

What happened to our WITH_GIT=? Well, since now we can’t have false on the right side of =, we’ve inverted the left side of it, turning “with” into “without”.

Comparing the two

Considering the Requirements, let’s compare the two solutions:

“natural” “flaggy” winner
Simplicity so-so good “flaggy”
Documentability so-so good “flaggy”
Other languages poor good “flaggy”

Conclusion

Implementing script mode switches as flaggy (“checkmark”) environment variables is more simple, affordable and versatile compared to traditional methods based on string-boolean matching.