This tutorial is written to help people understand some of the basics of shell script programming, and hopefully to introduce some of the possibilities of simple but powerful programming available under the bourne shell. As such, it has been written as a basis for one-on-one or group tutorials and exercises, and as a reference for subsequent use.
The most recent version of this tutorial is available from: http://steve-parker.org/sh/sh.shtml. Always check there for the latest copy.
Steve Bourne, wrote the Bourne shell which appeared in the Seventh
Edition Bell Labs Research version of Unix.
Many other shells have been written; this particular tutorial concentrates on
the Bourne and the Bourne Again shells.
Other shells include the Korn Shell (ksh), the C Shell (csh), and variations
such as tcsh.
This tutorial does not cover those shells. Maybe a future version will
cover ksh; I do not intend to write a tutorial for csh, as csh programming is considered
harmful.
This tutorial assumes some prior experience; namely:
Significant words will be written in italics when mentioned for the first time.
Code segments and script output will
be displayed as preformatted text.
Command-line entries will be preceded by the Dollar sign ($). If your prompt is
different, enter the command:
PS1="$ " ; export PS1
Then your interactions should match the examples given (such as $
./my-script.sh below).
Script output (such as "Hello World" below) is displayed at the start of the
line.
$ echo '#!/bin/sh' > my-script.sh
$ echo 'echo Hello World' >> my-script.sh
$ chmod 755 my-script.sh
$ ./my-script.sh
Hello World
$
Entire scripts will be surrounded by thick horizontal rules and include a reference where available to the plain text of the script:
#!/bin/sh
# This is a comment!
echo Hello World # This is a comment, too!
Note that to make a file executable, you must set the eXecutable bit, and for a shell script, the Readable bit must also be set:
$ chmod a+rx first.sh
Shell script programming has a bit of a bad press amongst some Unix systems administrators. This is normally because of one of two things:
It is partly due to this that there is
a certain machismo associated with creating good shell scripts.
Scripts which can be used as CGI programs, for example, without losing out too
much in speed to Perl (though both would lose to C, in many cases, were speed
the only criteria).
There are a number of factors which can go into good, clean, quick, shell
scripts.
A clear layout makes the difference between a shell script appearing
as "black magic" and one which is easily maintained and understood.
You may be forgiven for thinking that with a simple script, this is not too
significant a problem, but two things here are worth bearing in
mind.
Something about shell scripts seems to make them particularly likely to be badly indented, and since the main control structures are if/then/else and loops, indentation is critical for understanding what a script does.
One of the major weaknesses in many shell scripts is lines such as:
cat /tmp/myfile | grep "mystring"
which would run much faster as:
grep "mystring" /tmp/myfile
Not much, you may consider; the OS has to load up the
/bin/grep executable, which is a reasonably small 75600 bytes
on my system, open a pipe in
memory for the transfer, load and run the /bin/cat executable, which is an even smaller 9528 bytes on my system, attach it to
the input of the pipe, and let it run.
Of course, this kind of thing is what the OS is there for, and it's normally
pretty efficient at doing it. But if this command were in a loop being run many
times over, the saving of not locating and loading the
cat executable, setting up and releasing the pipe, can
make some difference, especially in, say, a CGI environment where there are
enough other factors to slow things down without the script itself being too
much of a hurdle. Some Unices are more efficient than others at what they call
"building up and tearing down processes" - ie, loading them up, executing them,
and clearing them away again. But however good your flavour of Unix is at doing
this, it'd rather not have to do it at all.
As a result of this, you may hear
mention of the Useless Use of Cat Award (UUoC), also known in some circles as
The Award For The Most Gratuitous Use Of The Word Cat In A Serious
Shell Script being bandied about on the comp.unix.shell newsgroup from time to time. This is purely a way of peers keeping
each other in check, and making sure that things are done right.
Speaking of which, I would like to
recommend the comp.os.unix.shell newsgroup to you, although its signal to noise ratio seems to have
decreased in recent years. There are still some real gurus who hang out there
with good advice for those of us who need to know more (and that's all of us!).
Sharing experiences is the key to all of this - the reason behind this tutorial
itself, and we can all learn from and contribute to open discussions about such
issues.
Which leads me nicely on to something else: Don't ever feel too close
to your own shell scripts; by their nature, the source cannot be closed. If you
supply a customer with a shell script, s/he can inspect it quite easily. So you
might as well accept that it will be inspected by anyone you pass it to; use
this to your advantage with the GPL - encourage people to
give you feedback and bugfixes for free!
For our first shell script, we'll just write a script which says
"Hello World". We will then try to get more out of a Hello World program than
any other tutorial you've ever read :-)
Create a file (first.sh) as follows:
#!/bin/sh
# This is a comment!
echo Hello World # This is a comment, too!
The first line tells Unix that the file is to be executed by /bin/sh. This is the standard location of the Bourne shell on just about every Unix system. If you're using GNU/Linux, /bin/sh is normally a symbolic link to bash.
The second line begins with a special
symbol: #. This marks the line as a comment, and it is
ignored completely by the shell.
The only exception is when the very first line of the file starts
with #! - as ours does. This is a special directive which
Unix treats specially. It means that even if you are using csh, ksh, or
anything else as your interactive shell, that what follows should be
interpreted by the Bourne shell.
Similarly, a Perl script may start with the line #!/usr/bin/perl to tell your interactive shell that the program which follows should
be executed by perl. For Bourne shell programming, we shall stick to
#!/bin/sh.
The third line runs a command:
echo, with two parameters, or arguments - the first is
"Hello"; the second is "World".
Note that echo will automatically put a single space between its
parameters.
The # symbol still marks a comment; the # and anything
following it is ignored by the shell.
now run chmod 755 first.sh to make the text file executable, and run ./first.sh.
Your screen should then look like this:
$ chmod 755 first.sh
$ ./first.sh
Hello World
$
You will probably have expected that! You could even just run:
$ echo Hello World
Hello World
$
Now let's make a few changes.
First, note that echo
puts ONE space between its parameters.
Put a few spaces between "Hello" and "World". What do you expect the output to
be? What about putting a TAB character between them?
As always with shell programming, try it and see.
The output is exactly the same! We are calling the echo program with two arguments; it doesn't care any more than
cp does about the gaps in between them.
Now modify the code again:
#!/bin/sh
# This is a comment!
echo "Hello World" # This is a comment, too!
This time it works. You probably expected that, too, if you have
experience of other programming languages. But the key to understanding what is
going on with more complex command and shell script, is to understand and be
able to explain: WHY? echo has
now been called with just ONE argument - the string "Hello World". It
prints this out exactly.
The point to understand here is that the shell parses the arguments BEFORE
passing them on to the program being called. In this case, it strips the quotes
but passes the string as one argument.
As a final example, type in the following script. Try to predict the outcome
before you run it:
#!/bin/sh
# This is a comment!
echo "Hello World" # This is a comment, too!
echo "Hello World"
echo "Hello * World"
echo Hello * World
echo Hello World
echo "Hello" World
echo Hello " " World
echo "Hello "*" World"
echo `hello` world
echo 'hello' world
Is everything as you expected? If not, don't worry! These are just
some of the things we will be covering in this tutorial ... and yes, we will be
using more powerful commands than echo!
Just about every programming language in existence has the concept of
variables - a symbolic name for a chunk of memory to which we can
assign values, read and manipulate its contents. The bourne shell is no
exception, and this section introduces idea. This is taken further in
Variables - Part II which looks into variables which are set for us by
the environment.
Let's look back at our first Hello World example. This could be done using
variables (though it's such a simple example that it doesn't really warrant
it!)
Note that there must be no spaces around the "=" sign:
VAR=value works; VAR = value doesn't work. In the first case, the shell sees the "=" symbol and treats
the command as a variable assignment. In the second case, the shell assumes
that VAR must be the name of a command and tries to execute it.
If you think about it, this makes sense - how else could you tell it to run the
command VAR with its first argument being "=" and its second argument being
"value"?
Enter the following code into var1.sh:
#!/bin/sh
MY_MESSAGE="Hello World"
echo $MY_MESSAGE
This assigns the string "Hello World" to the variable
MY_MESSAGE then echoes
out the value of the variable.
Note that we need the quotes around the string Hello World. Whereas we could
get away with echo Hello
World because echo will
take any number of parameters, a variable can only hold one value, so a string
with spaces must be quoted to that the shell knows to treat it all as one.
Otherwise, the shell will try to execute the command World after assigning MY_MESSAGE=Hello
The shell does not care about types of
variables; they may store strings, integers, real numbers - anything you
like.
People used to Perl may be quite happy with this; if you've grown up with C,
Pascal, or worse yet Ada, this may seem quite strange.
In truth, these are all stored as strings, but routines which expect a number
can treat them as such.
If you assign a string to a variable then try to add 1 to it, you will not get
away with it:
$ x="hello"
$ y=`expr $x + 1`
expr: non-numeric argument
$
Since the external program expr only
expects numbers. But there is no syntactic difference between:
MY_MESSAGE="Hello World"
MY_SHORT_MESSAGE=hi
MY_NUMBER=1
MY_PI=3.142
MY_OTHER_PI="3.142"
MY_MIXED=123abc
Note though that special characters must be properly escaped to avoid
interpretation by the shell.
This is discussed further in Escape Characters.
We can interactively set variable
names using the read
command; the following script asks you
for your name then greets you personally:
#!/bin/sh
echo What is your name?
read MY_NAME
echo "Hello $MY_NAME - hope you're well."
Mario Bacinsky kindly pointed out to me that I had originally missed out the double-quotes in line 3, which meant that the single-quote in the word "you're" was unmatched, causing an error. It is this kind of thing which can drive a shell programmer crazy, so watch out for them!
This is using the shell-builtin command read which reads a line from standard input into the variable supplied.
Note that even if you give it your full name and don't use double quotes around
the echo command, it still outputs correctly. How is this
done? With the MY_MESSAGE variable earlier we had to put double quotes around it to set it.
What happens, is that the read command automatically places quotes around its input, so that spaces are
treated correctly. (You will need to quote the output, of course - e.g.
echo "$MY_MESSAGE").
Variables in the bourne shell do not have to be declared, as they do
in languages like C. But if you try to read an undeclared variable, the result
is the empty string. You get no warnings or errors. This can cause some subtle
bugs - if you assign MY_OBFUSCATED_VARIABLE=Hello and then echo $MY_OSFUCATED_VARIABLE, you will get nothing (as the second OBFUSCATED is
mis-spelled).
There is a command called
export which has a fundamental effect on the scope of
variables. In order to really know what's going on with your variables, you
will need to understand something about how this is used.
Create a small shell script,
myvar2.sh:
#!/bin/sh
echo "MYVAR is: $MYVAR"
MYVAR="hi there"
echo "MYVAR is: $MYVAR"
Now run the script:
$ ./myvar2.sh
MYVAR is:
MYVAR is: hi there
MYVAR hasn't been set to any value, so it's blank. Then we give it a
value, and it has the expected result.
Now run:
$ MYVAR=hello
$ ./myvar2.sh
MYVAR is:
MYVAR is: hi there
It's still not been set! What's going on?!
When you call myvar2.sh from your interactive shell, a new shell is spawned to run the script. This
is partly because of the #!/bin/sh line at the start of the script, which we discussed earlier.
We need to export
the variable for it to be inherited by
another program - including a shell script. Type:
$ export MYVAR
$ ./myvar2.sh
MYVAR is: hello
MYVAR is: hi there
Now look at line 3 of the script: this is changing the value of
MYVAR. But there is no way that this will be passed back
to your interactive shell. Try reading the value of MYVAR:
$ echo $MYVAR
hello
$
Once the shell script exits, its environment is destroyed. But
MYVAR keeps its value of hello within your interactive shell.
In order to receive environment changes back from the script, we must
source the script - this effectively runs the script within our own
interactive shell, instead of spawning another shell to run it.
We can source a script via the "." command:
$ MYVAR=hello
$ echo $MYVAR
hello
$ . ./myvar2.sh
MYVAR is: hello
MYVAR is: hi there
$ echo $MYVAR
hi there
The change has now made it out into our shell again! This is how
your .profile
or .bash_profile file works, for example.
Note that in this case, we don't need to export MYVAR.
Thanks to sway for pointing out that I'd originally said
echo MYVAR above, not echo $MYVAR as it should be.
One other thing worth mentioning at this point about variables, is to consider the following shell script:
#!/bin/sh
echo "What is your name?"
read USER_NAME
echo "Hello $USER_NAME"
echo "I will create you a file called $USER_NAME_file"
touch $USER_NAME_file
Think about what result you would expect. For example, if you enter
"steve" as your USER_NAME, should the script create steve_file?
Actually, no. This will cause an error unless there is a variable called
USER_NAME_file. The shell does not know where the variable ends
and the rest starts. How can we define this?
The answer is, that we enclose the variable itself in curly
brackets:
#!/bin/sh
echo "What is your name?"
read USER_NAME
echo "Hello $USER_NAME"
echo "I will create you a file called ${USER_NAME}_file"
touch "${USER_NAME}_file"
The shell now knows that we are referring to the variable
USER_NAME and that we want it suffixed with
_file. This can be the downfall of many a new shell
script programmer, as the source of the problem can be difficult to track
down.
Also note the quotes around
"${USER_NAME}_file"
- if the user entered "Steve Parker"
(note the space) then without the quotes, the arguments passed to
touch would be Steve and Parker_file
- that is, we'd effectively be
saying touch Steve
Parker_file, which is two
files to be touched,
not one. The quotes avoid this. Thanks to Chris for highlighting
this.
Wildcards are really nothing new if you have used Unix at all
before.
It is not necessarily obvious how they are useful in shell scripts though. This
section is really just to get the old grey cells thinking how things look when
you're in a shell script - predicting what the effect of using different
syntaxes are. This will be used later on, particularly in the Loops section.
Think first how you would copy all the files from /tmp/a into /tmp/b.
All the .txt files? All the .html files?
Hopefully you will have come up with:
$ cp /tmp/a/* /tmp/b/
$ cp /tmp/a/*.txt /tmp/b/
$ cp /tmp/a/*.html /tmp/b/
Now how would you list the files in /tmp/a/ without using ls
/tmp/a/?
How about echo
/tmp/a/*? What are the two
key differences between this and the ls output? How can this be useful? Or a hinderance?
How could you rename all .txt files to .bak? Note that
$ mv *.txt *.bak
will not have the desired effect; think about how this gets expanded
by the shell before it is passed to mv. Try
this using echo
instead of mv if
this helps.
We will look into this further later on, as it uses a few concepts not yet
covered.
Certain characters are significant to the shell; we have seen, for example, that the use of double quotes (") characters affect how spaces and TAB characters are treated, for example:
$ echo Hello World
Hello World
$ echo "Hello World"
Hello World
So how do we display: Hello "World" ?
$ echo "Hello "World""
The first and last " characters wrap the whole lot into one parameter
passed to echo
so that the spacing between the two words
is kept as is. But the code:
$ echo "Hello "World""
would be interpreted as three parameters:
So the output would be
Hello World
Note that we lose the quotes entirely. This is because the first and second quotes mark off the Hello and following spaces; the second argument is an unquoted "World" and the third argument is the empty string; "".
Most characters
(*, ', etc)
are not interpreted (ie, they are taken literally) by means of placing them in
double quotes ("). They are taken as is and passed on the the command being
called. An example using the asterisk (*) goes:
$ echo *
case.shtml escape.shtml first.shtml
functions.shtml hints.shtml index.shtml
ip-primer.txt raid1+0.txt
$ echo *txt
ip-primer.txt raid1+0.txt
$ echo "*"
*
$ echo "*txt"
*txt
In the first example, * is expanded to mean all files in the current
directory.
In the second example, *txt means all files ending in txt.
In the third, we put the * in double quotes, and it is interpreted
literally.
In the fourth example, the same applies, but we have appended
txt to the string.
However, ", $, `, and are still interpreted by the shell, even when they're in double quotes.
The backslash () character is used to mark these special characters so that
they are not interpreted by the shell, but passed on to the command being run
(for example, echo).
So to output the string: (Assuming that the value of $X is
5):
A quote is ", backslash is , backtick is `.
A few spaces are and dollar is $. $X is 5.
we would have to write:
$ echo "A quote is ", backslash is , backtick is `."
A quote is ", backslash is , backtick is `.
$ echo "A few spaces are ; dollar is $. $X is ${X}."
A few spaces are ; dollar is $. $X is 5.
We have seen why the " is special for
preserving spacing. Dollar is special because it marks a variable, so
$X is replaced by the shell with the contents of the
variable X. Backslash is special because it is itself used to
mark other characters off; we need the following options for a complete
shell:
$ echo "This is a backslash"
This is a backslash
$ echo "This is " a quote and this is a backslash"
This is " a quote and this is a backslash
So backslash itself must be escaped to show that it is to be taken literally. The other special character, the backtick, is discussed later in External Programs.
Most languages have the concept of loops: If we want to repeat a task
twenty times, we don't want to have to type in the code twenty times, with
maybe a slight change each time.
As a result, we have for and while
loops in the Bourne shell. This is
somewhat fewer features than other languages, but nobody claimed that shell
programming has the power of C.
for loops
iterate through a set of values until the list is exhausted:
#!/bin/sh
for i in 1 2 3 4 5
do
echo "Looping ... number $i"
done
Try this code and see what it does. Note that the values can be anything at all:
#!/bin/sh
for i in hello 1 * 2 goodbye
do
echo "Looping ... i is set to $i"
done
is well worth trying. Make sure that you understand what is happening
here. Try it without the * and
grasp the idea, then re-read the Wildcards section and try
it again with the *
in place. Try it also in different
directories, and with the * surrounded by double quotes, and try it preceded by a backslash
(*)
In case you don't have access to a shell at the moment (it is very useful to have a shell to hand whilst reading this tutorial), the results of the above two scripts are:
Looping .... number 1
Looping .... number 2
Looping .... number 3
Looping .... number 4
Looping .... number 5
and, for the second example:
Looping ... i is set to hello
Looping ... i is set to 1
Looping ... i is set to (name of first file in current directory)
... etc ...
Looping ... i is set to (name of last file in current directory)
Looping ... i is set to 2
Looping ... i is set to goodbye
So, as you can see,
for simply loops through whatever input it is given,
until it runs out of input.
while
loops can be much more fun! (depending on
your idea of fun, and how often you get out of the house... )
#!/bin/sh
INPUT_STRING=hello
while [ "$INPUT_STRING" != "bye" ]
do
echo "Please type something in (bye to quit)"
read INPUT_STRING
echo "You typed: $INPUT_STRING"
done
What happens here, is that the echo and read statements will run
indefinitely until you type "bye" when prompted.
Review Variables - Part I to see why we set INPUT_STRING=hello before testing it. This makes it a repeat loop, not a traditional
while loop.
The colon (:) always
evaluates to true; whilst using this can be necessary sometimes, it is often
preferrable to use a real exit condition. Compare quitting the above loop with
the one below; see which is the more elegant. Also think of some situations in
which each one would be more useful than the other:
#!/bin/sh
while :
do
echo "Please type something in (^C to quit)"
read INPUT_STRING
echo "You typed: $INPUT_STRING"
done
Another useful trick is the while read f loop. This example uses the case statement, which
we'll cover later. It reads from the file myfile,
and for each line, tells you what language it thinks is being used. Each line
must end with a LF (newline) - if cat myfile doesn't end with a blank line, that final line will not be
processed.
#!/bin/sh
while read f
do
case $f in
hello) echo English ;;
howdy) echo American ;;
gday) echo Australian ;;
bonjour) echo French ;;
"guten tag") echo German ;;
*) echo Unknown Language: $f
;;
esac
done < myfile
On many Unix systems, this can be also be done as
#!/bin/sh
while f=`line`
do
.. process f ..
done < myfile
But since the while read
f works with any *nix, and
doesn't depend on the external program line, the
former is preferable. See External Programs to see
why this method uses the backtick (`).
Had I referred to $i
(not $f) in
the default ("Unknown Language") case above - you will get no warnings or
errors in this case, even though $i has
not been declared or defined. For example:
$ i=THIS_IS_A_BUG
$ export i
$ ./while3.sh something
Unknown Language: THIS_IS_A_BUG
$
So make sure that you avoid typos. This is also another good reason
for using ${x}
and not just $x -
if x="A" and you want to say "A1", you need
echo ${x}1, as echo
$x1 will try to use the
variable x1, which may not exist, or may be set to
B2.
I recently found an old thread on Usenet which I had been involved in, where I actually learned more ... Google has it here..
A handy Bash (but not Bourne Shell) tip I learned recently from the Linux From Scratch project is:
mkdir rc{0,1,2,3,4,5,6,S}.d
instead of the more cumbersome:
for runlevel in 0 1 2 3 4 5 6 S
do
mkdir rc${runlevel}.d
done
And this can be done recursively, too:
$ cd /
$ ls -ld {,usr,usr/local}/{bin,sbin,lib}
drwxr-xr-x 2 root root 4096 Oct 26 01:00 /bin
drwxr-xr-x 6 root root 4096 Jan 16 17:09 /lib
drwxr-xr-x 2 root root 4096 Oct 27 00:02 /sbin
drwxr-xr-x 2 root root 40960 Jan 16 19:35 usr/bin
drwxr-xr-x 83 root root 49152 Jan 16 17:23 usr/lib
drwxr-xr-x 2 root root 4096 Jan 16 22:22 usr/local/bin
drwxr-xr-x 3 root root 4096 Jan 16 19:17 usr/local/lib
drwxr-xr-x 2 root root 4096 Dec 28 00:44 usr/local/sbin
drwxr-xr-x 2 root root 8192 Dec 27 02:10 usr/sbin
Test is used by virtually every shell script written. It may not seem
that way, because test
is not often called directly.
test is more frequently called as
[. [
is a symbolic link to test, just to make
shell programs more readable. If is also normally a shell builtin (which means
that the shell itself will interpret [ as meaning test, even if your Unix
environment is set up differently):
$ type [
[ is a shell builtin
$ which [
/usr/bin/[
$ ls -l /usr/bin/[
lrwxrwxrwx 1 root root 4 Mar 27 2000 /usr/bin/[ -> test
This means that '[' is actually a program, just like
ls and other programs, so it must be surrounded by
spaces:
if [$foo == "bar" ]
will not work; it is interpreted as if test$foo == "bar" ], which is a ']' without a beginning '['. Put spaces
around all your operators I've highlighted the mandatory spaces with the word
'SPACE' - replace 'SPACE' with an actual space; if there isn't a space there,
it won't work:
if SPACE [ SPACE "$foo" SPACE == SPACE "bar" SPACE ]
Test is a simple but powerful comparison
utility. For full details, run man
test on your system, but
here are some usages and typical examples.
Test is most often invoked indirectly
via the if and while statements. It is also the reason you will come into difficulties if you
create a program called test and
try to run it, as this shell builtin will be called instead of your
program!
The syntax for if...then...else... is:
if [ ... ]
then
# if-code
else
# else-code
fi
Note that fi
is if backwards! This is used again later with case and
esac.
Also, be aware of the syntax - the "if [ ... ]" and the "then" commands must be
on different lines. Alternatively, the semicolon ";" can seperate
them:
if [ ... ]; then
# do something
fi
You can also use the elif,
like this:
if [ something ]; then
echo "Something"
elif [ something_else ]; then
echo "Something else"
else
echo "None of the above"
fi
This will echo "Something" if the [ something ] test succeeds, otherwise it will test [ something_else ], and echo "Something else" if that succeeds. If all else fails, it
will echo "None of the above".
Try the following code snippet, before running it set the variable X to various values (try -1, 0, 1, hello, bye, etc). You can do this as follows (thanks to Dave for pointing out the need to export the variable, as noted in Variables - Part I.):
$ X=5
$ export X
$ ./test.sh
... output of test.sh ...
$ X=hello
$ ./test.sh
... output of test.sh ...
$ X=test.sh
$ ./test.sh
... output of test.sh ...
Then try it again, with $X as the
name of an existing file, such as /etc/hosts.
#!/bin/sh
if [ "$X" -lt "0" ]
then
echo "X is less than zero"
fi
if [ "$X" -gt "0" ]; then
echo "X is more than zero"
fi
[ "$X" -le "0" ] &&
echo "X is less than or equal to zero"
[ "$X" -ge "0" ] &&
echo "X is more than or equal to zero"
[ "$X" = "0" ] &&
echo "X is the string or number "0""
[ "$X" = "hello" ] &&
echo "X matches the string "hello""
[ "$X" != "hello" ] &&
echo "X is not the string "hello""
[ -n "$X" ] &&
echo "X is of nonzero length"
[ -f "$X" ] &&
echo "X is the path of a real file" ||
echo "No such file: $X"
[ -x "$X" ] &&
echo "X is the path of an executable file"
[ "$X" -nt "/etc/passwd" ] &&
echo "X is a file which is newer than /etc/passwd"
Note that we can use the semicolon (;) to join two lines together.
This is often done to save a bit of space in simple if statements. The backslash simply tells the shell that this is not the end
of the line, but the two (or more) lines should be treated as one. This is
useful for readability. It is customary to indent the following
line.
As we see from these examples,
test can perform many tests on numbers, strings, and
filenames.
There is a simpler way of
writing if statements: The && and || commands give code to run if the result is
true.
#!/bin/sh
[ $X -ne 0 ] && echo "X isn't zero" || echo "X is zero"
[ -f $X ] && echo "X is a file" || echo "X is not a file"
[ -n $X ] && echo "X is of non-zero length" ||
echo "X is of zero length"
This syntax is possible because there is a file (or shell-builtin)
called [ which is linked to test. Be
careful using this construct, though, as overuse can lead to very hard-to-read
code. The if...then...else... structure is much more readable. Use of the
[...] construct is recommended for while loops and trivial
sanity checks with which you do not want to overly distract the
reader.
Note that when you set X to a non-numeric value, the first few comparisons result in the message:
test.sh: [: integer expression expected before -lt
test.sh: [: integer expression expected before -gt
test.sh: [: integer expression expected before -le
test.sh: [: integer expression expected before -ge
This is because the -lt, -gt, -le, -ge, comparisons are only designed
for integers, and do not work on strings. The string comparisons, such
as != will happily treat "5" as a string, but there is no
sensible way of treating "Hello" as an integer, so the integer comparisons
complain.
If you want your shell script to behave more gracefully, you will have to check
the contents of the variable before you test it - maybe something like
this:
echo $X | grep [^0-9] > /dev/null 2>&1
if [ "$?" -eq "0" ]; then
# If the grep found something other than 0-9
# then it's not an integer.
echo "Sorry, wanted a number"
else
# The grep found only 0-9, so it's an integer.
# We can safely do a test on it.
if [ "$X" -eq "7" ]; then
echo "You entered the magic number!"
fi
fi
In this way you can echo a
more meaningful message to the user, and exit gracefully. The
$? variable is explained in Variables - Part II,
and grep is a complicated beast, so here goes:
grep [0-9] finds lines of text which contain digits (0-9) and
possibly other characters, so the carat (^)
in grep [^0-9]
finds only those lines which don't
consist only of numbers. We can then take the opposite (by acting on failure,
not success). Okay? The >/dev/null 2>&1 directs any output or errors to the special "null"
device, instead of going to the user's screen.
Many thanks to Paul Schermerhorn for correcting me - this page used to claim
that grep -v [0-9]
would work, but this is clearly far too simplistic.
Wed, 20 Aug 2008
2008-05-15
2008-05-07
2008-04-23
2008-04-13
2008-04-06
2008-03-19
2008-02-23