The Shell Scripting Tutorial


Test

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. It 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
$ ls -l /usr/bin/test
-rwxr-xr-x 1 root root 35368 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 ]

Note: Some shells also accept "==" for string comparison; this is not portable, a single "=" should be used for strings, or "-eq" for integers.

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 separate 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 (you 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.


test.sh
#!/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 (\) serves a similar, but opposite purpose: it tells the shell that this is not the end of the line, but that the following line should be treated as part of the current line. This is useful for readability. It is customary to indent the following line after a backslash (\) or semicolon (;).

For example, the semicolon (;) is often used like this to join the if and then keywords:

if [ "$X" -nt "/etc/passwd" ]; then
      echo "X is a file which is newer than /etc/passwd"
fi

whilst the backslash (\) is used to split the single-line command across two lines in the shell script file, for readability purposes:

[ "$X" -nt "/etc/passwd" ] && \
      echo "X is a file which is newer than /etc/passwd"

As we see from these examples, test can perform many tests on numbers, strings, and filenames.

Note that -a, -e (both meaning "file exists"), -S (file is a Socket), -nt (file is newer than), -ot (file is older than), -ef (paths refer to the same file) and -O (file is owned by the user running the test) are not available in the traditional Bourne shell (eg, /bin/sh on Solaris, AIX, HPUX, etc).

There is a simpler way of writing if statements: The && and || commands give code to run if the result is true, or false, respectively.

#!/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 and -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 -en "Please guess the magic number: "
read X
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 caret (^) 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.
We all make mistakes - this page used to claim that grep -v [0-9] would work, but that is clearly far too simplistic.

We can use test in while loops as follows:


test2.sh
#!/bin/sh
X=0
while [ -n "$X" ]
do
  echo "Enter some text (RETURN to quit)"
  read X
  echo "You said: $X"
done

This code will keep asking for input until you hit RETURN (X is zero length). Another opportunity to point out an easy mistake to make - if you miss out the quotes around $X in the while [ -n "$X" ], there is nothing to test if $X is empty. while [ -n ] is invalid syntax, and will fail. while [ -n "" ] is valid syntax, so to allow for the variable being empty, you do need to include the quotes.
Note that running this script will end untidily:

$ ./test2.sh
Enter some text (RETURN to quit)
fred
You said: fred
Enter some text (RETURN to quit)
wilma
You said: wilma
Enter some text (RETURN to quit)

You said:
$ 

This can be tidied up with another test within the loop:

#!/bin/sh
X=0
while [ -n "$X" ]
do
  echo "Enter some text (RETURN to quit)"
  read X
  if [ -n "$X" ]; then
    echo "You said: $X"
  fi
done

Note also that I've used two different syntaxes for if statements on this page. These are:

if [ "$X" -lt "0" ]
then
  echo "X is less than zero"
fi

..........  and  ........

if [ -n "$X" ]; then
  echo "You said: $X"
fi

You must have a break between the if statement and the then construct. This can be a semicolon or a newline, it doesn't matter which, but there must be one or the other between the if and the then. It would be nice to just say:

if [ -n "$X" ]
  echo "You said: $X"

but the then and fi are absolutely required.


  Previous: Loops  Next: Case   

My Paperbacks and eBooks

My Shell Scripting books, available in Paperback and eBook formats. This tutorial is more of a general introduction to Shell Scripting, the longer Shell Scripting: Expert Recipes for Linux, Bash and more book covers every aspect of Bash in detail.

Shell Scripting Tutorial

Shell Scripting Tutorial
is this tutorial, in 88-page Paperback and eBook formats. Convenient to read on the go, and in paperback format good to keep by your desk as an ever-present companion.

Also available in PDF form from Gumroad:Get this tutorial as a PDF
Shell Scripting: Expert Recipes for Linux, Bash and more

Shell Scripting: Expert Recipes for Linux, Bash and more
is my 564-page book on Shell Scripting. The first half covers all of the features of the shell in every detail; the second half has real-world shell scripts, organised by topic, along with detailed discussion of each script.