Bash: Typed Variables with declare

๐Ÿ“… March 23, 2024
Variables are untyped in Bash by default, but there might be times when we need to create variables that hold only specific data types or have specific purposes.

The declare command allows us to achieve this in order to add extra protections on variables, and there are a few different types allowed.

Need a variable that only stores lowercase strings? How about an integer only and reject all other values? Maybe you need an indexed or associative array? Perhaps you need to create a constant?

Bash declare

declare is built into Bash, so there is no need to install a package. We create typed variables with declare using options, and there are 13 option attributes as of Bash 5.1.

declare: usage: declare [-aAfFgiIlnrtux] [-p] [name[=value] ...]
  • -a Create an indexed array with numbers as keys (indexes)
  • -A Associative array with names as keys
  • -fย  Show function name
  • -F Show function name and its code
  • -g Global scope inside a shell function.
  • -i Only integers may be stored in this variable. Anything else returns 0
  • -I (upper case “I”) Create variable of same type from previous scope
  • -l (lowercase “L”) Only lowercase strings allowed. Any uppercase characters are converted to lowercase
  • -n A variable name that points to another variable.
  • -r Create a read-only variable. Great for making a constant.
  • -t Tracing. For functions only. Function inherits DEBUG and RETURN traps.
  • -u Only uppercase strings are allowed. Any lowercase letters are converted to uppercase.
  • -x Export variable to a child process. Sames as using the bash builtin export command.

Quick Usage

Here are some examples about how to use each option.

Indexed Array

declare -a arrMonster
arrMonster=('slime' 'red slime' 'green slime')

echo ${arrMonster[@]}

Notice that we do not use commas to separate elements when declared. Bash uses whitespace. Do not do this:

arrMonster=('slime', 'red slime', 'green slime') # No, no, no!

Even though it will “work” (meaning no errors), you will end up with commas you will later need to remove through extra, unnecessary code.

We can also declare and assign in one statement like this:

declare -a arrMonster=('slime' 'red slime' 'green slime')

Since arrMonster is an indexed array, we can grab specific elements using a number that begins from 0 for the first element and so on.

echo ${arrMonster[0]} # Returns "slime"
echo ${arrMonster[1]} # Returns "red slime" ...includes the space within the string
echo ${arrMonster[2]} # Returns "green slime"
echo ${arrMonster[3]} # Returns nothing because it is nonexistent

Associative Array

This is an array that uses names for keys instead of numeric indexes as shown above.

declare -A arrMonster
arrMonster['goblin']='Eats villager crops'
arrMonster["hobgoblin"]='Eats the goblins'
arrMonster["badvillager"]='Leads the goblins to the crops'

echo ${arrMonster["goblin"]}      # Eats villager crops
echo ${arrMonster['hobgoblin']}   # Eats the goblins
echo ${arrMonster["badvillager"]} # Leads the goblins to the crops

echo ${arrMonster[@]}    # Show all values in order
echo ${!arrMonster[@]}   # List all keys in order

Note: You can use either single quotes or double quotes to define keys. For best color highlighting in some text editors, use double quotes to avoid confusing the editor.

One-liner

declare -A arrMonster=(['goblin']='Eats villager crops' ["hobgoblin"]='Eats the goblins' ["badvillager"]='Leads the goblins to the crops')

Again, note that we are using whitespace, not commas, to delimit key/value pairs.

Read-only Arrays

We can combine -a or -A with the -r (readonly) option to make an array read-only.

declare -Ar arrMonster=(['goblin']='Eats villager crops' ["hobgoblin"]='Eats the goblins' ["badvillager"]='Leads the goblins to the crops')
declare -ar arrColor=('red' 'green' 'blue') # Indexed, read-only array

This means we cannot change the array after its creation. If we try to perform these assignments on a readonly array,

arrMonster=(["supergoblin"]="Annihilates all other goblins")
arrMonster["goblin"]='Friendly goblin grows crops'
arrMOnster=10000

we will see an error:

arrMonster: readonly variable

This protects the contents of the variable arrMonster. We must use a one-liner that assigns values when the variable is declared. We cannot do this:

declare -Ar arrMonster
arrMonster["goblin"]='Eats villager crops'
arrMonster["hobgoblin"]='Eats the goblins'
arrMonster["badvillager"]='Leads the goblins to the crops'

Otherwise, we will see read-only errors and nothing is assigned.

What Functions Exist?

 

To demonstrate this better, create a user-defined function.

function sayHello {
    echo 'Hello'
}

To call the function, we use,

sayHello

alone without parentheses. Copy the function body to the end of the hidden file .bashrc located in your home directory. (Hidden files are not shown by default in GUI editors.) You will need to open a new terminal to reflect the new function addition.

declare -F shows a list of function names in the current shell. This includes our custom function sayHello that we added to the .bashrc file.

A listing of functions available to the current bash shell. Any user-defined functions found in .bashrc are also listed, such as our custom sayHello function.

declare -f is similar, but it shows function code. What is each function made of?

declare -f shows the code behind each function.

The function must exist in .bashrc. Script functions that last for the duration of a script do not appear when declare is used like this. Meaning, if you create a function in a script, run the script, and then use declare -F or declare -f after the script completes, the function from the script will not appear.

declare -f sayHello # Get function body of specific named function
declare -F sayHello # Does the given function exist in .bashrc?

Delete sayHello from .bashrc, close the terminal, and open a new terminal. Run declare -f and declare -F again. You will see that sayHello no longer appears in either result.

Edit Global Scope Variable from a Function When a Local Variable Exists

Variables in bash are global by default, so why would we ever use the -g global option? In practice, probably never, but there is a specific case where it is necessary. We need a script to demonstrate this one since declare -g and local only work inside functions:

#!/bin/bash

quote='If I were two-faced, would I be wearing this one?'
function makeQuote {
    quote='Today is the tomorrow you worried about yesterday.'
}

makeQuote
echo $quote

Here is the question: which quote will be displayed? The one declared outside the function or the one declared inside the function? Both use the same variable name quote. The idea is to call the function to change the quote, but does it?

Output

Today is the tomorrow you worried about yesterday.

Yes. The function changes the quote. Why? Because quote is global in scope. It can be accessed from within the function makeQuote, so makeQuote can change it.

“What if quote were local to the function?”

We achieve this by using the local command within the function.

#!/bin/bash

quote='If I were two-faced, would I be wearing this one?'
function makeQuote {
    local quote='Today is the tomorrow you worried about yesterday.'
}

makeQuote
echo $quote

local can only be used inside a function. Its purpose is to create a local variable that only the function can see. (Remember, bash variables are global by default including those declared inside functions.) If we run the script now, we see the first quote is displayed.

Output

If I were two-faced, would I be wearing this one?

The function now has no effect on the global quote variable due to scope. Both quote variables are treated as two separate variables by bash.

“Where does declare -g enter the picture?”

What if we had a local variable quote in the function but we needed to alter a global variable of the same name? We would do this:

#!/bin/bash

quote='If I were two-faced, would I be wearing this one?'
function makeQuote {
    local quote='Today is the tomorrow you worried about yesterday.'
    declare -g quote='Everyone with telekinetic powers, raise my hand.'
}

makeQuote
echo $quote

Output

Everyone with telekinetic powers, raise my hand.

Ah-ha! Now, we see how declare -g can be used. We already have a local variable named quote, but if we tried to assign a new value to it thinking that quote is global, it would not change the global variable.

Output without using declare -g

If I were two-faced, would I be wearing this one?

It was somewhat tricky thinking of an example for declare -g because we could just do the following and achieve the same result if all we need to do is change a global variable where a local one of the same name does not exist:

#!/bin/bash

quote='If I were two-faced, would I be wearing this one?'
function makeQuote {
  ย  quote='Today is the tomorrow you worried about yesterday.'
}

makeQuote
echo $quote

But if we needed to alter the global quote variable when a local quote variable also existed within the same function making the global change, then declare -g is how we do that. This can be confusing, so the best practice is to use different variable names if possible.

Integers Only

Probably the easiest, most obvious, and most useful option for declare.

declare -i x=1000
echo $x
x=44.55
echo $x

Output

1000
44.55: syntax error: invalid arithmetic operator (error token is ".55")
1000

We can create a variable that only accepts integers. Any attempt to assign a non-integer value will result in an error as shown above. The value is preserved in the event of an error.

Uppercase and Lowercase Only

Suppose you need to ensure that variables only contain uppercase or lowercase strings. Any attempt to assign values convert to all uppercase or lowercase depending upon which option is used. This can be useful for KEY=VALUE configuration files without needing to write extra conversion code.

Uppercase

declare -u quote='I am only human, although I regret it.'
echo $quote

Uppercase output

I AM ONLY HUMAN, ALTHOUGH I REGRET IT.

Lowercase

declare -l quote='ALERT! ALERT! Intruder detected!'
echo $quote

Lowercase output

alert! alert! intruder detected!

All characters are converted to either uppercase or lowercase depending upon which option was chosen.

Inheriting Previous Variable Attributes

Okay. Now, this one is truly esoteric, but it exists in case you might need it. declare -I (use an uppercase letter “I“) declares a local variable inside a function that will have the same name and attributes as the previous scope. We need to use a script to demonstrate this.

#!/bin/bash

declare -u quote='The difference between stupidity and genius is that genius has its limits.'

function sayQuote {
    declare -I quote='The best thing about the future is that it comes one day at a time.'
    echo $quote
}

sayQuote
echo $quote

Output

THE BEST THING ABOUT THE FUTURE IS THAT IT COMES ONE DAY AT A TIME.
THE DIFFERENCE BETWEEN STUPIDITY AND GENIUS IS THAT GENIUS HAS ITS LIMITS.

Why are both lines uppercase? We have two variables named quote: one global and one local. The global variable provides the template the local variable will use. Since we created a global variable named quote using declare -u, this sets the attribute to store uppercase strings. Anything lowercase will be converted to uppercase automatically. We have a variable of a specific type.

The function offers a different scope, and we create a local variable. declare -I creates a local variable inside the function — the same as using the local command. However, when we use declare -I quote, it says, “Find a global variable of the same name, quote, and create a local variable of the same name (quote) and type that only stores uppercase characters.”

declare -I duplicates a variable’s type from a previous scope. If we had created an integer type, then declare -I would create a local integer variable. If we had created an array, declare -I would create a local array. The bonus is that the local variable can have its own value. This is why we see two different quotes in the output. The two variables store different strings, but both variables are of the uppercase type, so when we assign a string to the local variable, it is automatically converted to uppercase since it was created with the global variable’s uppercase type.

If we set the global variable to read-only, watch what happens.

#!/bin/bash

declare -ur quote='The difference between stupidity and genius is that genius has its limits.'

function sayQuote {
    declare -I quote='The best thing about the future is that it comes one day at a time.'
    echo $quote
}

sayQuote
echo $quote

Output

declare: quote: readonly variable
THE DIFFERENCE BETWEEN STUPIDITY AND GENIUS IS THAT GENIUS HAS ITS LIMITS.
THE DIFFERENCE BETWEEN STUPIDITY AND GENIUS IS THAT GENIUS HAS ITS LIMITS.

This is correct and to be expected. We should see a readonly error message because we cannot assign a value to a constant. As a result, the local variable is not created, so the following echo statement within the function sees the global variable, not a nonexistent local. The same quote is printed twice.

This happens because declare -I behaves like a mime. It copies whatever attributes a variable from a previous scope has as it creates a new local variable. Previous scope used here is a global variable, but it could be from a nested function, for example.

Variable Reference

Here is another tricky-to-grasp, lesser-used option.

quote='A pessimist is a person who has had to listen to too many optimists.'
declare -n x=quote
echo $x # Display the quote. x is a pointer to quote, not a duplicate value

declare -n creates a reference to a variable that already exists. Make sure that the x=quote assignment does not contain the $ as in x=$quote. We want to assign the pointer, not the value to x. The best use case I can think of for -n is to create an “alias” for a variable name. I almost never see this one used.

Tracing Variables

declare -t adds the TRACE attribute to a variable. Specifically, it allows a function to inherit the DEBUG and RETURN traps so we do not have to specify traps separately. declare -t is a shortcut.

“Huh? What in the world is a TRACE attribute?”

To begin, the -t option only applies to functions, not variables. It has no effect on variables. DEBUG is a special trap that runs something before each command. That “something” is determined by a preset trap. DEBUG allows us to debug a program as we test it. It is not meant for production work.

Let’s create a function in a script to see this in action.

Plain function. Trapping DEBUG. No declare -t.

#!/bin/bash

trap 'echo DEBUG signal detected: $BASH_COMMAND' DEBUG

function loop {
    local i=1
    while [ $i -le 3 ]
    do
        echo $i
        sleep 1
        i=$(( i + 1 ))
    done
}

loop

Output

DEBUG signal detected: loop
1
2
3

Perfectly normal. When the function loop is called, DEBUG signal is trapped to immediately display a debugging message of our choosing, “DEBUG signal detected: loop.” $BASH_COMMAND reports the name of the function that runs as a result of this trap. In this case, it is the loop function.

Now, let’s use declare -t to see what changes. We still need to have a trap statement that will handle DEBUG.

#!/bin/bash

trap 'echo DEBUG signal detected: $BASH_COMMAND' DEBUG

function loop {
    local i=1
    while [ $i -le 3 ]
    do
        echo $i
        sleep 1
        i=$(( i + 1 ))
    done
}

declare -ft loop
loop

Output

DEBUG signal detected: declare -ft loop
DEBUG signal detected: loop
DEBUG signal detected: loop
DEBUG signal detected: local i=1
DEBUG signal detected: [ $i -le 3 ]
DEBUG signal detected: echo $i
1
DEBUG signal detected: sleep 1
DEBUG signal detected: i=$(( i + 1 ))
DEBUG signal detected: [ $i -le 3 ]
DEBUG signal detected: echo $i
2
DEBUG signal detected: sleep 1
DEBUG signal detected: i=$(( i + 1 ))
DEBUG signal detected: [ $i -le 3 ]
DEBUG signal detected: echo $i
3
DEBUG signal detected: sleep 1
DEBUG signal detected: i=$(( i + 1 ))
DEBUG signal detected: [ $i -le 3 ]

Wow! That is a lot of information. However, this can be useful when debugging scripts because a trap is handled for each statement. Before, the trap only let us know which function was being called, not each statement executed within the function.

We can see what is happening while our script runs, line by line, without needing to add echoes or other hackish tricks. declare -ft causes the loop function to inherit the DEBUG trace for each statement in its code block so we can see what is happening (the order of execution of each statement) within the function. $BASH_COMMAND displays the current statement of execution as it executes the function. This is a useful way to debug a function by watching it execute statement by statement. In declare -ft loop, the -f tells declare we are using a function.

With declare -t, the specified function inherits both DEBUG and RETURN.

Exporting a Variable

declare -x is the same as the export command. It will pass the variable to a child process (a subshell). Let’s try this without any export see what happens. In a new terminal (close all others first), type these lines:

quote='2B or !2B'

echo $quote

Output

2B or !2B

Good. That is what we expect. What we want to do now is view the contents of the quote variable in a subshell. To create a subshell (child process), just type bash in the current shell.

bash

This creates a subshell in the current terminal window even though it does not look like anything has changed. It will not create a new terminal window. Now, let’s see if we can access the contents of quote.

echo $quote

Output

Blank line

Nope. Nothing. This is because the quote variable is local to the previous shell, so the new shell, the subshell, cannot access it. Type exit to close the current shell but remain in the current terminal window.

exit

echo $quote should still work. This means the shell still has quote defined. Close the terminal and open a new one. This time, let’s use declare -x to create the variable.

declare -x quote='2B or !2B'

echo $quote

Output

2B or !2B

Good. Same as before. Now, let’s create a subshell.

bash

echo $quote

Output

2B or !2B

What is this? The subshell now sees the variable created in the previous shell. This is what it means to “export” a variable, and declare -x is one way to achieve this. The other way is to use the export command.

quote='2B or !2B'
export quote # Use quote, not $quote
bash
echo $quote

Output

2B or !2B

Both techniques export a variable to a subshell.

What Variables Exist?

declare -p will list all variables and functions.

declare -x OLDPWD
declare -- OPTERR="1"
declare -i OPTIND="1"
declare -- OSTYPE="linux-gnu"

[Truncated for brevity]

Any user-defined variables will appear in this list at the end.

Notice the – – and -x and -i? These are the variable attributes. If none exist for a variable, we will see – -. -x means export, -i means integer, and so on. Variable attributes will appear here. A plain variable created without using declare, such as x=500 will have the attribute – -.

Variable Creation Result

We can use declare -p to find out if a variable has been successfully created or not.

declare -i xpos=500 && declare -p | grep xpos

Output

declare -- _="xpos=500"
declare -i xpos="500"

we are interested in the last line, our variable we just created. If the variable does not exist, then the output will be empty.

declare -p | grep ypos   # ypos is a nonexistent variable

Output

Nothing.

What Type is declare?

Curious? Try this:

type -a declare

Output

declare is a shell builtin

Setting and Unsetting Attributes

The attribute options we choose can be removed from some variables. For example, if we create an exportable variable with -x we can remove the attribute with +x. Yes, you read that correctly. -x SETS and attribute, and +x REMOVES the attribute. Sounds backwards, but that is how it is established.

declare -x quoteย  # Create a variable that can be exported to a subshell

declare -x quote  # Unset the attribute from the variable above. It can no longer be exported.

The variable still contains a value, but it will no longer be exported.

I find the +x notation counterintuitive and have little reason to unset an attribute, so I tend to ignore the unset options. Not all attributes can be unset. Only a few. After all, when we use declare, it is because we have a specific reason to create a variable of a specific type, so there is little reason to change that.

Finding More Help

You can find more detailed information about using declare from…wait. There is no detailed help out there besides this:

declare --help

I use declare quite a bit for the more common data types involving integers and arrays, but locating well-written examples and explanations about the other options is almost nonexistent. As simple as declare might be, I could not find much information about it online. Just plenty of basics covering the most common attributes. A few older bash books were a little more helpful, but that’s it. You really are on your own when it comes to understanding how to use declare effectively.

Conclusion

declare is a useful command to help create specific variable types, but documentation is sparse. Especially for the lesser-used options like -I and -t. Hopefully this article helps others makes sense of what declare can do when faced with those scripting challenges where a typed variable saves the day.

Have fun!

 

 

 

,

  1. Leave a comment

Leave a comment