Bash: declare in depth, Part 1

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 and export 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!


You'll only receive email when they publish something new.

More from Dario Hamidi
All posts