I’ve been looking into declare, and also how it compares to typeset and local. It turns out that there’s a lot to know.

After working my way through the small ix script in Mr Rob’s dotfiles, writing three posts Using exec to jump, curl and multipart/form-data and Checking a command is available before use along the way, I’ve now turned my attention to the twitch script which he uses during his live streams. I haven’t gone very far when I light upon this section:

declare gold=$'\033[38;2;184;138;0m'
declare red=$'\033[38;2;255;0;0m'
...

So declare is a keyword that I’ve seen before but never fully understood or embraced. Seems like this is a good time to fix that.

Declare is a builtin

To start off, declare is a builtin, which means that rather than an external executable (such as echo, or even [), it’s part of the Bash runtime itself, as we can see thus:

> strings $(which bash) | grep declare
declare -%s %s=%s
declare
declare [-aAfFgilnrtux] [-p] [name[=value] ...]
    When used in a function, `declare' makes NAMEs local, as with the `local'
    A synonym for `declare'.  See `help declare'.
    be any option accepted by `declare'.
declare -%s

(If you’re curious about [ being an external executable, you might be interested in another post: The open square bracket [ is an executable.)

The typeset synonym

First off, let’s deal with the declare vs typeset question. Basically, typeset does in the Korn shell (ksh) pretty much what declare does in the Bash shell. And typeset has been added to Bash as a synonym for declare, to make it easier for developers to switch between the flavours. There are other synonyms relating to declare, but we’ll come to those in a bit.

Basics of declare

Next, let’s deal with the question: “But why is declare used here at all?”. Well, in this particular case it’s not absolutely necessary. Strings and array variables don’t actually need to be declared, so this would be fine, too:

gold=$'\033[38;2;184;138;0m'
red=$'\033[38;2;255;0;0m'
...

This would be a couple of simple assignments of values to (otherwise) previously undeclared variables. On the other hand, with the declare variant, subtly different, we’re declaring a couple of variables and also making assignments at the same time, which declare permits us to do.

The local synonym

Of course, the main point of declare is to declare variables and state certain attributes that they are to have. We haven’t seen an example of that yet, but before we do, there’s another subtle difference between declare var=value and simply var=value. This is briefly covered in a paragraph of the help information (run help declare in a Bash shell):

When used in a function, declare makes NAMEs local, as with the local command. The -g option suppresses this behavior.

So local is our next synonym for declare, in the context a function definition. An example script foo will help:

func1() {
  local var1
  var1="Apple"
  echo "func1: $var1"
}

func2() {
  declare var1
  var1="Banana"
  echo "func2: $var1"
}

func3() {
  var1="Carrot"
  echo "func3: $var1"
}

var1="Main"
echo "var1 is $var1"
func1
echo "var1 is $var1"
func2
echo "var1 is $var1"
func3
echo "var1 is $var1"

Let’s look at what we get when this script is executed:

> bash ./foo
var1 is Main
func1: Apple
var1 is Main
func2: Banana
var1 is Main
func3: Carrot
var1 is Carrot

The thing to spot here is that because neither local nor declare were used for var1 in the definition of the func3 function, the assignment of the value Carrot was not restricted to the scope of that function, and when back in the main part of the script, the value of var1 has the value that it was assigned within func3, i.e. Carrot, not Main any more.

Options for declare

Of course, given the main purpose of declare, it’s worth briefly looking at more specific uses. There are various options, adequately covered by various sources including Advanced Bash-Scripting Guide - Chapter 9: Another Look at Variables, and so only summarised here:

-r Read-only
-i Integer
-a Array
-A Associative array (i.e. a dictionary, or object)
-f Function
-x Exported
-g Global

There are other options, but these are the most common, at least as far as I’ve found in my research. Others are covered in the help declare output.

The readonly synonym

The -r option for declare has a sort of synonym too, which is readonly. However, there’s a difference relating to scope; while declare -r will use function-local scope (similar to how it was used in func1 and func2 earlier), readonly will not respect that and simply use the global scope, even inside functions.

In other words, if you were to add another function definition to the above foo script example, like this:

func4() {
  readonly var1
  var1="Damson"
  echo "func4: $var1"
}

… then when func4 had been executed, the value of var1 in the main section of the script would then also be Damson.

Using declare -r here instead is the safer approach, in that the local function scope is respected. Note however that if we add -g (denoting the “global” attribute) to this, i.e. use declare -r -g or declare -rg, then the effect would be the same as using readonly.

The export synonym and what -x implies

There’s one final synonym I found in this journey of discovery, and that’s export, which is the equivalent of declare -x. It took me a few minutes to properly think about what this “available for export” attribute actually implies.

Like me, you’ve most probably used export in your .bashrc file, to set “global” variables when your shell session starts, to be available to you in that session and in executables that you invoke there. Usually these variable names will be in upper case by convention, denoting variables that are “environment” wide. In your shell, you can use env to see what these are. Note that the list that env produces includes variables automatically available to you in the shell too, such as HOME and PATH.

So what does declare -x imply, in the context of a script that you might write and then invoke? It does not mean that once the script finishes, that variable will be available to you in the shell. As an example, consider this script bar:

declare -x var2="Raining"
echo "var2 is $var2"

When we run this, look at what we get:

> echo "$var2" # nothing up my sleeve

> ./bar
var2 is Raining
> echo "$var2"

>

But what if we also had another script baz:

echo "In baz: var2 is $var2"

and we invoked it from within the bar script:

declare -x var2="Raining"
echo "var2 is $var2"
./baz

You can guess what the output will be:

> ./bar
var2 is Raining
In baz: var2 is Raining

And on returning to the shell prompt, we can double check that var2 doesn’t have a value:

> echo "$var2"

>

The way I think about this in my mind is like a tree structure:

shell
 |
 +-- bar        <- var2 declared with -x as 'exported'
      |
      +-- baz   <- var2 available here too

Exporting descends, rather than ascends, so there’s no way var2 could ever be made available like this in the shell.

That’s about it, I think. I hope this is useful; I have found it helpful to try and explain these concepts to you, as it helps me learn. In researching, I came across some content in Stack Overflow and Stack Exchange - so thanks to those folks who took the time to explain things there. You may want to reference them too: