您的位置:首页 > 运维架构 > Shell

shell实用编程技巧

2013-10-02 12:51 495 查看

一. 特殊文件: /dev/null和/dev/tty

Linux系统提供了两个对Shell编程非常有用的特殊文件,/dev/null和/dev/tty。其中/dev/null将会丢掉所有写入它的数据,换句换说,当程序将数据写入到此文件时,会认为它已经成功完成写入数据的操作,但实际上什么事都没有做。如果你需要的是命令的退出状态,而非它的输出,此功能会非常有用,见如下Shell代码:
/> vi test_dev_null.sh

#!/bin/bash
if grep hello TestFile > /dev/null
then
echo "Found"
else
echo "NOT Found"
fi


在vi中保存并退出后执行以下命令:
/> chmod +x test_dev_null.sh  #使该文件成为可执行文件
/> cat > TestFile
hello my friend
CTRL + D                             #退出命令行文件编辑状态
/> ./test_dev_null.sh
Found                                 #这里并没有输出grep命令的执行结果。


将以上Shell脚本做如下修改:
/> vi test_dev_null.sh

#!/bin/bash
if grep hello TestFile
then
echo "Found"
else
echo "NOT Found"
fi


在vi中保存退出后,再次执行该脚本:
/> ./test_dev_null.sh
hello my friend                      #grep命令的执行结果被输出了。
Found


下面我们再来看/dev/tty的用途。当程序打开此文件是,Linux会自动将它重定向到一个终端窗口,因此该文件对于读取人工输入时特别有用。见如下Shell代码:
/> vi test_dev_tty.sh

#!/bin/bash
printf "Enter new password: "    #提示输入
stty -echo                               #关闭自动打印输入字符的功能
read password < /dev/tty         #读取密码
printf "\nEnter again: "             #换行后提示再输入一次
read password2 < /dev/tty       #再读取一次以确认
printf "\n"                               #换行
stty echo                                #记着打开自动打印输入字符的功能
echo "Password = " $password #输出读入变量
echo "Password2 = " $password2
echo "All Done"


在vi中保存并退出后执行以下命令:
/> chmod +x test_dev_tty.sh #使该文件成为可执行文件
/> ./test_dev_tty
Enter new password:             #这里密码的输入被读入到脚本中的password变量
Enter again:                          #这里密码的输入被读入到脚本中的password2变量
Password = hello
Password2 = hello
All Done

二. 简单的命令跟踪

Linux Shell提供了两种方式来跟踪Shell脚本中的命令,以帮助我们准确的定位程序中存在的问题。下面的代码为第一种方式,该方式会将Shell脚本中所有被执行的命令打印到终端,并在命令前加"+":加号的后面还跟着一个空格。
/> cat > trace_all_command.sh
who | wc -l                          #这两条Shell命令将输出当前Linux服务器登录的用户数量
CTRL + D                            #退出命令行文件编辑状态
/> chmod +x trace_all_command.sh
/> sh -x ./trace_all_command.sh #Shell执行器的-x选项将打开脚本的执行跟踪功能。
+ wc -l                               #被跟踪的两条Shell命令
+ who
2                                       #实际输出结果。
Linux Shell提供的另一种方式可以只打印部分被执行的Shell命令,该方法在调试较为复杂的脚本时,显得尤为有用。
/> cat > trace_patial_command.sh
#! /bin/bash
set -x                                #从该命令之后打开跟踪功能
echo 1st echo                     #将被打印输出的Shell命令
set +x                               #该Shell命令也将被打印输出,然而在该命令被执行之后,所有的命令将不再打印输出
echo 2nd echo                    #该Shell命令将不再被打印输出。
CTRL + D                           #退出命令行文件编辑状态
/> chmod +x trace_patial_command.sh
/> ./trace_patial_command.sh
+ echo 1st echo
1st echo
+ set +x
2nd echo

三. 正则表达式基本语法描述

Linux Shell环境下提供了两种正则表达式规则,一个是基本正则表达式(BRE),另一个是扩展正则表达式(ERE)。

下面是这两种表达式的语法列表,需要注意的是,如果没有明确指出的Meta字符,其将可同时用于BRE和ERE,否则将尽适用于指定的模式。
正则元字符模式含义用例
\通常用于关闭其后续字符的特殊意义,恢复其原意。\(...\),这里的括号仅仅表示括号。
.匹配任何单个字符。a.b,将匹配abb、acb等
*匹配它之前的0-n个的单个字符。a*b,将匹配ab、aab、aaab等。
^匹配紧接着的正则表达式,在行的起始处。^ab,将匹配abc、abd等,但是不匹配cab。
$匹配紧接着的正则表达式,在行的结尾处。ab$,将匹配ab、cab等,但是不匹配abc。
[...]方括号表达式,匹配其内部任何字符。其中-表示连续字符的范围,^符号置于方括号里第一个字符则有反向的含义,即匹配不在列表内(方括号)的任何字符。如果想让]和-表示其原意,需要将其放置在方括号的首字符位置,如[]ab]或[-ab],如这两个字符同时存在,则将]放置在首字符位置,-放置在最尾部,如[]ab-]。[a-bA-Z0-9!]表示所有的大小写字母,数字和感叹号。[^abc]表示a、b、c之外的所有字符。[Tt]om,可以匹配Tom和tom。
\{n,m\}区间表达式,匹配在它前面的单个字符重复出现的次数区间,\{n\}表示重复n次;\{n,\}表示至少重复n次;\{n,m\}表示重复n到m次。ab\{2\}表示abb;ab\{2,\}表示abb、abbb等。ab\{2,4\}表示abb、abbb和abbbb。
\(...\)将圆括号之间的模式存储在特殊“保留空间”。最多可以将9个独立的子模式存储在单个模式中。匹配于子模式的文本,可以通过转义序列\1到\9,被重复使用在相同模式里。\(ab\).*\1表示ab组合出现两次,两次之间可存在任何数目的任何字符,如abcdab、abab等。
{n,m}(ERE)其功能等同于上面的\{n,m\},只是不再写\转义符了。ab+匹配ab、abbb等,但是不匹配a。
+(ERE)和前面的星号相比,+匹配的是前面正则表达式的1-n个实例。 
?(ERE)匹配前面正则表达式的0个或1个。ab?仅匹配a或ab。
|(ERE)匹配于|符号前后的正则表达式。(ab|cd)匹配ab或cd。
[:alpha:]匹配字母字符。[[:alpha:]!]ab$匹配cab、dab和!ab。
[:alnum:]匹配字母和数字字符。[[:alnum:]]ab$匹配1ab、aab。
[:blank:]匹配空格(space)和Tab字符。[[:alnum:]]ab$匹配1ab、aab。
[:cntrl:]匹配控制字符。 
[:digit:]匹配数字字符。 
[:graph:]匹配非空格字符。 
[:lower:]匹配小写字母字符。 
[:upper:]匹配大写字母字符。 
[:punct:]匹配标点字符。 
[:space:]匹配空白(whitespace)字符。 
[:xdigit:]匹配十六进制数字。 
\w匹配任何字母和数字组成的字符,等同于[[:alnum:]_] 
\W匹配任何非字母和数字组成的字符,等同于[^[:alnum:]_] 
\<\>匹配单词的起始和结尾。\<read匹配readme,me\>匹配readme。
下面的列表给出了Linux Shell中常用的工具或命令分别支持的正则表达式的类型。
 grepsedviegrepawk
BRE***  
ERE   **

四. 使用cut命令选定字段

cut命令是用来剪下文本文件里的数据,文本文件可以是字段类型或是字符类型。下面给出应用实例:
/> cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
... ...
/> cut -d : -f 1,5 /etc/passwd     #-d后面的冒号表示字段之间的分隔符,-f表示取分割后的哪些字段
root:root                                 #这里取出的是第一个和第五个字段。
bin:bin
daemon:daemon
adm:adm
... ...
/> cut -d: -f 3- /etc/passwd       #从第三个字段开始显示,直到最后一个字段。
0:0:root:/root:/bin/bash
1:1:bin:/bin:/sbin/nologin
2:2:daemon:/sbin:/sbin/nologin
3:4:adm:/var/adm:/sbin/nologin
4:7:lp:/var/spool/lpd:/sbin/nologin
... ...
这里需要进一步说明的是,使用cut命令还可以剪切以字符数量为标量的部分字符,该功能通过-c选项实现,其不能与-d选项共存。
/> cut -c 1-4 /etc/passwd          #取每行的前1-4个字符。
/> cut -c-4 /etc/passwd            #取每行的前4个字符。
root
bin:
daem
adm:
... ...
/> cut -c4- /etc/passwd            #取每行的第4个到最后字符。
t:x:0:0:root:/root:/bin/bash
:x:1:1:bin:/bin:/sbin/nologin
mon:x:2:2:daemon:/sbin:/sbin/nologin
:x:3:4:adm:/var/adm:/sbin/nologin
... ...
/> cut -c1,4 /etc/passwd           #取每行的第一个和第四个字符。
rt
b:
dm
a:
... ...
/> cut -c1-4,5 /etc/passwd        #取每行的1-4和第5个字符。
root:
bin:x
daemo
adm:x

五. 计算行数、字数以及字符数

Linux提供了一个简单的工具wc用于完成该功能,见如下用例:
/> echo This is a test of the emergency broadcast system | wc
1    9    49                              #1行,9个单词,49个字符
/> echo Testing one two three | wc -c
22                                         #22个字符
/> echo Testing one two three | wc -l
1                                           #1行
/> echo Testing one two three | wc -w
4                                           #4个单词
/> wc /etc/passwd /etc/group    #计算两个文件里的数据。
39   71  1933  /etc/passwd
62   62  906    /etc/group
101 133 2839  总用量

六. 提取开头或结尾数行

有时,你会需要从文本文件里把几行字,多半是靠近开头或结尾的几行提取出来。如查看工作日志等操作。Linux Shell提供head和tail两个命令来完成此项工作。见如下用例:
/> head -n 5 /etc/passwd           #显示输入文件的前五行。
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

/> tail -n 5 /etc/passwd             #显示输入文件的最后五行。
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash
pulse:x:496:494:PulseAudio System Daemon:/var/run/pulse:/sbin/nologin
gdm:x:42:42::/var/lib/gdm:/sbin/nologin
stephen:x:500:500:stephen:/home/stephen:/bin/bash


如果使用者想查看不间断增长的日志(如服务程序输出的),可以使用tail的-f选项,这样可以让tail命令不会自动退出,必须通过CTRL+C命令强制退出,因此该选项不适合用于Shell脚本中,见如下用例:
/> tail -f -n 5 my_server_log
... ...
^C                                         #CTRL+C退出到命令行提示符状态。

七. grep家族:

1.  grep退出状态: 0: 表示成功;
1: 表示在所提供的文件无法找到匹配的pattern;
2: 表示参数中提供的文件不存在。
见如下示例:
/> grep 'root' /etc/passwd
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
/> echo $?
0

/> grep 'root1' /etc/passwd #用户root1并不存在
/> echo $?
1

/> grep 'root' /etc/passwd1 #这里的/etc/passwd1文件并不存在
grep: /etc/passwd1: No such file or directory
/> echo $?
2
2.  grep中应用正则表达式的实例:需要说明的是下面所涉及的正则表达式在上一篇中已经给出了详细的说明,因此在看下面例子的时候,可以与前一篇的正则说明部分结合着看。 /> cat testfile
northwest NW Charles Main 3.0 .98 3 34
western WE Sharon Gray 5.3 .97 5 23
southwest SW Lewis Dalsass 2.7 .8 2 18
southern SO Suan Chin 5.1 .95 4 15
southeast SE Patricia Hemenway 4.0 .7 4 17
eastern EA TB Savage 4.4 .84 5 20
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9
central CT Ann Stephens 5.7 .94 5 13

/> grep NW testfile #打印出testfile中所有包含NW的行。
northwest NW Charles Main 3.0 .98 3 34

/> grep '^n' testfile #打印出以n开头的行。
northwest NW Charles Main 3.0 .98 3 34
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9

/> grep '4$' testfile #打印出以4结尾的行。
northwest NW Charles Main 3.0 .98 3 34

/> grep '5\..' testfile #打印出第一个字符是5,后面跟着一个.字符,在后面是任意字符的行。
western WE Sharon Gray 5.3 .97 5 23
southern SO Suan Chin 5.1 .95 4 15
northeast NE AM Main Jr. 5.1 .94 3 13
central CT Ann Stephens 5.7 .94 5 13

/> grep '\.5' testfile #打印出所有包含.5的行。
north NO Margot Weber 4.5 .89 5 9

/> grep '^[we]' testfile #打印出所有以w或e开头的行。
western WE Sharon Gray 5.3 .97 5 23
eastern EA TB Savage 4.4 .84 5 20

/> grep '[^0-9]' testfile #打印出所有不是以0-9开头的行。
northwest NW Charles Main 3.0 .98 3 34
western WE Sharon Gray 5.3 .97 5 23
southwest SW Lewis Dalsass 2.7 .8 2 18
southern SO Suan Chin 5.1 .95 4 15
southeast SE Patricia Hemenway 4.0 .7 4 17
eastern EA TB Savage 4.4 .84 5 20
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9
central CT Ann Stephens 5.7 .94 5 13

/> grep '[A-Z][A-Z] [A-Z]' testfile #打印出所有包含前两个字符是大写字符,后面紧跟一个空格及一个大写字母的行。
eastern EA TB Savage 4.4 .84 5 20
northeast NE AM Main Jr. 5.1 .94 3 13
注:在执行以上命令时,如果不能得到预期的结果,即grep忽略了大小写,导致这一问题的原因很可能是当前环境的本地化的设置问题。对于以上命令,如果我将当前语言设置为en_US的时候,它会打印出所有的行,当我将其修改为中文环境时,就能得到我现在的输出了。 /> export LANG=zh_CN #设置当前的语言环境为中文。
/> export LANG=en_US #设置当前的语言环境为美国。
/> export LANG=en_Br #设置当前的语言环境为英国。

/> grep '[a-z]\{9\}' testfile #打印所有包含每个字符串至少有9个连续小写字符的字符串的行。
northwest NW Charles Main 3.0 .98 3 34
southwest SW Lewis Dalsass 2.7 .8 2 18
southeast SE Patricia Hemenway 4.0 .7 4 17
northeast NE AM Main Jr. 5.1 .94 3 13
第一个字符是3,紧跟着一个句点,然后是任意一个数字,然后是任意个任意字符,然后又是一个3,然后是制表符,然后又是一个3,需要说明的是,下面正则中的\1表示\(3\)。 /> grep '\(3\)\.[0-9].*\1 *\1' testfile
northwest NW Charles Main 3.0 .98 3 34

/> grep '\<north' testfile #打印所有以north开头的单词的行。
northwest NW Charles Main 3.0 .98 3 34
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9

/> grep '\<north\>' testfile #打印所有包含单词north的行。
north NO Margot Weber 4.5 .89 5 9

/> grep '^n\w*' testfile #第一个字符是n,后面是任意字母或者数字。
northwest NW Charles Main 3.0 .98 3 34
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9
3.  扩展grep(grep -E 或者 egrep):使用扩展grep的主要好处是增加了额外的正则表达式元字符集。下面我们还是继续使用实例来演示扩展grep。 /> egrep 'NW|EA' testfile #打印所有包含NW或EA的行。如果不是使用egrep,而是grep,将不会有结果查出。
northwest NW Charles Main 3.0 .98 3 34
eastern EA TB Savage 4.4 .84 5 20

/> grep 'NW\|EA' testfile #对于标准grep,如果在扩展元字符前面加\,grep会自动启用扩展选项-E。
northwest NW Charles Main 3.0 .98 3 34
eastern EA TB Savage 4.4 .84 5 20

/> egrep '3+' testfile
/> grep -E '3+' testfile
/> grep '3\+' testfile #这3条命令将会打印出相同的结果,即所有包含一个或多个3的行。
northwest NW Charles Main 3.0 .98 3 34
western WE Sharon Gray 5.3 .97 5 23
northeast NE AM Main Jr. 5.1 .94 3 13
central CT Ann Stephens 5.7 .94 5 13

/> egrep '2\.?[0-9]' testfile
/> grep -E '2\.?[0-9]' testfile
/> grep '2\.\?[0-9]' testfile #首先含有2字符,其后紧跟着0个或1个点,后面再是0和9之间的数字。
western WE Sharon Gray 5.3 .97 5 23
southwest SW Lewis Dalsass 2.7 .8 2 18
eastern EA TB Savage 4.4 .84 5 20

/> egrep '(no)+' testfile
/> grep -E '(no)+' testfile
/> grep '\(no\)\+' testfile #3个命令返回相同结果,即打印一个或者多个连续的no的行。
northwest NW Charles Main 3.0 .98 3 34
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9

/> grep -E '\w+\W+[ABC]' testfile #首先是一个或者多个字母,紧跟着一个或者多个非字母数字,最后一个是ABC中的一个。
northwest NW Charles Main 3.0 .98 3 34
southern SO Suan Chin 5.1 .95 4 15
northeast NE AM Main Jr. 5.1 .94 3 13
central CT Ann Stephens 5.7 .94 5 13

/> egrep '[Ss](h|u)' testfile
/> grep -E '[Ss](h|u)' testfile
/> grep '[Ss]\(h\|u\)' testfile #3个命令返回相同结果,即以S或s开头,紧跟着h或者u的行。
western WE Sharon Gray 5.3 .97 5 23
southern SO Suan Chin 5.1 .95 4 15

/> egrep 'w(es)t.*\1' testfile #west开头,其中es为\1的值,后面紧跟着任意数量的任意字符,最后还有一个es出现在该行。
northwest NW Charles Main 3.0 .98 3 34
4.  grep选项:这里先列出grep常用的命令行选项:
选项说明
-c只显示有多少行匹配,而不具体显示匹配的行。
-h不显示文件名。
-i在字符串比较的时候忽略大小写。
-l只显示包含匹配模板的行的文件名清单。
-L只显示不包含匹配模板的行的文件名清单。
-n在每一行前面打印改行在文件中的行数。
-v反向检索,只显示不匹配的行。
-w只显示完整单词的匹配。
-x只显示完整行的匹配。
-r/-R如果文件参数是目录,该选项将递归搜索该目录下的所有子目录和文件。
/> grep -n '^south' testfile  #-n选项在每一个匹配行的前面打印行号。
3:southwest     SW      Lewis Dalsass         2.7     .8      2       18
4:southern       SO      Suan Chin               5.1     .95     4       15
5:southeast      SE      Patricia Hemenway    4.0     .7      4       17

/> grep -i 'pat' testfile     #-i选项关闭了大小写敏感。
southeast       SE      Patricia Hemenway       4.0     .7      4       17

/> grep -v 'Suan Chin' testfile #打印所有不包含Suan Chin的行。
northwest       NW      Charles Main          3.0     .98     3       34
western          WE      Sharon Gray           5.3     .97    5       23
southwest       SW      Lewis Dalsass        2.7     .8      2       18
southeast        SE      Patricia Hemenway   4.0     .7      4       17
eastern           EA      TB Savage              4.4     .84     5       20
northeast        NE      AM Main Jr.             5.1     .94     3       13
north              NO      Margot Weber        4.5     .89     5       9
central            CT      Ann Stephens         5.7     .94     5       13

/> grep -l 'ss' testfile  #-l使得grep只打印匹配的文件名,而不打印匹配的行。
testfile

/> grep -c 'west' testfile #-c使得grep只打印有多少匹配模板的行。
3

/> grep -w 'north' testfile #-w只打印整个单词匹配的行。
north           NO      Margot Weber    4.5     .89     5       9

/> grep -C 2 Patricia testfile #打印匹配行及其上下各两行。
southwest      SW     Lewis Dalsass         2.7     .8       2       18
southern        SO      Suan Chin              5.1     .95     4       15
southeast       SE      Patricia Hemenway   4.0     .7      4       17
eastern          EA      TB Savage              4.4     .84     5       20
northeast       NE      AM Main Jr.             5.1     .94     3       13

/> grep -B 2 Patricia testfile #打印匹配行及其前两行。
southwest      SW      Lewis Dalsass         2.7     .8      2       18
southern        SO      Suan Chin               5.1     .95    4       15
southeast       SE      Patricia Hemenway   4.0     .7      4       17

/> grep -A 2 Patricia testfile #打印匹配行及其后两行。
southeast       SE      Patricia Hemenway   4.0     .7      4       17
eastern           EA      TB Savage              4.4     .84     5       20
northeast       NE       AM Main Jr.             5.1     .94     3       13

十三、格式化输出指定用户的当前运行进程:

在这个例子中,我们通过脚本参数的形式,将用户列表传递给该脚本,脚本在读取参数后,以树的形式将用户列表中用户的所属进程打印出来。/> cat > test13.sh
#!/bin/sh

#1. 循环读取脚本参数,构造egrep可以识别的用户列表变量(基于grep的扩展正则表达式)。
#2. userlist变量尚未赋值,则直接使用第一个参数为它赋值。
#3. 如果已经赋值,且脚本参数中存在多个用户,这里需要在每个用户名之间加一个竖线,在egrep中,竖线是分割的元素之间是或的关系。
#4. shift命令向左移动一个脚本的位置参数,这样可以使循环中始终操作第一个参数。

while [ $# -gt 0 ]
do
if [ -z "$userlist" ]; then
userlist="$1"
else
userlist="$userlist|$1"
fi
shift
done

#5. 如果没有用户列表,则搜索所有用户的进程。
#6. "^ *($userlist) ": 下面的调用方式,该正则的展开形式为"^ *(root|avahi|postfix|rpc|dbus) "。其含义为,以0个或多个空格开头,之后将是root、avahi、postfix、rpc或dbus之中的任何一个字符串,后面再跟随一个空格。

if [ -z "$userlist" ]; then
userlist="."
else
userlist="^ *($userlist) "
fi

#7. ps命令输出所有进程的user和命令信息,将结果传递给sed命令,sed将删除ps的title部分。
#8. egrep过滤所有进程记录中,包含指定用户列表的进程记录,再将过滤后的结果传递给sort命令。
#9. sort命令中的-b选项将忽略前置空格,并以user,再以进程名排序,将结果传递个uniq命令。
#10.uniq命令将合并重复记录,-c选项将会使每条记录前加重复的行数。
#11.第二个sort将再做一次排序,先以user,再以重复计数由大到小,最后以进程名排序。将结果传给awk命令。
#12.awk命令将数据进行格式化,并删除重复的user。

ps -eo user,comm | sed -e 1d | egrep "$userlist" |
sort -b -k1,1 -k2,2 | uniq -c | sort -b -k2,2 -k1nr,1 -k3,3 |
awk ' { user = (lastuser == $2) ? " " : $2;
lastuser = $2;
printf("%-15s\t%2d\t%s\n",user,$1,$3)
}'
CTRL+D

/> ./test13.sh root avahi postfix rpc dbus
avahi 2 avahi-daemon
dbus 1 dbus-daemon
postfix 1 pickup
1 qmgr
root 5 mingetty
3 udevd
2 sort
2 sshd
... ...
rpc 1 rpcbind

十四、用脚本完成which命令的基本功能:

我们经常会在脚本中调用其他的应用程序,为了保证脚本具有更好的健壮性,以及错误提示的准确性,我们可能需要在执行前验证该命令是否存在,或者说是否可以被执行。这首先要确认该命令是否位于PATH变量包含的目录中,再有就是该文件是否为可执行文件。/> cat > test14.sh

#!/bin/sh

#1. 该函数用于判断参数1中的命令是否位于参数2所包含的目录列表中。需要说明的是,函数里面的$1和$2是指函数的参数,而不是脚本的参数,后面也是如此。
#2. cmd=$1和path=$2,将参数赋给有意义的变量名,是一个很好的习惯。
#3. 由于PATH环境变量中,目录之间的分隔符是冒号,因此这里需要临时将IFS设置为冒号,函数结束后再还原。
#4. 在for循环中,逐个变量目录列表中的目录,以判断该命令是否存在,且为可执行程序。

isInPath() {
cmd=$1 path=$2 result=1
oldIFS=$IFS IFS=":"
for dir in $path
do
if [ -x $dir/$cmd ]; then
result=0
fi
done
IFS=oldifs
return $result
}

#5. 检查命令是否存在的主功能函数,先判断是否为绝对路径,即$var变量的第一个字符是否为/,如果是,再判断它是否有可执行权限。
#6. 如果不是绝对路径,通过isInPath函数判断是否该命令在PATH环境变量指定的目录中。

checkCommand() {
var=$1
if [ ! -z "$var" ]; then
if [ "${var:0:1}" = "/" ]; then
if [ ! -x $var ]; then
return 1
fi
elif ! isInPath $var $PATH ; then
return 2
fi
fi
}

#7. 脚本参数的合法性验证。

if [ $# -ne 1 ]; then
echo "Usage: $0 command" >&2;
fi

#8. 根据返回值打印不同的信息。我们可以在这里根据我们的需求完成不同的工作。

checkCommand $1
case $? in
0) echo "$1 found in PATH." ;;
1) echo "$1 not found or not executable." ;;
2) echo "$1 not found in PATH." ;;
esac
exit 0
CTRL+D

/> ./test14.sh echo
echo found in PATH.

/> ./test14.sh MyTest
MyTest not found in PATH.

/> ./test14.sh /bin/MyTest
/bin/MyTest not found or not executable.

十五、验证输入信息是否合法:

这里给出的例子是验证用户输入的信息是否都是数字和字母。需要说明的是,之所以将其收集到该系列中,主要是因为它实现的方式比较巧妙。/> cat > test15.sh
#!/bin/sh
echo -n "Enter your input: "
read input

#1. 事实上,这里的巧妙之处就是先用sed替换了非法部分,之后再将替换后的结果与原字符串比较。这种写法也比较容易扩展。

parsed_input=`echo $input | sed 's/[^[:alnum:]]//g'`
if [ "$parsed_input" != "$input" ]; then
echo "Your input must consist of only letters and numbers."
else
echo "Input is OK."
fi
CTRL+D

/> ./test15.sh
Enter your input: hello123
Input is OK.

/> ./test15.sh
Enter your input: hello world
Your input must consist of only letters and numbers.

十六、整数验证:

整数的重要特征就是只是包含数字0到9和负号(-)。/> cat > test16.sh
#!/bin/sh

#1. 判断变量number的第一个字符是否为负号(-),如果只是则删除该负号,并将删除后的结果赋值给left_number变量。
#2. "${number#-}"的具体含义,可以参考该系列博客中"Linux Shell常用技巧(十一)",搜索关键字"变量模式匹配运算符"即可。

number=$1
if [ "${number:0:1}" = "-" ]; then
left_number="${number#-}"
else
left_number=$number
fi

#3. 将left_number变量中所有的数字都替换掉,因此如果返回的字符串变量为空,则表示left_number所包含的字符均为数字。

nodigits=`echo $left_number | sed 's/[[:digit:]]//g'`
if [ "$nodigits" != "" ]; then
echo "Invalid number format!"
else
echo "You are valid number."
fi
CTRL+D

/> ./test16.sh -123
You are valid number.

/> ./test16.sh 123e
Invalid number format!

十七、判断指定的年份是否为闰年:

这里我们先列出闰年的规则:不能被4整除的年一定不是闰年;
可以同时整除4和400的年一定是闰年;
可以整除4和100,但是不能整除400的年,不是闰年;
其他可以整除的年都是闰年。
#!/bin/sh
year=$1
if [ "$((year % 4))" -ne 0 ]; then
echo "This is not a leap year."
exit 1
elif [ "$((year % 400))" -eq 0 ]; then
echo "This is a leap year."
exit 0
elif [ "$((year % 100))" -eq 0 ]; then
echo "This is not a leap year."
exit 1
else
echo "This is a leap year."
exit 0
fi
CTRL+D

/> ./test17.sh 1933
This is not a leap year.

/> ./test17.sh 1936
This is a leap year.

十八、将单列显示转换为多列显示:

我们经常会在显示时将单行的输出,格式化为多行的输出,通常情况下,为了完成该操作,我们将加入更多的代码,将输出的结果存入数组或临时文件,之后再重新遍历它们,从而实现单行转多行的目的。在这里我们介绍一个使用xargs命令的技巧,可以用更简单、更高效的方式来完成该功能。/> cat > test18.sh
#!/bin/sh

#1. passwd文件中,有可能在一行内出现一个或者多个空格字符,因此在直接使用cat命令的结果时,for循环会被空格字符切开,从而导致一行的文本被当做多次for循环的输入,这样我们不得不在sed命令中,将cat输出的每行文本进行全局替换,将空格字符替换为%20。事实上,我们当然可以将cat /etc/passwd的输出以管道的形式传递给cut命令,这里之所以这样写,主要是为了演示一旦出现类似的问题该如果巧妙的处理。
#2. 这里将for循环的输出以管道的形式传递给sort命令,sort命令将基于user排序。
#3. -xargs -n 2是这个技巧的重点,它将sort的输出进行合并,-n选项后面的数字参数将提示xargs命令将多少次输出合并为一次输出,并传递给其后面的命令。在本例中,xargs会将从sort得到的每两行数据合并为一行,中间用空格符分离,之后再将合并后的数据传递给后面的awk命令。事实上,对于awk而言,你也可以简单的认为xargs减少了对它(awk)的一半调用。
#4. 如果打算在一行内显示3行或更多的行,可以将-n后面的数字修改为3或其它更高的数字。你还可以修改awk中的print命令,使用更为复杂打印输出命令,以得到更为可读的输出效果。

for line in `cat /etc/passwd | sed 's/ /%20/g'`
do
user=`echo $line | cut -d: -f1`
echo $user
done | \
sort -k1,1 | \
xargs -n 2 | \
awk '{print $1, $2}'
CTRL+D

/> ./test18.sh
abrt adm
apache avahi
avahi-autoipd bin
daemon daihw
dbus ftp
games gdm
gopher haldaemon
halt lp
mail nobody
ntp operator
postfix pulse
root rtkit
saslauth shutdown
sshd sync
tcpdump usbmuxd
uucp vcsa

九. awk实用功能:

和sed一样,awk也是逐行扫描文件的,从第一行到最后一行,寻找匹配特定模板的行,并在这些行上运行“选择”动作。如果一个模板没有指定动作,这些匹配的行就被显示在屏幕上。如果一个动作没有模板,所有被动作指定的行都被处理。1. awk的基本格式:
/> awk 'pattern' filename
/> awk '{action}' filename
/> awk 'pattern {action}' filename
具体应用方式分别见如下三个用例:/> cat employees
Tom Jones 4424 5/12/66 543354
Mary Adams 5346 11/4/63 28765
Sally Chang 1654 7/22/54 650000
Billy Black 1683 9/23/44 336500

/> awk '/Mary/' employees #打印所有包含模板Mary的行。
Mary Adams 5346 11/4/63 28765
#打印文件中的第一个字段,这个域在每一行的开始,缺省由空格或其它分隔符。/> awk '{print $1}' employees
Tom
Mary
Sally
Billy

/> awk '/Sally/{print $1, $2}' employees #打印包含模板Sally的行的第一、第二个域字段。
Sally Chang
2. awk的格式输出:
awk中同时提供了print和printf两种打印输出的函数,其中print函数的参数可以是变量、数值或者字符串。字符串必须用双引号引用,参数用逗号分隔。如果没有逗号,参数就串联在一起而无法区分。这里,逗号的作用与输出文件的分隔符的作用是一样的,只是后者是空格而已。下面给出基本的转码序列:
转码含义
\n换行
\r回车
\t制表符
/> date | awk '{print "Month: " $2 "\nYear: ", $6}'
Month: Oct
Year: 2011

/> awk '/Sally/{print "\t\tHave a nice day, " $1,$2 "\!"}' employees
Have a nice day, Sally Chang!
在打印数字的时候你也许想控制数字的格式,我们通常用printf来完成这个功能。awk的特殊变量OFMT也可以在使用print函数的时候,控制数字的打印格式。它的默认值是"%.6g"----小数点后面6位将被打印。/> awk 'BEGIN { OFMT="%.2f"; print 1.2456789, 12E-2}'
1.25 0.12
现在我们介绍一下功能更为强大的printf函数,其用法和c语言中printf基本相似。下面我们给出awk中printf的格式化说明符列表:
格式化说明符功能示例结果
%c打印单个ASCII字符。printf("The
character is %c.\n",x)
The character is A.
%d打印十进制数。printf("The
boy is %d years old.\n",y)
The boy is 15 years old.
%e打印用科学记数法表示的数。printf("z
is %e.\n",z)
z is 2.3e+01.
%f打印浮点数。printf("z
is %f.\n",z)
z is 2.300000
%o打印八进制数。printf("y
is %o.\n",y)
y is 17.
%s打印字符串。printf("The
name of the culprit is %s.\n",$1);
The name of the culprit is Bob Smith.
%x打印十六进制数。printf("y
is %x.\n",y)
y is f.
注:假设列表中的变脸值为x = A, y = 15, z = 2.3, $1 = "Bob Smith"
/> echo "Linux" | awk '{printf "|%-15s|\n", $1}' # %-15s表示保留15个字符的空间,同时左对齐。
|Linux |

/> echo "Linux" | awk '{printf "|%15s|\n", $1}' # %-15s表示保留15个字符的空间,同时右对齐。
| Linux|
#%8d表示数字右对齐,保留8个字符的空间。/> awk '{printf "The name is %-15s ID is %8d\n", $1,$3}' employees
The name is Tom ID is 4424
The name is Mary ID is 5346
The name is Sally ID is 1654
The name is Billy ID is 1683
3. awk中的记录和域:
awk中默认的记录分隔符是回车,保存在其内建变量ORS和RS中。$0变量是指整条记录。/> awk '{print $0}' employees #这等同于print的默认行为。
Tom Jones 4424 5/12/66 543354
Mary Adams 5346 11/4/63 28765
Sally Chang 1654 7/22/54 650000
Billy Black 1683 9/23/44 336500
变量NR(Number of Record),记录每条记录的编号。/> awk '{print NR, $0}' employees
1 Tom Jones 4424 5/12/66 543354
2 Mary Adams 5346 11/4/63 28765
3 Sally Chang 1654 7/22/54 650000
4 Billy Black 1683 9/23/44 336500
变量NF(Number of Field),记录当前记录有多少域。/> awk '{print $0,NF}' employees
Tom Jones 4424 5/12/66 543354 5
Mary Adams 5346 11/4/63 28765 5
Sally Chang 1654 7/22/54 650000 5
Billy Black 1683 9/23/44 336500 5
#根据employees生成employees2。sed的用法可以参考上一篇blog。/> sed 's/[[:space:]]\+\([0-9]\)/:\1/g;w employees2' employees
/> cat employees
Tom Jones:4424:5/12/66:543354
Mary Adams:5346:11/4/63:28765
Sally Chang:1654:7/22/54:650000
Billy Black:1683:9/23/44:336500

/> awk -F: '/Tom Jones/{print $1,$2}' employees2 #这里-F选项后面的字符表示分隔符。
Tom Jones 4424
变量OFS(Output Field Seperator)表示输出字段间的分隔符,缺省是空格。/> awk -F: '{OFS = "?"}; /Tom/{print $1,$2 }' employees2 #在输出时,域字段间的分隔符已经是?(问号)了
Tom Jones?4424
对于awk而言,其模式部分将控制这动作部分的输入,只有符合模式条件的记录才可以交由动作部分基础处理,而模式部分不仅可以写成正则表达式(如上面的例子),awk还支持条件表达式,如:/> awk '$3 < 4000 {print}' employees
Sally Chang 1654 7/22/54 650000
Billy Black 1683 9/23/44 336500
在花括号内,用分号分隔的语句称为动作。如果模式在动作前面,模式将决定什么时候发出动作。动作可以是一个语句或是一组语句。语句之间用分号分隔,也可以用换行符,如:pattern { action statement; action statement; etc. } or
pattern {
action statement
action statement
}
模式和动作一般是捆绑在一起的。需要注意的是,动作是花括号内的语句。模式控制的动作是从第一个左花括号开始到第一个右花括号结束,如下:/> awk '$3 < 4000 && /Sally/ {print}' employees
Sally Chang 1654 7/22/54 6500004. 匹配操作符:
" ~ " 用来在记录或者域内匹配正则表达式。/> awk '$1 ~ /[Bb]ill/' employees #显示所有第一个域匹配Bill或bill的行。
Billy Black 1683 9/23/44 336500

/> awk '$1 !~ /[Bb]ill/' employees #显示所有第一个域不匹配Bill或bill的行,其中!~表示不匹配的意思。
Tom Jones 4424 5/12/66 543354
Mary Adams 5346 11/4/63 28765
Sally Chang 1654 7/22/54 650000
5. awk的基本应用实例:
/> cat testfile
northwest NW Charles Main 3.0 .98 3 34
western WE Sharon Gray 5.3 .97 5 23
southwest SW Lewis Dalsass 2.7 .8 2 18
southern SO Suan Chin 5.1 .95 4 15
southeast SE Patricia Hemenway 4.0 .7 4 17
eastern EA TB Savage 4.4 .84 5 20
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9
central CT Ann Stephens 5.7 .94 5 13

/> awk '/^north/' testfile #打印所有以north开头的行。
northwest NW Charles Main 3.0 .98 3 34
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9

/> awk '/^(no|so)/' testfile #打印所有以so和no开头的行。
northwest NW Charles Main 3.0 .98 3 34
southwest SW Lewis Dalsass 2.7 .8 2 18
southern SO Suan Chin 5.1 .95 4 15
southeast SE Patricia Hemenway 4.0 .7 4 17
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9

/> awk '$5 ~ /\.[7-9]+/' testfile #第五个域字段匹配包含.(点),后面是7-9的数字。
southwest SW Lewis Dalsass 2.7 .8 2 18
central CT Ann Stephens 5.7 .94 5 13

/> awk '$8 ~ /[0-9][0-9]$/{print $8}' testfile #第八个域以两个数字结束的打印。
34
23
18
15
17
20
13

十. awk表达式功能:

1. 比较表达式:
比较表达式匹配那些只在条件为真时才运行的行。这些表达式利用关系运算符来比较数字和字符串。见如下awk支持的条件表达式列表:
运算符含义例子
<小于x
< y
<=小于等于x
<= y
==等于x
== y
!=不等于x
!= y
>=大于等于x
>= y
>大于x
> y
~匹配x
~ /y/
!~不匹配x
!~ /y/
/> cat employees
Tom Jones 4424 5/12/66 543354
Mary Adams 5346 11/4/63 28765
Sally Chang 1654 7/22/54 650000
Billy Black 1683 9/23/44 336500

/> awk '$3 == 5346' employees #打印第三个域等于5346的行。
Mary Adams 5346 11/4/63 28765

/> awk '$3 > 5000 {print $1}' employees #打印第三个域大于5000的行的第一个域字段。
Mary

/> awk '$2 ~ /Adam/' employess #打印第二个域匹配Adam的行。
Mary Adams 5346 11/4/63 28765
2. 条件表达式:
条件表达式使用两个符号--问号和冒号给表达式赋值: conditional expression1 ? expression2 : expressional3,其逻辑等同于C语言中的条件表达式。其对应的if/else语句如下:{
if (expression1)
expression2
else
expression3
}
/> cat testfile
northwest NW Charles Main 3.0 .98 3 34
western WE Sharon Gray 5.3 .97 5 23
southwest SW Lewis Dalsass 2.7 .8 2 18
southern SO Suan Chin 5.1 .95 4 15
southeast SE Patricia Hemenway 4.0 .7 4 17
eastern EA TB Savage 4.4 .84 5 20
northeast NE AM Main Jr. 5.1 .94 3 13
north NO Margot Weber 4.5 .89 5 9
central CT Ann Stephens 5.7 .94 5 13

/> awk 'NR <= 3 {print ($7 > 4 ? "high "$7 : "low "$7) }' testfile
low 3
high 5
low 2
3. 数学表达式:
运算可以在模式内进行,其中awk将所有的运算都视为浮点运算,见如下列表:
运算符含义例子
+x
+ y
-x
- y
*x
* y
/x
/ y
%取余x
% y
^乘方x
^ y
/> awk '/southern/{print $5 + 10}' testfile #如果记录包含正则表达式southern,第五个域就加10并打印。
15.1

/> awk '/southern/{print $8 /2 }' testfile #如果记录包含正则表达式southern,第八个域除以2并打印。
7.5
4. 逻辑表达式:
见如下列表:
运算符含义例子
&&逻辑与a
&& b
||逻辑或a
|| b
!逻辑非!a
/> awk '$8 > 10 && $8 < 17' testfile #打印出第八个域的值大于10小于17的记录。
southern SO Suan Chin 5.1 .95 4 15
central CT Ann Stephens 5.7 .94 5 13
#打印第二个域等于NW,或者第一个域匹配south的行的第一、第二个域。/> awk '$2 == "NW" || $1 ~ /south/ {print $1,$2}' testfile
northwest NW
southwest SW
southern SO
southeast SE

/> awk '!($8 > 13) {print $8}' testfile #打印第八个域字段不大于13的行的第八个域。
3
9
13
5. 范围模板:
范围模板匹配从第一个模板的第一次出现到第二个模板的第一次出现,第一个模板的下一次出现到第一个模板的下一次出现等等。如果第一个模板匹配而第二个模板没有出现,awk就显示到文件末尾的所有行。/> awk '/^western/,/^eastern/ {print $1}' testfile #打印以western开头到eastern开头的记录的第一个域。
western WE
southwest SW
southern SO
southeast SE
eastern EA
6. 赋值符号:
#找到第三个域等于Ann的记录,然后给该域重新赋值为Christian,之后再打印输出该记录。/> awk '$3 == "Ann" { $3 = "Christian"; print}' testfile
central CT Christian Stephens 5.7 .94 5 13

/> awk '/Ann/{$8 += 12; print $8}' testfile #找到包含Ann的记录,并将该条记录的第八个域的值+=12,最后再打印输出。
25

二十五、通过FTP下载指定的文件

相比于手工调用FTP命令下载文件,该脚本提供了更为方便的操作方式。/> cat > test25.sh

#!/bin/sh

#1. 测试脚本参数数量的有效性。

if [ $# -ne 2 ]; then
echo "Usage: $0 ftp://... username" >&2
exit 1
fi

#2. 获取第一个参数的前六个字符,如果不是"ftp://",则视为非法FTP URL格式。这里cut的-c选项表示按照字符的方式截取第一到第六个字符。

header=`echo $1 | cut -c1-6`
if [ "$header" != "ftp://" ]; then
echo "$0: Invalid ftp URL." >&2
exit 1
fi

#3. 合法ftp URL的例子:ftp://ftp.myserver.com/download/test.tar
#4. 针对上面的URL示例,cut命令通过/字符作为分隔符,这样第三个域字段表示server(ftp.myserver.com)。
#5. 在截取filename时,cut命令也是通过/字符作为分隔符,但是"-f4-"将获取从第四个字段开始的后面所有字段(download/test.tar)。
#6. 通过basename命令获取filename的文件名部分。

server=`echo $1 | cut -d/ -f3`
filename=`echo $1 | cut -d/ -f4-`
basefile=`basename $filename`
ftpuser=$2

#7. 这里需要调用stty -echo,以便后面的密码输入不会显示,在输入密码之后,需要再重新打开该选项,以保证后面的输入可以恢复显示。
#8. echo "",是模拟一次换换。

echo -n "Password for $ftpuser: "
stty -echo
read password
stty echo
echo ""

#9. 通过HERE文档,批量执行ftp命令。

echo ${0}: Downloading $baseile from server $server.
ftp -n << EOF
open $server
user $ftpuser $password
get $filename $basefile
quit
EOF

#10.Shell内置变量$?表示上一个Shell进程的退出值,0表示成功执行,其余值均表示不同原因的失败。

if [ $? -eq 0 ]; then
ls -l $basefile
fi
exit 0

CTRL+D

/> ./test25.sh ftp://ftp.myserver.com/download/test.tar stephen
Password for stephen:
./test25.sh: Downloading from server ftp.myserver.com.
-rwxr-xr-x. 1 root root 678 Dec 9 11:46 test.tar

二十六、文件锁定

在工业应用中,有些来自于工业设备的文件将会被放到指定的目录下,由于这些文件需要再被重新格式化后才能被更高层的软件进行处理。而此时负责处理的脚本程序极有可能是多个实例同时运行,因此这些实例之间就需要一定的同步,以避免多个实例同时操作一个文件而造成的数据不匹配等问题的发生。文件锁定命令可以帮助我们实现这一同步逻辑。/> cat > test26.sh

#!/bin/sh

#1. 这里需要先确认flock命令是否存在。

if [ -z $(which flock) ]; then
echo "flock doesn't exist."
exit 1
fi

#2. flock中的-e选项表示对该文件加排它锁,-w选项表示如果此时文件正在被加锁,当前的flock命令将等待20秒,如果能锁住该文件,就继续执行,否则退出该命令。
#3. 这里锁定的文件是/var/lock/lockfile1,-c选项表示,如果成功锁定,则指定其后用双引号括起的命令,如果是多个命令,可以用分号分隔。
#4. 可以在两个终端同时启动该脚本,然后观察脚本的输出,以及lockfile1文件的内容。

flock -e -w 20 /var/lock/lockfile1 -c "sleep 10;echo `date` | cat >> /var/lock/lockfile1"
if [ $? -ne 0 ]; then
echo "Fail."
exit 1
else
echo "Success."
exit 0
fi

CTRL+D

二十七、用小文件覆盖整个磁盘

假设我们现在遇到这样一个问题,公司的关键资料copy到测试服务器上了,在直接将其删除后,仍然担心服务器供应商可以将其恢复,即便是通过fdisk进行重新格式化,也仍然存在被恢复的风险,鉴于此,我们需要编写一个脚本,创建很多小文件(5MB左右),之后不停在关键资料所在的磁盘中复制该文件,以使Linux的inode无法再被重新恢复,为了达到这里效果,我们需要先构造该文件,如:/> find . -name "*" > testfile
/> ls -l testfile
-rwxr-xr-x. 1 root root 5123678 Dec 9 11:46 testfile

/> cat > test27.sh
#!/bin/sh

#1. 初始化计数器变量,其中max的值是根据当前需要填充的磁盘空间和testfile的大小计算出来的。

counter=0
max=2000000
remainder=0

#2. 每次迭代counter变量都自增一,以保证每次生成不同的文件。当该值大于最大值时退出。
#3. 对计数器变量counter按1000取模,这样可以在每生成1000个文件时打印一次输出,以便看到覆盖的进度,输出时间则便于预估还需要多少时间可以完成。
#4. 创建单独的、用于存放这些覆盖文件的目录。
#5. 生成临时文件,如果写入失败打印出提示信息。

while true
do
((counter=counter+1))
if [ #counter -ge $max ]; then
break
fi
((remainder=counter%1000))
if [ $remainder -eq 0 ]; then
echo -e "counter = $counter\t date = " $(date)
fi
mkdir -p /home/temp2
cat < testfile > "/home/temp/myfiles.$counter"
if [[ $? -ne 0 ]]; then
echo "Failed to wrtie file."
exit 1
fi
done
echo "Done"

CTRL+D

/> ./test27.sh
counter = 1000 Fri Dec 9 17:25:04 CST 2011
counter = 2000 Fri Dec 9 17:25:24 CST 2011
counter = 3000 Fri Dec 9 17:25:54 CST 2011
... ...与此同时,可以通过执行下面的命令监控磁盘空间的使用率。/> watch -n 2 'df -h'
Every 2.0s: df -h Fri Dec 9 17:31:56 2011

Filesystem Size Used Avail Use% Mounted on
/dev/sda2 3.9G 2.3G 1.4G 63% /
tmpfs 504M 100K 504M 1% /dev/shm
/dev/sda1 49M 36M 11M 77% /boot
/dev/sda3 15G 172M 14G 2% /home我们也可以在执行的过程中通过pidstat命令监控脚本进程的每秒读写块数。

二十八、统计当前系统中不同运行状态的进程数量

在Linux系统中,进程的运行状态主要分为四种:运行时、睡眠、停止和僵尸。下面的脚本将统计当前系统中,各种运行状态的进程数量。/> cat > test28.sh

#!/bin/sh

#1. 初始化计数器变量,分别对应于运行时、睡眠、停止和僵尸。

running=0
sleeping=0
stopped=0
zombie=0

#2. 在/proc目录下,包含很多以数字作为目录名的子目录,其含义为,每个数字对应于一个当前正在运行进程的pid,该子目录下包含一些文件用于描述与该pid进程相关的信息。如1表示init进程的pid。那么其子目录下的stat文件将包含和该进程运行状态相关的信息。
#3. cat /proc/1/stat,通过该方式可以查看init进程的运行状态,同时也可以了解该文件的格式,其中第三个字段为进程的运行状态字段。
#4. 通过let表达式累加各个计数器。

for pid in /proc/[1-9]*
do
((procs=procs+1))
stat=`awk '{print $3}' $pid/stat`
case $stat in
R) ((running=runing+1));;
S) ((sleeping=sleeping+1));;
T) ((stopped=stopped+1));;
Z) ((zombie=zombie+1));
esac
done
echo -n "Process Count: "
echo -e "Running = $running\tSleeping = $sleeping\tStopped = $stopped\tZombie = $zombie."

CTRL+D

/> ./test28.sh
Process Count: Running = 0 Sleeping = 136 Stopped = 0 Zombie = 0.

二十九、浮点数验证

浮点数数的重要特征就是只是包含数字0到9、负号(-)和点(.),其中负号只能出现在最前面,点(.)只能出现一次。/> cat > test29.sh

#!/bin/sh

#1. 之前的一个条目已经介绍了awk中match函数的功能,如果匹配返回匹配的位置值,否则返回0。
#2. 对于Shell中的函数而言,返回0表示成功,其他值表示失败,该语义等同于Linux中的进程退出值。调用者可以通过内置变量$?获取返回值,或者作为条件表达式的一部分直接判断。

validint() {
ret=`echo $1 | awk '{start = match($1,/^-?[0-9]+$/); if (start == 0) print "1"; else print "0"}'`
return $ret
}

validfloat() {
fvalue="$1"

#3. 判断当前参数中是否包含小数点儿。如果包含则需要将其拆分为整数部分和小数部分,分别进行判断。

if [ ! -z $(echo $fvalue | sed 's/[^.]//g') ]; then
decimalpart=`echo $fvalue | cut -d. -f1`
fractionalpart=`echo $fvalue | cut -d. -f2`

#4. 如果整数部分不为空,但是不是合法的整型,则视为非法格式。

if [ ! -z $decimalpart ]; then
if ! validint "$decimalpart" ; then
echo "decimalpart is not valid integer."
return 1
fi
fi

#5. 判断小数部分的第一个字符是否为-,如果是则非法。

if [ "${fractionalpart:0:1}" = "-" ]; then
echo "Invalid floating-point number: '-' not allowed after decimal point." >&2
return 1
fi

#6. 如果小数部分不为空,同时也不是合法的整型,则视为非法格式。

if [ "$fractionalpart" != "" ]; then
if ! validint "$fractionalpart" ; then
echo "fractionalpart is not valid integer."
return 1
fi
fi

#7. 如果整数部分仅为-,或者为空,如果此时小数部分也是空,则为非法格式。

if [ "$decimalpart" = "-" -o -z "$decimalpart" ]; then
if [ -z $fractionalpart ]; then
echo "Invalid floating-point format." >&2
return 1
fi
fi
else

#8. 如果当前参数仅为-,则视为非法格式。

if [ "$fvalue" = "-" ]; then
echo "Invalid floating-point format." >&2
return 1
fi

#9. 由于参数中没有小数点,如果该值不是合法的整数,则为非法格式。

if ! validint "$fvalue" ; then
echo "Invalid floating-point format." >&2
return 1
fi
fi
return 0
}
if validfloat $1 ; then
echo "$1 is a valid floating-point value."
fi
exit 0

CTRL+D

/> ./test29.sh 47895
47895 is a valid floating-point value.

/> ./test29.sh 47895.33
47895.33 is a valid floating-point value.

/> ./test29.sh 47895.3e
fractionalpart is not valid integer.

/> ./test29.sh 4789t.34
decimalpart is not valid integer.

三十、统计英文文章中每个单词出现的频率

这个技巧的主要目的是显示如何更好的使用awk命令的脚本。/> cat > test30.sh

#!/bin/sh

#1. 通过当前脚本的pid,生成awk脚本的临时文件名。
#2. 捕捉信号,在脚本退出时删除该临时文件,以免造成大量的垃圾临时文件。

awk_script_file="/tmp/scf_tmp.$$"
trap "rm -f $awk_script_file" EXIT

#3. while循环将以当前目录下的testfile作为输入并逐行读取,在读取到末尾时退出循环。
#4. getline读取到每一行将作为awk的正常输入。在内层的for循环中,i要从1开始,因为$0表示整行。NF表示域字段的数量。
#5. 使$i作为数组的键,如果$i的值匹配正则表达式"^[a-zA-Z]+$",我们将其视为单词而不是标点。每次遇到单词时,其键值都会递增。
#6. 最后通过awk脚本提供的特殊for循环,遍历数组的键值数据。

cat << 'EOF' > $awk_script_file
BEGIN {
while (getline < "./testfile" > 0) {
for (i = 1; i <= NF; ++i) {
if (match($i,"^[a-zA-Z]+$") != 0)
arr[$i]++
}
}
for (word in arr) {
printf "word = %s\t count = %s\n",word,arr[word]
}
}
EOF
awk -f $awk_script_file

CTRL+D

/> cat testfile
hello world liu liu , ,
stephen liu , ?

/> ./test30.sh
word = hello count = 1
word = world count = 1
word = stephen count = 1
word = liu count = 3
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  shell bash 编程