Learning the Basics of Shell Scripting in Bash


Learning the Basics of Shell Scripting in Bash

Now that you have seen examples of simple shell scripts, the next few sections provide an overview of Bash programming.

Writing a Simple Shell Script

Earlier in this chapter, you learn how to place frequently used commands in a file and use the chmod command to make the file executable. Voila-you have a shell script. Just as most Linux commands accept command-line options, a Bash script accepts command-line options. Inside the script, you can refer to the options as $1, $2, and so on. The special name $0 refers to the name of the script itself.

Consider the following Bash script:

#!/bin/sh
echo "This script's name is: $0."
echo Argument 1: $1
echo Argument 2: $2

The first line causes Linux to run the /bin/sh program, which subsequently processes the rest of the lines in the script. The name /bin/sh traditionally refers to the Bourne shell-the first UNIX shell. In Linux, /bin/sh is a symbolic link to /bin/bash, which is the executable program for Bash. Therefore, in Linux, Bash runs Bourne shell scripts (Bash happens to be compatible with the Bourne shell).

If you save this simple script in a file named simple, and you make that file executable with the command chmod +x simple, you can run the script as follows:

./simple
This script's name is: ./simple
Argument 1:
Argument 2:

The script file's name appears relative to the current directory, which is represented by a period. Because you have run the script without arguments, the script does not display any arguments.

Now, try running the script with a few arguments, as follows:

./simple "This is one argument." second-argument third
This script's name is: ./simple
Argument 1: This is one argument
Argument 2: second-argument

As this example shows, the shell treats the entire string within double quotation marks as a single argument. Otherwise, the shell uses spaces as separators between arguments on the command line.

Note that this sample script ignores the third argument because the script is designed to print only the first two arguments. The script ignores all arguments after the first two.

Getting an Overview of Bash Programming

Like any programming language, Bash includes the following features:

  • Variables that store values, including special built-in variables for accessing command-line arguments passed to a shell script and other special values

  • The capability to evaluate expressions

  • Control structures that enable you to loop over several shell commands or to execute some commands conditionally

  • The capability to define functions that can be called in many places within a script. Bash also includes many built-in commands that you can use in any script.

The next few sections illustrate some of Bash's programming features through simple examples. Because you are already running Bash, you can try the examples by typing them at the shell prompt in a terminal window.

Understanding Bash Variables

You define variables in Bash just as you define environment variables. Thus, you might define a variable as follows:

count=12  # note no embedded spaces allowed

To use a variable's value, prefix the variable's name with a dollar sign ($). $PATH, for example, is the value of the variable PATH (yes, the famous PATH environment variable that contains the list of directories to search for any commands the user types). To display the value of the variable count, use the following command:

echo $count

Bash has some special variables for accessing command-line arguments. In a shell script, $0 refers to the name of the shell script. The variables $1, $2, and so on refer to the command-line arguments. The variable $* stores all the command-line arguments as a single variable, and $? contains the exit status of the last command the shell executes.

In addition, you can prompt the user for input and use the read command to read the input into a variable. Following is an example:

echo -n "Enter value: "
read value
echo "You entered: $value."
Insider Insight 

The -n option prevents the echo command from automatically adding a new line at the end of the string that it displays.

Writing Shell Functions

You can group a number of shell commands into a function and assign it a name. Later, you can execute that group of commands by using the single name assigned to the function. Here is a simple script that illustrates the syntax of shell functions:

#!/bin/sh

hello() {
        echo -n "Hello, "
        echo $1 $2
}

hello Jane Doe

When you run this script, it displays the following output:

Hello, Jane Doe

This script defines a shell function named hello. The function expects two arguments; in the body of the function, these arguments are referenced by $1 and $2. The function definition begins with hello()-the name of the function, followed by parentheses. The body of the function is enclosed in curly braces-{...}. In this case, the body uses the echo built-in command (see Table 24-1 for a list of built-in commands).

Using Bash Control Structures

In Bash scripts, the control structures-such as if, case, for, and while-depend on the exit status of a command to decide what to do next. When any command executes, it returns an exit status: a numeric value that indicates whether or not the command has succeeded. By convention, an exit status of zero means the command has succeeded. (Yes, you read it right: zero indicates success.) A nonzero exit status indicates that something has gone wrong with the command.

As an example of using control structures, consider the following script, which makes a backup copy of a file before opening it with the vi text editor:

#!/bin/sh
if cp "$1" "#$1"
then
    vi "$1"
else
    echo "Failed to create backup copy"
fi

This script illustrates the syntax of the if-then-else structure and shows how the exit status of the cp command is used by the if structure to determine the next action. If cp returns zero, the script invokes vi to edit the file; otherwise, the script displays a message and exits. By the way, the script names the backup file whose name is the same as that of the original, except for a number sign (#) added at the beginning of the filename.

Insider Insight 

Don't forget the final fi that terminates the if structure. Forgetting fi is a common source of errors in Bash scripts.

Bash includes the test command to enable you to evaluate any expression and to use the expression's value as the exit status of the command. Suppose that you want a script that enables you to edit a file only if it exists. Using test, you might write such a script as follows:

#!/bin/sh
if test -f "$1"
then
    vi "$1"
else
    echo "No such file"
fi

Another common control structure is the for loop. The following script adds the numbers 1 through 10:

#!/bin/sh
sum=0
for i in 1 2 3 4 5 6 7 8 9 10
do
    sum=`expr $sum + $i'
done
echo "Sum = $sum"

This example also illustrates the use of the expr command to evaluate an expression.

The case statement is used to execute a group of commands based on the value of a variable. For example, consider the following script:

#!/bin/sh
echo -n "What should I do -- (Y)es/(N)o/(C)ontinue? [Y] "
read answer
case $answer in
    y|Y|"")
      echo "YES"
    ;;
    c|C)
      echo "CONTINUE"
    ;;
    n|N)
      echo "NO"
    ;;
    *)
      echo "UNKNOWN"
    ;;
esac

Save this in a file named confirm, and type chmod +x confirm to make it executable. Then, try it out like this:

./confirm
What should I do -- (Y)es/(N)o/(C)ontinue? [Y] c
CONTINUE

The script displays a prompt and reads the input you type. Your input is stored in a variable named answer. Then the case statement executes a block of code based on the value of the answer variable. For example, when I type c, the following block of commands is executed:

    c|C)
      echo "CONTINUE"
    ;;

The echo command causes the script to display CONTINUE.

From this example, you can see that the general syntax of the case command is as follows:

case $variable in
    value1 | value2)
    command1
    command2
    ...other commands...
    ;;

    value3)
    command3
    command4
    ...other commands...
    ;;
esac

Essentially, the case command begins with the word case and ends with esac. Separate blocks of code are enclosed between the values of the variable, followed by a right parenthesis and terminated by a pair of semicolons (;;).

Insider Insight 

Don't forget the final esac that terminates the case structure. Forgetting esac is a common source of errors in Bash scripts.

Taking Stock of Built-in Commands In Bash

Bash has more than 50 built-in commands, including common commands such as cd and pwd, as well as many others that are used infrequently. You can use these built-in commands in any Bash script or at the shell prompt. Appendix A describes many of the built-in commands that you use typically at the shell prompt.

Although this chapter does not have enough space to cover all built-in Bash commands, Table 24-1 describes most of these commands and their arguments. After looking through this information, type help cmd (where cmd is a command's name) to read more about a specific built-in command. For example, to learn more about the built-in command test, type the following:

help test | more
test: test [expr]
    Exits with a status of 0 (trueness) or 1 (falseness) depending on
    the evaluation of EXPR.  Expressions may be unary or binary. Unary
    expressions are often used to examine the status of a file.  There
    are string operators as well, and numeric comparison operators.

    File operators:

        -b FILE        True if file is block special.
        -c FILE        True if file is character special.
        -d FILE        True if file is a directory.
        -e FILE        True if file exists.
        -f FILE        True if file exists and is a regular file.
        -g FILE        True if file is set-group-id.
        -h FILE        True if file is a symbolic link.
        -L FILE        True if file is a symbolic link.
        -k FILE        True if file has its `sticky' bit set.
        -p FILE        True if file is a named pipe.
        -r FILE        True if file is readable by you.
        -s FILE        True if file exists and is not empty.
        -S FILE        True if file is a socket.
        -t FD          True if FD is opened on a terminal.
        -u FILE        True if the file is set-user-id.
        -w FILE        True if the file is writable by you.
        -x FILE        True if the file is executable by you.
        -O FILE        True if the file is effectively owned by you.
(... Lines deleted ...) 

Where necessary, the online help from the help command includes a considerable amount of detail.

Note that some external programs may have the same name as Bash built-in commands. If you want to run any such external program, you have to specify explicitly the full pathname of that program. Otherwise, Bash executes the built-in command of the same name.

Table 24-1: Summary of Built-in Commands in Bash Shell

Function

Description

. filename [arguments]

Reads and executes commands from the specified file using the optional arguments (same as source)

: [arguments]

Expands the arguments but does not process them

[ expr ]

Evaluates the expression expr and returns zero status if expr is true

alias [name[=value] ...]

Defines an alias or lists all aliases if no arguments provided

bg [job]

Puts the specified job in the background. If no job is specified, it puts the currently executing command in the background.

bind [-m keymap] [-lvd]

Binds a key sequence to a macro [-q name]

break [n]

Exits from a for, while, or until loop. If n is specified, the nth enclosing loop is exited.

builtin builtin_command

Executes a shell built-in command [arguments]

cd [dir]

Changes the current directory to dir or to user's home directory if no arguments provided

command [-pVv] cmd [arg ...]

Runs the command cmd with the specified arguments (ignoring any shell function named cmd)

continue [n]

Starts the next iteration of the for, while, or until loop. If n is specified, the next iteration of the nth enclosing loop is started.

declare [-frxi] [name[=value]]

Declares a variable with the specified name and, optionally, assigns it a value

dirs [-l] [+/-n]

Displays the list of currently remembered directories

echo [-neE] [arg ...]

Displays the arguments on standard output

enable [-n] [-all]

Enables or disables the specified built-in commands [name ...]

eval [arg ...]

Concatenates the arguments and executes them as a command

exec [command [arguments]]

Replaces the current instance of the shell with a new process that runs the specified command

exit [n]

Exits the shell with the status code n

export [-nf] [name[=word]] ...

Defines a specified environment variable and exports it to future processes

fc -s [pat=rep] [cmd]

Reexecutes the command after replacing the pattern pat with rep

fg [jobspec]

Puts the specified job in the foreground. If no job is specified, it puts the most recent job in the foreground.

getopts optstring name [args]

Gets optional parameters (called in shell scripts to extract arguments from the command line)

hash [-r] [name]

Remembers the full pathname of a specified command

help [cmd ...]

Displays help information for specified built-in commands

history [n]

Displays past commands or past n commands, if you specify a number n

jobs [-lnp] [ jobspec ... ]

Lists currently active jobs

kill [-s sigspec |

Sends a specified signal to one or more processes -sigspec] [pid | jobspec] ...

let arg [arg ...]

Evaluates each argument and returns 1 if the last arg is 0

local [name[=value] ...]

Creates a local variable with the specified name and value (used in shell functions)

logout

Exits a login shell

popd [+/-n]

Removes entries from the directory stack

pushd [dir]

Adds a specified directory to the top of the directory stack

pwd

Prints the full pathname of the current working directory

read [-r] [name ...]

Reads a line from standard input and parses it

readonly [-f] [name ...]

Marks the specified variables as read-only, so that the variables cannot be changed later

return [n]

Exits the shell function with the return value n

set [-abefhkmnptuvxldCHP]

Sets various flags [-o option] [arg ...]

shift [n]

Makes the n+1 argument $1, the n+2 argument $2, and so on

source filename [arguments]

Reads and executes commands from a file

suspend [-f]

Stops execution until a SIGCONT signal is received

test expr

Evaluates the expression expr and returns zero if expr is true

times

Prints the accumulated user and system times for processes run from the shell

trap [-l] [cmd] [sigspec]

Executes cmd when the signal sigspec is received

type [-all] [-type | -path]

Indicates how the shell interprets each name name [name ...]

ulimit [-SHacdfmstpnuv

Controls resources available to the shell [limit]]

umask [-S] [mode]

Sets the file creation mask-the default permission for files

unalias [-a] [name ...]

Undefines a specified alias

unset [-fv] [name ...]

Removes the definition of specified variables

wait [n]

Waits for a specified process to terminate