curl -O -s https://www.labs.cs.uregina.ca/330/Shell/Lab11.zip unzip Lab11.zip
There are several different shells, they offer their own advantages and disadvantages. For instance, some allow for auto completion using the tab key; others don't.
A few common shells are the following:
For more on shells, see http://docstore.mik.ua/orelly/linux/lnut/ch08_07.htm.
To see what shells exist on your current Unix system, try the following command:
$ cat /etc/shells
From the command line, you can type various commands which have the same responses in each of these shells. For instance chmod, ls, cd, and touch.
Let's say that you want to type the same commands over and over again. If that it the case, it is good to create a shell script.
For example, you could create a basic shell script by:
ls -l mkdir tom cd tom touch greta monica george
chmod +x people.sh
% people.sh
GOOD HABIT- To better identify your shell scripts from other files you can add .sh as an extension (however, it is not required) |
---|
Executing people.sh will do the following: first do a long listing of the current directory, then make a directory called tom, and create files: greta, monica, and george under the tom directory.
We can also add some comments to this file. Any line preceded with # is a comment (or anything after the # is ignored):
#list the files in the current directory ls -l #make a directory called tom mkdir tom #change into the tom directory and create three files. cd tom touch greta monica george
I've led you to believe that all shells are created equal. That is not the case. There is a major split between Bourne-compatible shells (such as sh and bash) and csh-compatible shells (such as csh and tcsh).
These differences show up in such things as conditional statements and loops. The following table shows the basic syntax for an if and while statement in both bash and tcsh
bash | tcsh |
---|---|
if [expression] then commands fi |
if (expression) then commands endif |
while [expression] do commands done |
while (expression) commands end |
The difference is very subtle, but it is enough to create problems.
For the remainder of this lab, we will be focusing on bash shell scripts.
In the first section, we stated that there are several shells and corresponding shell scripts. The question is: how do you indicate which shell environment you are using to execute the commands?
The answer is: there is a special notation used on the first line of the shell script. The line begins with a number sign (#), then an exclamation point (!), followed by the full path name of the shell on the computer's file system.
For instance, to state that you will interpret the above commands with the bash shell (on our Linux system), you would add the following to the beginning of the file:
#!/bin/bash ls -l mkdir tom cd tom touch greta monica george
The location of this bash shell is dependent on the system. You can verify the location by using:
$ cat /etc/shells
For instance, on os1 and os2, you will use:
#!/bin/bash(This also works for Linux)
There are a couple of different stories:
What would a program be without variables? Bash shells have their own way of handling variables.
Some rules about variables are the following:
There is a difference in shell scripts between the name of a variable and its value:
Assignment can be made in three major ways:
Note 1: When setting variables in bash shells, make sure that there are no spaces on either side of the equal sign. Note2: $myvar is actually a simplified form of ${myvar}. Some situations may require the longer usage. |
---|
An example of assigning and using variables is the following code (echo allows us to print to the screen):
#!/bin/bash #file: variables.sh var1="My string with spaces" echo "var1 is: " $var1 echo echo "Enter a number: " read var2 echo "var2 is: " $var2 echo for var3 in 1 2 3 do echo "var3 is: " $var3 done
A sample run would yield the following results:
% variables.sh
var1 is: My string with spaces
Enter a number:
30
var2 is: 30
var3 is: 1
var3 is: 2
var3 is: 3
In the topic of quotes, we will discuss four special characters:
Double quotes do not interfere with variable substitution. They are sometimes referred to as weak quotes.
For instance:
var1=20 echo "This is $var1"
Will print: This is 20
Single quotes cause the variable name to be used literally. They prevent the shell from interpreting special characters. This is sometimes known as full quoting or strong quoting.
For instance:
var2=30 echo 'This is $var2'
Will print: This is $var2
The backslash is another way of preventing the shell from interpreting special characters. It only works on the character immediately following the backslash. We can also say that the backslash escapes the special meaning of the succeeding character.
For instance:
#Escape the normal meaning of the space character var3=The\ price\ is-- #Escape the normal meaning of a $ var4=\$20.00 echo $var3 $var4
Will print : The price is-- $20.00
Note that without the escape from the space, the shell will try and interpret "price" and "is--" as commands.
Back quotes enable an expression to be evaluated as a command, and replaced with whatever the expression prints to its standard output. Back quotes are sometimes also known as back ticks.
For instance:
var5=`date` echo "The date is $var5"
Will print: The date is Wed Nov 24 20:38:09 CST 2004
Note that on the keyboard, the (`) key is usually on the same key as the (~)There are two conditional structures:
Before we talk about the if statement, we need to talk about the test command that is used to evaluate conditional expressions.
The basic format is:
test expression
or
[ expression ]
Where expression has built-in operators. The operators are classified into four different groups:
The following chart summarizes these operators:
Integer Comparisons | |
int1 -gt int2 | Greater-than |
int1 -lt int2 | Less-than |
int1 -ge int2 | Greater-than-or-equal-to |
int1 -le int2 | Less-than-or-equal-to |
int1 -eq int2 | Equal |
int1 -ne int2 | Not-equal |
String Comparisons | |
-z str | Returns true if length of str is equal to zero |
-n str | Returns true if str length is greater than zero |
str1 = str2 | Equal strings |
str1 != str2 | Not-equal strings |
str | Returns true if str is not null |
Logical Operations | |
expr1 && expr2 | Logical AND (true if expr1 and expr2 are true) |
expr1 || expr2 | Logical OR (true if expr1 or expr2 are true) |
! expr | Logical NOT |
File Tests (See TLDP.org for more) | |
-f file | File exists and is a regular file |
-s file | File is not empty |
-r file | File is readable |
-w file | File can be written to, modified |
-x file | File is executable |
-d file | Filename is a directory name |
-h file | Filename is a symbolic link |
-c file | Filename references a character device |
-b file | Filename references a block file |
The If Statement. The basic format for the if statement is the following:
if condition ; then commands [elif condition ; then commands] ... [else commands] fi
The statements shown in square brackets are optional. Notice that you end the if with a fi
The Case Statement. The basic format for the case statement is the following:
case expression in pattern1) commands;; pattern2) commands;; *) commands;; esac
Some sample code making use of if and case statements is the following
#!/bin/bash #file: conditions.sh if [ -f ./conditions.sh ]; then echo "file exists" fi #notice this read statement includes the prompt read -p "Enter a letter " var1 if test $var1 = 'a' ; then echo "a selected" else echo "a not selected" fi read -p "Enter a string " var2 case $var2 in [Hh][Ee][Ll][Ll][Oo]) echo "hello found";; *bye|good*) echo "something starting with good or ending with bye found";; *) echo "nothing found";; esac read -p "Enter a number " var3 if [ $var3 -gt 0 ] && [ $var3 -lt 10 ] ; then echo "1 to 9" elif [ $var3 -gt 9 ] && [ $var3 -lt 20 ] ; then echo "10 to 19" elif [ $var3 -gt 19 ] || [ $var3 -eq 0 ] ; then echo "greater than 19 or equal to 0" else echo "other range" fi
A "condition" can also be any command. If a command returns a zero exit status, the condition is true; otherwise, the condition is false. Or, if you need to check more carefully, you can access the return value through the automatic variable $?. You can write things like the following:
#!/bin/bash #file: conditions2.sh user=temp0 if grep -q $user /etc/passwd; then echo "$user has an account" else echo "$user doesn't have an account" fi read -p "Let's try another username: " user # -q flag is for quiet mode (output supressed) grep -q $user /etc/passwd case $? in 0) echo "$user has an account" ;; 1) echo "$user doesn't have an account" ;; *) echo "An error occurred" ;; esac
Note: When using the square brackets, ensure that there is a space before and after the bracket. |
---|
We will go over the syntax of two looping structures:
The While Loop. The basic format for the while loop is the following:
while condition; do commands done
As you would expect, the while loop executes the commands as long as the condition is true.
The For Loop. The basic format for the for loop is the following:
for var in list; do commands done
The for loop iterates over all of the elements in a list. As it is iterating, var will be assigned the value of each item in the list in turn.
The following is an example of these two looping structures:
#!/bin/bash #file: loops.sh IFS=$'\n' #A while loop with user input read -p "Enter a letter or x to quit " var1 while [ $var1 != "x" ] ; do read -p "Enter a letter or x to quit " var1 done #A for loop echoing the contents of file if [ ! -f /usr/include/asm-generic/errno-base.h ] ; then exit 1 else for var2 in `cat /usr/include/asm-generic/errno-base.h`; do #get the error name and number var3=`echo $var2 | awk -F\ '{print $2 "," $3}'` #get the description descr=`echo $var2 | cut -d "/" -f 2` #get rid of extra '*' descr=${descr/\* /} descr=${descr/ \*/} echo "$var3,$descr" done fi
Sometimes it is useful to strip off parts of a file path or name that match a pattern. You have the following at your disposal:
The following example changes all *.JPG files to *.jpg and truncates the home path
#!/bin/bash # file: truncate.sh #first create some bogus *.JPG files touch me.JPG you.JPG harry.JPG #view them ls -l *.JPG #rename them for i in `ls *.JPG` # OR for i in *.JPG do newname=${i%.*} echo $newname newer=$newname.jpg mv $i $newer done #display your current working directory var1=$PWD echo $var1 newpath=${var1#/home/hercules} echo $newpathA sample run does the following:
% truncate.sh -rw------- 1 temp0 temp 0 Nov 25 01:37 harry.JPG -rw------- 1 temp0 temp 0 Nov 25 01:37 me.JPG -rw------- 1 temp0 temp 0 Nov 25 01:37 you.JPG harry me you /home/hercules/t/temp0/330 /t/temp0/330
If you are having trouble understanding the difference between the shortest part and the longest part of the pattern, you can refer to the following resource for more examples: https://tldp.org/LDP/abs/html/parameter-substitution.html. You can also try the following:
#!/bin/bash #file: truncate2.sh echo $PWD echo "${PWD##*/}" echo "${PWD#*/}" echo "${PWD%/*}" echo "${PWD%%/*}"
The last one may surprise you. Why?
The above code could also be replaced by a substitution pattern. You have a choice of two pattern replacements:
In both of these situations, if replacement is omitted, then patt is replaced by nothing, that is, deleted.
#!/bin/bash # file: substitute.sh #first create some bogus *.JPG files touch me.JPG you.JPG harry.JPG #view them ls -l *.JPG #rename them for i in `ls *.JPG` do newname=${i/JPG/jpg} echo $newname mv $i $newname done #display your current working directory var1=$PWD echo $var1 newpath=${var1/\/home\/hercules/} echo $newpath
Sometimes you might perform arithmetic operations on variables. To do this, you use the let command in combination with an arithmetic expression.
You have two kinds of operators to use with the let:
The following table summarizes some of these operators:
Arithmetic Operators | |
* | multiplication |
/ | division |
+ | addition |
- | subtraction |
% | modulo-results in the remainder of a division |
++ | increment operator |
-- | decrement operator |
Relational Operators | |
> | greater-than |
< | less-than |
>= | greater-than-or-equal-to |
<= | less-than-or-equal-to |
= | equal in expr |
== | equal in let |
!= | not-equal |
& | logical AND |
| | logical OR |
! | logical NOT |
An example of several of the uses of let are in the following sample code:
#!/bin/bash # file: arithmetic.sh num1=3 num2=10 let res=$num1+$num2 #note no spaces echo "num1 + num2 = $res" let "res2= $num1 + 5" #note how to make it work with spaces echo "num1 + 5 = $res2" #Decrement or increment #let res2++ #increment operator let res2-- #decrement operator echo $res2 #Logical assignment #let "mybool=$res2<$res" #needs quotes to escape the pipe meaning of < #let mybool=$res2\<$res #or backslash let mybool=$res2\>$res echo $mybool
A sample run of the program would look like this:
% arithmetic.sh num1 + num2 = 13 num1 + 5 = 8 7 0
Note that you cannot just say: res=$num1+$num2, because it interprets it as a string "3+10". You must use the let keyword.
When you run a shell program that has command line options, each of the options are stored in a positional parameter. The idea is much the same as the argv[] in C programming. For instance, the command name (the name of the shell program) is stored in a variable called 0, the first command line argument is stored in a variable called 1, the second argument is stored in a variable called 2, and so on.
The following table contains a list of built in variables that are related to the command line. This table was taken from: http://lib.ru/LINUXGUIDE/linux_survive/lsg26.htm
Variable | Use |
$# | Stores the number of command line arguments that were passed to the shell program |
$? | Stores the exit value of the last executed command |
$0 | Stores the first word of the entered command, which is the name of the shell program |
$* | Stores all the arguments that were entered on the command line ("$1 $2 ...") |
"$@" | Stores all arguments that were entered on the command line, individually quoted ("$1" "$2" ...) |
A sample program using the positional parameters is the following:
#!/bin/bash #command.sh if [ $# -lt 3 ] ; then echo "Please include three command line arguments" else echo "First one: $1" echo "Second one: $2" echo "Third one: $3" fi
A sample run might do the following:
% command.sh testing this command First one: testing Second one: this Third one: command
In bash shell scripts, you can use pipes and I/O redirections as you would on the command line. For instance, you can combine commands (such as awk and grep) with the pipe (|), and you can redirect standard output or standard error to files.
You have use of three default file descriptors:
The following chart summarizes some redirectors (taken from Linux in a Nutshell). Commands in blue are csh only, whereas commands in green are bash only.
Redirector | Function |
---|---|
> file | Direct standard output to file. |
< file | Take standard input from file. |
cmd1 | cmd2 | Pipe; take standard output of cmd1 as standard input to cmd2. |
>> file | Direct standard output to file; append to file if it already exists. |
>| file | Force standard output to file even if noclobber is set. |
n>| file | Force output from the file descriptor n to file even if noclobber is set. |
<> file | Use file as both standard input and standard output. |
<< text | Read standard input up to a line identical to text (text can be stored in a shell variable). Input is usually typed on the screen or in the shell program. Commands that typically use this syntax include cat, echo, ex, and sed. If text is enclosed in quotes, standard input will not undergo variable substitution, command substitution, etc. |
n> file | Direct file descriptor n to file. |
n< file | Set file as file descriptor n. |
>&n | Duplicate standard output to file descriptor n. |
<&n | Duplicate standard input from file descriptor n. |
&>file | Direct standard output and standard error to file. |
<&- | Close the standard input. |
>&- | Close the standard output. |
n>&- | Close the output from file descriptor n. |
n<&- | Close the input from file descriptor n. |
The following program provides a sample of three of these uses:
#!/bin/bash # file: pipes.sh #To redirect standard output to standard error: #only for this one command echo echo "Redirecting stdout to stderr" echo "Usage error: see administrator" 1 >&2 #send the files found (stdout) to a file "filelist" #send standard error to a file "no_access" #try the find without the redirection to see what the output looks like echo echo "Using find..." echo "Redirecting stdout to \"filelist\" and stderr to \"no_access\"" find / -name "linux" -print >filelist 2>no_access #a pipeline with two filters #prints out temp0's home directory echo echo "Using a pipe to find temp0's home directory" grep temp0 /etc/passwd | awk -F: '{print $7}'