|
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
Warning: include(/home/cust10011/www/site001/includes-mactech/includefiles/mt_footer.inc) [function.include]: failed to open stream: No such file or directory in /home/cust10011/www/site001_files/staticcontent/articles/mactech/Vol.21/21.11/bashBasicsII/index.html on line 290
Warning: include() [function.include]: Failed opening '/home/cust10011/www/site001/includes-mactech/includefiles/mt_footer.inc' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /home/cust10011/www/site001_files/staticcontent/articles/mactech/Vol.21/21.11/bashBasicsII/index.html on line 290