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 isfalse
;DEBUG=
to print extended debug information like commands being executed, default isfalse
;WITH_GIT=
to initialize a Git repository right after site generation, default istrue
.
Implementation requirements
Our upcoming implementation should:
- Be simple to implement in plain Unix shell code without 3rd party dependencies.
- Be documentable and unambiguous.
- 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:
- We still have to enumerate truthy and falsy string values, despite the fact they’re wrapped in DRY functions.
- There’s still a challenge of selecting a “canonical” truthy/falsy representation for documentation purposes.
with_git()
is different from its two siblings due to the fact its default value istrue
.- Bash implementation took five functions and we’d expect it to be at least equally complicated in other languages.
- 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.