Back to bash Basics: Part 2 Time to Advance Ourselves
Volume Number: 21 (2005)
Issue Number: 11
Column Tag: Programming
Mac In The Shell
Back to bash Basics: Part 2 Time to Advance Ourselves
by Edward Marczak
With any exercise, you need to continually push yourself. Without that extra effort, what once
was a challenge becomes easy, something to drift through. At the same time, you may be missing
advanced techniques that make other areas easier or more efficient. Similarly, shell scripting can
go many layers deep, and you can exercise your knowledge in many ways. Last month in, "Back to bash
Basics Part 1," we focused on flow control. You may have noticed some of the things I didn't cover
explicitly. There's always more to learn! Let's tie up those loose ends.
More Looping
Since we discussed looping constructs so much last month, that's where we'll pick up. In the
select example, you'll see a break statement - that could use some explanation. break simply
terminates the current loop. If it were removed from the select example, you would be asked
repeatedly which file you want to inspect. Let's see what that would look like:
#!/bin/bash
select theItem; do
if [ $theItem ]; then
file $theItem
fi
done
When this is run, the output looks like this:
Jack-Kerouak:~/bin marczak$ ./st.sh *
1) BidToJob.dmg
2) bomcheck.sh
3) cl.txt
4) createdmg
5) diskrep.sh
6) exscript
#? 3
cl.txt: ASCII text
#? 4
createdmg: ASCII text
#? 5
diskrep.sh: Bourne-Again shell script text executable
#? ^C
Notice that this time, we need to press ctrl-c to stop the program. break applies to any loop:
#!/bin/bash
for i in $*;
do
if [ ! -O $i ]; then
echo You do not own $i! I am outta here!
break
fi
echo $i is your file!
done
Running as me, I'm shown:
$ ./bt.sh *
BidToJob.dmg is your file!
bomcheck.sh is your file!
bt.sh is your file!
cl.txt is your file!
[...clipped for brevity]
Running in that same directory as root gives us:
# ./bt.sh *
You do not own BidToJob.dmg! I am outta here!
AOT (or, how the shell will separate files)
Have you ever crossed your AOT (Acronym Overload Threshold)? "There's a problem with the RIP!"
Raster Image Processor, or Routing Information Protocol? While there's only so many TLAs (Three
Letter Acronyms) that you can deal with, I need you add one more: IFS (no, not Iterative Fractal
Systems!). The shell uses the Internal Field Separator to determine how to break apart tokens, and
how to separate incoming parameters. By default, IFS is equal to space, tab and newline.
When we discussed the for loop last month, several things were quickly touched upon that can be
expanded. In addition to the $@ variable, which expands to individual double-quoted strings, there
is the $* variable, which is a single string containing each positional parameter. How do you know
where each parameter breaks? $* separates each parameter by using the first character of your IFS
variable. We'll get back to how this can be very useful.
Also, last month showed an example that looked something like this:
FILES=`ls *.sh`
for i in $FILES
do
...
done
This example 'just works' because ls *.sh will separate its output with linefeeds. Hey, that's
one of the characters in IFS by default! What good fortune! This same example will fall apart if
you reassign the IFS variable prior to the loop:
IFS="-"
FILES=`ls *.sh`
for i in $FILES; do
...
done
$i will still hold $FILES, but it won't be tokenized - not the way you'd expect (the line feeds
will still be in there, but $i won't break on them).
So, then, why would we ever touch IFS? Well, what if you wanted to search through something that
is not broken up by a space, newline or tab? Like $PATH, for instance:
#!/bin/bash
IFS=:
for theDir in $PATH
do
theLatest=`ls -lotr $theDir | tail -1`
echo Newest file in $theDir:
echo $theLatest
echo
done
This simply goes through our $PATH and tells us the newest file in each directory. Could be
useful.
Oh, and another thing
Like Apple, shell scripting always seems to have "one more thing." For this month, this thing
comes in the form of being able to effectively handle parameters. Time to introduce shift and
getopts.
When writing a script, parameters can be accessed a few ways. If you always rely on direct
access ($1, $2...etc), you run into some limits. One way to simply loop through all parameters is to
use shift. shift makes $1 = $2, $2 = $3...etc. You lose the first value that was assigned to $1. To
look for a few specific parameters, you can loop through the values:
#!/bin/bash
while [ `echo $1 | grep "-"` ]; do
case $1 in
-a ) echo "You supplied the -a flag";;
-b ) echo "You supplied the -b flag";;
-c ) echo "You supplied the -c flag";;
* ) echo "Usage: $0 -a -b -c";
exit 1;;
esac
shift
done
Run this code and you'll see:
$ ./shifttest.sh -b -a
You supplied the -b flag
You supplied the -a flag
Now, shift is cool, and still comes in handy, but to truly handle command-line options smoothly,
we have to employ getopts. Sure, you can roll your own each time, however, people have come to
expect their options to behave in certain ways. One should be able to combine options, as with tar,
for example: tar -xzvf blah.tar.gz. Basically, you don't need to roll your own because getopts
exists.
getopts allows you to handle options in a standard way. Seeing it in action is the quickest way
to get up to speed:
#!/bin/bash
while getopts ":xyz:t" theOption; do
case $theOption in
x ) echo "Option x chosen";;
y ) echo "Option $theOption chosen";;
z ) echo "Option z chosen with argument: $OPTARG";;
t ) echo "Option t chosen";;
\? ) echo "Unknown option chosen"
exit 1;;
* ) echo "You need to supply an option!"
exit 2;;
esac
done
getopts is designed to be dumped in a loop that will feed it arguments passed into the script. It
accepts a string that defines the allowed options, followed by a variable that will hold the current
option, sans the "-" or "+" (nicely, either are allowed). Using getopts will define two variables:
$OPTIND, the current index and $OPTARG, the current argument passed with an option. Running this
produces this output:
$ ./gotest.sh -yxz test
Option y chosen
Option x chosen
Option z chosen with argument: test
The string that getopts accepts can only contain letters and the colon character. Each letter is
an option you wish to support. If a letter is followed by a colon, that tells getopts that an
argument is required. By having a lead colon character in the parameter list string, you suppress
the error message that getopts will print if an option is not recognized. In either case, an
unrecognized option will set the variable to "?", so you can deal with it.
Put it all Together
I've gotten a request or two asking how to deal with math in the shell. While there are
specialized CLI apps that will deal with arithmetic, the shell can do some basic functions, and
sometimes, that's all you need. The trick is the underused declare statement. declare tells the
shell how you want to treat variables, which are strings by default. So, this doesn't do what one
would expect:
$ number1=7
$ number2=8
$ total=number1*number2
When you echo $total, you get "number1*number2": strings. We need to tell the shell that $total
should be treated as an integer:
$ declare -i total
$ total=number1*number2
$ echo $total
56
Much better! declare can define several different types of variables:
-a variable is an array
-i treat as integer
-r makes variable read-only
-x automatic export (like the 'export' built-in)
There are some others, but this is all we need concentrate on for now. All of the usual suspects
are available as mathematic operators:
+ Addition
- Subtraction
* Multiply
/ Divide
% Remainder
<< Bit-shift left
>> Bit-shift right
& Bitwise and
| Bitwise or
~ Bitwise not
! Bitwise not
^ Xor
In addition to declare-ing a variable to be an integer, you can use let to make the assignment:
let theTotal='5 * 7'
Ah, let....brings me back to my C64 BASIC days...
Now, you should be able to write fairly sophisticated shell script that includes slick input
processing, good error handling and even some basic computations!
Make Yourself Useful...
...to everyone. Just remember that bash scripting will help you not only with OS X, but with
Linux, IRIX, FreeBSD, and even Windows - if you install a Unix shell there (which can be had for
free from Cygwin or Microsoft).
This month highlights the fact that shell scripting is relatively easy, can be fun and powerful.
Even better, you'll find bash built-in to every OS X machine you touch! Let this all sink in: while
I'll get back to bash scripting in future columns, more Unix detours next month!
Ed Marczak keeps it simple. Tech simplicity at http://www.radiotope.com