

Discover more from hrbrmstr's Daily Drop
Drop #216 (2023-03-09): EPA (Environment Protection & Agency)
declare/readonly; direnv/quickenv; autoenv
Since we encountered some terminal velocity in yesterday's Drop, we'll join Master Splinter's 🐢 crew for the day and stay in our (Linux/macOS/BSD) shells.
Today's Drop looks at some ways to make our CLI environment variable ops safer and how to wield them with the efficiency of a 🥷🏼 with some new and new-ish helper tools.
typeset/readonly
Just under a year ago, we covered ShellCheck, a linter for your shell scripts. ShellCheck helps us write defensively, making our scripts more resistant to bugs/info leaks, and less likely to have exploitable weaknesses.
Now, the typeset
(a.k.a. declare
) and readonly
(alternatively typeset -r
) shell commands are not in any way, shape, or form novel entities. However, the other sections in today's Drop are focused on some new-ish ways to better manage environment variables, and it never hurts to toss in a PSA every now and again.
If you already know about these commands, feel free to skip to the remainder of the content, and just consider this your, now persistent, conscience nudge to write safer scripts 😎.
For those that have stuck around, I 100% know you would rather not read a 5,000-word essay on two simple shell builtins, so we'll make this quick. Plus, we're assuming bash
and ksh
are in play, and there are enough not-so-subtle differences in the typeset
command between them:
bash:
typeset [-afFgrxilnrtux] [-p] [name[=value] …]
ksh:
typeset [ -CDHLRZfilrtux [n]] [name[=value]] …
that digging into all the functionality of typeset
is left as an exercise to the reader (we'll just be covering a couple concepts, below).
Now, I know you know how to make an environment variable:
BEST_SHIP="rocinante"
And, I know that you even know how to make that variable available to the environment of subsequently executed commands:
export BEST_SHIP="rocinanate"
And, I trust that you always follow good quoting practices, right?
👍🏽
Because we “live”/work in the shell REPL, it's often easy to forget that we're using a full-on programming language. Even when we write scripts semi-regularly, I bet a decent percentage of our collective noggins lean into treating them like just lists of commands to execute, with the occasional conditional/loop/variable tossed in for good measure. But, they truly are real programming languages, and we can and should treat their variable ops with the same care and attention we would in any other programming language.
The readonly
command does what it says on the tin — prevents accidental (or malicious) value modifications elsewhere in a script or other scripts. This will improve overall script stability, predictability, and security. Typing eight extra characters (nine, if you include a space) is a small price to pay, and the benefits far outweigh any increase in script file size. Plus, tagging variables as readonly
also helps communicate intent to your future self and anyone else who needs to read or modify the script.
As far as typeset
goes, the core item I wanted to convey in this section is that using it helps enforce and communicate the expected type of a given variable. We'll work through one, short example, since I suspect you're eager to get to the other sections and are quite capable of digging into your local help for typeset
.
There are originally four members of the crew of the Rocinante, so we'll make that integer (i
) fact known in a script we're writing, and make sure it's available (x
) by anything we call, and restrict modification (r
) of the value:
$ typeset -xri ROCI_CREW_COUNT=1
Folks who use ksh
have a nice way of validating that:
$ echo ${(t)ROCI_CREW_COUNT}
integer-readonly-export
If you haven't poked at these typeset
/ declare
/ readonly
(and the other shell helpers to make working with variables safer), consider this license to do so.
direnv/quickenv
Back in February, we lightly touched on “run commands” when talked about dotfile management. These files usually end in rc
, and that idiom has carried over into other contexts.
One such context is the direnv project, which aims to help you manage your environment variables in a slightly similar fashion as you may use .gitignore
files.
Git “ignore” files let you specify files and directories that should be excluded from git
ops. You can put one in nested directories to customize how those ignore rules are applied.
Direnv
— which is a single Golang binary — takes that concept and applies to your environment variables. Once installed, it becomes an extension for your shell. By that, I mean it hooks pretty deep into your shell, such that — when you cd
around the place — loads and unloads environment variables depending on the current directory. This is handy if you need different environment variables for different projects, and helps declutter your environment.
It does so by looking for local .envrc
files.
Four popular shells are supported:
bash
zsh
tcsh
fish
And the documentation {GH} is solid enough that I can leave you in their hands.
If you're looking for something a bit less intrusive, quickenv provides similar functionality, but requires a bit more work on your part.
autoenv
Autoenv is a Rust utility that works pretty much like the aforementioned direnv
with a few differences:
it uses
.env
vs.envrc
has a “view-before-execute” mode
if the option is enabled and an
.env.leave
file is present in a directory, it will get executed when youcd
outyou can control which
.env[.leave]
files are authorized (to prevent someone from injecting code by slipping in an unexpected.env[.leave]
file into random directories)supports
dash
(and does not supporttcsh
)
FIN
There are many limitations with the “legacy” shells we mentioned today. Just like other programming languages, creative minds continue to adopt, adapt, and improve upon the concept of an operating system shell, and you may want to check out one or more alternative ones to level up your shell game. ☮
#declare
#readonly
#shell
#bash
#ksh
#env
#printenv
#cli
#repl
#autoenv
#quickenv
#direnv
#dash