Bash Shell Scripting/Environment
Subshells
[edit | edit source]In Bash, one or more commands can be wrapped in parentheses, causing those commands to be executed in a "subshell". (There are also a few ways that subshells can be created implicitly; we will see those later.) A subshell receives a copy of the surrounding context's "execution environment", which includes any variables, among other things; but any changes that the subshell makes to the execution environment are not copied back when the subshell completes. So, for example, this script:
#!/bin/bash
foo=bar
echo "$foo" # prints 'bar'
# subshell:
(
echo "$foo" # prints 'bar' - the subshell inherits its parents' variables
baz=bip
echo "$baz" # prints 'bip' - the subshell can create its own variables
foo=foo
echo "$foo" # prints 'foo' - the subshell can modify inherited variables
)
echo "$baz" # prints nothing (just a newline) - the subshell's new variables are lost
echo "$foo" # prints 'bar' - the subshell's changes to old variables are lost
prints this:
bar bar bip foo bar
The same is true of function definitions; just like a regular variable, a function defined within a subshell is not visible outside the subshell.
A subshell also delimits changes to other aspects of the execution environment; in particular, the cd
("change directory") command only affects the subshell. So, for example, this script:
#!/bin/bash
cd /
pwd # prints '/'
# subshell:
(
pwd # prints '/' - the subshell inherits the working directory
cd home
pwd # prints '/home' - the subshell can change the working directory
) # end of subshell
pwd # prints '/' - the subshell's changes to the working directory are lost
prints this:
/ / /home /
An exit
statement within a subshell terminates only that subshell. For example, this script:
#!/bin/bash
( exit 0 ) && echo 'subshell succeeded'
( exit 1 ) || echo 'subshell failed'
prints this:
subshell succeeded subshell failed
Like in a script as a whole, exit
defaults to returning the exit status of the last-run command, and a subshell that does not have an explicit exit
statement will return the exit status of the last-run command.
Environment variables
[edit | edit source]We have already seen that, when a program is called, it receives a list of arguments that are explicitly listed on the command line. What we haven't mentioned is that it also receives a list of name-value pairs called "environment variables". Different programming languages offer different ways for a program to access an environment variable; C programs can use getenv("variable_name")
(and/or accept them as a third argument to main
), Perl programs can use $ENV{'variable_name'}
, Java programs can use System.getenv().get("variable_name")
, and so forth.
In Bash, environment variables are simply made into regular Bash variables. So, for example, the following script prints out the value of the HOME
environment variable:
#!/bin/bash
echo "$HOME"
The reverse, however, is not true: regular Bash variables are not automatically made into environment variables. So, for example, this script:
#!/bin/bash
foo=bar
bash -c 'echo $foo'
will not print bar
, because the variable foo
is not passed into the bash
command as an environment variable. (bash -c script arguments…
runs the one-line Bash script script
.)
To turn a regular Bash variable into an environment variable, we have to "export" it into the environment. The following script does print bar
:
#!/bin/bash
export foo=bar
bash -c 'echo $foo'
Note that export
doesn't just create an environment variable; it actually marks the Bash variable as an exported variable, and later assignments to the Bash variable will affect the environment variable as well. That effect is illustrated by this script:
#!/bin/bash
foo=bar
bash -c 'echo $foo' # prints nothing
export foo
bash -c 'echo $foo' # prints 'bar'
foo=baz
bash -c 'echo $foo' # prints 'baz'
The export
command can also be used to remove a variable from an environment, by including the -n
option; for example, export -n foo
undoes the effect of export foo
. And multiple variables can be exported or unexported in a single command, such as export foo bar
or export -n foo bar
.
It's important to note that environment variables are only ever passed into a command; they are never received back from a command. In this respect, they are similar to regular Bash variables and subshells. So, for example, this command:
#!/bin/bash
export foo=bar
bash -c 'foo=baz' # has no effect
echo "$foo" # print 'bar'
prints bar
; the change to $foo
inside the one-line script doesn't affect the process that invoked it. (However, it would affect any scripts that were called in turn by that script.)
If a given environment variable is desired for just one command, the syntax var=value command
may be used, with the syntax of a variable assignment (or multiple variable assignments) preceding a command on the same line. (Note that, despite using the syntax of a variable assignment, this is very different from a normal Bash variable assignment, in that the variable is automatically exported into the environment, and in that it only exists for the one command. If you want avoid the confusion of similar syntax doing dissimilar things, you can use the common Unix utility env
for the same effect. That utility also makes it possible to remove an environment variable for one command — or even to remove all environment variables for one command.) If $var
already exists, and it's desired to include its actual value in the environment for just one command, that can be written as var="$var" command
.
An aside: sometimes it's useful to put variable definitions — or function definitions — in one Bash script (say, header.sh
) that can be called by another Bash script (say, main.sh
). We can see that simply invoking that other Bash script, as ./header.sh
or as bash ./header.sh
, will not work: the variable definitions in header.sh
would not be seen by main.sh
, not even if we "exported" those definitions. (This is a common point of confusion: export
exports variables into the environment so that other processes can see them, but they're still only seen by child processes, not by parents.) However, we can use the Bash built-in command .
("dot") or source
, which runs an external file almost as though it were a shell function. If header.sh
looks like this:
foo=bar
function baz ()
{
echo "$@"
}
then this script:
#!/bin/bash
. header.sh
baz "$foo"
will print 'bar'
.
Scope
[edit | edit source]We have now seen some of the vagaries of variable scope in Bash. To summarize what we've seen so far:
- Regular Bash variables are scoped to the shell that contains them, including any subshells in that shell.
- They are not visible to any child processes (that is, to external programs).
- If they are created inside a subshell, they are not visible to the parent shell.
- If they are modified inside a subshell, those modifications are not visible to the parent shell.
- This is also true of functions, which in many ways are similar to regular Bash variables.
- Function-calls are not inherently run in subshells.
- A variable modification within a function is generally visible to the code that calls the function.
- Bash variables that are exported into the environment are scoped to the shell that contains them, including any subshells or child processes in that shell.
- The
export
built-in command can be used to export a variable into the environment. (There are other ways as well, but this is the most common way.) - They differ from non-exported variables only in that they are visible to child processes. In particular, they are still not visible to parent shells or parent processes.
- The
- External Bash scripts, like other external programs, are run in child processes. The
.
orsource
built-in command can be used to run such a script internally, in which case it's not inherently run in a subshell.
To this we now add:
- Bash variables that are localized to a function-call are scoped to the function that contains them, including any functions called by that function.
- The
local
built-in command can be used to localize one or more variables to a function-call, using the syntaxlocal var1 var2
orlocal var1=val1 var2=val2
. (There are other ways as well — for example, thedeclare
built-in command has the same effect — but this is probably the most common way.) - They differ from non-localized variables only in that they disappear when their function-call ends. In particular, they still are visible to subshells and child function-calls. Furthermore, like non-localized variables, they can be exported into the environment so as to be seen by child processes as well.
- The
In effect, using local
to localize a variable to a function-call is like putting the function-call in a subshell, except that it only affects the one variable; other variables can still be left non-"local".
It's important to note that, although local variables in Bash are very useful, they are not quite as local as local variables in most other programming languages, in that they're seen by child function-calls. For example, this script:
#!/bin/bash
foo=bar
function f1 ()
{
echo "$foo"
}
function f2 ()
{
local foo=baz
f1 # prints 'baz'
}
f2
will actually print baz
rather than bar
. This is because the original value of $foo
is hidden until f2
returns. (In programming language theory, a variable like $foo
is said to be "dynamically scoped" rather than "lexically scoped".)
One difference between local
and a subshell is that whereas a subshell initially takes its variables from its parent shell, a statement like local foo
immediately hides the previous value of $foo
; that is, $foo
becomes locally unset. If it is desired to initialize the local $foo
to the value of the existing $foo
, we must explicitly specify that, by using a statement like local foo="$foo"
.
When a function exits, variables regain the values they had before their local
declarations (or they simply become unset, if they had previously been unset). Interestingly, this means that a script such as this one:
#!/bin/bash
function f ()
{
foo=baz
local foo=bip
}
foo=bar
f
echo "$foo"
will actually print baz
: the foo=baz
statement in the function takes effect before the variable is localized, so the value baz
is what is restored when the function returns.
And since local
is simply an executable command, a function can decide at execution-time whether to localize a given variable, so this script:
#!/bin/bash
function f ()
{
if [[ "$1" == 'yes' ]] ; then
local foo
fi
foo=baz
}
foo=bar
f yes # modifies a localized $foo, so has no effect
echo "$foo" # prints 'bar'
f # modifies the non-localized $foo, setting it to 'baz'
echo "$foo" # prints 'baz'
will actually print
bar baz