Bash: declare in depth, Part 1
October 13, 2021•522 words
If you run help declare
in Bash, you will see that this builtin supports an overwhelming 14 different options for defining variables.
Actually, declare
is a family of functions and you have used some of its cousins like export
and local
already.
Some of these options allow for powerful metaprogramming (like namerefs), others help you make your scripts more robust. Let's look into some of the more commonly useful options.
constants
You can declare a variable as read-only (or constant), by using the -r
option:
declare -r x=1
printf "x=%s\n" "$x" # => x=1
x=2 # -bash: x: readonly variable
Since this is very common, it comes with a handy alias: readonly
readonly x=1
works just like the above.
exporting variables
I'm sure you have some variation of this line in your shell's configuration file:
export PATH="$HOME/.yarn/bin:$PATH"
Using export
sets a flag on the given variable (e.g. PATH
), which causes the shell to put this variable into the environment of any processes started by it.
A variable is also export (for the invocation of one command only) if you place the assignment at the beginning of the line.
x=1
bash -c 'printf "x=%s\n" "$x"' # prints x=
declare -x x # export x
bash -c 'printf "x=%s\n" "$x"' # prints x=1
y=2 bash -c 'printf "y=%s\n" "$y"' # prints y=2
# y is undefined here
exporting functions
Running help declare
you can learn about the -f
option:
Options:
-f restrict action or display to function names and definitions
This allows us to export functions to subshells!
now() {
date +%s
}
bash -c 'now' # bash: line 1: now: command not found
declare -xf now
bash -c 'now' # 1634108076
How does this work?
Bash puts the function definition into an environment variable that is exported to subprograms:
bash -c 'printenv | grep now'
# BASH_FUNC_now%%=() { date +%s
Putting it all together
Combining readonly
with export
we can easily create little embedded DSLs that are interpreted directly by Bash.
The plan roughly looks like this:
- create a script that will interpret our DSL
- in that script, define all available DSL commands, make them
readonly
andexport
them - source the actual DSL program
Why mark exported functions as readonly? Well, if a user of the DSL unknowingly defines a function that is vital for the DSL to work, it would wreak havoc on the whole script.
minimake
Let's build a tiny DSL emulating make, which could be useful for managing dependencies in your dotfiles.
This is how we would like to specify our dependencies and how to satisfy them:
# MiniMakefile
target rbenv:all needs rbenv rbenv-init
test rbenv 'which rbenv'
provide rbenv '
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash
'
test rbenv-init 'grep -q "rbenv init" ~/.bashrc ~/.bash_profile'
provide rbenv-init 'printf "eval \"$(~/.rbenv/bin/rbenv init -)\"\n"'
Once we have our minimake implementation, we'll be able to run:
minimake rbenv:all
and it will automatically install rbenv and configure your shell if necessary.
We'll look into the implementation of minimake another time!