2.9. Variables in bash#

Although bash is primarily designed for managing processes, files, and representing everything as text, its syntax is in actually quite expressive and posses simple programming language constructs. For example, similar to other programming languages, bash has the ability to use variables. Variables in bash have various uses, including:

  • Defining something configurable once, and then easily reusing that definition in various later commands.

  • Holding some value that is not known in advance, for example text from user input, or the output of some command.

  • Configuring the shell that you are working in, also referred to as your working environment. This includes, for example, modifying the list of directories where the shell will look for executable programs.

Variables are commonly used in shell scripts, which will be discussed later in Section 2.10, but can also be used when you interact with the shell yourself in a terminal.

Let’s start with the basic syntax of using variables. To set or change a variable, use the following syntax in your bash shell: <variable>=<contents> For example, try this:

$ MYNAME="A. User"

Note: there should be no space before and after the = sign). Again, the assigned value = immediately follows the variable name MYNAME, without a space.

The content of a variable is just plain text, and we can now insert this text in commands, just as if we typed it ourselves. To insert a variable’s content in the comment, write $ directly followed by the variable name:

$ echo "My name is $MYNAME"

Observe that the $ is only use to “look up” the value of a variable, but it is not part of the name itself, so we should not include the $ when assigning a value to a variable.

Variables can be updated if needed. You can express a variable’s (new) value in terms of variables too, of course:

$ MyVal="Hello"
$ echo $MyVal
$ MyVal="$MyVal World!"
$ echo $MyVal

Exercise 2.179

Set a variable called MYNAME to your own name, then use echo to print the sentence My name is <your name> using that variable.

{
  "checks": [
    { "type": "varSet", "name": "MYNAME", "desc": "MYNAME should be set" },
    { "type": "compareOutput", "referenceCommand": "echo My name is ${MYNAME:-UNSETVARIABLEDEFAULTVALUE}",
      "desc": "should echo a sentence starting with \"My name is\"" }
  ]
}

Exercise 2.180

What happens when you echo a variable that was never set, e.g. echo Start $KJHDS Stop? Try it out on your own Linux installation.

  • Bash reports an error and stops

  • The unset variable is replaced by an empty string, so it’s as if it wasn’t there

  • The literal text $KJHDS is printed

  • Bash asks you to provide a value for KJHDS

You can use Bash’s Tab-completion to complete variable names. Once you have typed a $ and the first letters of the variable, a single Tab will complete the name. If multiple variables start with the same letter sequence, the completion will only complete up to the part that they share. You can press Tab twice to get all variable name options that start with what you currently typed.

Tip

You can also use this mechanism to quickly see all set variables, both regular and environment variables, by trying to complete just $. For example, by starting to type (don’t press Enter yet),

$ echo $

and then pressing Tab twice, you should see all variables.

Exercise 2.181

Set a variable called AVERYLONGVARIABLENAME to any value. Then print its value with echo, but type only $AVER and press Tab to complete the rest instead of typing the full name.

{
  "checks": [
    { "type": "tabCompletion", "result": "$AVERYLONGVARIABLENAME", "kind": "variable",
      "desc": "should Tab-complete the variable name" },
    { "type": "commandEvent", "eventType": "builtin.echo",
      "desc": "should echo the completed variable" }
  ]
}

Sometimes you will see that variable lookups are placed in quotes, which can have subtle different results compared to looking up the variable without quotes:

$ MSG="Hello         World!"
$ echo $MSG
$ echo "$MSG"

Exercise 2.182

Set MSG="Hello         World!" (with multiple spaces), then run both echo $MSG and echo "$MSG" and compare their output.

Why does the output of echo commands here look different? Hint: recall Section 2.4.4.1.

{
  "checks": [
    { "type": "varValue", "name": "MSG", "value": "Hello         World!",
      "desc": "MSG should be set to \"Hello         World!\" (keep the extra spaces)" },
    { "type": "commandSucceeded", "pattern": "^echo\\s+\\$MSG$",
      "desc": "should run echo $MSG (without quotes)" },
    { "type": "commandSucceeded", "pattern": "^echo\\s+\"\\$MSG\"$",
      "desc": "should run echo \"$MSG\" (with quotes)" }
  ]
}

A common use for variables is to hold a value that we want to define once, and reuse later various times, e.g. a long filename:

$ MYFILE=/usr/share/common-licenses/GPL
$ head -1 $MYFILE
$ tail -1 $MYFILE
$ wc $MYFILE

Exercise 2.183

The file report.txt already exists in your home directory. Assign its full path (not relative path) to a variable called MYFILE, then use that variable with both head and wc (instead of typing the filename twice).

{
  "filesystem": { "/home/student/report.txt": "line one\nline two\nline three\n" },
  "checks": [
    { "type": "varValue", "name": "MYFILE", "value": "/home/student/report.txt",
      "desc": "MYFILE should hold the path to report.txt" },
    { "type": "commandEvent", "eventType": "coreutil.head", "match": { "absPaths": ["/home/student/report.txt"] },
      "desc": "should run head on $MYFILE" },
    { "type": "commandEvent", "eventType": "coreutil.wc", "match": { "absPaths": ["/home/student/report.txt"] },
      "desc": "should run wc on $MYFILE" }
  ]
}

Another common use of variables is to capture the output of commands with $( ... ), as was done in Section 2.7.5, so that these can processed by various other commands without pipes. For example:

$ TXTFILES=$(find . -name "*.txt")
$ echo "Found: $TXTFILES"
$ wc -l $TXTFILES

Exercise 2.184

Try this exercise on your Linux laptop, in a working directory with several files with a .txt extension. For example, your home directory might already contain several (hidden) directories with such files.

$ TXTFILES=$(find . -name "*.txt")
$ echo $TXTFILES
$ echo "$TXTFILES"

What is the difference between the two echo commands here?

  • only with quotes new line separators in the captured output of the find command is kept.

  • with quotes, the echo command just prints $TXTFILES

  • they are the same

  • without quotes, all spaces in the captured output of the find command is discarded.

Exercise 2.185

Continuing the previous exercise, where TXTFILES contains the captured output of the find command, what is the difference between the following two commands?

$ wc -l $TXTFILES
$ echo "$TXTFILES" | wc -l
  • They both do the same.

  • The first let’s wc report the number lines for each file separately, the second counts the number of file paths in TXTFILES.

  • The first counts the number of file paths in TXTFILES, the second let’s wc report the number lines for each file separately.

  • Nothing useful like this; The first one should have quotes, and the second should not have quotes.

2.9.1. Environment variables#

A variable can also be marked as a special environment variable, which have several uses.

First, environment variables can reflect properties about your shell environment, such as your username and home directory, and changing them may even affect how the shell works. In fact, some environment variables are already defined when you start a new shell.

Second, unlike regular variables, environment variables are passed on to any program that you start in the shell. This means a program may access your environment variables, and adjust their behavior based on their set values. For example, a program can obtain your home directory from an environment variable, and use it as a default location to save its files. But a program can also be programmed to look for an optional environment variable that could set to configure specific settings.

Some common environment variables are:

  • HOME is the environment variable which contains your home directory;

  • PATH contains a list of directories the shell will search for programs;

  • TERM shows you what type of terminal you are working on.

Some of the environment variables contain useful information, such as USER, and PWD etc. You can get a list of environment variables (without regular variables) and their current values with the env command,

$ env

Exercise 2.186

Get a list containing only all environment variables that contain your user name, using a single command.

Hint: use a combination of env, grep, and a pipe.

Exercise 2.187

Get a list containing only the environment variables that contain your username ($USER), using a single command that pipes env into grep.

{

  "filesystem": {
    "/home/student/.bashrc": "export PS1='$ '\nexport FOO='Hello world'\n"
  },
  "checks": [
    { "type": "commandEvent", "eventType": "builtin.env", "desc": "should use env" },
    { "type": "commandEvent", "eventType": "syntax.pipeline", "desc": "should use a pipe (|)" },
    { "type": "commandEvent", "eventType": "coreutil.grep", "match": { "pattern": "student" },
      "desc": "should grep for your username (student)" }
  ]
}

Exercise 2.188

Now try the same on your Ubuntu Linux installation with your actual user name. Which of these following environment variables contains your user name?

  • DISPLAY

  • HOME

  • LANGUAGE

  • LOGNAME

  • PWD

  • SHELL

  • TERM

  • USER

  • USERNAME

  • XAUTHORITY

You use environment variables just as you would use regular variables, for example:

$ echo "My name is $USER and my home is $HOME"

Some of these variables affect how your shell behaves. For example, when you write ~ to represent your home directory as part of a path,

$ cd ~

your bash shell will lookup the value of the HOME variable, and replace the ~ by that value. So this line is equivalent to

$ cd $HOME

Another useful environment variable to familiarize yourself with is PATH, which we will discuss later in Section 2.10.6.

2.9.2. Defining new environment variables#

You can also define your own environment variables. To mark a variable as an environment variable, write export before setting the variable, e.g.

$ export NEWENVVAR=Hi

Check the output of env if NEWENVVAR is listed.

Exercise 2.189

Test if changing the value of an exported variable is reflected in the environment:

$ export NEWENVVAR=Hi
$ NEWENVVAR=Hello

What value does env report?

  • NEWENVVAR has just an empty value, because there is a conflict now.

  • NEWENVVAR is not listed by env anymore when you reassign an export.

  • “Hi”, so it is the value assignment NEWENVAR=Hi that was exported, and regular variable assignments don’t affect exported values.

  • “Hello”, so it is the variable NEWENVAR that as exported, irrespective of what its value is or becomes.

  • “HiHello”, the variable keeps collecting all assignments as a single value.

Exercise 2.190

Let’s try to set a regular variable, and test what env reports.

$ MYNAME="A. User"

Does env also show variables that have not been exported?

  • Yes.

  • No.

  • Depends on the weather.

Exercise 2.191

Set an environment variable, and confirm with env that it is properly set. Then close the terminal, which will terminate your shell session. Now open a terminal again, which will start a new bash shell process. Is the environment variable still set? Check with env again.

  • Yes, once it is exported, that is automatically saved by the operating system.

  • Yes, the operating system remembers, but only until you restart your computer.

  • No, it is only set as long as the bash process is active, and other bash processes do not share the value unless it is set there too.

  • No, but it will be visible on other terminals that are opened, as long as the terminal that set the environment variable is not closed.

Just like aliases, custom set environment variables are lost when you close a terminal, unless you put them in your .bashrc startup file which is automatically executed when you start a new terminal. The .bashrc startup file will be discussed later in Section 2.10.7.

2.9.3. Environment variables and child processes#

As explained before, environment variables are also accessible in child processes. We can simply test this by starting a child bash process from in your existing bash session.

Close your terminal, and open a new one, to make sure you have no custom variables set yet.

Let’s set two variables, one regular and one an environment variable:

$ TEST_REGULAR=Foo
$ export TEST_ENV=Bar

Let’s confirm that both variables can be used now

$ echo $TEST_REGULAR $TEST_ENV

should show Foo Bar.

Next, start a new bash process from within the current bash process (so, without opening a new terminal)

$ bash

You should see a normal prompt again, but you should be able to confirm you are now working in a child bash process,

$ pstree

Let’s check which of the two variables are now set, using the same test as before:

$ echo $TEST_REGULAR $TEST_ENV

You should be able to confirm from the output that only the exported environment variable TEST_ENV was kept.

Exercise 2.192

Redo the walkthrough above yourself: set TEST_REGULAR=Foo as a regular variable and export TEST_ENV=Bar as an environment variable, start a child bash process, confirm inside it that only TEST_ENV is visible, then exit back to the parent.

{
  "checks": [
    { "type": "commandSucceeded", "pattern": "^TEST_REGULAR=Foo",
      "desc": "set TEST_REGULAR=Foo in the parent shell" },
    { "type": "commandSucceeded", "pattern": "^export\\s+TEST_ENV=Bar",
      "desc": "export TEST_ENV=Bar in the parent shell" },
    { "type": "commandEvent", "eventType": "process.start", "match": { "command": "bash" },
      "desc": "start a child bash process" },
    { "type": "varSet", "name": "TEST_ENV", "passOnce": true,
      "desc": "TEST_ENV should still be set inside the child" },
    { "type": "varSet", "name": "TEST_REGULAR", "negate": true, "passOnce": true,
      "desc": "TEST_REGULAR should NOT be set inside the child" },
    { "type": "commandEvent", "eventType": "process.exit", "match": { "reason": "exit" },
      "desc": "exit back to the parent shell" }
  ]
}

Exercise 2.193

Does setting a variable value in a child process affect the parent process? Let’s test it.

As before, set a regular and exported variable with some value; For example:

$ TEST_REGULAR=Foo
$ export TEST_ENV=Bar

Now start a child bash process again, and try setting both variables to two different values

$ TEST_REGULAR=Ping
$ export TEST_ENV=Pong

Then terminate the child process using exit, and confirm with pstree that you are back in your original shell process.

What do you think the values of these variables in the parent process are? After you made your guess, show the variable values one more time:

$ echo $TEST_REGULAR $TEST_ENV

What can you conclude? After starting child process, …

  • … all non-exported variables are unset (so have no value) in the parent process

  • … all non-exported variables are changed in the parent process

  • … all exported variables are changed in the parent process

  • … all variables are changed in the parent process

  • … no variables are changed in the parent process

Exercise 2.194

This time try to set a new environment variable that was not yet set in the original parent bash process. Can a child process add new environment variables to a parent process?

Start a child bash process, and inside it set export CHILDVAR=New (a variable that did not exist in the parent). Then exit, and confirm with env that CHILDVAR is not set in the parent shell.

{
  "checks": [
    { "type": "commandEvent", "eventType": "process.start", "match": { "command": "bash" },
      "desc": "start a child bash process" },
    { "type": "commandSucceeded", "pattern": "^export\\s+CHILDVAR=New",
      "desc": "export CHILDVAR=New inside the child" },
    { "type": "commandEvent", "eventType": "process.exit", "match": { "reason": "exit" },
      "desc": "exit back to the parent shell" },
    { "type": "varSet", "name": "CHILDVAR", "negate": true,
      "desc": "CHILDVAR should NOT exist in the parent shell" },
    { "type": "commandEvent", "eventType": "builtin.env", "desc": "should use env" }
  ]
}

Exercise 2.195

Based on the exercises above, which statement about environment variables and child processes is correct?

  • A child process shares the exact same variables as its parent – changes in either direction are visible in both

  • A child inherits its parent’s exported variables at the moment it starts, but nothing the child does to variables (new, changed, or exported) is ever visible back in the parent

  • A child only inherits variables that are exported and changed after the child has started

  • Only regular (non-exported) variables are inherited by child processes