Why would I use declare / typeset in a shell script instead of just X=y?

Viewed 633

I've recently come across a shell script that uses declare -- FOO="" which apparently is spelled typeset -- FOO="" in non-bash shells.

Why might I want to do that instead of plain FOO="" or export FOO?

3 Answers

The most important purpose of using declare is to control scope, or to use array types that aren't otherwise accessible.


Using Function-Local Variables

To give you an example:

print_dashes() { for (( i=0; i<10; i++; do printf '-'; done; echo; }

while read -p "Enter a number: " i; do
  print_dashes
  echo "You entered: $i"
done

You'd expect that to print the number the user entered, right? But instead, it'll always print the value of i that print_dashes leaves when it's complete.

Consider instead:

print_dashes() {
  declare i  # ''local i'' would also have the desired effect
  for (( i=0; i<10; i++; do printf '-'; done; echo;
}

...now i is local, so the newly-assigned value doesn't last beyond its invocation.


Declaring Explicitly Global Variables

Contrariwise, you sometimes want to declare a global variable, and make it clear to your code's readers that you're doing that by intent, or to do so while also declaring something as an array (or otherwise where declare would otherwise implicitly specify global state). You can do that too:

myfunc() {
  declare arg                     # make arg local
  declare -g -A myfunc_args_seen  # make myfunc_args_seen a global associative array
  for arg; do
    myfunc_args_seen["$arg"]=1
  done

  echo "Across all invocations of myfunc, we have seen the following arguments:"
  printf ' - %q\n' "${!myfunc_args_seen[@]}"
}

Declaring Associative Arrays

Normal shell arrays can just be assigned: my_arr=( one two three )

However, that's not the case for associative arrays, which are keyed as strings. For those, you need to declare them:

declare -A my_arr=( ["one"]=1 ["two"]=2 ["three"]=3 )
declare -i cnt=0

declares an integer-only variable, which is faster for math and always evaluates in arithmetic context.

declare -l lower="$1"

declares a variabl that automatically lowercases anything put in it, without any special syntax on access.

declare -r unchangeable="$constant"

declares a variable read-only.

Take a look at https://unix.stackexchange.com/questions/254367/in-bash-scripting-whats-the-different-between-declare-and-a-normal-variable for some useful discussion - you might not need these things often, but if you don't know what's available you're likely to work harder than you should.

A great reason to use declare, typeset, and/or readonly is code compartmentalization and reuse (i.e. encapsulation). You can write code in one script that can be sourced by others. (Note declared/typeset/readonly constants/variables/functions lose their "readonly-ness" in a subshell, but they retain it when a child script sources their defining script since sourcing loads a script into the current shell, not a subshell.)

Since sourcing loads code from the script into the current shell though, the namespaces will overlap. To prevent a variable in a child script from being overwritten by its parent (or vice-versa, depending on where the script is sourced and the variable used), you can declare a variable readonly so it won't get overwritten.

You have to be careful with this because once you declare something readonly, you cannot unset it, so you do not want to declare something readonly that might naturally be redefined in another script. For example, if you're writing a library for general use that has logging functions, you might not want to use typeset -f on a function called warn, error, or info, since it is likely other scripts will create similar logging functions of their own with that name. In this case, it is actually standard practice to prefix the function, variable, and/or constant name with the name of the defining script and then make it readonly (e.g. my_script_warn, my_script_error, etc.). This preserves the values of the functions, variables, and/or constants as used in the logic in the code in the defining script so they don't get overwritten by sourcing scripts and accidentally fail.

Related