Shell scripts
This section covers material for topic 1.109.2 for the Junior Level Administration (LPIC-1) exam 102. The topic has a weight of 3.
In this section, learn how to:
- Use standard shell syntax, such as loops and tests
- Use command substitution
- Test return values for success or failure or other information provided by a command
- Perform conditional mailing to the superuser
- Select the correct script interpreter through the shebang (#!) line
- Manage the location, ownership, execution, and suid-rights of scripts
This section builds on what you learned about simple functions in the last
section and demonstrates some of the techniques and tools that add programming
capability to the shell. You have already see simple logic using the
&& and || operators, which allow you to execute one command
based on whether the previous command exits normally or with an error. In the
ldirs function, you used this to alter the call to
ls according to whether or not parameters were passed
to your ldirs function. Now you will learn how to
extend these basic techniques to more complex shell programming.
The first thing you need to do in any kind of programming language after
learning how to assign values to variables and pass parameters is to test those
values and parameters. In shells the tests you do set the return status, which
is the same thing that other commands do. In fact,
test is a builtin command!
The test builtin command returns 0 (True) or 1
(False) depending on the evaluation of an expression expr. You can also
use square brackets so that test
expr
and [ expr ] are equivalent. You can examine the return value by
displaying $?; you can use the return value as you have before with
&& and ||; or you can test it using the various conditional
constructs that are covered later in this section.
Listing 28. Some simple tests
[ian@pinguino ~]$ test 3 -gt 4 && echo True || echo false
false
[ian@pinguino ~]$ [ "abc" != "def" ];echo $?
0
[ian@pinguino ~]$ test -d "$HOME" ;echo $?
0
|
In the first of these examples, the -gt operator was
used to perform an arithmetic comparison between two literal values. In the
second, the alternate [ ] form was used to
compare two strings for inequality. In the final example, the value of the HOME
variable is tested to see if
it is a directory using the -d unary operator.
Arithmetic values may be compared using one of -eq, -ne, -lt, -le, -gt, or -ge, meaning equal, not equal, less than, less than or equal, greater than, and greater than or equal, respectively.
Strings may be compared for equality, inequality, or whether the first string
sorts before or after the second one using the operators =, !=, < and
>, respectively. The unary operator -z tests
for a null string, while -n or no operator at all
returns True if a string is not empty.
Note: the < and > operators are also used by the shell for redirection, so you must escape them using \< or \>. Listing 29 shows some more examples of string tests. Check that they are as you expect.
Listing 29. Some string tests
[ian@pinguino ~]$ test "abc" = "def" ;echo $?
1
[ian@pinguino ~]$ [ "abc" != "def" ];echo $?
0
[ian@pinguino ~]$ [ "abc" \< "def" ];echo $?
0
[ian@pinguino ~]$ [ "abc" \> "def" ];echo $?
1
[ian@pinguino ~]$ [ "abc" \<"abc" ];echo $?
1
[ian@pinguino ~]$ [ "abc" \> "abc" ];echo $?
1
|
Some of the more common file tests are shown in Table 5. The result is True if the file tested is a file that exists and that has the specified characteristic.
| Operator | Characteristic |
|---|---|
| -d | Directory |
| -e | Exists (also -a) |
| -f | Regular file |
| -h | Symbolic link (also -L) |
| -p | Named pipe |
| -r | Readable by you |
| -s | Not empty |
| -S | Socket |
| -w | Writable by you |
| -N | Has been modified since last being read |
In addition to the unary tests above, two files can be compared with the binary operators shown in Table 6.
| Operator | True if |
|---|---|
| -nt | Test if file1 is newer than file 2. The modification date is used for this and the next comparison. |
| -ot | Test if file1 is older than file 2. |
| -ef | Test if file1 is a hard link to file2. |
Several other tests allow you to check things such as the permissions of the
file. See the man pages for bash for more details or use
help test to see brief information on the test
builtin. You can use the help command for other
builtins too.
The -o operator allows you to test various shell
options that may be set using
set -o
option
, returning True
(0) if the option is set and False (1) otherwise, as shown in Listing 30.
Listing 30. Testing shell options
[ian@pinguino ~]$ set +o nounset
[ian@pinguino ~]$ [ -o nounset ];echo $?
1
[ian@pinguino ~]$ set -u
[ian@pinguino ~]$ test -o nounset; echo $?
0
|
Finally, the -a and -o
options allow you to combine expressions with logical AND and OR, respectively,
while the unary ! operator inverts the sense of the
test. You may use parentheses to group expressions and override the default
precedence. Remember that the shell will normally run an expression between
parentheses in a subshell, so you will need to escape the parentheses using \(
and \) or enclosing these operators in single or double quotes. Listing 31
illustrates the application of de Morgan's laws to an expression.
Listing 31. Combining and grouping tests
[ian@pinguino ~]$ test "a" != "$HOME" -a 3 -ge 4 ; echo $?
1
[ian@pinguino ~]$ [ ! \( "a" = "$HOME" -o 3 -lt 4 \) ]; echo $?
1
[ian@pinguino ~]$ [ ! \( "a" = "$HOME" -o '(' 3 -lt 4 ')' ")" ]; echo $?
1
|
The test command is very powerful, but somewhat
unwieldy with its requirement for escaping and the difference between string and
arithmetic comparisons. Fortunately bash has two other ways of testing that are
somewhat more natural for people who are familiar with C, C++, or Java syntax.
The (( ))
compound command evaluates an arithmetic expression and sets the exit
status to 1 if the expression evaluates to 0, or to 0 if the expression
evaluates to a non-zero value. You do not need to escape operators between
(( and )). Arithmetic is
done on integers. Division by 0 causes an error, but overflow does not. You may
perform the usual C language arithmetic, logical, and bitwise operations. The
let command can also execute one or more arithmetic
expressions. It is usually used to assign values to arithmetic variables.
Listing 32. Assigning and testing arithmetic expressions
[ian@pinguino ~]$ let x=2 y=2**3 z=y*3;echo $? $x $y $z
0 2 8 24
[ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
0 3 8 16
[ian@pinguino ~]$ (( w=(y/x) + ( (~ ++x) & 0x0f ) )); echo $? $x $y $w
0 4 8 13
|
As with (( )), the
[[ ]] compound command allows you to use more
natural syntax for filename and string tests. You can combine tests that are
allowed for the test command using parentheses and
logical operators.
Listing 33. Using the [[ compound
[ian@pinguino ~]$ [[ ( -d "$HOME" ) && ( -w "$HOME" ) ]] &&
> echo "home is a writable directory"
home is a writable directory
|
The [[ compound can also do pattern matching on
strings when the = or !=
operators are used. The match behaves as for wildcard globbing as illustrated in
Listing 34.
Listing 34. Wildcard tests with [[
[ian@pinguino ~]$ [[ "abc def .d,x--" == a[abc]*\ ?d* ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def c" == a[abc]*\ ?d* ]]; echo $?
1
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* ]]; echo $?
1
|
You can even do arithmetic tests within [[
compounds, but be careful. Unless within a ((
compound, the < and
> operators will compare the operands as
strings and test their order in the current collating sequence. Listing 35
illustrates this with some examples.
Listing 35. Including arithmetic tests with [[
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || (( 3 > 2 )) ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 -gt 2 ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || 3 > 2 ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a > 2 ]]; echo $?
0
[ian@pinguino ~]$ [[ "abc def d,x" == a[abc]*\ ?d* || a -gt 2 ]]; echo $?
-bash: a: unbound variable
|
While you could accomplish a huge amount of programming with the above tests
and the && and
|| control operators, bash includes the more familiar
"if, then, else" and case constructs. After you learn about these, you will
learn about looping constructs and your toolbox will really expand.
The bash if command is a compound command that tests
the return value of a test or command ($? and
branches based on whether it is True (0) or False (not 0). Although the tests
above returned only 0 or 1 values, commands may return other values. You will
learn more about testing those a little later in this tutorial. The
if command in bash has a
then clause containing a list of commands to be
executed if the test or command returns 0. The command also has one or more optional
elif clauses. Each of these optional elif clauses has an additional test and
then clause with an associated list of commands, an
optional final else clause, and list of commands to be
executed if neither the original test, nor any of the tests used in the
elif clauses was true. A terminal
fi marks the end of the construct.
Using what you have learned so far, you could now build a simple calculator to evaluate arithmetic expressions as shown in Listing 36.
Listing 36. Evaluating expressions with if, then, else
[ian@pinguino ~]$ function mycalc ()
> {
> local x
> if [ $# -lt 1 ]; then
> echo "This function evaluates arithmetic for you if you give it some"
> elif (( $* )); then
> let x="$*"
> echo "$* = $x"
> else
> echo "$* = 0 or is not an arithmetic expression"
> fi
> }
[ian@pinguino ~]$ mycalc 3 + 4
3 + 4 = 7
[ian@pinguino ~]$ mycalc 3 + 4**3
3 + 4**3 = 67
[ian@pinguino ~]$ mycalc 3 + (4**3 /2)
-bash: syntax error near unexpected token `('
[ian@pinguino ~]$ mycalc 3 + "(4**3 /2)"
3 + (4**3 /2) = 35
[ian@pinguino ~]$ mycalc xyz
xyz = 0 or is not an arithmetic expression
[ian@pinguino ~]$ mycalc xyz + 3 + "(4**3 /2)" + abc
xyz + 3 + (4**3 /2) + abc = 35
|
The calculator makes use of the local statement to
declare x as a local variable that is available only within the scope of the
mycalc function.
The
let function has several possible options, as does
the declare function to which it is closely related.
Check the man pages for bash, or use help let
for more information.
As you see in Listing 36, you need to be careful making sure that your expressions are properly escaped if they use shell metacharacters such as (, ), *, >, and <. Nevertheless, you have quite a handy little calculator for evaluating arithmetic as the shell does it.
You may have noticed the else clause and the last
two examples. As you see, it is not an error to pass
xyz to mycalc, but it evaluates to 0. This function
is not smart enough to identify the character values in the final example of use
and thus be able to warn the user. You could use a string pattern matching test
such as
[[ ! ("$*" == *[a-zA-Z]* ]]
(or the appropriate form for
your locale) to eliminate any expression containing alphabetic characters, but
that would prevent using hexadecimal notation in your input, since you might use
0x0f to represent 15 using hexadecimal notation. In fact, the shell allows bases
up to 64 (using
base#value
notation),
so you could legitimately use any alphabetic character, plus _ and @ in your
input. Octal and hexadecimal use the usual notation of a leading 0 for octal and
leading 0x or 0X for hexadecimal. Listing 37 shows some examples.
Listing 37. Calculating with different bases
[ian@pinguino ~]$ mycalc 015
015 = 13
[ian@pinguino ~]$ mycalc 0xff
0xff = 255
[ian@pinguino ~]$ mycalc 29#37
29#37 = 94
[ian@pinguino ~]$ mycalc 64#1az
64#1az = 4771
[ian@pinguino ~]$ mycalc 64#1azA
64#1azA = 305380
[ian@pinguino ~]$ mycalc 64#1azA_@
64#1azA_@ = 1250840574
[ian@pinguino ~]$ mycalc 64#1az*64**3 + 64#A_@
64#1az*64**3 + 64#A_@ = 1250840574
|
Additional laundering of the input is beyond the scope of this tutorial, so use your calculator with appropriate care.
The elif statement is really a convenience. It will
help you in writing scripts by allowing you to simplify the indenting. You may
be surprised to see the output of the type command
for the mycalc function as shown in Listing 38.
Listing 38. Type mycalc
[ian@pinguino ~]$ type mycalc
mycalc is a function
mycalc ()
{
local x;
if [ $# -lt 1 ]; then
echo "This function evaluates arithmetic for you if you give it some";
else
if (( $* )); then
let x="$*";
echo "$* = $x";
else
echo "$* = 0 or is not an arithmetic expression";
fi;
fi
}
|
The case compound command simplifies testing when
you have a list of possibilities and you want to take action based on whether a
value matches a particular possibility. The case
compound is introduced by case WORD in
and terminated by esac ("case"
spelled backwards). Each case consists of a single pattern, or multiple patterns
separated by |, followed by ), a list of statements, and finally a pair of
semicolons (;;).
To illustrate, imagine a store that serves coffee, decaffeinated coffee (decaf), tea, or soda. The function in Listing 39 might be used to determine the response to an order.
Listing 39. Using case commands
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don't serve that here"
;;
esac
}
[ian@pinguino ~]$ myorder decaf
Hot coffee coming right up
[ian@pinguino ~]$ myorder tea
Hot tea on its way
[ian@pinguino ~]$ myorder milk
Sorry, we don't serve that here
|
Note the use of '*' to match anything that had not already been matched.
Bash has another construct similar to case that can
be used for printing output to a terminal and having a user select items. It is
the select statement, which will not be covered here.
See the bash man pages, or type help select to
learn more about it.
Of course, there are many problems with such a simple approach to the problem; you can't order two drinks at once, and the function doesn't handle anything but lower-case input. Can you do a case-insensitive match? The answer is "yes", so let's see how.
The Bash shell has a shopt builtin that allows you
to set or unset many shell options. One is
nocasematch, which, if set, instructs the shell to
ignore case in string matching. Your first thought might be to use the
-o operand that you learned about with the
test command. Unfortunately,
nocasematch is not one of the options you can test
with -o, so you'll have to resort to something else.
The shopt command, like most UNIX and Linux commands
sets a return value that you can examine using $?. The tests that you learned
earlier are not the only things with return values. If you think about the tests
that you do in an if statement, they really test the
return value of the underlying test command for being True (0) or False (1 or
anything other than 0). This works even if you don't use a test, but use some
other command. Success is indicated by a return value of 0, and failure by a
non-zero return value.
Armed with this knowledge, you can now test the
nocasematch option, set it if it is not already set,
and then return it to the user's preference when your function terminates. The
shopt command has four convenient options,
-pqsu to print the current value, don't print
anything, set the option, or unset the option. The -p
and -q options set a return value of 0 to indicate
that the shell option is set, and 1 to indicate it is unset. The
-p options prints out the command required to set the
option to its current value, while the -q option
simply sets a return value of 0 or 1.
Your modified function will use the return value from
shopt to set a local variable representing the
current state of the nocasematch option, set the
option, run the case command, then reset the
nocasematch option to its original value. One way to
do this is shown in Listing 40.
Listing 40. Testing return values from commands
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
local restorecase;
if shopt -q nocasematch; then
restorecase="-s";
else
restorecase="-u";
shopt -s nocasematch;
fi;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don't serve that here"
;;
esac;
shopt $restorecase nocasematch
}
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
[ian@pinguino ~]$ # nocasematch is currently unset
[ian@pinguino ~]$ myorder DECAF
Hot coffee coming right up
[ian@pinguino ~]$ myorder Soda
Your ice-cold soda will be ready in a moment
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
[ian@pinguino ~]$ # nocasematch is unset again after running the myorder function
|
If you want your function (or script) to return a value that other functions or
commands can test, use the return statement in your
function. Listing 41 shows how to return 0 for a drink that you can serve and 1
if the customer requests something else.
Listing 41. Setting your own return values from functions
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
local restorecase=$(shopt -p nocasematch) rc=0;
shopt -s nocasematch;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don't serve that here";
rc=1
;;
esac;
$restorecase;
return $rc
}
[ian@pinguino ~]$ myorder coffee;echo $?
Hot coffee coming right up
0
[ian@pinguino ~]$ myorder milk;echo $?
Sorry, we don't serve that here
1
|
If you don't specify your own return value, the return value will be that of the last command executed. Functions have a habit of being reused in situations that you never anticipated, so it is good practice to set your own value.
Commands may return values other than 0 and 1, and sometimes you will want to
distinguish between them. For example, the grep
command returns 0 if the pattern is matched and 1 if it is not, but it also
returns 2 if the pattern is invalid or if the file specification doesn't match
any files. If you need to distinguish more return values besides just success
(0) or failure (non-zero), then you will probably use a
case command or perhaps an
if command with several
elif parts.
You met command substitution in the "LPI exam 101 prep (topic 103): GNU and UNIX commands" tutorial, but let's do a quick review.
Command substitution allows you to use the output of a command as input to another command by simply surrounding the command with $( and ) or with a pair of backticks - `. You will find the $() form advantageous if you want to nest output from one command as part of the command that will generate the final output, and it can be easier to figure out what's really going on as the parentheses have a left and right form as opposed to two identical backticks. However, the choice is yours, and backticks are still very common,
You will often use command substitution with loops (covered later under
Loops). However, you can also use it to simplify the
myorder function that you just created. Since
shopt -p nocasematch actually prints
the command that you need to set the nocasematch
option to its current value, you only need to save that output and then execute
it at the end of the case statement. This will
restore the nocasematch option regardless of whether
you actually changed it or not. Your revised function might now look like
Listing 42. Try it for yourself.
Listing 42. Command substitution instead of return value tests
[ian@pinguino ~]$ type myorder
myorder is a function
myorder ()
{
local restorecase=$(shopt -p nocasematch) rc=0;
shopt -s nocasematch;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don't serve that here"
rc=1
;;
esac;
$restorecase
return $rc
}
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
[ian@pinguino ~]$ myorder DECAF
Hot coffee coming right up
[ian@pinguino ~]$ myorder TeA
Hot tea on its way
[ian@pinguino ~]$ shopt -p nocasematch
shopt -u nocasematch
|
If you have typed functions yourself and made typing errors that left you
wondering what was wrong, you might also be wondering how to debug functions.
Fortunately the shell lets you set the -x option to
trace commands and their arguments as the shell executes them. Listing 43 shows
how this works for the myorder function of Listing
42.
Listing 43. Tracing execution
[ian@pinguino ~]$ set -x
++ echo -ne '\033]0;ian@pinguino:~'
[ian@pinguino ~]$ myorder tea
+ myorder tea
++ shopt -p nocasematch
+ local 'restorecase=shopt -u nocasematch' rc=0
+ shopt -s nocasematch
+ case "$*" in
+ echo 'Hot tea on its way'
Hot tea on its way
+ shopt -u nocasematch
+ return 0
++ echo -ne '\033]0;ian@pinguino:~'
[ian@pinguino ~]$ set +x
+ set +x
|
You can use this technique for your aliases, functions, or scripts. If you need
more information, add the -v option for verbose
output.
Bash and other shells have three looping constructs that are somewhat similar
to those in the C language. Each will execute a list of commands zero or more
times. The list of commands is surrounded by the words
do and done, each preceded
by a semicolon.
- for
- loops come in two flavors. The most common form in shell scripting iterates over a set of values, executing the command list once for each value. The set may be empty, in which case the command list is not executed. The other form is more like a traditional C for loop, using three arithmetic expressions to control a starting condition, step function, and end condition.
- while
- loops evaluate a condition each time the loop starts and execute the command list if the condition is true. If the condition is not initially true, the commands are never executed.
- until
- loops execute the command list and evaluate a condition each time the loop ends. If the condition is true the loop is executed again. Even if the condition is not initially true, the commands are executed at least once.
If the conditions that are tested are a list of commands, then the return value of the last one executed is the one used. Listing 44 illustrates the loop commands.
Listing 44. For, while, and until loops
[ian@pinguino ~]$ for x in abd 2 "my stuff"; do echo $x; done
abd
2
my stuff
[ian@pinguino ~]$ for (( x=2; x<5; x++ )); do echo $x; done
2
3
4
[ian@pinguino ~]$ let x=3; while [ $x -ge 0 ] ; do echo $x ;let x--;done
3
2
1
0
[ian@pinguino ~]$ let x=3; until echo -e "x=\c"; (( x-- == 0 )) ; do echo $x ; done
x=2
x=1
x=0
|
These examples are somewhat artificial, but they illustrate the concepts. You will most often want to iterate over the parameters to a function or shell script, or a list created by command substitution. Earlier you discovered that the shell may refer to the list of passed parameters as $* or $@ and that whether you quoted these expressions or not affected how they were interpreted. Listing 45 shows a function that prints out the number of parameters and then prints the parameters according to the four alternatives. Listing 46 shows the function in action, with an additional character added to the front of the IFS variable for the function execution.
Listing 45. A function to print parameter information
[ian@pinguino ~]$ type testfunc
testfunc is a function
testfunc ()
{
echo "$# parameters";
echo Using '$*';
for p in $*;
do
echo "[$p]";
done;
echo Using '"$*"';
for p in "$*";
do
echo "[$p]";
done;
echo Using '$@';
for p in $@;
do
echo "[$p]";
done;
echo Using '"$@"';
for p in "$@";
do
echo "[$p]";
done
}
|
Listing 46. Printing parameter information with testfunc
[ian@pinguino ~]$ IFS="|${IFS}" testfunc abc "a bc" "1 2
> 3"
3 parameters
Using $*
[abc]
[a]
[bc]
[1]
[2]
[3]
Using "$*"
[abc|a bc|1 2
3]
Using $@
[abc]
[a]
[bc]
[1]
[2]
[3]
Using "$@"
[abc]
[a bc]
[1 2
3]
|
Study the differences carefully, particularly for the quoted forms and the parameters that include white space such as blanks or newline characters.
The break command allows you to exit from a loop
immediately. You can optionally specify a number of levels to break out of if
you have nested loops. So if you had an until loop
inside a for loop inside another
for loop and all inside a
while loop, then
break 3 would immediately terminate the
until loop and the two for
loops, and return control to the code in the while
loop.
The continue statement allows you to bypass
remaining statements in the command list and go immediately to the next
iteration of the loop.
Listing 47. Using break and continue
[ian@pinguino ~]$ for word in red blue green yellow violet; do
> if [ "$word" = blue ]; then continue; fi
> if [ "$word" = yellow ]; then break; fi
> echo "$word"
> done
red
green
|
Remember how much work you did to get the ldirs
function to extract the file name from a long listing and also figure out if it
was a directory or not? The final function that you developed was not too bad,
but suppose you had all the information you now have. Would you have created the
same function? Perhaps not. You know how to test whether a name is a directory
or not using [ -d $name ], and
you know about the for compound. Listing 48 shows
another way you might have coded the ldirs function.
Listing 48. Another approach to ldirs
[ian@pinguino developerworks]$ type ldirs
ldirs is a function
ldirs ()
{
if [ $# -gt 0 ]; then
for file in "$@";
do
[ -d "$file" ] && echo "$file";
done;
else
for file in *;
do
[ -d "$file" ] && echo "$file";
done;
fi;
return 0
}
[ian@pinguino developerworks]$ ldirs
my dw article
my-tutorial
readme
schema
tools
web
xsl
[ian@pinguino developerworks]$ ldirs *s* tools/*
schema
tools
xsl
tools/java
[ian@pinguino developerworks]$ ldirs *www*
[ian@pinguino developerworks]$
|
You will note that the function quietly returns if there are no directories
matching your criteria. This may or may not be what you want, but if it is, this
form of the function is perhaps easier to understand than the version that used
sed to parse output from
ls. At least you now have another tool in your
toolbox.
Recall that myorder could handle only one drink at a
time? You could now combine that single drink function with a
for compound to iterate through the parameters and
handle multiple drinks. This is as simple as placing your function in a file and
adding the for instruction. Listing 49 illustrates
the new myorder.sh script.
Listing 49. Ordering multiple drinks
[ian@pinguino ~]$ cat myorder.sh
function myorder ()
{
local restorecase=$(shopt -p nocasematch) rc=0;
shopt -s nocasematch;
case "$*" in
"coffee" | "decaf")
echo "Hot coffee coming right up"
;;
"tea")
echo "Hot tea on its way"
;;
"soda")
echo "Your ice-cold soda will be ready in a moment"
;;
*)
echo "Sorry, we don't serve that here";
rc=1
;;
esac;
$restorecase;
return $rc
}
for file in "$@"; do myorder "$file"; done
[ian@pinguino ~]$ . myorder.sh coffee tea "milk shake"
Hot coffee coming right up
Hot tea on its way
Sorry, we don't serve that here
|
Note that the script was sourced to run in the current shell environment
rather than its own shell using the . command. To be
able to execute a script, either you have to source it, or the script file must
be marked executable using the chmod -x
command as illustrated in Listing 50.
Listing 50. Making the script executable
[ian@pinguino ~]$ chmod +x myorder.sh
[ian@pinguino ~]$ ./myorder.sh coffee tea "milk shake"
Hot coffee coming right up
Hot tea on its way
Sorry, we don't serve that here
|
Now that you have a brand-new shell script to play with, you might ask whether it works in all shells. Listing 51 shows what happens if you run the exact same shell script on a Ubuntu system using first the Bash shell, then the dash shell.
Listing 51. Shell differences
ian@attic4:~$ ./myorder tea soda
-bash: ./myorder: No such file or directory
ian@attic4:~$ ./myorder.sh tea soda
Hot tea on its way
Your ice-cold soda will be ready in a moment
ian@attic4:~$ dash
$ ./myorder.sh tea soda
./myorder.sh: 1: Syntax error: "(" unexpected
|
That's not too good.
Remember earlier when we mentioned that the word 'function' was optional in a bash function definition, but that it wasn't part of the POSIX shell specification? Well, dash is a smaller and lighter shell than bash and it doesn't support that optional feature. Since you can't guarantee what shell your potential users might prefer, you should always ensure that your script is portable to all shell environments, which can be quite difficult, or use the so-called shebang (#!) to instruct the shell to run your script in a particular shell. The shebang line must be the first line of your script, and the rest of the line contains the path to the shell that your program must run under, so it would be #!/bin/bash the myorder.sh script.
Listing 52. Using shebang
$ head -n3 myorder.sh
#!/bin/bash
function myorder ()
{
$ ./myorder.sh Tea Coffee
Hot tea on its way
Hot coffee coming right up
|
You can use the cat command to display /etc/shells,
which is the list of shells on your system. Some systems do list shells that are
not installed, and some listed shells (possibly /dev/null) may be there to
ensure that FTP users cannot accidentally escape from their limited environment.
If you need to change your default shell, you can do so with the
chsh command, which updates the entry for your userid
in /etc/passwd.
Suid rights and script locations
In the earlier tutorial
LPI exam 101 prep: Devices, Linux filesystems, and the Filesystem
Hierarchy Standard
you learned how to change a file's owner and group and how to set the suid and
sgid permissions. An executable file with either of these permissions set will
run in a shell with effective permissions of the file's owner (for suid)
or group (for suid). Thus, the program will be able to do anything that the
owner or group could do, according to which permission bit is set. There are
good reasons why some programs need to do this. For example, the
passwd program needs to update /etc/shadow, and the
chsh command, which you use to change your default
shell, needs to update /etc/passwd. If you use an alias for
ls, listing these programs is likely to result in a
red, highlighted listing to warn you, as shown in Figure 2. Note that both of
these programs have the suid big (s) set and thus operate as if root were
running them.
Figure 2. Programs with suid permission
Listing 53 shows that an ordinary user can run these and update files owned by root.
Listing 53. Using suid programs
ian@attic4:~$ passwd
Changing password for ian
(current) UNIX password:
Enter new UNIX password:
Retype new UNIX password:
passwd: password updated successfully
ian@attic4:~$ chsh
Password:
Changing the login shell for ian
Enter the new value, or press ENTER for the default
Login Shell [/bin/bash]: /bin/dash
ian@attic4:~$ find /etc -mmin -2 -ls
308865 4 drwxr-xr-x 108 root root 4096 Jan 29 22:52 /etc
find: /etc/cups/ssl: Permission denied
find: /etc/lvm/archive: Permission denied
find: /etc/lvm/backup: Permission denied
find: /etc/ssl/private: Permission denied
311170 4 -rw-r--r-- 1 root root 1215 Jan 29 22:52 /etc/passwd
309744 4 -rw-r----- 1 root shadow 782 Jan 29 22:52 /etc/shadow
ian@attic4:~$ grep ian /etc/passwd
ian:x:1000:1000:Ian Shields,,,:/home/ian:/bin/dash
|
You can set suid and sgid permissions for shell scripts, but most modern shells ignore these bits for scripts. As you have seen, the shell has a powerful scripting language, and there are even more features that are not covered in this tutorial, such as the ability to interpret and execute arbitrary expressions. These features make it a very unsafe environment to allow such wide permission. So, if you set suid or sgid permission for a shell script, don't expect it to be honored when the script is executed.
Earlier, you changed the permissions of myorder.sh to mark it executable
(x). Despite that, you still had to qualify the name by prefixing ./ to actually
run it, unless you sourced it in the current shell. To execute a shell by name
only, it needs to be on your path, as represented by the PATH variable.
Normally, you do not want the current directory on your path, as it is a
potential security exposure. Once you have tested your script and found it
satisfactory, you should place it in ~/nom if it is a personal script, or
/usr/local/bin if it is to be available for others on the system. If you simply
used chmod -x to mark it executable, it is
executable by everyone (owner, group and world). This is generally what you
want, but refer back to the earlier
tutorial, LPI exam 101 prep: Devices, Linux filesystems, and the Filesystem
Hierarchy Standard, if you need to restrict the script so that only members of a certain
group can execute it.
You may have noticed that shells are usually located in /bin rather than in /usr/bin. According to the Filesystem Hierarchy Standard, /usr/bin may be on a filesystem shared among systems, and so it may not be available at initialization time. Therefore, certain functions, such as shells, should be in /bin so they are available even if /urs/bin is not yet mounted. User-created scripts do not usually need to be in /bin (or /sbin), as the programs in these directories should give you enough tools to get your system up and running to the point where you can mount the /usr filesystem.
If your script is running some administrative task on your system in the dead
of night while you're sound asleep, what happens when something goes wrong?
Fortunately, it's easy to mail error information or log files to yourself or to
another administrator or to root. Simply pipe the message to the
mail command, and use the
-s option to add a subject line as shown in Listing
54.
Listing 54. Mailing an error message to a user
ian@attic4:~$ echo "Midnight error message" | mail -s "Admin error" ian
ian@attic4:~$ mail
Mail version 8.1.2 01/15/2001. Type ? for help.
"/var/mail/ian": 1 message 1 new
>N 1 ian@localhost Mon Jan 29 23:58 14/420 Admin error
&
Message 1:
From ian@localhost Mon Jan 29 23:58:27 2007
X-Original-To: ian
To: ian@localhost
Subject: Admin error
Date: Mon, 29 Jan 2007 23:58:27 -0500 (EST)
From: ian@localhost (Ian Shields)
Midnight error message
& d
& q
|
If you need to mail a log file, use the <
redirection function to redirect it as input to the mail command. If you need to
send several files, you can use cat to combine them
and pipe the output to mail. In Listing 54, mail was sent to user ian who
happened to also be the one running the command, but admin scripts are more
likely to direct mail to root or another administrator. As usual, consult the
man pages for mail to learn about other options that you can specify.
This brings us to the end of this tutorial. We have covered a lot of material on shells and scripting. Don't forget to rate this tutorial and give us your feedback.


