Understanding Bash Variables

7 minute read     Updated:

Adam Gordon Bell %     Adam Gordon Bell

Bash is not just a UNIX shell, it’s also a programming language. And like most programming languages, it has variables. You use these shell variables whenever you append to your PATH or refer to $HOME or set JAVA_HOME.

So let me walk you through how variables work in bash, starting with local shell variables and then covering special and environment variables. I think you’ll find understanding the basics to be extremely helpful.

First, let’s take a look at all the currently defined variables in your current shell session. You can see this at any time by running set.

> set
'!'=0
'#'=0
0=/bin/bash
ARGC=0
...

Local Shell Variables

Local shell variables are local to the current shell session process and do not get carried to any sub-process the shell may start. That is, in Bash I can export variables and variables I don’t export are called local. This distinction will make more sense once we get to exporting.

For now, though, let’s look at some examples.

You can define shell variables like this:

> one="1"
> two=100

And access them by using a dollar sign ($):

> echo $one
1
> echo $two
100

You can also refer to them within double-quoted strings:

> test="test value"
> echo "Test Value: $test"
Test Value: test value

Use single quotes when you don’t want variable substitution to happen:

> test="test value"
> echo 'Test Value: $test'
Test Value: $test

Bash Arrays

Bash also has arrays. You can define them like this:

numbers[0]=0
numbers[1]=1
numbers[2]=2

Or like this:

moreNumbers=(3 4)

And access them like this:

#!/bin/bash

numbers[0]=0
numbers[1]=1
numbers[2]=2
echo "zero: ${numbers[0]}"
echo "one: ${numbers[1]}"
echo "two: ${numbers[2]}"
echo "\$numbers: ${numbers[@]}"

moreNumbers=(3 4)
echo "three: ${moreNumbers[0]}"
echo "four: ${moreNumbers[1]}"
echo "\$moreNumbers: ${moreNumbers[@]}"
zero: 0
one: 1
two: 2
$numbers: 0 1 2
three: 3
four: 4
$moreNumbers: 3 4

Bash v4 also introduced associative arrays. I won’t cover them here but they are powerful and little used feature (little used since even the newest versions of macOS only include bash 3.2).

ℹ️ Running Shell Scripts

You can declare bash variables interactively in a bash shell session or in a bash script. I will put a shebang at the top of the scripts (#!/bin/bash).

To run these, save the code in a file:

#!/bin/bash

echo "This is a bash script bash.sh"

Then make the file executable:

> chmod +x bash.sh

Then run it:

> ./bash.sh
This is a bash script bash.sh

Often I’m going to skip these steps and just show the output:

This is a bash script bash.sh

This makes the examples more concise but if you need clarification, or would like to learn more about shebangs, check out Earthly’s understanding Bash tutorial.

You can use unset to unset a variable. This is nearly equivalent to setting it to a blank value, but unset will also remove it from the set list.

> s_1=1
> s_2=2
> set | grep "s_"
s_1=1
s_2=2

> s_1=
> unset s_2
> set | grep "s_"
s_1=''

Bash Special Variables

Bash has built-in shell variables that are automatically set to specific values. I end up reaching for these primarily inside shell scripts. First up is the built-ins for accessing command-line arguments.

Bash Command Line Arguments

When writing a shell script that will take arguments, $1 will contain the first argument’s value.

#!/bin/bash

echo "$1"
> ./cli1.sh one
one

Arguments continue from there to $9:

#!/bin/bash

echo "$1 $2 $3 $4 $5 $6 $7 $8 $9"
> ./cli2.sh 1 2 3 4 5 6 7 8 9 
1 2 3 4 5 6 7 8 9

For arguments above nine, you need to use ${} to delimit them. Otherwise, you get an unforeseen result:

#!/bin/bash

echo "This is unexpected: $15"
echo "But this works: ${15}"
> ./cli3.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 fifteen
This is unexpected: 15
But this works: fifteen

I am getting 15 for $15 because bash expands it as the first parameter ($1) followed by the literal number five (5).

You can also access an array of command-line arguments using the built-in $@ and the number of arguments using $#.

#!/bin/bash

echo "Count : $#"
echo "Args: $@"
> ./args.sh 1 2 
Count : 2
Args: 1 2

You manipulate that array of command line arguments using a for loop.

#!/bin/bash

echo "Count : $#"
echo "Args: $@"
echo "----------------"
i=1
for arg in "$@" 
do
  echo "Arg $i:$arg"
  i=$((i+1))
done
> ./args.sh 1 2 
Count : 2
Args: 1 2
----------------
Arg 1: 1
Arg 2: 2

Passing Variables as Arguments in Bash

If you need to send in arguments that contain spaces, you want to use quotes (double or single).

> ./args.sh "a b" 'c d' 
Scr : 2
Args: 2
----------------
Arg 1: a b
Arg 2: c d

Command-line arguments follow the same rules as local variables, so I can use double quotes when I want to expand a variable inside of the string or single quotes when I don’t want to.

> test='this is not a test'
> ./args.sh "$test" '$test'  
Scr : 2
Args: 2
----------------
Arg 1: this is not a test
Arg 2: $test

Prepending a Variable in Bash

Another way you can pass variables to a subshell is by including the definition before the call to your executable:

#!/bin/bash

echo "test1: $test1"
echo "test2: $test2"
> test1="test1" test2="test2" ./preprend.sh
test1: test1
test2: test2

To prepend.sh these look like global environmental variables, which we will be covering next. But in fact, they are only scoped to the specific process this is running this script.

Exit Codes

Where programs finish executing they can pass an exit code to the parent process which can be read using $?:

> bash -c 'exit 255'
> echo $?
255

A return code of zero indicates success and if you don’t indicate otherwise, zero is returned by default.

> echo "what will I echo?"
what will I echo?
> echo $?
0

You can assign this exit status a variable use it later.

> bash -c 'exit 1'
> exitCode=$?
> echo $exitCode
1

Environmental Variables

Environmental variables work exactly like other variables except for their scope. By convention, environmental variables are named all in upper-case. You list them with env and view the value of specific vars using printenv.

> env
HOSTNAME=46ae620081da
PWD=/
HOME=/root
TERM=xterm
SHLVL=2
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env
> printenv HOME
/root

You can use them directly in the terminal:

$ echo $HOME
/root
$ echo "$HOME"
/root
$ echo '$HOME'
$HOME

And just like local shell variables, you can change their value:

$ echo $HOME
/root
$ HOME="hello"
$ echo '$HOME'
hello

And use them in a bash scripts:

#!/bin/bash

echo "PWD: $PWD"
echo "PATH: $PATH"
PWD: /
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

Bash Export Variable

The local variables I started with never leave the current shell process. If I declare a variable and then start a new bash process, it won’t be set.

> name="adam"
> echo $name
adam
> bash -c 'echo $name' 

You can pass these variables in one by one using the prepend syntax i showed earlier (name="name" bash -c 'echo $name') but there is another way and that is using export.

> export name="adam"
> echo $name
adam
> bash -c 'echo $name' 
adam

When I use export to define a variable, I’m telling bash to pass it along to any child process created, thus creating a global environmental variable.

> export name="adam"
> env | grep name
name=adam

This is a one-way street, though. Exporting from a child process does not make the shell variable accessible to the parent.

> bash -c 'export name1="adam"'
> echo name1

Also, these variables are not persistent. If I start a new shell session, the variables I exported in a previous session won’t be present. To get that behavior, I need to export them from my .bashrc.

...
export NAME="Adam"
> echo $NAME
Adam

.bashrc is a bash script found at ~/.bashrc. It runs whenever a new interactive bash shell starts. So, by adding an export there, I am ensuring that it will present in all shell sessions started after that point. I have to start a new shell session or source ~/.bashrc to see this change, though.

(Using .bashrc is how you can configure your bash prompt, using the variable $PS1, and many other things, but that is a topic for a different article.)

Conclusion

Those are the basics of bash shell variables. There is much that I haven’t covered, but this is the basics. I hope this overview gave you enough depth to understand most use-cases you encounter in your day-to-day work.

Also, if you’re the type of person who’s not afraid to solve problems in bash, then take a look at Earthly. It’s a excellent tool for creating repeatable builds in an approachable syntax.

While you’re here:

Earthly is a syntax for defining your build. It works with your existing build system. Get repeatable and understandable builds today.

Feedback

If you have any tips or tricks about variables in bash or spot any problems with my examples, let me know on Twitter @AdamGordonBell.

Adam Gordon Bell %
Adam Gordon Bell

Spreading the word about Earthly. Host of CoRecursive podcast. Physical Embodiment of Cunningham’s Law