Command line wizardry, part two: Variables and loops in Bash – Ars Technica

Getting the hang of iteratively building commands interactively is all it really takes to become a command line wizard.
Enlarge / Getting the hang of iteratively building commands interactively is all it really takes to become a command line wizard.

In our first tutorial on command line wizardry, we covered simple redirection and the basics of sed, awk, and grep. Today, we’re going to introduce the concepts of simple variable substitution 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—particularly one with significant logical branching and evaluation—I strongly recommend a “real language” instead of Bash. Luckily, there are plenty of options. I’m personally a big fan of Perl, in part because it’s available on pretty much any *nix system you’ll ever encounter. Others might reasonably choose, say, Python or Go instead, and I wouldn’t judge.

The real point is that we’re focusing on the command line itself. Everything below is something you can easily learn to think in and use in real time with a little practice.

Setting and getting values in Bash

Bash handles variables a bit oddly compared to any of the “real” languages I’ve used. In particular, you must refer to the same variable with different syntax when setting its value versus when retrieving it.

Let’s take a look at a very simple example:

me@banshee:~$ hello="Hello, world!"

me@banshee:~$ echo $hello
Hello, world!

If you’ve never worked with variables in Bash before, you might think there’s a typo in the above example. We set the value of hello but read back its value as $hello. That’s not a mistake—you don’t use the leading $ when defining/setting a variable, but you must use it when reading from the same variable later.

Destroying variables

If we need to clear that variable, we can use the unset command—but, again, we must refer to hello instead of $hello:

me@banshee:~$ echo $hello
Hello, world!

me@banshee:~$ unset $hello
bash: unset: `Hello,': not a valid identifier
bash: unset: `world!': not a valid identifier

me@banshee:~$ unset hello

me@banshee:~$ echo $hello

Thinking about the error messages we get when trying incorrectly to unset $hello should make the problem clear. When we unset $hello, we’re passing the value of hello to unset rather than passing the variable name itself. That means exactly what you think it does:

me@banshee:~$ hello="Hello, world!"

me@banshee:~$ myhello="hello"

me@banshee:~$ unset $myhello

me@banshee:~$ echo $hello

Although the switch back and forth between referencing hello and $hello is deeply unsettling if you’re only familiar with “real” languages, Bash is at least relatively consistent about when it uses each style. With a little practice, you’ll get used to this—even if you never feel truly comfortable about it.

Variable scope

Before we move on from the basics of variables, we need to talk about variable scope. Basically, a Bash variable is limited in scope to only the current process. It won’t pass your variables onto any child processes you initiate. We can see this in action most easily by calling bash from bash:

me@banshee:~$ hello="Hello, world!"

me@banshee:~$ echo $hello
Hello, world!

me@banshee:~$ bash

me@banshee:~$ echo $hello

me@banshee:~$ exit
exit

me@banshee:~$ echo $hello
Hello, world!

In our original Bash session, we set the variable hello to equal “Hello, world!” and access it successfully with the echo command. But when we start a new bash session, hello is not passed onto that child session. So when we again try to echo $hello, we get nothing. But after exiting the child shell, we can once again successfully echo $hello and receive the value we originally set.

If you need to carry variables set in one session into a child process, you need to use the export command:

me@banshee:~$ unset hello

me@banshee:~$ hello="Hello, future children!"

me@banshee:~$ export hello

me@banshee:~$ bash

me@banshee:~$ echo $hello
Hello, future children!

As you can see, export successfully flagged our variable hello for passing down to child processes—in this case, another instance of bash. But the same technique works, in the same way, for calling any child process that references environment variables. We can see this in action by writing a very small Perl script:

me@banshee:~$ cat perlexample
    #!/usr/bin/perl
    print "$ENV{hello}n";
    exit 0;

me@banshee:~$ hello="Hello from Perl!"

me@banshee:~$ ./perlexample

me@banshee:~$ export hello

me@banshee:~$ ./perlexample
Hello from Perl!

Just like our earlier example of a child session of bash, our Perl script can’t read our hello environment variable unless we first export it.

The last thing we’ll say about export is that it can be used to set the value of the variable it’s exporting, in one step:

me@banshee:~$ export hello="Hi again!"

me@banshee:~$ ./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: backticks.

Capturing command output with backticks

me@banshee:~$ echo test
test

me@banshee:~$ echo `echo test`
test

The backticks operator captures the output of a command. Sounds simple enough, right? In the above example, we demonstrate that the output of the command echo test is, of course, “test”—so when we echo `echo test` we get the same result.

We can take this a step further and instead assign the output of a command into a variable:

me@banshee:~$ du -hs ~
13G	/home/me

me@banshee:~$ myhomedirsize=`du -hs ~`

me@banshee:~$ echo $myhomedirsize
13G /home/me

Finally, we can combine this with the lessons from our first tutorial and strip the first column from du‘s output to assign to our variable:

me@banshee:~$ myhomedirsize=`du -hs ~ | awk '{print $1}'`

me@banshee:~$ echo $myhomedirsize
13G

At this point, we understand how variables and the backticks operator work. So let’s talk about simple looping structures.

For loops in Bash

me@banshee:~$ y="1 2 3"

me@banshee:~$ for x in $y ; do echo "x equals $x" ; done
x equals 1
x equals 2
x equals 3

The above example concisely demonstrates the way a simple for loop works in Bash. It iterates through a supplied sequence of items, assigning the current item from the list to a loop variable. We used semicolons to separate the for loop itself, the do command that marks the interior of the loop, and the done which lets us know the loop is over. Alternatively, we could also enter them as technically separate lines.

me@banshee:~$ y="1 2 3"

me@banshee:~$ for x in $y
> do
> echo "x=$x"
> done
x=1
x=2
x=3

In this example, the leading angle brackets aren’t something you type—they’re a prompt supplied by Bash itself. This lets you know you’re still inside a loop despite having hit Enter. And that’s important, because you can have as many commands as you’d like inside the loop itself!

me@banshee:~$ y="1 2 3"

me@banshee:~$ 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 string as many commands as we’d like together inside our for loop. Within the loop, each command may refer to the loop variable if it likes—or it can ignore the loop variable entirely.

So far, we’ve only looked at numbers in sequences, but the for loop doesn’t care about that a bit. Any sequence of items, in any order, will work:

me@banshee:~$ y="cats dogs bears"

me@banshee:~$ for x in $y ; do echo "I like $x" ; done
I like cats
I like dogs
I like bears

Finally, you don’t have to supply the list with a variable; you can also embed it directly in the loop:

me@banshee:~$ for x in "cats dogs bears" ; do echo "I like $x" ; done
I like cats
I like dogs
I like bears

Leave a Comment

Home Terms Of Use Contact Us Affiliate Disclosure DMCA Earnings Disclaimer