Shell script notes

Shell script notes

I used this tutorial to learn shell scripting: Shell Scripting Tutorial-Steve Parker. I given my executing result for almost every program, and sometimes I alternated the original program or wrote some new program.

All the programs related are pushed to my Github site: Codsir-shell_notes.

Escape Characters

", $, ` and \ are still interpreted by the shell, even when they‘re in double quotes.

$echo "A quote is \", backslash is \\, backtick is \`."
A quote is ", backslash is \, backtick is `.
$X=5
$echo "A few spaces are    ; dollar is \$. \$X is ${X}."
A few spaces are    ; dollar is $. $X is 5.

Loops

For

#!/bin/sh
for i in 1 2 3 4 5
do
  echo "Looping ... number $i"
done
OUTPUT
Looping .... number 1
Looping .... number 2
Looping .... number 3
Looping .... number 4
Looping .... number 5
#!/bin/sh
for i in hello 1 * 2 goodbye 
do
  echo "Looping ... i is set to $i"
done

OUTPUT:
Looping ... i is set to hello
Looping ... i is set to 1
Looping ... i is set to anaconda3
Looping ... i is set to bin
Looping ... i is set to config_hicpro.txt
Looping ... i is set to dead.letter
Looping ... i is set to Desktop
Looping ... i is set to Documents
Looping ... i is set to Downloads
Looping ... i is set to examples.desktop
Looping ... i is set to ftp
Looping ... i is set to Music
Looping ... i is set to opt
Looping ... i is set to Pictures
Looping ... i is set to Public
Looping ... i is set to Software
Looping ... i is set to Templates
Looping ... i is set to Videos
Looping ... i is set to 2
Looping ... i is set to goodbye

while

What happens here, is that the echo and read statements will run indefinitely until you type "bye" when prompted.

#!/bin/sh
INPUT_STRING=hello
while [ "$INPUT_STRING" != "bye" ]
do
  echo "Please type something in (bye to quit)"
  read INPUT_STRING
  echo "You typed: $INPUT_STRING"
done

This example uses the case statement. It reads from the file myfile, and for each line, tells you what language it thinks is being used. Each line must end with a LF (newline) - if cat myfile doesn‘t end with a blank line, that final line will not be processed.

#!/bin/sh
while read f
do
  case $f in
	hello)		echo English	;;
	howdy)		echo American	;;
	gday)		echo Australian	;;
	bonjour)	echo French	;;
	"guten tag")	echo German	;;
	*)		echo Unknown Language: $f
		;;
   esac
done < myfile
while.txt:
hello
darkness my old friend
bonjour
I‘ve come to talk with you

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./while_loop2.sh 
English
Unknown Language: darkness my old friend
French
Unknown Language: I‘ve come to talk with you
Unknown Language:

Tips

The following codes have the same effect

mkdir rc{0,1,2,3,4,5,6,S}.d

for runlevel in 0 1 2 3 4 5 6 S
do
  mkdir rc${runlevel}.d
done

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes/test$ tree
.
├── rc0.d
├── rc1.d
├── rc2.d
├── rc3.d
├── rc4.d
├── rc5.d
├── rc6.d
├── rcdir
└── rcS.d

9 directories, 0 files

This can be done recursively

$ cd /
tiger@tiger-OptiPlex-990:/$ ls -ld {,usr,usr/local}/{bin,sbin,lib}
drwxr-xr-x   2 root root  4096 8月  20 09:31 /bin
drwxr-xr-x  22 root root  4096 3月  31 13:07 /lib
drwxr-xr-x   2 root root 12288 8月  21 06:50 /sbin
drwxr-xr-x   2 root root 69632 8月  30 06:15 usr/bin
drwxr-xr-x 171 root root 12288 8月  29 21:26 usr/lib
drwxr-xr-x   2 root root  4096 8月   9 09:15 usr/local/bin
drwxr-xr-x   8 root root  4096 8月  28 19:04 usr/local/lib
drwxr-xr-x   2 root root  4096 8月   1  2017 usr/local/sbin
drwxr-xr-x   2 root root 12288 8月  29 19:47 usr/sbin

Test

Test is used by virtually every shell script written. It may not seem that way, because test is not often called directly. test is more frequently called as [. [ is a symbolic link to test, just to make shell programs more readable. It‘s actually a program.

tiger@tiger-OptiPlex-990:~$ type [
[ is a shell builtin
tiger@tiger-OptiPlex-990:~$ which [
/usr/bin/[
tiger@tiger-OptiPlex-990:~$ whereis [
[: /usr/bin/[ /usr/share/man/man1/[.1.gz
tiger@tiger-OptiPlex-990:~$ ls -l /usr/bin/[
-rwxr-xr-x 1 root root 51920 3月   3  2017 /usr/bin/[
tiger@tiger-OptiPlex-990:~$ ls -l /usr/bin/test
-rwxr-xr-x 1 root root 47824 3月   3  2017 /usr/bin/test


NOTICE: just like ls and other programs, so it must be surrounded by spaces!!!

if SPACE [ SPACE "$foo" SPACE = SPACE "bar" SPACE ] # work
if [$foo = "bar" ]                                  # not work

NOTICE: Some shells also accept "==" for string comparison; this is not portable, a single "=" should be used for strings, or "-eq" for integers.

If...then...else...

if [ ... ]
then
  # if-code
else
  # else-code
fi


if [ ... ]; then
  # do something
fi


if  [ something ]; then
 echo "Something"
 elif [ something_else ]; then
   echo "Something else"
 else
   echo "None of the above"
fi


Note that fi is if backwards! This is used again later with case and esac.

test.sh

#!/bin/sh
if [ "$X" -lt "0" ]
then
  echo "X is less than zero"
fi
if [ "$X" -gt "0" ]; then
  echo "X is more than zero"
fi
[ "$X" -le "0" ] &&       echo "X is less than or equal to  zero"
[ "$X" -ge "0" ] &&       echo "X is more than or equal to zero"
[ "$X" = "0" ] &&       echo "X is the string or number \"0\""
[ "$X" = "hello" ] &&       echo "X matches the string \"hello\""
[ "$X" != "hello" ] &&       echo "X is not the string \"hello\""
[ -n "$X" ] &&       echo "X is of nonzero length"
[ -f "$X" ] &&       echo "X is the path of a real file" ||       echo "No such file: $X"
[ -x "$X" ] &&       echo "X is the path of an executable file"
[ "$X" -nt "/etc/passwd" ] &&       echo "X is a file which is newer than /etc/passwd"


tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./test.sh
X is more than zero
X is more than or equal to zero
X is not the string "hello"
X is of nonzero length
No such file: 5


Simpler way of writing IF statements:The && and || commands give code to run if the result is true, or false, respectively.

#!/bin/sh
[ $X -ne 0 ] && echo "X isn‘t zero" || echo "X is zero"
[ -f $X ] && echo "X is a file" || echo "X is not a file"
[ -n $X ] && echo "X is of non-zero length" ||       echo "X is of zero length"

Some characters and their meanings

characters meaning
-lt less than
-gt greater than
-nt newer than
-n "$X" if X is of nonzero length
-f "$X" if X is the path of a real file
-x "$X" if X is the path of an executable file
-a -e both meaning "file exists"
-S file is a Socket
-ot file is older than
-ef paths refer to the same file
-O file is owned by my user
echo -en "Please guess the magic number: "
read X
echo $X | grep "[^0-9]" > /dev/null 2>&1
if [ "$?" -eq "0" ]; then
  # If the grep found something other than 0-9
  # then it‘s not an integer.
  echo "Sorry, wanted a number"
else
  # The grep found only 0-9, so it‘s an integer. 
  # We can safely do a test on it.
  if [ "$X" -eq "7" ]; then
    echo "You entered the magic number!"
  fi
fi

test in while loops

#!/bin/sh
X=0
while [ -n "$X" ]
do
  echo "Enter some text (RETURN to quit)"
  read X
  echo "You said: $X"
done

#!/bin/sh
X=0
while [ -n "$X" ]
do
  echo "Enter some text (RETURN to quit)"
  read X
  if [ -n "$X" ]; then
    echo "You said: $X"
  fi
done

Case

talk.sh

#!/bin/sh

echo "Please talk to me ..."
while :
do
  read INPUT_STRING
  case $INPUT_STRING in
	hello)
		echo "Hello yourself!"
		;;
	"how are you")
		echo "fine,how are you"
		;;
	"what is your name")
		echo "tiger machine, how about you"	      
		;;
	"i am tired")
		echo "take a rest"
		;;		    
	bye)
		echo "See you again!"
		break
		;;
	*)
		echo "Sorry, I don‘t understand"
		;;
  esac
done
echo 
echo "That‘s all folks!"


tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./talk.sh
Please talk to me ...
i am tired
take a rest
how are you
fine,how are you
what is your name
tiger machine, how about you
tiger
Sorry, I don‘t understand
bye
See you again!

That‘s all folks!


Variables

  • The first set of variables we will look at are $0 .. $9 and $#.
  • The variable $0 is the basename of the program as it was called.
  • $1 .. $9 are the first 9 additional parameters the script was called with. The variable $@ is all parameters $1 .. whatever. The variable $*, is similar, but does not preserve any whitespace, and quoting, so "File with spaces" becomes "File" "with" "spaces". This is similar to the echo stuff we looked at in A First Script. As a general rule, use $@ and avoid $*.
  • $# is the number of parameters the script was called with.

var3.sh

#!/bin/sh
echo "I was called with $# parameters"
echo "My name is $0"
echo "My first parameter is $1"
echo "My second parameter is $2"
echo "All parameters are $@"

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./var3.sh 
I was called with 0 parameters
My name is ./var3.sh
My first parameter is 
My second parameter is 
All parameters are 
tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./var3.sh  hello darkeness my old friend
I was called with 5 parameters
My name is ./var3.sh
My first parameter is hello
My second parameter is darkeness
All parameters are hello darkeness my old friend


What is the function of shift?

var4.sh

#!/bin/sh
while [ "$#" -gt "0" ]
do
  echo "\$1 is $1"
  shift
done      

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./var4.sh 1 2 3 4 5 6 7 8 9 10 11 
$1 is 1
$1 is 2
$1 is 3
$1 is 4
$1 is 5
$1 is 6
$1 is 7
$1 is 8
$1 is 9
$1 is 10
$1 is 11


Another special variable is $?. This contains the exit value of the last run command.

#!/bin/sh
/usr/local/bin/my-command
if [ "$?" -ne "0" ]; then
  echo "Sorry, we had a problem there!"
fi

will attempt to run /usr/local/bin/my-command which should exit with a value of zero if all went well, or a nonzero value on failure. We can then handle this by checking the value of $? after calling the command. This helps make scripts robust and more intelligent. Well-behaved applications should return zero on success.

One of the main causes of the fall of the Roman Empire was that, lacking zero, they had no way to indicate successful termination of their C Programs. -----Robert Firth

The other two main variables set for you by the environment are $$ and $!. These are both process numbers. The $$ variable is the PID (Process IDentifier) of the currently running shell. This can be useful for creating temporary files, such as /tmp/my-script.$$ which is useful if many instances of the script could be run at the same time, and they all need their own temporary files.

The $! variable is the PID of the last run background process. This is useful to keep track of the process as it gets on with its job.

Another interesting variable is IFS. This is the Internal Field Separator. The default value is SPACE TAB NEWLINE, but if you are changing it, it‘s easier to take a copy.

var5.sh

#!/bin/sh
old_IFS="$IFS"
IFS=:
echo "Please input some data separated by colons ..."
read x y z
IFS=$old_IFS
echo "x is $x y is $y z is $z"

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./var5.sh
Please input some data separated by colons ...
hello:darkness:my old friend
x is hello y is darkness z is my old friend

name.sh

#!/bin/sh
echo -en "What is your name [ `whoami` ] "
read myname
if [ -z "$myname" ]; then
  myname=`whoami`
fi
echo "Your name is : $myname"

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./name.sh
-en What is your name [ tiger ] 

Your name is : tiger
tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./name.sh
-en What is your name [ tiger ] 
tiger zhang
Your name is : tiger zhang
tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ whoami
tiger


Passing the "-en" to echo tells it not to add a linebreak (for bash and csh).

This could be done better using a shell variable feature. By using curly braces and the special ":-" usage, you can specify a default value to use if the variable is unset:

echo -en "What is your name [ `whoami` ] "
read myname
echo "Your name is : ${myname:-`whoami`}"

The more canonical example is to use fixed text, like this:

echo "Your name is :${myname:-Tiger Zhang}"

There is another syntax, ":=", which sets the variable to the default if it is undefined:

echo "Your name is :${name:=Tiger Zhang}"

External Programs

backtick(`)

The backtick (`)is also often associated with external commands. Because of this, we will discuss the backtick first. The backtick is used to indicate that the enclosed text is to be executed as a command.

tiger@tiger-OptiPlex-990:~$ grep "^${USER}:" /etc/passwd | cut -d: -f5
Tiger Zhang,,,
tiger@tiger-OptiPlex-990:~$ MYNAME=`grep "^${USER}:" /etc/passwd | cut -d: -f5`
tiger@tiger-OptiPlex-990:~$ echo $MYNAME
Tiger Zhang,,,
tiger@tiger-OptiPlex-990:~$ whoami
tiger


It can also improve performance if you want to run a slow command or set of commands and parse various bits of its output. I executed the two following programs, and the second is really much faster than the first one.

backtick.sh

#!/bin/sh
find / -name "*.html" -print | grep "/index.html$"
find / -name "*.html" -print | grep "/contents.html$"

backtick1.sh

#!/bin/sh
HTML_FILES=`find / -name "*.html" -print`
echo "$HTML_FILES" | grep "/index.html$"
echo "$HTML_FILES" | grep "/contents.html$"

Output:

root@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes# time ./backtick.sh

...
real	0m6.331s
user	0m2.457s
sys	0m2.673s

root@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes# time ./backtick1.sh


...
real	0m3.132s
user	0m1.177s
sys	0m1.360s


Note: the quotes around $HTML_FILES are essential to preserve the newlines between each file listed. Otherwise, grep will see one huge long line of text, and not one line per file.

Functions

A function may return a value in one of four different ways:

  • Change the state of a variable or variables
  • Use the exit command to end the shell script
  • Use the return command to end the function, and return the supplied value to the calling section of the shell script
  • echo output to stdout, which will be caught by the caller just as c=expr $a + $b is caught

NOTE:a shell function cannot change its parameters, though it can change global parameters.

function.sh

#!/bin/sh
# A simple script with a function...

add_a_user()
{
  USER=$1
  PASSWORD=$2
  shift; shift;
  # Having shifted twice, the rest is now comments ...
  COMMENTS=$@
  echo "Adding user $USER ..."
  echo useradd -c "$COMMENTS" $USER
  echo passwd $USER $PASSWORD
  echo "Added user $USER ($COMMENTS) with pass $PASSWORD"
}

###
# Main body of script starts here
###
echo "Start of script..."
add_a_user bob letmein Bob Holness the presenter
add_a_user fred badpassword Fred Durst the singer
add_a_user bilko worsepassword Sgt. Bilko the role model
echo "End of script..."

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./function.sh
Start of script...
Adding user bob ...
useradd -c Bob Holness the presenter bob
passwd bob letmein
Added user bob (Bob Holness the presenter) with pass letmein
Adding user fred ...
useradd -c Fred Durst the singer fred
passwd fred badpassword
Added user fred (Fred Durst the singer) with pass badpassword
Adding user bilko ...
useradd -c Sgt. Bilko the role model bilko
passwd bilko worsepassword
Added user bilko (Sgt. Bilko the role model) with pass worsepassword
End of script...


Scope of variables

scope.sh

#!/bin/sh

myfunc()
{
  echo "I was called as : $@"
  x=2
}

### Main script starts here 

echo "Script was called with $@"
x=1
echo "x is $x"
myfunc 1 2 3
echo "x is $x"

tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./scope.sh a b c d
Script was called with a b c d
x is 1
I was called as : 1 2 3
x is 2

Functions cannot change the values they have been called with, either - this must be done by changing the variables themselves, not the parameters as passed to the script.

function1.sh

#!/bin/sh

myfunc()
{
  echo "\$1 is $1"
  echo "\$2 is $2"
  # cannot change $1 - we‘d have to say:
  # 1="Goodbye Cruel"
  # which is not a valid syntax. However, we can
  # change $a:
  a="Goodbye Cruel"
}

### Main script starts here 

a=Hello
b=World
myfunc $a $b
echo "a is $a"
echo "b is $b"


tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./function1.sh
$1 is Hello
$2 is World
a is Goodbye Cruel
b is World

Recursion

factorial.sh

#!/bin/sh

factorial()
{
  if [ "$1" -gt "1" ]; then
    i=`expr $1 - 1`
    j=`factorial $i`
    k=`expr $1 \* $j`
    echo $k
  else
    echo 1
  fi
}


while :
do
  echo "Enter a number:"
  read x
  factorial $x
done                      


tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./factorial.sh 
Enter a number:
10
3628800
Enter a number:
11
39916800
Enter a number:
20
2432902008176640000


fibonacci.sh

#!/bin/sh

# A resursion program to implement fibonacci sequence
fibonacci()
{
    if [ "$1" -gt "2" ]; then
	i=`expr $1 - 1`
	j=`expr $1 - 2`
	f_i=`fibonacci $i`
	f_j=`fibonacci $j`
	k=`expr $f_i + $f_j`
	echo $k
    elif [ "$1" -eq "1" ]; then
	 echo 0
    elif [ "$1" -eq "2" ];then
	 echo 1
    fi
    

}

INPUT_STRING=1
while [ "$INPUT_STRING" -gt "0" ]
do
    echo "What is the order of the fibonacci number you want to know?(type number<=0 to exit)"
    read x
    INPUT_STRING="${x}"
    if [ "$INPUT_STRING" -gt "0" ];then
	echo "The ${x}th fibonacci number is `fibonacci $x`"
    else
	echo "Goodbye, you can take a rest now"
    fi
    #fibonacci $x
done


tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ ./fibonacci.sh
What is the order of the fibonacci number you want to know?(type number<=0 to exit)
10
The 10th fibonacci number is 34
What is the order of the fibonacci number you want to know?(type number<=0 to exit)
-1
Goodbye, you can take a rest now
What is the order of the fibonacci number you want to know?(type number<=0 to exit)
0
Goodbye, you can take a rest now


Library file is end with .lib and

# common.lib
# Note no #!/bin/sh as this should not spawn 
# an extra shell. It‘s not the end of the world 
# to have one, but clearer not to.
#
STD_MSG="About to rename some files..."

rename()
{
  # expects to be called as: rename .txt .bak 
  FROM=$1
  TO=$2

  for i in *$FROM
  do
    j=`basename $i $FROM`
    mv $i ${j}$TO
  done
}

#!/bin/sh
# function2.sh
. ./common.lib
echo $STD_MSG
rename .txt .bak

Return Codes

#!/bin/sh

adduser()
{
  USER=$1
  PASSWORD=$2
  shift ; shift
  COMMENTS=$@
  useradd -c "${COMMENTS}" $USER
  if [ "$?" -ne "0" ]; then
    echo "Useradd failed"
    return 1
  fi
  passwd $USER $PASSWORD
  if [ "$?" -ne "0" ]; then
    echo "Setting password failed"
    return 2
  fi
  echo "Added user $USER ($COMMENTS) with pass $PASSWORD"
}

## Main script starts here

adduser bob letmein Bob Holness from Blockbusters
ADDUSER_RETURN_CODE=$?
if [ "$ADDUSER_RETURN_CODE" -eq "1" ]; then
  echo "Something went wrong with useradd"
elif [ "$ADDUSER_RETURN_CODE" -eq "2" ]; then 
   echo "Something went wrong with passwd"
else
  echo "Bob Holness added to the system."
fi


tiger@tiger-OptiPlex-990:/media/tiger/Tiger_Passport/Documents/shell_notes$ sudo ./return_code.sh 
[sudo] password for tiger: 
Usage: passwd [options] [LOGIN]

Options:
  -a, --all                     report password status on all accounts
  -d, --delete                  delete the password for the named account
  -e, --expire                  force expire the password for the named account
  -h, --help                    display this help message and exit
  -k, --keep-tokens             change password only if expired
  -i, --inactive INACTIVE       set password inactive after expiration
                                to INACTIVE
  -l, --lock                    lock the password of the named account
  -n, --mindays MIN_DAYS        set minimum number of days before password
                                change to MIN_DAYS
  -q, --quiet                   quiet mode
  -r, --repository REPOSITORY   change password in REPOSITORY repository
  -R, --root CHROOT_DIR         directory to chroot into
  -S, --status                  report password status on the named account
  -u, --unlock                  unlock the password of the named account
  -w, --warndays WARN_DAYS      set expiration warning days to WARN_DAYS
  -x, --maxdays MAX_DAYS        set maximum number of days before password
                                change to MAX_DAYS

Setting password failed
Something went wrong with passwd


This script checks the two external calls it makes (useradd and passwd), and lets the user know if they fail. The function then defines a return code of 1 to indicate any problem with useradd, and 2 to indicate any problem with passwd. That way, the calling script knows where the problem lay.

You have to save $?, because as soon as you run another command, such as if, its value will be replaced. That is why we save the adduser return value in the $ADDUSER_RETURN_CODEvariable, before acting on its content. $ADDUSER_RETURN_CODE is certain to remain the same; $? will change with every command that is executed.

Hints and Tips

Shell File Test Operators Example

cut(unix)

All Shell Scripting Tips

Interactive Shell

A very useful tip: Ctrl+r will do a reverse-search, matching any part of the command line. Hit ESC and the selected command will be pasted into the current shell for you to edit as required.

Exercises

References

Shell Scripting Tutorial-Steve Parker

For operators:

some of the less easily guessed commands and codes of shell scripts

Shell script notes

上一篇:Mac 安装 Homebrew


下一篇:EasyExcel 导出 复杂表头