该系列将重点介绍Linux Shell中的高级使用技巧,其主要面向有一定经验的Shell开发者、Linux系统管理员,以及Linux的爱好者。博客中的示例主要来源于网络和一些经典书籍,在经过本人的收集和整理之后,以系列博客的形式呈现给诸位。如果大家有更多更好的Shell脚本经典示例,且愿意在这里与我们一同分享的话,可以以邮件、博客回复等形式与我联系,我将会尽量保证该系列的持续更新。
一、将输入信息转换为大写字符后再进行条件判断:
我们在读取用户的正常输入后,很有可能会将这些输入信息用于条件判断,那么在进行比较时,我们将不得不考虑这些信息的大小写匹配问题。
/> cat > test1.sh
#!/bin/sh
echo -n "Please let me know your name. "
read name
#将变量name的值通过管道输出到tr命令,再由tr命令进行大小写转换后重新赋值给name变量。
name=`echo $name | tr [a-z] [A-Z]`
if [[ $name == "STEPHEN" ]]; then
echo "Hello, Stephen."
else
echo "You are not Stephen."
fi
CTRL+D
/> ./test1.sh
Please let me know your name. stephen
Hello, Stephen.
二、为调试信息设置输出级别:
我们经常在调试脚本时添加一些必要的调试信息,以便跟踪到程序中的错误。在完成调试后,一般都会选择删除这些额外的调试信息,在过了一段时间之后,如果脚本需要添加新的功能,那么我们将不得不重新进行调试,这样又有可能需要添加这些调试信息,在调试成功之后,这些信息可能会被再次删除。如果我们能够为我们的调试信息添加调试级别,使其只在必要的时候输出,我想这将会是一件非常惬意的事情。
/> cat > test2.sh
#!/bin/sh
if [[ $# == 0 ]]; then
echo "Usage: ./test2.sh -d debug_level"
exit 1
fi
#1. 读取脚本的命令行选项参数,并将选项赋值给变量argument。
while getopts d: argument
do
#2. 只有到选项为d(-d)时有效,同时将-d后面的参数($OPTARG)赋值给变量debug,表示当前脚本的调试级别。
case $argument in
d) debug_level=$OPTARG ;;
\?) echo "Usage: ./test2.sh -d debug_level"
exit 1
;;
esac
done
#3. 如果debug此时的值为空或者不是0-9之间的数字,给debug变量赋缺省值0.
if [[ -z $debug_level || $debug_level != [0-9] ]]; then
debug_level=0
fi
echo "The current debug_level level is $debug_level."
echo -n "Tell me your name."
read name
name=`echo $name | tr [a-z] [A-Z]`
if [ $name = "STEPHEN" ];then
#4. 根据当前脚本的调试级别判断是否输出其后的调试信息,此时当debug_level > 0时输出该调试信息。
test $debug_level -gt 0 && echo "This is stephen."
#do something you want here.
elif [ $name = "ANN" ]; then
#5. 当debug_level > 1时输出该调试信息。
test $debug_level -gt 1 && echo "This is ann."
#do something you want here.
else
#6. 当debug_level > 2时输出该调试信息。
test $debug_level -gt 2 && echo "This is others."
#do any other else.
fi
CTRL+D
/> ./test2.sh
Usage: ./test2.sh -d debug_level
/> ./test2.sh -d 1
The current debug level is 1.
Tell me your name. ann
/> ./test2.sh -d 2
The current debug level is 2.
Tell me your name. ann
This is ann.
三、判断参数是否为数字:
有些时候我们需要验证脚本的参数或某些变量的值是否为数字,如果不是则需要需要给出提示,并退出脚本。
/> cat > test3.sh
#!/bin/sh
#1. $1是脚本的第一个参数,这里作为awk命令的第一个参数传入给awk命令。
#2. 由于没有输入文件作为输入流,因此这里只是在BEGIN块中完成。
#3. 在awk中ARGV数组表示awk命令的参数数组,ARGV[0]表示命令本身,ARGV[1]表示第一个参数。
#4. match是awk的内置函数,返回值为匹配的正则表达式在字符串中(ARGV[1])的起始位置,没有找到返回0。
#5. 正则表达式的写法已经保证了匹配的字符串一定是十进制的正整数,如需要浮点数或负数,仅需修改正则即可。
#6. awk执行完成后将结果返回给isdigit变量,并作为其初始化值。
#7. isdigit=`echo $1 | awk '{ if (match($1, "^[0-9]+$") != 0) print "true"; else print "false" }' `
#8. 上面的写法也能实现该功能,但是由于有多个进程参与,因此效率低于下面的写法。
isdigit=`awk 'BEGIN { if (match(ARGV[1],"^[0-9]+$") != 0) print "true"; else print "false" }' $1`
if [[ $isdigit == "true" ]]; then
echo "This is numeric variable."
number=$1
else
echo "This is not numeric variable."
number=0
fi
CTRL+D
/> ./test3.sh 12
This is numeric variable.
/> ./test3.sh 12r
This is not numeric variable.
四、判断整数变量的奇偶性:
为了简化问题和突出重点,这里我们假设脚本的输入参数一定为合法的整数类型,因而在脚本内部将不再进行参数的合法性判断。
/> cat > test4.sh
#!/bin/sh
#1. 这里的重点主要是sed命令中正则表达式的写法,它将原有的数字拆分为两个模式(用圆括号拆分),一个前面的所有高位数字,另一个是最后一位低位数字,之后再用替换符的方式(\2),将原有数字替换为只有最后一位的数字,最后将结果返回为last_digit变量。
last_digit=`echo $1 | sed 's/\(.*\)\(.\)$/\2/'`
#2. 如果last_digit的值为0,2,4,6,8,就表示其为偶数,否则为奇数。
case $last_digit in
0|2|4|6|8)
echo "This is an even number." ;;
*)
echo "This is not an even number." ;;
esac
CTRL+D
/> ./test4.sh 34
This is an even number.
/> ./test4.sh 345
This is not an even number.
五、将Shell命令赋值给指定变量,以保证脚本的移植性:
有的时候当我们在脚本中执行某个命令时,由于操作系统的不同,可能会导致命令所在路径的不同,甚至是命令名称或选项的不同,为了保证脚本具有更好的平台移植性,我们可以将该功能的命令赋值给指定的变量,之后再使用该命令时,直接使用该变量即可。这样在今后增加更多OS时,我们只需为该变量基于新系统赋予不同的值即可,否则我们将不得不修改更多的地方,这样很容易导致因误修改而引发的Bug。
/> cat > test5.sh
#!/bin/sh
#1. 通过uname命令获取当前的系统名称,之后再根据OS名称的不同,给PING变量赋值不同的ping命令的全称。
osname=`uname -s`
#2. 可以在case的条件中添加更多的操作系统名称。
case $osname in
"Linux")
PING=/usr/sbin/ping ;;
"FreeBSD")
PING=/sbin/ping ;;
"SunOS")
PING=/usr/sbin/ping ;;
*)
;;
esac
CTRL+D
/> . ./test5.sh
/> echo $PING
/usr/sbin/ping
六、获取当前时间距纪元时间(1970年1月1日)所经过的天数:
在获取两个时间之间的差值时,需要考虑很多问题,如闰年、月份中不同的天数等。然而如果我们能够确定两个时间点之间天数的差值,那么再计算时分秒的差值时就非常简单了。在系统提供的C语言函数中,获取的时间值是从1970年1月1日0点到当前时间所流经的秒数,如果我们基于此计算两个时间之间天数的差值,将会大大简化我们的计算公式。
/> cat > test6.sh
#!/bin/sh
#1. 将date命令的执行结果(秒 分 小时 日 月 年)赋值给数组变量DATE。
declare -a DATE=(`date +"%S %M %k %d %m %Y"`)
#2. 为了提高效率,这个直接给出1970年1月1日到新纪元所流经的天数常量。
epoch_days=719591
#3. 从数组中提取各个时间部分值。
year=${DATE[5]}
month=${DATE[4]}
day=${DATE[3]}
hour=${DATE[2]}
minute=${DATE[1]}
second=${DATE[0]}
#4. 当月份值为1或2的时候,将月份变量的值加一,否则将月份值加13,年变量的值减一,这样做主要是因为后面的公式中取月平均天数时的需要。
if [ $month -gt 2 ]; then
month=$((month+1))
else
month=$((month+13))
year=$((year-1))
fi
#5. year变量参与的运算是需要考虑闰年问题的,该问题可以自行去google。
#6. month变量参与的运算主要是考虑月平均天数。
#7. 计算结果为当前日期距新世纪所流经的天数。
today_days=$(((year*365)+(year/4)-(year/100)+(year/400)+(month*306001/10000)+day))
#8. 总天数减去纪元距离新世纪的天数即可得出我们需要的天数了。
days_since_epoch=$((today_days-epoch_days))
echo $days_since_epoch
seconds_since_epoch=$(((days_since_epoch*86400)+(hour*3600)+(minute*60)+second))
echo $seconds_since_epoch
CTRL+D
/> . ./test6.sh
15310
1322829080
需要说明的是,推荐将该脚本的内容放到一个函数中,以便于我们今后计算类似的时间数据时使用。
一、将输入信息转换为大写字符后再进行条件判断:
我们在读取用户的正常输入后,很有可能会将这些输入信息用于条件判断,那么在进行比较时,我们将不得不考虑这些信息的大小写匹配问题。
/> cat > test1.sh
#!/bin/sh
echo -n "Please let me know your name. "
read name
#将变量name的值通过管道输出到tr命令,再由tr命令进行大小写转换后重新赋值给name变量。
name=`echo $name | tr [a-z] [A-Z]`
if [[ $name == "STEPHEN" ]]; then
echo "Hello, Stephen."
else
echo "You are not Stephen."
fi
CTRL+D
/> ./test1.sh
Please let me know your name. stephen
Hello, Stephen.
二、为调试信息设置输出级别:
我们经常在调试脚本时添加一些必要的调试信息,以便跟踪到程序中的错误。在完成调试后,一般都会选择删除这些额外的调试信息,在过了一段时间之后,如果脚本需要添加新的功能,那么我们将不得不重新进行调试,这样又有可能需要添加这些调试信息,在调试成功之后,这些信息可能会被再次删除。如果我们能够为我们的调试信息添加调试级别,使其只在必要的时候输出,我想这将会是一件非常惬意的事情。
/> cat > test2.sh
#!/bin/sh
if [[ $# == 0 ]]; then
echo "Usage: ./test2.sh -d debug_level"
exit 1
fi
#1. 读取脚本的命令行选项参数,并将选项赋值给变量argument。
while getopts d: argument
do
#2. 只有到选项为d(-d)时有效,同时将-d后面的参数($OPTARG)赋值给变量debug,表示当前脚本的调试级别。
case $argument in
d) debug_level=$OPTARG ;;
\?) echo "Usage: ./test2.sh -d debug_level"
exit 1
;;
esac
done
#3. 如果debug此时的值为空或者不是0-9之间的数字,给debug变量赋缺省值0.
if [[ -z $debug_level || $debug_level != [0-9] ]]; then
debug_level=0
fi
echo "The current debug_level level is $debug_level."
echo -n "Tell me your name."
read name
name=`echo $name | tr [a-z] [A-Z]`
if [ $name = "STEPHEN" ];then
#4. 根据当前脚本的调试级别判断是否输出其后的调试信息,此时当debug_level > 0时输出该调试信息。
test $debug_level -gt 0 && echo "This is stephen."
#do something you want here.
elif [ $name = "ANN" ]; then
#5. 当debug_level > 1时输出该调试信息。
test $debug_level -gt 1 && echo "This is ann."
#do something you want here.
else
#6. 当debug_level > 2时输出该调试信息。
test $debug_level -gt 2 && echo "This is others."
#do any other else.
fi
CTRL+D
/> ./test2.sh
Usage: ./test2.sh -d debug_level
/> ./test2.sh -d 1
The current debug level is 1.
Tell me your name. ann
/> ./test2.sh -d 2
The current debug level is 2.
Tell me your name. ann
This is ann.
三、判断参数是否为数字:
有些时候我们需要验证脚本的参数或某些变量的值是否为数字,如果不是则需要需要给出提示,并退出脚本。
/> cat > test3.sh
#!/bin/sh
#1. $1是脚本的第一个参数,这里作为awk命令的第一个参数传入给awk命令。
#2. 由于没有输入文件作为输入流,因此这里只是在BEGIN块中完成。
#3. 在awk中ARGV数组表示awk命令的参数数组,ARGV[0]表示命令本身,ARGV[1]表示第一个参数。
#4. match是awk的内置函数,返回值为匹配的正则表达式在字符串中(ARGV[1])的起始位置,没有找到返回0。
#5. 正则表达式的写法已经保证了匹配的字符串一定是十进制的正整数,如需要浮点数或负数,仅需修改正则即可。
#6. awk执行完成后将结果返回给isdigit变量,并作为其初始化值。
#7. isdigit=`echo $1 | awk '{ if (match($1, "^[0-9]+$") != 0) print "true"; else print "false" }' `
#8. 上面的写法也能实现该功能,但是由于有多个进程参与,因此效率低于下面的写法。
isdigit=`awk 'BEGIN { if (match(ARGV[1],"^[0-9]+$") != 0) print "true"; else print "false" }' $1`
if [[ $isdigit == "true" ]]; then
echo "This is numeric variable."
number=$1
else
echo "This is not numeric variable."
number=0
fi
CTRL+D
/> ./test3.sh 12
This is numeric variable.
/> ./test3.sh 12r
This is not numeric variable.
四、判断整数变量的奇偶性:
为了简化问题和突出重点,这里我们假设脚本的输入参数一定为合法的整数类型,因而在脚本内部将不再进行参数的合法性判断。
/> cat > test4.sh
#!/bin/sh
#1. 这里的重点主要是sed命令中正则表达式的写法,它将原有的数字拆分为两个模式(用圆括号拆分),一个前面的所有高位数字,另一个是最后一位低位数字,之后再用替换符的方式(\2),将原有数字替换为只有最后一位的数字,最后将结果返回为last_digit变量。
last_digit=`echo $1 | sed 's/\(.*\)\(.\)$/\2/'`
#2. 如果last_digit的值为0,2,4,6,8,就表示其为偶数,否则为奇数。
case $last_digit in
0|2|4|6|8)
echo "This is an even number." ;;
*)
echo "This is not an even number." ;;
esac
CTRL+D
/> ./test4.sh 34
This is an even number.
/> ./test4.sh 345
This is not an even number.
五、将Shell命令赋值给指定变量,以保证脚本的移植性:
有的时候当我们在脚本中执行某个命令时,由于操作系统的不同,可能会导致命令所在路径的不同,甚至是命令名称或选项的不同,为了保证脚本具有更好的平台移植性,我们可以将该功能的命令赋值给指定的变量,之后再使用该命令时,直接使用该变量即可。这样在今后增加更多OS时,我们只需为该变量基于新系统赋予不同的值即可,否则我们将不得不修改更多的地方,这样很容易导致因误修改而引发的Bug。
/> cat > test5.sh
#!/bin/sh
#1. 通过uname命令获取当前的系统名称,之后再根据OS名称的不同,给PING变量赋值不同的ping命令的全称。
osname=`uname -s`
#2. 可以在case的条件中添加更多的操作系统名称。
case $osname in
"Linux")
PING=/usr/sbin/ping ;;
"FreeBSD")
PING=/sbin/ping ;;
"SunOS")
PING=/usr/sbin/ping ;;
*)
;;
esac
CTRL+D
/> . ./test5.sh
/> echo $PING
/usr/sbin/ping
六、获取当前时间距纪元时间(1970年1月1日)所经过的天数:
在获取两个时间之间的差值时,需要考虑很多问题,如闰年、月份中不同的天数等。然而如果我们能够确定两个时间点之间天数的差值,那么再计算时分秒的差值时就非常简单了。在系统提供的C语言函数中,获取的时间值是从1970年1月1日0点到当前时间所流经的秒数,如果我们基于此计算两个时间之间天数的差值,将会大大简化我们的计算公式。
/> cat > test6.sh
#!/bin/sh
#1. 将date命令的执行结果(秒 分 小时 日 月 年)赋值给数组变量DATE。
declare -a DATE=(`date +"%S %M %k %d %m %Y"`)
#2. 为了提高效率,这个直接给出1970年1月1日到新纪元所流经的天数常量。
epoch_days=719591
#3. 从数组中提取各个时间部分值。
year=${DATE[5]}
month=${DATE[4]}
day=${DATE[3]}
hour=${DATE[2]}
minute=${DATE[1]}
second=${DATE[0]}
#4. 当月份值为1或2的时候,将月份变量的值加一,否则将月份值加13,年变量的值减一,这样做主要是因为后面的公式中取月平均天数时的需要。
if [ $month -gt 2 ]; then
month=$((month+1))
else
month=$((month+13))
year=$((year-1))
fi
#5. year变量参与的运算是需要考虑闰年问题的,该问题可以自行去google。
#6. month变量参与的运算主要是考虑月平均天数。
#7. 计算结果为当前日期距新世纪所流经的天数。
today_days=$(((year*365)+(year/4)-(year/100)+(year/400)+(month*306001/10000)+day))
#8. 总天数减去纪元距离新世纪的天数即可得出我们需要的天数了。
days_since_epoch=$((today_days-epoch_days))
echo $days_since_epoch
seconds_since_epoch=$(((days_since_epoch*86400)+(hour*3600)+(minute*60)+second))
echo $seconds_since_epoch
CTRL+D
/> . ./test6.sh
15310
1322829080
需要说明的是,推荐将该脚本的内容放到一个函数中,以便于我们今后计算类似的时间数据时使用。