linuxchina 发表于 2002-12-15 00:35:48

LINUX 033 UNIT 15 of REDHAT(下)

接UNIT15(上)

目的:使用UNIX所提供的最常用工具来完成所需复杂任务的强大功能。

(1)最简单的Shell 编程
$ls -R / |grep myname |more

每天数据的备份:
$ cd /usr/yourname; ls * |cpio -o > /dev/rmt/0h

书写程序的目的是一次编程,多次使用(执行)!

$ cat > backup.sh
cd /home/hbwork
ls * | cpio -o > /dev/rmt/0h
^D

执行:
$ sh backup.sh

或:
$ chmod +x backup.sh
$ ./backup.sh

技巧:在shell script中加入必要的注释,以便以后阅读及维护。

(2)shell是一个(编程)语言
同传统的编程语言一样,shell提供了很多特性,这些特性可以使你的shell script
编程更为有用,如:数据变量、参数传递、判断、流程控制、数据输入和输出,子
程序及以中断处理等。

. 在shell编程中使用数据变量可以将程序变量更为通用,如在上面backup.sh中:
cd $WORKDIR
ls * | cpio -o > /dev/rmt/0h

. Shell编程中的注释以#开头
. 对shell变量进行数字运算,使用expr命令
expr integer operator integer
其中operator为+ - * / %, 但对*的使用要用转义符\,如:
$expr 4 \* 5
20
$int=`expr 5 + 7`
$echo $int
12

(3)Shell编程的参数传递, 可通过命令行参数以及交互式输入变量(read)

restoreall.sh 对backup.sh程序的备份磁带进行恢复
$cat > restoreall.sh
cd $WORKDIR
cpio -i < /dev/rmt/0h
^D
restore1.sh:只能恢复一个文件
#restore1 --program to restore a single file
cd $WORKDIR
cpio -i $i < /dev/rmt/0h

$restore1 file1

恢复多个文件restoreany :
#restoreany --program to restore a single file
cd $WORKDIR
cpio -i $* < /dev/rmt/0h

$ restoreany file1 file2 file3


(4)条件判断
. if-then语句,格式如下:
if command_1
then
command_2
command_3
fi
command_4

在if-then语句中使用了命令返回码$?,即当command_1执行成功时才执行command_2和
command_3,而command_4总是执行.

示例程序unload: 在备份成功时删除原始文件,带有错误检查

cd $1
#备份时未考虑不成功的情况!
ls -a | cpio -o > /dev/rmt/0h
rm -rf *

改进如下:

#当使用了管道命令时,管理命令的返回代码为最后一个命令的返回代码
if ls -a | cpio -o > /dev/rmt/0h
then
rm -rf *
fi

. if-then-else语句
if command_1
then
command_2
else
command_3
fi

技巧: 由于shell对命令中的多余的空格不作任何处理,一个好的程序员会用这一特性
对自己的程序采用统一的缩进格式,以增强自己程序的可读性.

. 使用test命令进行进行条件测试
格式: test conditions

test在以下四种情况下使用: a. 字符比较 b.两个整数值的比较
c. 文件操作,如文件是否存在及文件的状态等
d. 逻辑操作,可以进行and/or,与其他条件联合使用

a. 测试字符数据: shell变量通常民政部下均作为字符变量
str1 = str2 二者相长,相同
str1 != str2 不同
-n string string不为空(长度不为零)
-z string string为空
string string不为空

例:
$ str1=abcd #在含有空格时必须用引号括起来
$ test $str1=abcd
$ echo $?
0
$ str1="abcd "
$ test $str1=abcd
$ echo $?
1
Note: 在test处理含有空格的变量时最好用引号将变量括起来,否则会出现错误的结果,
因为shell在处理命令行时将会去掉多余的空格,而用引号括起来则可以防止
shell去掉这些空格.
例:
$ str1=" "
$ test $str1
$ echo $?
1
$ test "$str1"
$ echo $?
0
$ test -n $str1
test: argument expected
$ test -n "$str1"
$ echo $?
0
$

b. 整数测试: test与expr相同,可以将字符型变量转换为整数进行操作,expr进行
整数的算术运算,而test则进行逻辑运算.

表达式 说明
---------------------------------------
int1 -eq int2 相等?
int1 -ne int2 不等?
int1 -gt int2 int1 > int2 ?
int1 -ge int2 int1 >= int2 ?
int1 -lt int2 int1 < int2 ?
int1 -le int2 int1 <= int2 ?

例:
$ int1=1234
$ int2=01234
$ test $int1 -eq $int2
$ echo $?
0

c. 文件测试:检查文件状态如存在及读写权限等

-r filename 用户对文件filename有读权限?
-w filename 用户对文件filename有写权限?
-x filename 用户对文件filename有可执行权限?
-f filename 文件filename为普通文件?
-d filename 文件filename为目录?
-c filename 文件filename为字符设备文件?
-b filename 文件filename为块设备文件?
-s filename 文件filename大小不为零?
-t fnumb 与文件描述符fnumb(默认值为1)相关的设备是一个终端设备?

d. 测试条件之否定,使用!
例:
$ cat /dev/null > empty
$ test -r empty
$ echo $?
0
$ test -s empty
1
$ test ! -s empty
$ echo $?
0
e. 测试条件之逻辑运算
-a And
-o Or

例: $ test -r empty -a -s empty
$ echo $?
1
f. 进行test测试的标准方法
因为test命令在 shell编程中占有很重要的地位,为了使shell能同其他编程语言一样
便于阅读和组织, Bourne Shell在使用test测试时使用了另一种方法:用方括号将整个
test测试括起来:

$ int1=4
$ [ $int1 -gt 2 ]
$ echo $?
0

例: 重写unload程序,使用test测试
#!/bin/sh
#unload - program to backup and remove files
#syntax: unload directory

#check arguments
if [ $# -ne 1 ]
then
echo "usage: $0 directory"
exit 1
fi

#check for valid directory name
if [ ! -d "$1" ]
then
echo "$1 is not a directory"
exit 2
fi

cd $1

ls -a | cpio -o > /dev/rmt/0h

if [ $? -eq 0 ]
then
rm -rf *
else
echo "A problem has occured in creating backup"
echo "The directory will not be ereased"
echo "Please check the backup device"
exit 3
fi
# end of unload

在如上示例中出现了exit, exit有两个作用:一是停止程序中其他命令的执行,二是
设置程序的退出状态

g. if嵌套及elif结构
if command
then
command
else
if command
then
command
else
if command
then
command
fi
fi
fi

改进:使用elif结构
if command
then
command
elif command
then
command
elif command
then
command
fi

elif结构同if结构类似,但结构更清淅,其执行结果完全相同.


Bourne Shell及Shell编程(2)
h.交互式从键盘读入数据
使用read语句,其格式如下:

read var1 var2 ... varn

read将不作变量替换,但会删除多余的空格,直到遇到第一个换行符(回车),
并将输入值依次赋值给相应的变量。

例:
$ read var1 var2 var3
Hello my friends
$ echo $var1 $var2 $var3
Hello my friends
$ echo $var1
Hello
$ read var1 var2 var3
Hello my dear friends
$ echo $var3
dear friends <-输入多余变量时,输入值余下的内容赋给最后一个变量
$ read var1 var2 var3
Hello friends
$ echo $var3
<- var3为空
$

在shell script中可使用read语句进行交互操作:

...
#echo -n message 输出结果后不换行
echo -n "Do you want to continue: Y or N"
read ANSWER

if [ $ANSWER=N -o $ANSWER=n ]
then
exit
fi

i. case结构:结构较elif-then结构更清楚

比较if-then语句:

if [ variable1 = value1 ]
then
command
command
elif [ variable1 = value2 ]
then
command
command
elif [ variable1 = value3 ]
then
command
command
fi

相应的case结构:

case value in
pattern1)
command
command;;
pattern2)
command
command;;
...
patternn)
command;
esac

* case语句只执行第一个匹配模式

例:使用case语句建立一个菜单选择shell script

#Display a menu
echo _
echo "1 Restore"
echo "2 Backup"
echo "3 Unload"
echo

#Read and excute the user's selection
echo -n "Enter Choice:"
read CHOICE

case "$CHOICE" in
1) echo "Restore";;
2) echo "Backup";;
3) echo "Unload";;
*) echo "Sorry $CHOICE is not a valid choice
exit 1
esac

在上例中,*指默认匹配动作。此外,case模式中也可以使用逻辑操作,如下所示:

pattern1 | pattern2 ) command
command ;;

这样可以将上面示例程序中允许用户输入数字或每一个大写字母。

case "$CHOICE" in
1|R) echo "Restore";;
2|B) echo "Backup";;
3|U) echo "Unload";;
*) echo "Sorry $CHOICE is not a valid choice
exit 1
esac

(5)循环控制
<1> while循环:
格式:
while command
do
command
command
command
...
done

例: 计算1到5的平方
#!/bin/sh
#
#Filename: square.sh
int=1

while [ $int -le 5 ]
do
sq=`expr $int \* $int`
echo $sq
int=`expr $int + 1`
done
echo "Job completed"

$ sh square.sh
1
4
9
16
25
Job completed

<2> until循环结构:
格式:
until command
do
command
command
....
command
done

示例:使用until结构计算1-5的平方
#!/bin/sh

int=1

until [ $int -gt 5 ]
do
sq=`expr $int \* $int`
echo $sq
int=`expr $int + 1`
done
echo "Job completed"

<3> 使用shift对不定长的参数进行处理
在以上的示例中我们总是假设命令行参数唯一或其个数固定,或者使用$*将整个命令
行参数传递给shell script进行处理。对于参数个数不固定并且希望对每个命令参数
进行单独处理时则需要shift命令。使用shift可以将命令行位置参数依次移动位置,
即$2->$1, $3->$2. 在移位之前的第一个位置参数$1在移位后将不在存在。

示例如下:

#!/bin/sh
#
#Filename: shifter

until [ $# -eq 0 ]
do
echo "Argument is $1 and `expr $# - 1` argument(s) remain"
shift
done


$ shifter 1 2 3 4
Argument is 1 and 3 argument(s) remain
Argument is 2 and 2 argument(s) remain
Argument is 3 and 1 argument(s) remain
Argument is 4 and 0 argument(s) remain
$

使用shift时,每进行一次移位,$#减1,使用这一特性可以用until循环对每个参
数进行处理,如下示例中是一个求整数和的shell script:

#!/bin/sh
# sumints - a program to sum a series of integers
#

if [ $# -eq 0 ]
then
echo "Usage: sumints integer list"
exit 1
fi

sum=0

until [ $# -eq 0 ]
do
sum=`expr $sum + $1`
shift
done
echo $sum


$ sh sumints 324 34 34 12 34
438
$

使用shift的另一个原因是Bourne Shell的位置参数变量为$1~$9, 因此通过位置变量
只能访问前9个参数。但这并不等于在命令行上最多只能输入9个参数。此时如果想访问
前9个参数之后的参数,就必须使用shift.

另外shift后可加整数进行一次多个移位,如:

shift 3


<4>. for循环
格式:
for var in arg1 arg2 ... argn
do
command
....
command
done

示例:
$ for letter in a b c d e; do echo $letter;done
a
b
c
d
e

对当前目录下的所有文件操作:
$ for i in *
do
if [ -f $i ]
then
echo "$i is a file"
elif [ -d $i ]
echo "$i is a directory"
fi
done

求命令行上所有整数之和:
#!/bin/sh

sum=0

for INT in $*
do
sum=`expr $sum + $INT`
done

echo $sum


<6> 从循环中退出: break和continue命令
break 立即退出循环
continue 忽略本循环中的其他命令,继续下一下循环

在shell编程中有时我们要用到进行无限循环的技巧,也就是说这种循环一直执行碰
到break或continue命令。这种无限循环通常是使用true或false命令开始的。UNIX
系统中的true总是返加0值,而false则返回非零值。如下所示:

#一直执行到程序执行了break或用户强行中断时才结束循环
while true
do
command
....
command
done

上面所示的循环也可以使用until false, 如下:

until false
do
command
....
command
done

在如下shell script中同时使用了continue,break以及case语句中的正规表达式用法:

#!/bin/sh
# Interactive program to restore, backup, or unload
# a directory

echo "Welcome to the menu driven Archive program"

while true
do
# Display a Menu
echo
echo "Make a Choice from the Menu below"
echo _
echo "1 Restore Archive"
echo "2 Backup directory"
echo "3 Unload directory"
echo "4 Quit"
echo

# Read the user's selection
echo -n "Enter Choice: "

read CHOICE

case $CHOICE in
) echo
# Read and validate the name of the directory

echo -n "What directory do you want? "
read WORKDIR

if [ ! -d "$WORKDIR" ]
then
echo "Sorry, $WORKDIR is not a directory"
continue
fi

# Make the directory the current working directory
cd $WORKDIR;;

4) :;; # :为空语句,不执行任何动作
*) echo "Sorry, $CHOICE is not a valid choice"
continue
esac

case "$CHOICE" in
1) echo "Restoring..."
cpio -i
2) echo "Archiving..."
ls | cpio -o >/dev/rmt/0h;;

3) echo "Unloading..."
ls | cpio -o >/dev/rmt/0h;;

4) echo "Quitting"
break;;
esac

#Check for cpio errors

if [ $? -ne 0 ]
then
echo "A problem has occurred during the process"
if [ $CHOICE = 3 ]
then
echo "The directory will not be erased"
fi

echo "Please check the device and try again"
continue
else
if [ $CHOICE = 3 ]
then
rm *
fi
fi
done


(6)结构化编程:定义函数
同其他高级语言一样,shell也提供了函数功能。函数通常也称之为子过程(subroutine),
其定义格式如下:

funcname()
{
command
...
command; #分号
}

定义函数之后,可以在shell中对此函数进行调用,使用函数定义可以将一个复杂的程序分
为多个可管理的程序段,如下所示:

# start program

setup ()
{ command list ; }

do_data ()
{ command list ; }

cleanup ()
{ command list ; }

errors ()
{ command list ; }

setup
do_data
cleanup
# end program

技巧:
. 在对函数命名时最好能使用有含义的名字,即函数名能够比较准确的描述函数所完成
的任务。
. 为了程序的维护方便,请尽可能使用注释


使用函数的另一个好处就是可以在一个程序中的不同地方执行相同的命令序列(函数),
如下所示:

iscontinue()
{
while true
do
echo -n "Continue?(Y/N)"
read ANSWER

case $ANSWER in
) return 0;;
) return 1;;
*) echo "Answer Y or N";;
esac
done
}

这样可以在shell编程中调用iscontinue确定是否继续执行:

if iscontinue
then
continue
else
break
fi


** shell函数与shell程序非常相似,但二者有一个非常重要的差别:shell程序是由子shell
执行的,而shell函数则是作为当前shell的一部分被执行的,因此在当前shell中可以改
变函数的定义。此外在任意shell(包括交互式的shell)中均可定义函数,如:

$ dir
dir: not found
$ dir () { ls -l ;}
$ dir
total 5875
-rw-r--r-- 1 hbwork 100 Nov 10 23:16 doc
-rw-r--r-- 1 hbwork 2973806 Nov 10 23:47 ns40docs.zip
-rw-r--r-- 1 hbwork 1715011 Nov 10 23:30 ns840usr.pdf
-rw-r--r-- 1 hbwork 1273409 Sep 23 1998 radsol21b6.tar.Z
-rw-r--r-- 1 hbwork 7526 Nov 10 23:47 wget-log
-rw-r--r-- 1 hbwork 1748 Nov 13 21:51 wget-log.1
$ unset dir
$ dir () {
> echo "Permission Link Owner Group File_SZ LastAccess FileName"
> echo "-----------------------------------------------------------"
> ls -l $*;
> }

$ dir
Permission Link Owner Group File_SZ LastAccess FileName
-----------------------------------------------------------
total 5875
-rw-r--r-- 1 hbwork 100 Nov 10 23:16 doc
-rw-r--r-- 1 hbwork 2973806 Nov 10 23:47 ns40docs.zip
-rw-r--r-- 1 hbwork 1715011 Nov 10 23:30 ns840usr.pdf
-rw-r--r-- 1 hbwork 1273409 Sep 23 1998 radsol21b6.tar.Z
-rw-r--r-- 1 hbwork 7526 Nov 10 23:47 wget-log
-rw-r--r-- 1 hbwork 1748 Nov 13 21:51 wget-log.1

通常情况下,shell script是在子shell中执行的,困此在此子shell中对变量所作的
修改对父shell不起作用。点(.) 命令使用shell在不创建子shell而由当前shell读取
并执行一个shell script, 可以通过这种方式来定义函数及变量。此外点(.)命令最
常用的功能就是通过读取.profile来重新配置初始化login变量。如下所示:

$ . .profile
(csh相对于.命令的是source命令).

(7)使用And/Or结构进行有条件的命令执行
<1> And , 仅当第一个命令成功时才有执行后一个命令,如同在逻辑与表达式中如果前面的
结果为真时才有必要继续运算,否则结果肯定为假。

格式如下:

command1 && command2

例:rm $TEMPDIR/* && echo "File successfully removed"

等价于

if rm $TEMPDIR/*
then
echo "File successfully removed"
fi

<2>Or, 与AND相反,仅当前一个命令执行出错时才执行后一条命令

例: rm $TEMPDIR/* || echo "File not removed"

等价与:

if rm $TEMPDIR/*
then
command
else
echo "File not removed"
fi

<3> 混合命令条件执行
command1 && command2 && command3
当command1, command2成功时才执行command3

command1 && command2 || comamnd3
仅当command1成功,command2失败时才执行command3

当然可以根据自己的需要进行多种条件命令的组合,在此不多讲述。


(8) 使用getopts命令读取unix格式选项
UNIX格式选项指如下格式的命令行参数:
command -options parameters

使用格式:
getopts option_string variable

具体使用方法请参考getopts的在线文档(man getopts).

示例如下:

#newdate
if [ $# -lt 1 ]
then
date
else
while getopts mdyDHMSTjJwahr OPTION
do
case $OPTION
in
m) date '+%m ';; # Month of Year
d) date '+%d ';; # Day of Month
y) date '+%y ';; # Year
D) date '+%D ';; # MM/DD/YY
H) date '+%H ';; # Hour
M) date '+%M ';; # Minute
S) date '+%S ';; # Second
T) date '+%T ';; # HH:MM:SS
j) date '+%j ';; # day of year
J) date '+%y%j ';;# 5 digit Julian date
w) date '+%w ';; # Day of the Week
a) date '+%a ';; # Day abbreviation
h) date '+%h ';; # Month abbreviation
r) date '+%r ';; # AM-PM time
\?) echo "Invalid option $OPTION";;
esac
done
fi

$ newdate -J
94031
$ newdate -a -h -d
Mon
Jan
31
$ newdate -ahd
Mon
Jan
31
$


示例程序:复制程序

# Syntax: duplicate [-c integer] [-v] filename
# where integer is the number of duplicate copies
# and -v is the verbose option

COPIES=1
VERBOSE=N


while getopts vc: OPTION
do
case $OPTION
in
c) COPIES=$OPTARG;;
v) VERBOSE=Y;;
\?) echo "Illegal Option"
exit 1;;
esac
done

if [ $OPTIND -gt $# ]
then
echo "No file name specified"
exit 2
fi


shift `expr $OPTIND -1`

FILE=$1
COPY=0

while [ $COPIES -gt $COPY ]
do
COPY=`expr $COPY + 1`
cp $FILE ${FILE}${COPY}
if [ VERBOSE = Y ]
then
echo ${FILE}${COPY}
fi
done


$ duplicate -v fileA
fileA1
$ duplicate -c 3 -v fileB
fileB1
fileB2
fileB3


4. Shell的定制
通常使用shell的定制来控制用户自己的环境,比如改变shell的外观(提示符)以及增强
自己的命令。

(1)通常环境变量来定制shell
通常改变环境变量可以定制shell的工作环境。shell在处理信息时会参考这些环境变量
,改变环境变量的值在一定程度上改变shell的操作方式,比如改变命令行提示符。

.使用IFS增加命令行分隔符
默认状态下shell的分隔符为空格、制表符及换行符,但可以通过改变IFS的值加入自己
的分隔符。如下所示:


$ IFS=":"
$ echo:Hello:my:Friend
Hello my Friend

(2)加入自己的命令及函数
如下程序:
#Directory and Prompt change program
#Syntax: chdir directory

if [ ! -d "$1" ]
then
echo "$1 is not a directory"
exit 1
fi

cd $1
PS1=`pwd`$
export PS1

$ chdir /usr/home/teresa
$

但此程序在执行时系统提示符并不会改变,因为此程序是在子shell中执行的。因此其变量
对当前shell并无影响,要想对当前shell起作用,最好是将此作为函数写在自己的.profile中
或建立自己的个人函数文件.persfuncs

#Personal function file persfuncs

chdir()
{
#Directory and Prompt change program
#Syntax: chdir directory
if [ ! -d "$1" ]
then
echo "$1 is not a directory"
exit 1
fi

cd $1
PS1=`pwd`$
export PS1;
}

再执行:
$ . .persfuncs
$ chdir temp
/home/hbbwork/temp$

也可在自己的.profile文件中用 . .persfuncs调用.persfuncs.

说明:在bash/tcsh中已经使用别名,相对而言别名比此方法更为方便。


5. 有关shell的专门讨论
(1)shell程序的调试
切记:程序员(人)总是会犯错误的,而计算机是不会错的。
使用-x进行跟踪执行,执行并显示每一条指令。

(2)命令组
用小括号将一组命令括起来,则这些命令会由子shell来完成;而{command_list;}则在当
前shell中执行。这两者的主要区别在于其对shell变量的影响,子shell执行的命令不会
影响当前shell中的变量。

$ NUMBER=2
$ (A=2;B=2;NUMBER=`expr $A + $B`; echo $NUMBER)
4
$ echo $NUMBER
2
$ { A=2;B=2;NUMBER=`expr $A + $B`; echo $NUMBER; }
4
$ echo $NUMBER
4


总结:
在本章中讲述了Bourne Shell的基本知识,使用shell变量,shell script基础。这些概念
对于理解学习Korn Shell, csh以及其他script编程都是非常有用的。

很多OS都有不少语言及一些script功能,但很少有象UNIX SHELL这样灵活强大的script脚
本语言能力。

对于系统管理员或程序员来说,熟练地使用shell script将对日常工作(系统维护及管理)
非常有用,如果你想作一个合格的系统管理员,强烈建议你进一步深入的了解和使用
shell.

另外,对于系统管理员来说,PERL也是一个必不可少的script编程语言,尤其是对于处理
文本格式的各种文件,PERL具有shell, awk, sed, grep等的功能,但使用起来更为灵活,
功能也更强大。大家可以参考“Perl By Examples"来学习和使用PERL。

本文结合大量实例阐述如何编写一个shell脚本。

为什么要进行shell编程

在Linux系统中,虽然有各种各样的图形化接口工具,但是sell仍然是一个非常灵活的工具。Shell不仅仅是命令的收集,而且是一门非常棒的编程语言。您可以通过使用shell使大量的任务自动化,shell特别擅长系统管理任务,尤其适合那些易用性、可维护性和便携性比效率更重要的任务。
下面,让我们一起来看看shell是如何工作的:

建立一个脚本

Linux中有好多中不同的shell,但是通常我们使用bash (bourne again shell) 进行shell编程,因为bash是免费的并且很容易使用。所以在本文中笔者所提供的脚本都是使用bash(但是在大多数情况下,这些脚本同样可以在bash的大姐,bourne shell中运行)。

如同其他语言一样,通过我们使用任意一种文字编辑器,比如nedit、kedit、emacs、vi
等来编写我们的shell程序。
程序必须以下面的行开始(必须方在文件的第一行):
   #!/bin/sh

符号#!用来告诉系统它后面的参数是用来执行该文件的程序。在这个例子中我们使用/bin/sh来执行程序。
当编辑好脚本时,如果要执行该脚本,还必须使其可执行。
要使脚本可执行:
chmod +x filename
然后,您可以通过输入: ./filename 来执行您的脚本。

注释

在进行shell编程时,以#开头的句子表示注释,直到这一行的结束。我们真诚地建议您在程序中使用注释。如果您使用了注释,那么即使相当长的时间内没有使用该脚本,您也能在很短的时间内明白该脚本的作用及工作原理。

变量

在其他编程语言中您必须使用变量。在shell编程中,所有的变量都由字符串组成,并且您不需要对变量进行声明。要赋值给一个变量,您可以这样写:

变量名=值

取出变量值可以加一个美元符号($)在变量前面:

#!/bin/sh
#对变量赋值:
a="hello world"
# 现在打印变量a的内容:
echo "A is:"
echo $a

在您的编辑器中输入以上内容,然后将其保存为一个文件first。之后执行chmod +x first
使其可执行,最后输入./first执行该脚本。
这个脚本将会输出:
A is:
hello world

有时候变量名很容易与其他文字混淆,比如:
num=2
echo "this is the $numnd"
这并不会打印出"this is the 2nd",而仅仅打印"this is the ",因为shell会去搜索变量numnd的值,但是这个变量时没有值的。可以使用花括号来告诉shell我们要打印的是num变量:
num=2
echo "this is the ${num}nd"
这将打印: this is the 2nd

有许多变量是系统自动设定的,这将在后面使用这些变量时进行讨论。

如果您需要处理数学表达式,那么您需要使用诸如expr等程序(见下面)。
除了一般的仅在程序内有效的shell变量以外,还有环境变量。由export关键字处理过的变量叫做环境变量。我们不对环境变量进行讨论,因为通常情况下仅仅在登录脚本中使用环境变量。

Shell命令和流程控制

在shell脚本中可以使用三类命令:

1)Unix 命令:

虽然在shell脚本中可以使用任意的unix命令,但是还是由一些相对更常用的命令。这些命令通常是用来进行文件和文字操作的。


常用命令语法及功能

echo "some text": 将文字内容打印在屏幕上

ls: 文件列表

wc –l filewc -w filewc -c file: 计算文件行数计算文件中的单词数计算文件中的字符数

cp sourcefile destfile: 文件拷贝

mv oldname newname : 重命名文件或移动文件

rm file: 删除文件

grep 'pattern' file: 在文件内搜索字符串比如:grep 'searchstring' file.txt

cut -b colnum file: 指定欲显示的文件内容范围,并将它们输出到标准输出设备比如:输出每行第5个到第9个字符cut -b5-9 file.txt千万不要和cat命令混淆,这是两个完全不同的命令

cat file.txt: 输出文件内容到标准输出设备(屏幕)上

file somefile: 得到文件类型

read var: 提示用户输入,并将输入赋值给变量

sort file.txt: 对file.txt文件中的行进行排序

uniq: 删除文本文件中出现的行列比如: sort file.txt | uniq

expr: 进行数学运算Example: add 2 and 3expr 2 "+" 3

find: 搜索文件比如:根据文件名搜索find . -name filename -print

tee: 将数据输出到标准输出设备(屏幕) 和文件比如:somecommand | tee outfile

basename file: 返回不包含路径的文件名比如: basename /bin/tux将返回 tux

dirname file: 返回文件所在路径比如:dirname /bin/tux将返回 /bin

head file: 打印文本文件开头几行

tail file : 打印文本文件末尾几行

sed: Sed是一个基本的查找替换程序。可以从标准输入(比如命令管道)读入文本,并将结果输出到标准输出(屏幕)。该命令采用正则表达式(见参考)进行搜索。不要和shell中的通配符相混淆。比如:将linuxfocus 替换为 LinuxFocus :cat text.file | sed 's/linuxfocus/LinuxFocus/' > newtext.file

awk: awk 用来从文本文件中提取字段。缺省地,字段分割符是空格,可以使用-F指定其他分割符。cat file.txt | awk -F, '{print $1 "," $3 }'这里我们使用,作为字段分割符,同时打印第一个和第三个字段。如果该文件内容如下: Adam Bor, 34, IndiaKerry Miller, 22, USA命令输出结果为:Adam Bor, IndiaKerry Miller, USA


2) 概念: 管道, 重定向和 backtick

这些不是系统命令,但是他们真的很重要。

管道 (|) 将一个命令的输出作为另外一个命令的输入。
   grep "hello" file.txt | wc -l
在file.txt中搜索包含有”hello”的行并计算其行数。
在这里grep命令的输出作为wc命令的输入。当然您可以使用多个命令。

重定向:将命令的结果输出到文件,而不是标准输出(屏幕)。
> 写入文件并覆盖旧文件
>> 加到文件的尾部,保留旧文件内容。

反短斜线
使用反短斜线可以将一个命令的输出作为另外一个命令的一个命令行参数。
命令:
find . -mtime -1 -type f -print
用来查找过去24小时(-mtime –2则表示过去48小时)内修改过的文件。如果您想将所有查找到的文件打一个包,则可以使用以下脚本:
#!/bin/sh
# The ticks are backticks (`) not normal quotes ('):
tar -zcvf lastmod.tar.gz `find . -mtime -1 -type f -print`

3) 流程控制

"if" 表达式 如果条件为真则执行then后面的部分:
if ....; then
....
elif ....; then
....
else
....
fi
大多数情况下,可以使用测试命令来对条件进行测试。比如可以比较字符串、判断文件是否存在及是否可读等等…
通常用" [ ] "来表示条件测试。注意这里的空格很重要。要确保方括号的空格。
[ -f "somefile" ] :判断是否是一个文件
[ -x "/bin/ls" ] :判断/bin/ls是否存在并有可执行权限
[ -n "$var" ] :判断$var变量是否有值
[ "$a" = "$b" ] :判断$a和$b是否相等

执行man test可以查看所有测试表达式可以比较和判断的类型。
直接执行以下脚本:
#!/bin/sh
if [ "$SHELL" = "/bin/bash" ]; then
echo "your login shell is the bash (bourne again shell)"
else
echo "your login shell is not bash but $SHELL"
fi
变量$SHELL包含了登录shell的名称,我们和/bin/bash进行了比较。

快捷操作符

熟悉C语言的朋友可能会很喜欢下面的表达式:
[ -f "/etc/shadow" ] && echo "This computer uses shadow passwors"
这里 && 就是一个快捷操作符,如果左边的表达式为真则执行右边的语句。您也可以认为是逻辑运算中的与操作。上例中表示如果/etc/shadow文件存在则打印” This computer uses shadow passwors”。同样或操作(||)在shell编程中也是可用的。这里有个例子:
#!/bin/sh
mailfolder=/var/spool/mail/james
[ -r "$mailfolder" ]' '{ echo "Can not read $mailfolder" ; exit 1; }
echo "$mailfolder has mail from:"
grep "^From " $mailfolder
该脚本首先判断mailfolder是否可读。如果可读则打印该文件中的"From" 一行。如果不可读则或操作生效,打印错误信息后脚本退出。这里有个问题,那就是我们必须有两个命令:
-打印错误信息
-退出程序
我们使用花括号以匿名函数的形式将两个命令放到一起作为一个命令使用。一般函数将在下文提及。
不用与和或操作符,我们也可以用if表达式作任何事情,但是使用与或操作符会更便利很多。

case表达式可以用来匹配一个给定的字符串,而不是数字。
case ... in
...) do something here ;;
esac
让我们看一个例子。 file命令可以辨别出一个给定文件的文件类型,比如:
file lf.gz
这将返回:
lf.gz: gzip compressed data, deflated, original filename,
last modified: Mon Aug 27 23:09:18 2001, os: Unix
我们利用这一点写了一个叫做smartzip的脚本,该脚本可以自动解压bzip2, gzip 和zip 类型的压缩文件:
#!/bin/sh
ftype=`file "$1"`
case "$ftype" in
"$1: Zip archive"*)
   unzip "$1" ;;
"$1: gzip compressed"*)
   gunzip "$1" ;;
"$1: bzip2 compressed"*)
   bunzip2 "$1" ;;
*) error "File $1 can not be uncompressed with smartzip";;
esac

您可能注意到我们在这里使用了一个特殊的变量$1。该变量包含了传递给该程序的第一个参数值。也就是说,当我们运行:
smartzip articles.zip
$1 就是字符串 articles.zip

select 表达式是一种bash的扩展应用,尤其擅长于交互式使用。用户可以从一组不同的值中进行选择。
select var in ... ; do
break
done
.... now $var can be used ....
下面是一个例子:
#!/bin/sh
echo "What is your favourite OS?"
select var in "Linux" "Gnu Hurd" "Free BSD" "Other"; do
       break
done
echo "You have selected $var"
下面是该脚本运行的结果:
What is your favourite OS?
1) Linux
2) Gnu Hurd
3) Free BSD
4) Other
#? 1
You have selected Linux

您也可以在shell中使用如下的loop表达式:
while ...; do
....
done

while-loop 将运行直到表达式测试为真。will run while the expression that we test for is true. 关键字"break" 用来跳出循环。而关键字”continue”用来不执行余下的部分而直接跳到下一个循环。

for-loop表达式查看一个字符串列表 (字符串用空格分隔) 然后将其赋给一个变量:
for var in ....; do
....
done

在下面的例子中,将分别打印ABC到屏幕上:
#!/bin/sh
for var in A B C ; do
echo "var is $var"
done

下面是一个更为有用的脚本showrpm,其功能是打印一些RPM包的统计信息:
#!/bin/sh
# list a content summary of a number of RPM packages
# USAGE: showrpm rpmfile1 rpmfile2 ...
# EXAMPLE: showrpm /cdrom/RedHat/RPMS/*.rpm
for rpmpackage in $*; do
if [ -r "$rpmpackage" ];then
   echo "=============== $rpmpackage =============="
   rpm -qi -p $rpmpackage
else
   echo "ERROR: cannot read file $rpmpackage"
fi
done

这里出现了第二个特殊的变量$*,该变量包含了所有输入的命令行参数值。如果您运行showrpm openssh.rpm w3m.rpm webgrep.rpm
此时 $* 包含了 3 个字符串,即openssh.rpm, w3m.rpm and webgrep.rpm.


引号
在向程序传递任何参数之前,程序会扩展通配符和变量。这里所谓扩展的意思是程序会把通配符(比如*)替换成合适的文件名,它变量替换成变量值。为了防止程序作这种替换,您可以使用引号:让我们来看一个例子,假设在当前目录下有一些文件,两个jpg文件, mail.jpg 和tux.jpg。

#!/bin/sh
echo *.jpg
这将打印出"mail.jpg tux.jpg"的结果。
引号 (单引号和双引号) 将防止这种通配符扩展:
#!/bin/sh
echo "*.jpg"
echo '*.jpg'
这将打印"*.jpg" 两次。
单引号更严格一些。它可以防止任何变量扩展。双引号可以防止通配符扩展但允许变量扩展。
#!/bin/sh
echo $SHELL
echo "$SHELL"
echo '$SHELL'

运行结果为:
/bin/bash
/bin/bash
$SHELL

最后,还有一种防止这种扩展的方法,那就是使用转义字符——反斜杆:
echo *.jpg
echo $SHELL
这将输出:
*.jpg
$SHELL
Here documents

当要将几行文字传递给一个命令时,here documents(译者注:目前还没有见到过对该词适合的翻译)一种不错的方法。对每个脚本写一段帮助性的文字是很有用的,此时如果我们四有那个here documents就不必用echo函数一行行输出。 一个 "Here document" 以 << 开头,后面接上一个字符串,这个字符串还必须出现在here document的末尾。下面是一个例子,在该例子中,我们对多个文件进行重命名,并且使用here documents打印帮助:

#!/bin/sh
# we have less than 3 arguments. Print the help text:
if [ $# -lt 3 ] ; then
cat <<HELP
ren -- renames a number of files using sed regular expressions

USAGE: ren 'regexp' 'replacement' files...

EXAMPLE: rename all *.HTM files in *.html:
ren 'HTM$' 'html' *.HTM

HELP
exit 0
fi
OLD="$1"
NEW="$2"
# The shift command removes one argument from the list of
# command line arguments.
shift
shift
# $* contains now all the files:
for file in $*; do
   if [ -f "$file" ] ; then
   newfile=`echo "$file" | sed "s/${OLD}/${NEW}/g"`
   if [ -f "$newfile" ]; then
       echo "ERROR: $newfile exists already"
   else
       echo "renaming $file to $newfile ..."
       mv "$file" "$newfile"
   fi
   fi
done

这是一个复杂一些的例子。让我们详细讨论一下。第一个if表达式判断输入命令行参数是否小于3个 (特殊变量$# 表示包含参数的个数) 。如果输入参数小于3个,则将帮助文字传递给cat命令,然后由cat命令将其打印在屏幕上。打印帮助文字后程序退出。 如果输入参数等于或大于3个,我们就将第一个参数赋值给变量OLD,第二个参数赋值给变量NEW。下一步,我们使用shift命令将第一个和第二个参数从参数列表中删除,这样原来的第三个参数就成为参数列表$*的第一个参数。然后我们开始循环,命令行参数列表被一个接一个地被赋值给变量$file。接着我们判断该文件是否存在,如果存在则通过sed命令搜索和替换来产生新的文件名。然后将反短斜线内命令结果赋值给newfile。这样我们就达到了我们的目的:得到了旧文件名和新文件名。然后使用mv命令进行重命名。

函数

如果您写了一些稍微复杂一些的程序,您就会发现在程序中可能在几个地方使用了相同的代码,并且您也会发现,如果我们使用了函数,会方便很多。一个函数是这个样子的:
functionname()
{
# inside the body $1 is the first argument given to the function
# $2 the second ...
body
}
您需要在每个程序的开始对函数进行声明。

下面是一个叫做xtitlebar的脚本,使用这个脚本您可以改变终端窗口的名称。这里使用了一个叫做help的函数。正如您可以看到的那样,这个定义的函数被使用了两次。
#!/bin/sh
# vim: set sw=4 ts=4 et:

help()
{
   cat <<HELP
xtitlebar -- change the name of an xterm, gnome-terminal or kde konsole

USAGE: xtitlebar [-h] "string_for_titelbar"

OPTIONS: -h help text

EXAMPLE: xtitlebar "cvs"

HELP
   exit 0
}

# in case of error or if -h is given we call the function help:
[ -z "$1" ] && help
[ "$1" = "-h" ] && help

# send the escape sequence to change the xterm titelbar:
echo -e "33]0;$107"
#

在脚本中提供帮助是一种很好的编程习惯,这样方便其他用户(和您)使用和理解脚本。

命令行参数

我们已经见过$* 和 $1, $2 ... $9 等特殊变量,这些特殊变量包含了用户从命令行输入的参数。迄今为止,我们仅仅了解了一些简单的命令行语法(比如一些强制性的参数和查看帮助的-h选项)。但是在编写更复杂的程序时,您可能会发现您需要更多的自定义的选项。通常的惯例是在所有可选的参数之前加一个减号,后面再加上参数值 (比如文件名)。

有好多方法可以实现对输入参数的分析,但是下面的使用case表达式的例子无遗是一个不错的方法。
#!/bin/sh
help()
{
cat <<HELP
This is a generic command line parser demo.
USAGE EXAMPLE: cmdparser -l hello -f -- -somefile1 somefile2
HELP
exit 0
}

while [ -n "$1" ]; do
case $1 in
   -h) help;shift 1;; # function help is called
   -f) opt_f=1;shift 1;; # variable opt_f is set
   -l) opt_l=$2;shift 2;; # -l takes an argument -> shift by 2
   --) shift;break;; # end of options
   -*) echo "error: no such option $1. -h for help";exit 1;;
   *) break;;
esac
done

echo "opt_f is $opt_f"
echo "opt_l is $opt_l"
echo "first arg is $1"
echo "2nd arg is $2"

您可以这样运行该脚本:
cmdparser -l hello -f -- -somefile1 somefile2

返回的结果是:
opt_f is 1
opt_l is hello
first arg is -somefile1
2nd arg is somefile2

这个脚本是如何工作的呢?脚本首先在所有输入命令行参数中进行循环,将输入参数与case表达式进行比较,如果匹配则设置一个变量并且移除该参数。根据unix系统的惯例,首先输入的应该是包含减号的参数。

实例

一般编程步骤

现在我们来讨论编写一个脚本的一般步骤。任何优秀的脚本都应该具有帮助和输入参数。并且写一个伪脚本(framework.sh),该脚本包含了大多数脚本都需要的框架结构,是一个非常不错的主意。这时候,在写一个新的脚本时我们只需要执行一下copy命令:
cp framework.sh myscript
然后再插入自己的函数。

让我们再看两个例子:

二进制到十进制的转换

脚本 b2d 将二进制数 (比如 1101) 转换为相应的十进制数。这也是一个用expr命令进行数学运算的例子:
#!/bin/sh
# vim: set sw=4 ts=4 et:
help()
{
cat <<HELP
b2h -- convert binary to decimal

USAGE: b2h [-h] binarynum

OPTIONS: -h help text

EXAMPLE: b2h 111010
will return 58
HELP
exit 0
}

error()
{
   # print an error and exit
   echo "$1"
   exit 1
}

lastchar()
{
   # return the last character of a string in $rval
   if [ -z "$1" ]; then
       # empty string
       rval=""
       return
   fi
   # wc puts some space behind the output this is why we need sed:
   numofchar=`echo -n "$1" | wc -c | sed 's/ //g' `
   # now cut out the last char
   rval=`echo -n "$1" | cut -b $numofchar`
}

chop()
{
   # remove the last character in string and return it in $rval
   if [ -z "$1" ]; then
       # empty string
       rval=""
       return
   fi
   # wc puts some space behind the output this is why we need sed:
   numofchar=`echo -n "$1" | wc -c | sed 's/ //g' `
   if [ "$numofchar" = "1" ]; then
       # only one char in string
       rval=""
       return
   fi
   numofcharminus1=`expr $numofchar "-" 1`
   # now cut all but the last char:
   rval=`echo -n "$1" | cut -b 0-${numofcharminus1}`
}


while [ -n "$1" ]; do
case $1 in
   -h) help;shift 1;; # function help is called
   --) shift;break;; # end of options
   -*) error "error: no such option $1. -h for help";;
   *) break;;
esac
done

# The main program
sum=0
weight=1
# one arg must be given:
[ -z "$1" ] && help
binnum="$1"
binnumorig="$1"

while [ -n "$binnum" ]; do
   lastchar "$binnum"
   if [ "$rval" = "1" ]; then
       sum=`expr "$weight" "+" "$sum"`
   fi
   # remove the last position in $binnum
   chop "$binnum"
   binnum="$rval"
   weight=`expr "$weight" "*" 2`
done

echo "binary $binnumorig is decimal $sum"
#
该脚本使用的算法是利用十进制和二进制数权值 (1,2,4,8,16,..),比如二进制"10"可以这样转换成十进制:
0 * 1 + 1 * 2 = 2
为了得到单个的二进制数我们是用了lastchar 函数。该函数使用wc –c计算字符个数,然后使用cut命令取出末尾一个字符。Chop函数的功能则是移除最后一个字符。

文件循环程序
或许您是想将所有发出的邮件保存到一个文件中的人们中的一员,但是在过了几个月以后,这个文件可能会变得很大以至于使对该文件的访问速度变慢。下面的脚本rotatefile 可以解决这个问题。这个脚本可以重命名邮件保存文件(假设为outmail)为outmail.1,而对于outmail.1就变成了outmail.2 等等等等...
#!/bin/sh
# vim: set sw=4 ts=4 et:
ver="0.1"
help()
{
   cat <<HELP
rotatefile -- rotate the file name

USAGE: rotatefile [-h] filename

OPTIONS: -h help text

EXAMPLE: rotatefile out
This will e.g rename out.2 to out.3, out.1 to out.2, out to out.1
and create an empty out-file

The max number is 10

version $ver
HELP
   exit 0
}

error()
{
   echo "$1"
   exit 1
}
while [ -n "$1" ]; do
case $1 in
   -h) help;shift 1;;
   --) break;;
   -*) echo "error: no such option $1. -h for help";exit 1;;
   *) break;;
esac
done

# input check:
if [ -z "$1" ] ; then
error "ERROR: you must specify a file, use -h for help"
fi
filen="$1"
# rename any .1 , .2 etc file:
for n in 9 8 7 6 5 4 3 2 1; do
   if [ -f "$filen.$n" ]; then
       p=`expr $n + 1`
       echo "mv $filen.$n $filen.$p"
       mv $filen.$n $filen.$p
   fi
done
# rename the original file:
if [ -f "$filen" ]; then
   echo "mv $filen $filen.1"
   mv $filen $filen.1
fi
echo touch $filen
touch $filen

这个脚本是如何工作的呢?在检测用户提供了一个文件名以后,我们进行一个9到1的循环。文件9被命名为10,文件8重命名为9等等。循环完成之后,我们将原始文件命名为文件1同时建立一个与原始文件同名的空文件。
调试
最简单的调试命令当然是使用echo命令。您可以使用echo在任何怀疑出错的地方打印任何变量值。这也是绝大多数的shell程序员要花费80%的时间来调试程序的原因。Shell程序的好处在于不需要重新编译,插入一个echo命令也不需要多少时间。

shell也有一个真实的调试模式。如果在脚本"strangescript" 中有错误,您可以这样来进行调试:
sh -x strangescript
这将执行该脚本并显示所有变量的值。
shell还有一个不需要执行脚本只是检查语法的模式。可以这样使用:
sh -n your_script
这将返回所有语法错误。
我们希望您现在可以开始写您自己的shell脚本,希望您玩得开心。



—————————————————————————————————

资料提供到这里:接下来一起来做个LAB,希望大家一块动脑筋

把你编写好的SHELL脚本跟贴上来就可以了,看一看谁能最快写出脚本

题目:

      要求如下:假设有2个用户aaa和bbb

         aaa的登陆密码是bbb,bbb登陆密码是aaa

         编写一个脚本,脚本运行后有如下提示:

      Welcome to www.linuxfans.org website.

         Please type your name:

         这些文字要求在屏幕顶部显示,然后等待用户输入用户名

      用户必须输入正确的用户名,否则输入错误的用户名三次后

      屏幕提示:Sorry your use name is wrong! 程序结束。

         如果用户名输入正确,那么屏幕继续提示:

      Please input your password:

         用户必须输入正确的密码,否则输入错误的密码三次后

      屏幕提示:Sorry your use password is wrong! 程序结束。

      如果用户输入了正确的用户名和匹配的正确的用户密码后

      屏幕提示:hello aaa Welcome! 程序结束。

         如果是bbb登陆了,屏幕提示:hello bbb Welcome! 程序结束。

      请大家踊跃跟帖回复,讨论这个程序的编写方法。谢谢大家
页: [1]
查看完整版本: LINUX 033 UNIT 15 of REDHAT(下)