Shell Script
作者:Danbo 日期:2015-7-3
什么是Shell:Shell是一个命令解析器,它在OS的最外层,负责直接与用户对话。
shell在系统四大层次所处的位置:(从内向外)
1.硬件
2.Kernel
3.Shell
4.外围应用程序
什么是Shell脚本:当命令或语句不在命令行执行,而是通过一个程序文件执行时,该程序就被称为Shell脚本或Shell程序。类似DOS下的批处理。通过命令、变量和流程控制语句等有机的结合起来,就形成了一个功能强大的Shell脚本。
示例1::清空日志的三种方法:把所有命令堆积起来形成了脚本示例。
echo " " >/var/log/messages
>/var/log/messages
cat /dev/null >/var/log/messages
echo "Logs cleaned up"
示例2:有判断的shell的脚本
#!/bin/bash
ROOT_UID=0
LOG_DIR=/var/log
if [ "$UID" -ne "ROOT_UID" ]; then
echo "You must be root to run this script"
exit 1
fi
cd $LOG_DIR || {
echo "Cannot change to necessary directory." >&2
exit 1
}
cat /dev/null > messages
echo "Logs cleaned up."
exit 0 #注意返回0表示成功,返回1表示失败。
Shell脚本是弱类型语言,较为通用的shell有标准的sh和csh,其中sh已被bash shell取代了。Shell相较于php、python、perl的差别:shell的优势在于吃力OS底层的业务(有大量的系统命令做支撑,2000多个命令,比grep、awk、sed )。比如一键安装、报警脚本、shel开发快速。
规范的shell脚本开头。
#!/bin/bash
#!又称为幻数,在执行bash及哦啊本的时候,内核会根据它来确定哪一程序来解析脚本中的内容。这一行必须在脚本的顶端的第一行,比如不是第一行则是注释。
我们发现sh本质上是bash的软链接:
[root@localhost ~]# which sh
/bin/sh
[root@localhost ~]# ll /bin/sh
lrwxrwxrwx 1 root root 4 Jun 29 17:03 /bin/sh -> bash
Shell脚本的执行
当Shell脚本以非交互的方式运行时,它会先检查环境变量env,该便来个支出一个环境文件(.bashrc)后,从该变量环境变量文件开始执行,当读取了env文件后,Shell才开始执行Shell脚本的内容。
Shell脚本的执行有3中方式
①.bash script-name 或 sh script-name #当脚本本身没有x权限或脚本开始没有指定解析器。
②.path/script-name 或 ./script-name
③.source script-name 或 . script-name
使用source或者"."的话,可以将子shell中的变量传递到父shell中
例如:
[root@localhost uestc]# echo 'userdir=`pwd`' >test.sh
[root@localhost uestc]# cat test.sh
userdir=`pwd`
[root@localhost uestc]# sh test.sh
[root@localhost uestc]# echo $userdir
[root@localhost uestc]# . ./test.sh
[root@localhost uestc]# echo $userdir
/uestc
这是为什么呢?我们当前执行脚本的窗口是一个shell(通过echo $$来查看当前窗口的shell)。而test.sh有处于另外一个shell中 。因此当我们执行sh test.sh后,虽然tesh.sh中已经定义了userdir,但是无法将其传递到父shell(当前窗口所处的shell)中来,而. /test.sh执行的方式就可以。
变量
变量可分为环境变量(全局变量)和局部变量(本地变量)。
我们设置环境变量在用户家目录的.bash_profile文件中,或者/etc/bashrc,或者/etc/profile,或者/etc/profile环境变量可以在创建他们的shell和子shell,他们通常被称为全局变量以区别局部变量。通常环境变量应该大写。环境变量可以使用export内置命令或者declare -x导出为全局变量。取消本地变量命令:unset 变量名
本地变量是在用户当前的shell生存期的脚本中使用:定义方式为:locate UESTC
定义变量的时候:"" '' 不加引号区别
不加引号:内容一般为简单连续的数字、字符串、路径名等
单引号:输出变量时引号里面是什么就输出什么
双引号:双引号中的变量会经过解析然后再输出
注意再awk则相反:双引号原样输出,单引号解析后再输出
[root@localhost uestc]# UESTC=123456
[root@localhost uestc]# awk 'BEGIN{print "$UESTC"}'
$UESTC
[root@localhost uestc]# awk 'BEGIN{print '$UESTC'}'
123456
把命令定义为变量
cmd=`date +%F`或者 cmd=$(date +%F)
当我们对文件进行打包压缩备份时为显示什么时间备份的,我们可以将打包时间写到文件名当中
tar -zcvf etc_$(date +%F)_backup.tar.gz /etc
Shell特殊变量
位置变量
$0:获得当前执行Shell脚本的文件名,包括完整路路径;只取名字:basename $0;只取路径:dirname $0
[root@localhost uestc]# cat sh.sh
#!/bin/bash
echo $0
[root@localhost uestc]# cd scripts/
[root@localhost scripts]# sh /uestc/sh.sh
/uestc/sh.sh
$n:获得当前指定行的Shell脚本第n个参数值,n=1,2,3,4....9当大于10的时候就需要括起来$(10);
[root@localhost scripts]# cat 1.sh
#!/bin/bash
echo $1 $2 $3 $4 $5 $6 $7 $8 $9 $(10)
[root@localhost scripts]# sh 1.sh 1 2 3 4 //即取后面的形式
1 2 3 4
$*:获得当前Shell的所有参数,并将所有命令行参数视为单个字符串
$#:获得当前Shell命令行中参数的总个数
$@:获得当前Shell的所有参数,每个参数还是独立的。
我们通过实例来看一下其作用
#!/bin/bash
case "$1" in
start)
start
;;
stop)
stop
;;
statue)
status portmap
;;
restart|reload)
restart
;;
condrestart)
[ -f /var/lock/subsys/portmap ] && restart || :
;;
*)
echo "Usage:$0{start|stop|status|restart|reload|condrestart}"
exit 1
esac
我们这里看一下$#的作用
[root@localhost scripts]# set -- "I am " an uestc student
[root@localhost scripts]# echo $#
4
我们这里看一下$*与$@的区别
[root@localhost scripts]# for i in "$@";do echo $i; done
I am
an
uestc
student
[root@localhost scripts]# for i in "$*";do echo $i; done
I am an uestc student
进程状态变量
$$:获取当前shell的进程号
$!:获得Shell最后运行后台的PID
$?:或者执行上一个指令的返回值(0为成功;非零为失败)
$_:在此之前执行命令或脚本的最后一个参数
[root@localhost ~]# cat ba.sh
#!/bin/bash
cd /etc
tar -zcvf service.tar.gz ./services >/dev/null 2>&1
[ $? -eq 0 ] && echo Success || echo Failed
我们这里总结一下$?返回值的意义:
0 成功
2 权限拒绝
126 找到命令但是无法执行
127 未找打目标命令
大于128 命令被系统强制结束
另一个例子:
#!/bin/bash
echo $$ >/uestc/a.log
while true
do
uptime > /dev/null 2>&1
sleep 2
done
此时我们cat a.log此时我们就能看到当前脚本的PID
[root@localhost ~]# sh ba1.sh &
[1] 13038
[root@localhost ~]# cat a.log
13038
Bash内部命令变量及shift实践讲解
有些内部命令在目录列表是看不到的,可以通过man bash来查看,常见的有:echo、eval、exec、 export、read、shift、exit和点(.)
shift语句按如下方式重新命令所有的位置参数变量,即$2成为$1;$3成为$2。在程序中每使用一次shift作用是所有的位置参数依次向左移动一个位置并且是位置参数$#减1,知道其值减到为0
某些脚本加了一些选项再接参数。
eval(evalargs)读入参数args并将他们组合成一个新的命令,然后执行。
比如:eval command-line
其中command-line是在终端上键入的一条普通命令行。然而当在它前面放上eval时,其结果是shell在执行命令行之前扫描它两次。如:
pipe="|"
eval ls $pipe wc -l
shell第1次扫描命令行时,它替换出pipe的值|,接着eval使它再次扫描命令行,这时shell把|作为管道符号了。
exec:当Shell执行到exec语句,不会去创建新的子进程,而是转去执行指定的命令,当指定的明林执行完时,该进程(也就是最初的Shell)就终止了,所以Shell程序中exec后面的语句将不再被执行。
Shell变量的字串常用操作
${#string} #返$string的长度 //echo ${string} | wc -m,但是这种技术比#string计数多一个
[root@localhost ~]# string=uestc
[root@localhost ~]# echo ${#string}
5
[root@localhost ~]# echo $string | wc -m
6
${string:position}:提取含有position关键字的字符串
${string:position:length}:从position之后开始提取长度为length的字串
${string#substring}:从变量string开头开始删除最短匹配substring字串
${string##substring}:从变量string开头开始删除最长匹配substring字串
${string%substring}:从变量string结尾开始删除最短匹配substring字串
${string%%substring}:从变量strig结尾开始删除最长匹配substring字串
${string/substring/replace}:使用$replace,来代替第一个匹配$substring字串
${string/#substring/replace}:如果string前缀匹配substring,就用replace来替代/substring
比如我们批量改名:
我们首先将要创建的名字写入到一个文件当中,比如:a.log
然后使用:for f in `cat a.log`; do touch $f ; done创建文件
我们需要将每个文件后面的finished去掉,该如何做?脚本如下:
#!/bin/bash
for f in `ls *.jpg`
do
mv $f `echo ${f%finished*}.jpg`
done
我们将后缀jpg改为JPG,我们先通过mv命令实现
#!/bin/bash
for f in `ls *.jpg`
do
mv $f `echo ${f/%jpg/JPG}`
done
通过sed命令也可以实现:
#!/bin/bash
for f in `ls *.JPG`
do
mv $f `echo $f | sed 's/JPG/jpg/g'`
done
通过rename是最简单的实现改名的方式:
rename "finished" '' * 第二种:rename .jpg .JPG *
其用法如下:
rename from to file
from:需要替换或需要处理的字符;
to:把前面from替换为to;
file:代替换的文件,可以用*处理所有文件。
扩展其他变量
${value:-word}:如果变量名存在且非空,则返回变量的值。否则,word字符串用途:字符变量未定义,则返回默认值。 范例:${vlaue:-word},如果value未定义,则表达式的值为word。
${value:=word}如果变量名非空,则返回变量值。否则,设置这个变量值为word,并返回气值。用途,如果:如果变量未定义,则设置变量为默认值,并返回默认值 。范例:${value:=word},如果value为定义,则设置value值为word,返回表达式的值也为word。
${value:?message}:如果变量赋值的话正常替换,否则将消息message送到标准错误输出。
实例:
path1="/uestc"
rm -rf ${path:-/tmp/}
这样之后就比较保险了,即使目录不存在会删除tmp目录内的临时文件。
在脚本执行之前,我们可以使用sh -x来对脚本进行调试。
生产常见中我们可以看这两个系统脚本:
/etc/init.d/httpd
/etc/init.d/crond
我们再学习脚本的时候可以多看一些系统脚本文件,可以给系统脚本写备注。
变量长度相关
三种计算字符串长度的方法:
1.echo {#char}
2.echo ${char} | wc -c #这种计算方式会多计算一个换行字符"
",比实际多1。
3.echo $(expr length "$char")
程序执行效率问题
user 0m0.727s
sys 0m0.007s
real 0m28.207s
user 0m6.423s
sys 0m21.683s
real 0m28.004s
user 0m6.218s
sys 0m21.150s
我们通过time+要执行的命令发现运用bash内置的命令进行执行操作时效率最高。
双括号(())运算的示例
请你设计一个加、减、乘、除等功能的计算器。通过名列给你行的传参的方式
#!/bin/bash
echo $(($1$2$3))
这个其实是通过把后面的表达式传递到前面的脚本中执行,就是简答的把3个参数排在一起就OK了
Let变量的数值运算
[root@localhost ~]# i=2
[root@localhost ~]# let i=i+8
[root@localhost ~]# echo $i
10
[root@localhost ~]# i=1
[root@localhost ~]# i=i+1
[root@localhost ~]# echo $i
i+1
提示:let i=i+8 等同于((i+8)),但是后者的效率更高。
变量的数值运算与特殊应用expr命令
expr命令一般用于整数值,但也可用于字符串,用来求表达式变量的值,同时expr也是一个手工命令行计算器。
我们之前用户expr来计算字符串的长度:echo $(expr length "$char") //双引号可以不加
1.expr也可以用在计算:
expr 2 + 2
expr 2 * 2
注意运算符与数字之前都有空格。、
2.expr在循环中可以用于增量计算。首先循环初始化为0,然后循环值加1,反引号的用法为命令替换。最基本的一种是从expr命令接受输入并将之放入循环变量。
例如:给自变量i+1
[root@localhost ~]# i=0
[root@localhost ~]# i=`expr $i + 1`
[root@localhost ~]# echo $i
1
3.expr $[$a+$b]表达式形式,其中$a$b可为整数值
expr $[2+3] #中括号数字与运算符之前可以不加空格
5
以上这种书写形式符号两边不需要加空格。expr将其后的串解释为表达式并计算其值,运算符前需要空格。
4.其他特殊用法:此时我们注意到ssh-cpoy-id脚本
if expr "$1" : ".*.pub";then #匹配*.pub格式的文件如果是则为真。例如:
expr "uestc.pub" : ".*.pub"
9 #返回非0值为真,如果返回值为0则为假。我们注意到这个非0值其实是返回前面字符个数。
5.为了更方面阅读我们在纪委再加上两个语句:expr "uestc.pub" : ".*.pub" && echo MATCH || echo Not-MATCH
通过expr判断变量是否为整数。
#!/bin/bash
read -p "Please input an integer:" a
expr $a + 0 >&/dev/null
[ $? -eq 0 ] && echo INIT||echo Chars
这种判断上输入数字是否为整数的一种重要的方式
6.通过expr计算字符串的长度
expr length "$UESTC"
变量的数值计算
bc命令的用法
1)、echo 3+4 | bc
7
2)、我们要计算1+2+3+...+10也可以通过管道接bc
seq -s "+" 10 |bc
55
3)、保留小数点问题:通过命令scale=n
[root@localhost ~]# echo "scale=3;4.35/2.11"|bc
2.061
4)、进制转换obase=n
echo "obase=2; 8" | bc #这里是将十进制的8转化为二进制。
[root@localhost ~]# echo "obase=2;8" | bc
1000
5)、typeset -i A=1 B=3
A=A+B
echo $A
4
不过这种方式很少见
6.$[]这种方式前面我们使用expr讲过了
[root@localhost ~]# echo $[1+2]
3
不过我们还是推荐使用$(())这样的用法比较好
Shell内置变量read的用法
最常用的-p:prompt:设置提示信息
-t timeout设置输入等待的时间,单位为秒
read -t 10 -p "Please input two number:" a b #10s后不输入退出
还有另外一种方法:
echo -n "Please input two number:"
read a b
我们写个脚本对read读入的是不是数字进行判断
#!/bin/bash
while true
do
read -p "Please input two number:" a b
expr $a + 0 >/dev/null
[ $? -ne 0 ] && continue
expr $b + 0 >/dev/null
[ $? -ne 0 ] && continue || break
done
echo "a-b=$(($a-$b))"
echo "a+b=$[$a+$b]"
将上面改为命令行传参的方式并优化
#!/bin/bash
a="$1"
b="$2"
Usage(){
echo "USAGE:sh $0 num1 num2"
exit 1
}
if [ $# -ne 2 ];then
Usage
fi
expr $1 + 0 >&/dev/null
[ $? -ne 0 ] && Usage
expr $2 + 0 >&/dev/null
[ $? -ne 0 ] && Usage
echo "a+b=$(($a+$b))"
echo "a+b=$[$a+$b]"
条件测试的多种方式
在bash的各种流程控制结构中通常要进行各种测试,然后根据测试结果执行不同的操作,优势也会通过与if等条件语句相结合,使我们可以方便的完成判断。
格式1:test <测试表达式>
格式2:[ <测试表达式> ]
格式3:[[ <测试表达式> ]]
说明:格式1有格式2等价,格式3为扩展test命令
常用的文件测试操作符号:
-f 文件存在
-d 目录存在
-s 文件存在且非空
-e 文件存在即为真
-r 文件存在且可读
-w 文件存在且可写
-x 文件存在且可执行
-L 文件存在且为链接
f1 -nt f2 文件1比文件2更新
f1 -ot f2 文件1比文件2更久
我们可以通过查看/etc/init.d/nfs这个脚本来学习这些文件操作符的使用
test -f file && echo true||echo false
[ -f file ] && echo true|| echo false
[[ -f file && -d folder ]] && echo true||echo false
注意:&&或者||只能用在[]之间与[[]]之内。不过他们之间是可以相互转换的。
并且-o -a 可以单[]中
字符串测试操作符
作用:比较两个字符串是否相同,字符串长度是否为零,是否为null
-z "字符串" #若串长度为0则真,-z可以立即为zero
-n "字符串" #若串的长度不会0则为真
"串1"="串2" #相等为真,此时也可以使用符号==代替=
"串1"!="串2" #不相等为真
注意:以上字符串测试操作符号一定要用""引起来!!!!
整数二元比较操作符
注意我们看下面这个实例:
[root@localhost ~]# [ 2>1 ] && echo OK||echo False
False
[root@localhost ~]# [ 2<1 ] && echo OK||echo False
False
为什么?我们必须注意当使用单中括号时必须使用转义字符:
[root@localhost ~]# [ 2<1 ] && echo OK||echo False
OK
所以说单括号我们最好不要使用符号表示法:
[ 2 -lt 1 ] && echo OK||echo False
OK
所以我们在[]中使用的比较 在(())和[[]]中
-eq ==
-ne !=
-gt >
-ge >=
-lt <
-le <=
逻辑操作符
在[]使用的逻辑操作符 在[[]]中使用的逻辑操作符
-a &&
-o ||
! !
我们看一下一下示例
[ -f "$UESTC" ] $$echo 1||echo 0 #这个是条件表达式的用法:返回1为真返回0为假,这点状态变量的$?不同
file1=/etc/services; file2=/etc/rc.local
echo $file1 $file2
[ -f "file1" ] && echo 1||echo 0
一般系统脚本中使用中会用到大量的判断语句
[ -r /etc/ssysconfig/network ] && . /etc/sysconfig/network
判断条件后执行多条命令语句,也即是我们刚开始经shell所用到的语句:
[ 判断 ] || {命令1 命令2 命令3}
其用法实例:cd $LOG_DIR||{echo "Cannot change to necessary directory" >&2 exit 1}
对于上面的例子我们可以这样写:
[ 3 -ne 3 ] ||{
echo "I am a UESTC student"
echo "I am a GOOD man"
exit 1
}
如果要写在一行中,每个命令还需要分号结尾。
脚本中编写实例:[ 3 -ne 3 ]||{echo "I am a UESTC Student";echo "LAAL";}
命令行中我们可以用:[ 3 -ne 3 ]||(echo s="1";echo "2")
还有一个例子:[ $ERROR -eq 0 ] && echo "jdk安装成功" || (echo "jdk安装失败,请检查" && exit 1)
我们在nfs中找到相关的应用:
字符串测试bind系统启动脚本举例:named
a1=10;a2=13
[ $a1 -eq $a2 ] && echo 1||echo 0 #注意这里$a1 $a2虽然是数字,但是我们也可以把其当做字符串来处理。
0
显示单号我们用echo 显示多行我们用cat
cat <<END
1.[INSTALL LAMP]
2.[INSTALL LANP]
3.[INSTALL NFS]
4.[INSTALL RSYNC]
Please input which you want to install:
END
read a
[ $a -eq 1 ] && {
cat <<END
1.[INSTALL APACHE]
2.[INSTALL MYSQL]
3.[INSTALL PHP]
4.[Back]
}
read "a1"
[ $a1 -eq 1 ] && {
echo "You want to install: Apache"
}
[ $a1 -eq 2 ] && {
echo "You want to install: MYSQL"
}
[ $a1 -eq 3 ] && {
echo "You want to install: PHP"
}
if语句
单分支结构
if [条件]; then
command
fi
双分支结构