
In our first tutorial on command line wizardry, we covered simple redirection and the basics of sed, awk, and grep. Today we are going to introduce the concepts of simple substitution of variables and loops – again, with a specific focus on the Bash command line itself, rather than Bash scripting.
If you need to write a script for repeated use, especially one with significant logical ramifications and evaluation, I highly recommend using “real language” rather than Bash. Fortunately, there are plenty of options. Personally, I’m a big fan of Perl, partly because it’s available on pretty much every *nix system you’ll ever come across. Others could reasonably choose Python or Go, for example, and I wouldn’t judge.
The real point is that we’re focusing on the command line itself. Everything below is something that you can easily learn to think and use in real time with a little practice.
Set and retrieve values in Bash
Bash treats variables a bit strangely compared to any of the “real” languages I’ve used. Specifically, you need to reference the same variable with a different syntax when setting its value versus when retrieving it.
Let’s look at a very simple example:
[email protected]:~$ hello="Hello, world!"
[email protected]:~$ echo $hello
Hello, world!
If you’ve never worked with variables in Bash, you might think there’s a typo in the example above. We set the value of hello
but read back its value as $hello
† That’s not a mistake – you’re not using the lead $
when you define/set a variable, but you must use it when you read from the same variable later.
Destroy variables
If we need to clear that variable, we can use the unset
command – but again, we need to refer to hello
instead of $hello
†
[email protected]:~$ echo $hello
Hello, world!
[email protected]:~$ unset $hello
bash: unset: `Hello,': not a valid identifier
bash: unset: `world!': not a valid identifier
[email protected]:~$ unset hello
[email protected]:~$ echo $hello
Thinking about the error messages we get when we try wrong unset $hello
should clarify the problem. When we unset $hello
we pass the where the from hello
nasty unset
instead of passing the variable name itself. That means exactly what you think it does:
[email protected]:~$ hello="Hello, world!"
[email protected]:~$ myhello="hello"
[email protected]:~$ unset $myhello
[email protected]:~$ echo $hello
Although switching back and forth between references hello
and $hello
is very disturbing if you are only familiar with “real” languages, Bash is at least relatively consistent about when it uses each style. With a little practice you will get used to this, even if you never feel real comfortable about the.
Variable Range
Before moving on to the basics of variables, let’s talk about the range of variables. Basically, a Bash variable is limited to only the current process. It will not pass your variables to child processes you start. The easiest way to see this in action is by calling bash
from bash
†
[email protected]:~$ hello="Hello, world!"
[email protected]:~$ echo $hello
Hello, world!
[email protected]:~$ bash
[email protected]:~$ echo $hello
[email protected]:~$ exit
exit
[email protected]:~$ echo $hello
Hello, world!
In our original Bash session we have the variable hello
Similar to “Hello, world!” and access it successfully with the echo
order. But if we start a new one bash
session, hello
will not be passed to that children’s session. So if we try again echo $hello
, we get nothing. But after leaving the children’s scale, we can be successful again echo $hello
and get the value we originally set.
If you need to transfer variables set in one session to a child process, you must use the export
order:
[email protected]:~$ unset hello
[email protected]:~$ hello="Hello, future children!"
[email protected]:~$ export hello
[email protected]:~$ bash
[email protected]:~$ echo $hello
Hello, future children!
As you can see, export
our variable successfully marked hello
to pass to child processes – in this case another instance of bash
† But the same technique works, the same way, for calling each underlying process that references environment variables. We can see this in action by writing a very small Perl script:
[email protected]:~$ cat perlexample
#!/usr/bin/perl
print "$ENV{hello}\n";
exit 0;
[email protected]:~$ hello="Hello from Perl!"
[email protected]:~$ ./perlexample
[email protected]:~$ export hello
[email protected]:~$ ./perlexample
Hello from Perl!
Like our previous example of a children’s session from bash
our Perl script can use our . don’t read hello
environment variable unless we first export
the.
The last thing we’ll talk about export
is that it can be used to set the value of the variable it exports, in one step:
[email protected]:~$ export hello="Hi again!"
[email protected]:~$ ./perlexample
Hi again!
Now that we understand how to set and read values of variables on the command line, let’s move on to a very useful operator: $()
†
Capture command output with $()
[email protected]:~$ echo test
test
[email protected]:~$ echo $(echo test)
test
The $()
command substitution operator captures the output of a command. Sounds simple enough, right? In the example above, we show that the output of the command echo test
is of course “test” – so if we… echo $(echo test)
we get the same result.
Backticks can produce the same result, e.g. echo `echo test`
-but $()
is generally preferred, because unlike backticks, the $()
operator can be nested.
We can go one step further and assign the output of a command to a variable instead:
[email protected]:~$ du -hs ~
13G /home/me
[email protected]:~$ myhomedirsize=$(du -hs ~)
[email protected]:~$ echo $myhomedirsize
13G /home/me
Finally, we can combine this with the lessons from our first tutorial and get rid of the first column du
‘s output to our variable:
[email protected]:~$ myhomedirsize=$(du -hs ~ | awk '{print $1}')
[email protected]:~$ echo $myhomedirsize
13G
At this point we understand how variables and the $()
operator work. So let’s talk about simple loop structures.
For loops in Bash
[email protected]:~$ y="1 2 3"
[email protected]:~$ for x in $y ; do echo "x equals $x" ; done
x equals 1
x equals 2
x equals 3
The above example briefly demonstrates how a simple for
loop works in Bash. It cycles through a specified set of items and maps the current item from the list to a loop variable. We used semicolons around the . to separate for
loop itself, the do
command that marks the inside of the loop, and the done
, which lets us know that the loop is over. Alternatively, we could also introduce them as technically separate rules.
[email protected]:~$ y="1 2 3"
[email protected]:~$ for x in $y
> do
> echo "x=$x"
> done
x=1
x=2
x=3
In this example, the leading chevrons aren’t something you type – they’re a prompt provided by Bash itself. This lets you know you’re still in a loop, despite hitting Enter
† And that’s important, because you can have as many commands as you want in the loop itself!
[email protected]:~$ y="1 2 3"
[email protected]:~$ for x in $y ; do echo "x equals $x" ; echo "x=$x" ; echo "next!" ; done
x equals 1
x=1
next!
x equals 2
x=2
next!
x equals 3
x=3
next!
As you can see, we can merge as many commands as we want in our for
loop. Within the loop, any command can reference the loop variable if desired, or it can ignore the loop variable entirely.
So far we have only looked at numbers in series, but the for
walk doesn’t care much for that. Each order of items, in any order, will work:
[email protected]:~$ y="cats dogs bears"
[email protected]:~$ for x in $y ; do echo "I like $x" ; done
I like cats
I like dogs
I like bears
Finally, you don’t need to provide the list with a variable; you can also embed it directly in the loop… but be careful about using quotes, because bash treats a quoted “list” as a single item:
[email protected]:~$ for x in "cats dogs bears" ; do echo "I like $x" ; done
I like cats dogs bears
[email protected]:~$ for x in cats dogs bears ; do echo "I like $x" ; done
I like cats
I like dogs
I like bears