Skip to main content

By clicking Submit, you agree to the developerWorks terms of use.

The first time you sign into developerWorks, a profile is created for you. Select information in your profile (name, country/region, and company) is displayed to the public and will accompany any content you post. You may update your IBM account at any time.

All information submitted is secure.

  • Close [x]

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerworks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

By clicking Submit, you agree to the developerWorks terms of use.

All information submitted is secure.

  • Close [x]

developerWorks Community:

  • Close [x]

LPI exam 102 prep, Topic 109: Shells, scripting, programming, and compiling

Junior Level Administration (LPIC-1) topic 109

Ian Shields, Senior Programmer, IBM
Ian Shields
Ian Shields works on a multitude of Linux projects for the developerWorks Linux zone. He is a Senior Programmer at IBM at the Research Triangle Park, NC. He joined IBM in Canberra, Australia, as a Systems Engineer in 1973, and has since worked on communications systems and pervasive computing in Montreal, Canada, and RTP, NC. He has several patents and has published several papers. His undergraduate degree is in pure mathematics and philosophy from the Australian National University. He has an M.S. and Ph.D. in computer science from North Carolina State University. Learn more about Ian in Ian's profile on developerWorks Community.
(An IBM developerWorks Contributing Author)

Summary:  In this tutorial, Ian Shields continues preparing you to take the Linux Professional Institute® Junior Level Administration (LPIC-1) Exam 102. In this fifth in a series of nine tutorials, Ian introduces you to the Bash shell, and scripts and programming in the Bash shell. By the end of this tutorial, you will know how to customize your shell environment, use shell programming structures to create functions and scripts, set and unset environment variables, and use the various login scripts.

View more content in this series

Date:  30 Jan 2007
Level:  Intermediate PDF:  A4 and Letter (595 KB | 38 pages)Get Adobe® Reader®

Activity:  35992 views
Comments:  

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.

Tests

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!

test and [

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.

Table 5. Some file tests
OperatorCharacteristic
-dDirectory
-eExists (also -a)
-fRegular file
-hSymbolic link (also -L)
-pNamed pipe
-rReadable by you
-sNot empty
-SSocket
-wWritable by you
-NHas 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.

Table 6. Testing pairs of files
OperatorTrue if
-ntTest if file1 is newer than file 2. The modification date is used for this and the next comparison.
-otTest if file1 is older than file 2.
-efTest 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


(( and [[

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


Conditionals

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.

If, then, else statements

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
}

Case statements

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.


Return values

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.


Command substitution

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


Debugging

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.


Loops

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.

Break and continue

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

Revisiting ldirs

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.


Creating scripts

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


Specify a shell

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
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.


Mail to root

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.

3 of 5 | Previous | Next

Comments



static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Linux, Open source, AIX and UNIX
ArticleID=193176
TutorialTitle=LPI exam 102 prep, Topic 109: Shells, scripting, programming, and compiling
publish-date=01302007
author1-email=ishields@us.ibm.com
author1-email-cc=