Documente Academic
Documente Profesional
Documente Cultură
quoted. Each shell (Bourne, C, Bash etc) has its own syntax. Shell programs are typically used to develop: "User-friendly'' commands System administration utilities Application utilities A common criticism of Unix or the shell in particular, is that it is unfriendly. Shell programs can be written to prompt the user for parameters, check them and so on, thus cushioning the user from the powerful shell, and making the system as user-friendly as required. Experienced users often get fed up of such prompting and usually prefer using the normal shell commands. System administrators can build shell programs to add new users, to shutdown the system and so on. Some examples are given in these notes. Finally useful applications can be developed with the shell e.g. a phone lookup system is briefly described. The development of applications often requires
Shell Notes. Joe Carthy
some knowledge of other Unix utilities such as grep, tr, ed and awk. A knowledge of shell programming is essential for the serious Unix user, and will more than reward the few hours invested in exploring the shell, in time saved developing new commands and utilities. This document describes the Bourne shell but similar features are available in the other Unix shells, with notational differences.
Shell Programs
Shell commands may be stored in a file. There are a number of ways to execute the commands in such a file. One is to use the `sh' command with the file as a command line parameter e.g. prog.sh is a file containing 3 Unix commands Usage:
$ sh prog.sh
Code in prog.sh: #!/bin/sh pwd ls -l
date Commands may be separated using newlines as above or using semicolons: pwd ; ls -l ; date Any file name may be used, the extension `.sh' has no significance and is not a Unix convention. The second and perhaps more natural way is to use the `chmod' command to make the file "executable''. Then the file name can be entered directly, just like any Unix command. $ chmod +x prog.sh Make file executable - only done once
$ prog.sh
Comments Shell
in
Bourne
The shell ignores text following the # character up to the end of the line, so it used for commenting shell scripts.
Bourne Shell Variables String variables are the only type of variable provided by the Bourne shell. Variable names
4
begin with a letter and are composed of letters, digits and underscores. They do not have to be declared. They are given values by assignment or by using `read' to read a user value. The following are some examples. These can be entered interactively at your terminal or in a shell program.
the last variable will contain the rest of the line e.g.
word1
Output
The echo command is used to display messages and the values of variables. The program echo simply prints its command line parameters on the standard output. To access the value of a variable you must precede the variable name with a '$' symbol e.g.
$FILENAME
echo has one option '-n' which suppresses printing a newline after the output. This is useful for prompting for input e.g.
Variables may be used as a convenient short-hand for long strings. So to move a file to /usr/user/joe/jbin you can enter:
$ $mybin
cp
prog
The shell simply substitutes the value of the variable for each occurrence of the variable name. So you can use the variable value as part of a string:
$ $mybin/prog
ls
-l
Variables are local to your shell unless you 'mark' them for export. Program contains: prog1.sh
export mybin mybin=/usr/user/joe/jbin current=/usr/user/joe prog2.sh # Call prog2.sh echo "Back in prog1.sh'' echo "Value of mybin'' $mybin echo "Value of current'' $current
Program contains
prog2.sh
echo "Prog2.sh: echo "Value of mybin'' $mybin echo "Value of current'' $current current=/usr/user mybin=/bin echo
Shell Notes. Joe Carthy
"Value
8
of mybin'' $mybin
Usage:
$ prog1.sh
Output:
Prog2.sh: Value of mybin /usr/user/joe/jbin Value current Value mybin /bin Back prog1.sh Value of mybin /usr/user/joe/jbin Value of current /usr/user/joe
Exported variables can be accessed in shell programs (commands) executed by this shell. Note: A shell program cannot change the environment in which it was called. A subcommand can change the value of exported variables locally, but any changes made have no effect in the calling shell. Using export is similar in effect to 'call by value' in parameter passing in C. So prog2.sh can change the variable
Shell Notes. Joe Carthy
of of in
"mybin'' locally, but back in prog1.sh, the value of "mybin'' is unchanged. Also, the variable "current'' has no value in prog2.sh, since it was not marked for export in prog1.sh. The above are all examples of user-defined variables.
10
that the right number of parameters have been provided. Another useful system variable is $$. It is the process number of the current shell, which will be unique among all processes. Typically, it used to create temporary file names e.g.
> >>
The file will have a name like /tmp/record102373. A special parameter $* is used to stand for all the command line parameters except $0. So echo $* is equivalent to echo $1, $2, $3 ....... e.g.
echo $*
Then executing prog.sh:
arg2
The shift command is used to promote the command line parameters so that $2 becomes $1, $3 becomes $2 and so on. Parameter $1 is lost. Example:
12
Other variables having a special meaning to the shell are: HOME Home directory -- default argument for cd command. Set up in the password file /etc/passwd. PATH Search path for commands. A list of directories that is searched by shell for command binary files. By default it is the current directory, /bin and /usr/bin. The directory names are separated by a : e.g. PATH=.:/bin:/usr/bin: $HOME/bin This PATH would cause the current, /bin, /usr/bin and $HOME/bin directories to be searched, in that order. PS1 Primary shell prompt, by default '$' (for Bourne shell). PS2 Secondary shell prompt, by default '> ' (for Bourne shell). It is displayed when a command such as a loop is being entered over a number of lines.
13
Any of the above variables can be changed to suit user requirements. These changes would normally be made in your login command-file '.profile', which is executed once by the shell when you login. The values of these variables may be displayed with echo:
$ $PATH $ $HOME
echo echo
14
Control Flow
The shell provides the usual control flow mechanisms associated with structured programming: for, while, until, case, if-then, and ifthen-else structures.
var in word2
commands done
Example:
i in menu.c echo
lpr $i done
15
This example simply displays the name of each file in the list and sends the file to the printer. The commands between 'do' and 'done' are executed each time around the loop. ('do' and 'done' delimit the body of the loop and are also used in the while and until structures.) It is very common to use the for structure to process the command line parameters. For example, to build a create command which creates empty files, the file names being specified on the command line:
16
Usage:
to
create
17
18
A useful example to show some of the power of a very simple shell program is to develop a program called, say, tel which searches a file called phone.dat in the home directory, which has the form:
Dept.
The tel program can search for phone numbers by name or any text in the file.
Usage:
Dept.
or
$ tel 6767
Output:
19
Dept.
Code:
20
wildcard characters. (These allow you build regular expressions for pattern matching.) The commonest are * and ?. The * matches any sequence of characters, and on its own the shell will expand it into the list of filenames, in alphabetical order, that are in the current directory. Try:
22
Another example of developing a useful utility using the for structure is a shutdown procedure to shutdown the operating system, but give users a warning beforehand, so that they may save their files and logout: Usage:
$ shutdown
Code:
for i in 5 4 3 2 1 do echo "Going down in $i minutes'' | /etc/wall sleep 60 done /etc/wall <<! System Going Down Now Bye ! kill -1 1
The program goes around the for loop 5 times, sleeping for 60 seconds during each pass. 'sleep' is a program that simply causes your program to be suspended for the number of seconds specified i.e. it does not waste CPU time. The program '/etc/wall'
23
writes a message to all terminals, from its standard input. Two methods of providing the standard input to '/etc/wall' are shown. In the first, the output of echo is simply piped into the '/etc/wall' program. In the second case, a "here document'' is used. This is the text enclosed by "<<!......text.......!''. This provides a mechanism for providing input to any program from within a shell program. It is most often used to provide input for the ed editor, and the mail program. You can use any character instead of ! to start and terminate the text. The terminating character, !, in this case must be at the start of a new line. The "here document'' is to be preferred for multi-line messages. Finally, the 'kill' command is used to terminate a process, in this case, we send signal number 1 to process 1, which is the ancestor of all processes in the system (This may vary for different versions of Unix/Linux). By terminating process 1 we shutdown the
24
25
loop body are executed. An endless loop may be constructed, e.g. the asp program (anti-sleep program) to prevent the your PC from going to sleep:
#asp program-you could get bitten for using it ! while true do echo "Sleeping....... sleep 240 # Sleep for 4 minutes done
The
program sends a message to your terminal every 4 minutes and may be halted by an interrupt (Ctrl/C). Until is the opposite to while, looping as long as the condition returns false.
until false # Endless loop do echo "Sleeping..........'' sleep 240 # Sleep for 4 minutes
Shell Notes. Joe Carthy
26
done
Another example of a useful utility is the watchfor program which waits for a user to login and writes a message to his terminal and also echos a message to your terminal: Usage:
120 # every 2 mins done # User has logged on write $1 <<! Hello $1, Can you contact me ! echo $1 "has logged on''
The pipeline " who | grep
sleep check
27
$1 '' causes the output of the who command to become the input for grep, which searches it for the specified user e.g. tom in this example. If grep succeeds in finding tom in the input, then it returns an exit status of 0, which terminates the until loop. The 'write' command is used to send a message to a user's terminal. A "here document'' is used to provide the input for 'write'. In this example, we sleep for 120 seconds if the user has not logged on before going around the loop again.
28
test -s File True if File exists and is non-empty test -f File True if File exists and is not a directory test -r FileTrue if File is readable (also -w
29
test -n Str True if Str has non-zero length test str1 = str2 True if strings are equal test n1 -eq True if n1, algebraically equal n2 n2
(Also -ne, -gt, -e, -le, -lt for not equals, etc. )
You can invert the above conditions by using ! e.g.
30
Sample Usage:
-r
This code waits for a file called '/tmp/lockfile' to disappear. This could be used to implement a crude form of semaphore system, and is used in the program myprint below to provide a mechanism for preventing users from trying to access the printer simultaneously :
Usage: $ myprint file.c file2.c Code: # myprint while test -r /tmp/lockfile # Wait until printer free do sleep 5 done >/tmp/lockfile # Lock printer for i in $* do pr $i > /dev/lp # pr file on print device done rm
31
/tmp/lockfile printer
Free
This is a good example of the shell being used to create a useful "system'' program. Note it is not foolproof, two users could test for the presence of the lockfile at the same time.
32
The Structure
if-then-fi
if command
conditional-
then commands else commands fi if command then commands elif conditionalcommand then commands else commands fi Example 1 conditional-
if test $# = 0 then
Shell Notes. Joe Carthy
33
"Usage: username'' fi
Here we check if no command line parameter is entered, displaying an error message if this is so. This check should be included at the start of the "watchfor'' program shown earlier. .
34
Example 2 if test $# -ne 2 then "Invalid 2 expected'' echo arguments exit else echo " 2 parameters entered'' fi
It is often used to test conditions about files. We could use it to modify the create program developed earlier, so that it does not truncate existing files:
# safecreate program for i in $* do if test -f $i then echo $i "already exists'' else >$i done
This program loops over the command line parameters, testing if each one exists, creating a file if it does not already exist.
35
We could make the program interactive, prompting the user if the file already exists, to find out if he wishes to truncate it:
36
# safecreate2 program for i in $* do if test -f $i then echo "already exists'' echo "Truncate it (Y|N)'' read ANS if test $ANS = Y -o $ANS =y then >$i elif test $ANS = N -o $ANS =n then continue # Go around loop again else echo "Enter Y or N'' $i -n
37
38
This feature, is probably most often used when processing files in directories. Some of the files may be directories themselves and they in turn may contain subdirectories which contain subdirectories and so on. The sample program lsdir is an example. It lists the files in a directory, and lists the contents of subdirectories and so on. Usage:
$ directory-name
Example:
lsdir
$ /usr/user/year3
Code:
lsdir
# program
lsdir
PATH=/bin:/usr/bin:/use r/usr/joe/bin if test $# = 0 then lsdir . # Use current directory elif test ! -d $1 # Check if $1 is a directory
Shell Notes. Joe Carthy
39
then echo $1 "Not a directory'' ls -l $1 # List the file exit else for i in $1/* # Loop over files in $1 do if test -d $i # If it is a directory then echo "Directory: '' $i ( cd $i ; lsdir . ) else ls -l $i ordinary file fi
If no arguments are supplied to lsdir, it lists the current directory by calling itself recursively with parameter ".''. The for structure, takes the first parameter, expands it to a
Shell Notes. Joe Carthy
\#
fi done
40
list of files in that directory and loops over each file in the list. For example lsdir will give i the value '.' and loop over the values './begin.c', './bin', './c' where . contains the files 'begin.c', 'bin' and 'c'. By enclosing shell commands in parentheses, the commands are executed in a subshell, so that the cd command (which is built-in) will execute in a subshell and when that subshell is finished, we will still be in the directory where we started. ( cd $i ; lsdir $i ) This is the one of the commonest uses of parentheses. Note that PATH is set explicitly in 'lsdir', thus preventing the shell executing any other copy of lsdir that might appear in some subdirectory visited. This is a wise precaution, especially for the superuser, where someone might "plant'' their own copy of 'lsdir' to do some dastardly deed !! It may also be necessary, if 'lsdir'
41
is not in a public 'bin' ( i.e. /bin or /usr/bin ) and PATH has not been set in .profile. Another example of using "while'' and "if-then'' is illustrated in the sample monitor command. This command records a list of who logs on and what they are doing, every five minutes. The record is kept in the file '/tmp/monlist'.
42
PATH=/bin:/usr/bin monlist=/tmp/monlist if test ! -r $monlist then >$monlist fi while : do date >> $monlist echo " '' >> $monlist who >> $monlist echo " '' >> $monlist ps -a >> $monlist echo " '' >> $monlist 300 minutes done
It first checks to see if the file '/tmp/monlist' exists, creating it if it is not present. This program uses ":'' after the "while'' which always returns true, but is built-in and so is more efficient than using the 'true' program.
sleep # 5
43
The echo command is used to put blank lines in the file between the output of the various commands. The program could be executed a "daemon'', a command which is always running in the background. Finally, the system variable PATH is set to '/bin' and '/usr/bin' to reduce the amount of searching that the shell performs when looking for commands.
44
case $# in 0) Echo "No arguments supplied'' ; exit ;; 2) Echo "Correct '' ;; *) Echo "Incorrect number of arguments'' ;; esac
45
Example 2: command
An append
case $# in 1) cat >> $1 ;; 2) >> $2 < $1 ;; *) echo 'Usage: append [fromfile] to-file' ;; esac
Usage: $ append thisfile ontothatfile
cat
The append command allows you append two files when 2 arguments are supplied. With 1 argument, it appends from the standard input onto the specified file.
case $1 in -o) echo "-o option entered'' ;; -c) echo "-c option entered'' ;; esac
A range of characters may be enclosed in []
Shell Notes. Joe Carthy
46
[Yy]) echo "y or Y entered'' ;; -[oc]) echo "-o or -c options entered'' ;; You can specify alternative patterns by using | e.g Y|y|yes|Yes) echo "Y or y or yes or Yes entered'' ;; Another version of the create command shows an example.
# safecreate3 program using case for i in $* do if test -f $i then echo "already exists'' echo "Truncate it (Y|N) '' read ANS case $ANS in y|Y) >$i ;; n|N) continue ;; \# Go around loop again $i -n
47
echo
safecreate3
$ tidy *
48
Code: # Tidy program if test $# -eq 0 then echo "No files specified" exit fi # Display menu cat <<! Exit x Next file n Display file t Delete file d List file l Print file p ! # Process command line parameters for i in $* do echo "File : '' $i # Display file name FIN=n # Process each file while test $FIN = n do echo -n "Enter command: ''
49
read COM case $COM in n|N) break ;; x|X) exit ;; t|T) cat $i ;; p|P) lp $i ;; d|D) rm -i $i ;; l|L) ls -l $i ;; *) "Unknown $COM'';; esac echo echo -n "Finished this file (y/n) '' read FIN # Only n is checked done # Next command for this file done # Next file echo command
Note that it is more efficient to display the menu using cat with a "here document'' (the text to be displayed is included "here" in your file) than to use a sequence of
50
echo commands, since only one program has to be loaded when using cat, whereas if we use a list of echo commands, each command will have to be loaded and executed separately. Alternatively, the menu could be stored in a file and cat used to display it, making it easy to add a "show menu'' option to the commands: m) cat menufile ;; Break allows you break out of a loop, as in C. This is a good example of using the shell to build useful utilities. Basically, you can program it to build almost any kind of similar utility that you can think of. As in any programming language, there are numerous variations and combinations of methods to solve a problem. User creativity is the only real limitation.
Command Substitution
This is a very powerful mechanism allowing the output of a command to be used inline e.g.
today='date'
Shell Notes. Joe Carthy
51
current='pwd'
Note that the quotes are grave quotes (') and not the usual single quotes ('). The variable today gets as value the string output by the 'date' command and current the output of the 'pwd' command. So
"You "You
Mon Apr 10:20:45 GMT 1996 You /usr/user/joe You /usr/user/joe are are
4 in in
Command substitution allows you build some very useful programs. Suppose you wish to mail a group of users a particular message then if you create a file of user names called namelist:
joe
52
year2 message
The contents of the file message is sent to users appearing the file namelist. You could also use a "here document'' instead of using the file message:
If you want to loop over the files in a directory, in order of last modification, then
Shell Notes. Joe Carthy
53
you can use 'ls -t' to produce the list of files and use it inline, in a for loop:
$ expr 2 + 2 4
By executing the command inline, we can perform arithmetic on shell variables. This can be used to loop over command a specific number of times. For example, we could rewrite the shutdown command presented earlier, to use $1 as the number of minutes until shutdown time. If no command line parameters are specified then 5 is chosen as the default number of minutes until
54
shutdown Usage:
10 system mins
55
Code:
#shutdown2 program if test $# = 0 then count=5 # Default is 5 minutes else count=$1 fi i=1 tleft=$count while test $i -le $count do echo "System going down in $tleft minutes'' | /etc/wall i='expr $i + 1' # increment i tleft='expr $tleft - 1' sleep 60 done echo "System going down now !!!!!'' | /etc/wall kill -1 1 Output: System going down in 10 minutes System going down in 9 minutes .... System going
Shell Notes. Joe Carthy
56
57
echo The backslash character: \\ yields The star character: * The backslash character: \ 2) Single quotes ' ' are used to quote a group of characters: echo '***** Warning **** ???? ' displays ***** Warning **** ???? 3) Double quotes are also used but only quote file generation characters * and ? e.g. echo ''$HOME has no filenames with a * in them'' displays /usr/user/j
Shell Notes. Joe Carthy
58
oe has no filenames with a * in them Variable and parameter substitution are carried out inside double quotes, no substitutions are performed inside single quotes. Here Documents We have already seen the use of "here documents''. One very common use is to prepare edit scripts for the ed editor. Instead of using ed interactively, it can be used by placing ed commands in a file and redirecting the input to come from the file or from a here document: ed file.txt < ed-script where ed-script contains editor commands. This is useful where the same commands have to be applied to a number of files:
added to the password file /etc/passwd for the user specifying user name, group, number etc. Password entries have the general form: uname:pwd:uid:gid:misc:ho me:shell user: user name pwd: encrypted password, empty if not set uid: gid: user id. number group id number
misc: usually full name or address -- any text home: login directory shell: optional, defaults to Bourne shell e.g. tom:xHyzio89ws:68:10:Tom Thumb X2134:/usr/user/tom:/bin/s h A directory must be created for the user (his home directory), and the user made the owner of this directory. The protection on this directory may be set appropriately, and the user added to the appropriate group. A command called {\bf mkuser} can easily be
Shell Notes. Joe Carthy
62
programmed to accomplish the task. A simple form of 'mkuser' is given below. No checks are carried out to see if the name is already in use and so on, but these could easily be added. It also assumes that users will all have gid (group identifier) 10. Only the superuser can execute 'mkuser'.
63
Usage: $ mkuser jack 92 Code: # mkuser program: expects two parameters if test $# ! = 2 then echo "Usage: mkuser user-name user-id'' exit fi # Add entry password file to
ed /etc/passwd <<! .a $1::$2::10::/usr/us er/$1:/bin/sh . w q ! mkdir /usr/user/$1 chown $1 /usr/user/$1 chgrp user /user/usr/$1 chmod go-w /usr/user/$1
This is a useful command especially since the password file entry is
Shell Notes. Joe Carthy
64
complicated and it would be easy to make a mistake if you edit it by hand. Note the "$'' is not quoted in the "here document'' since we want the shell to replace it by the value of $1.
Signal Handling
In Unix, programs are sent signals by the kernel in the event of a program error, (such as divide by zero, or memory addressing error) or when the user hits the interrupt key (CTRL C), the quit key (CTRL \), logs out (Hangs up) or the user generates a signal with the 'kill' command. When a program receives any signal, the default action is to terminate, which is usually desirable. However, there are times when, control should be returned to the program, where appropriate action can be taken for a given signal. This allows the program delete temporary files before termination, or the program may be restarted at a suitable point, or finally the signal may be ignored and the program continues running. For this reason, the shell allows the user to "trap'' signals and take
Shell Notes. Joe Carthy
65
appropriate action. (This can also be done from a C program via the signal system call.) Each signal has an associated number, there are approximately 19 different signals numbered from 1 upwards. Some of the common ones are: Signal Number Signal Name Generated by 1 2 3 9 -9 pid 10 Hangup logout Interrupt Ctrl/C Quit Ctrl \ (sure) kill kill error
The trap command allows the user trap the signal ignore it or take action: trap 'rm /tmp/lockfile ; exit' 1 2 3 15 In the 'lpr' command we created a lock file to prevent concurrent access to the printer. However, if the lpr command is interrupted, the lock file will not be deleted, and the
66
printer will be unusable. The above trap command illustrates how to prevent this situation. It can be interpreted as "if any of signals 1, 2, 3, 15 are received, then execute:
rm /etc/lockfile ; exit
thus freeing the printer and terminating lpr. This line should be inserted at the start of the lpr program. Signals can be ignored by:
trap '' 1 2 3
trap 1 2 3
As an exercise you could rewrite the 'asp' program, to read a user code-word, go into the sleep loop as before, and when interrupted, prompt for the code-word terminating if it matches that previously entered, otherwise returning to the sleep loop. Efficiency of Shell Programs The efficiency of shell programs can be improved in a number of ways. By setting the value of PATH so that the shell only searches the necessary
Shell Notes. Joe Carthy
67
directories, in the right order is one method. The use of built-in commands where possible such as ":'' instead of the program 'true' is another improvement. Another point is that many Unix utilities such as 'cat', 'rm', 'ls' will themselves loop over their command line arguments. Where possible, you should let the utility do this, instead of writing:
cat $*
When you use a shell loop, then the shell must locate and load the program each time around the loop. When you write 'cat $*' the 'cat' program is only loaded once, and it performs the argument processing (after the shell has expanded $*). The use of a "here document'' instead of a series of 'echo' commands is also worthwhile. Exec and Commands
(dot)
68
Normally when the shell executes user commands, it forks a subprocess for each command. When the command finishes, the subprocess dies and control returns to the shell. There are two ways to execute programs without creating new subprocesses. A shell program may overlay itself with another program by using the exec command: exec newprog #Never get to here unless cannot run newprog The program 'newprog' simply replaces the program of the current process and is executed as part of the current process. It not used very frequently. A shell program can run another shell program as part of the current shell (i.e. a new process is not created to run it) by using the ".'' command: other shell command(s) .newprog # Execution resumes here Here the calling program is not overlayed. It is similar to calling a subroutine. When the called program
69
'newprog' terminates, control returns to the calling program. This allows the new program to change variables in the current shell. This is why the login command file is called '.profile', it is executed as part of the user's shell so that variables that are set in '.profile' remain set when '.profile' terminates. Debugging Shell Programs Finally, when debugging shell programs, you can invoke them "verbosely'' with sh -v prog which causes the program lines to be printed as they are being read. This is useful for finding syntax errors. You can achieve the same effect by by entering set -v in your program. You can use set -n if you want to switch execution off, to test the procedure without running it. set -x
70
provides an execution trace. All flags may be turned off by: set -
71