1. shell script
1.1 第一个script的编写与执行
shell 脚本其实就是纯文本文件,我们可以编辑这个文件,然后让这个文件来帮我们一次执行多个命令,或者利用一些运算与逻辑判断来帮我们达成某些功能。
在编写shell脚本,我们呢需要注意一下事项:
- 命令的执行是从上而下,从左而右的分析执行。
- 命令的执行:命令、参数间的多个空格会被忽略。
- 空白行也将被忽略,并且[tab]按键所得的 空白同样视为空格键。
- 如果读取到一个Enter符号(CR),就尝试开始执行该行命令。
- 如果一行的内容太多,可以使用“\[Enter]”来扩展下一行。
- “#”可作为批注
- 编写第一个脚本
[] # vi sh01.sh#!/bin/bashecho -e "Hello World! \a \n"exit 0 [] # sh sh01.sh
1. 第一行#!/bin/bash 声明这个脚本使用的shell名称
2. 第二行输出Hello World!
3. 第三行告知执行结果。可用$?查看
另外,也可用"chmod a+x sh01.sh; ./sh01.sh“来执行这个脚本
1.2 简单范例
下面的范例在很多的脚本程序中都会用到,而下面的范例又都很简单,先拿来讲解。
- 交互式脚本:变量内容由用户决定
1 [] # vi sh01.sh2 #!/bin/bash3 read -p "please input your first name: " firstname #提示用户输入4 read -p "please input your last name: " lastname #提示用户输入5 echo -e "\nyour full name is : $firstname $lastname" #输出结果
- 随日期变化:利用日期进行文件的创建
1 [] # vi sh03.sh 2 #!/bin/bash 3 # 1. 让用户输入文件名,并取得fileuser这个变量; 4 echo -e "I will use 'touch' command to creagte 3 file," 5 read -p "Please input your filename: " fileuser 6 7 # 2. 为了避免用户随意按[Enter],利用变量功能分析文件名是否有设置 8 filename=${fileuser:-"filename"} # 开始判断是否配置文件名 9 10 # 3. 开始利用date命令取得所需的文件名11 date1=$(date --date='2 days ago' +%Y%m%d) # 前两天的日期12 date2=$(date --date='1 days ago' +%Y%m%d) # 前一天的日期13 date3=$(date +%Y%m%d) # 今天的日期14 15 file1=${filename}${date1)16 file2=${filename}${date2}17 file3=${filename}${date3}18 19 # 4. 创建文件20 touch "$file1"21 touch "$file2"22 touch "$file3"
- 数值运算:简单的加减乘除
1 [] # vi sh04.sh2 #!/bin/bash3 4 echo -e "You should input 2 numbers, I will cross them! \n"5 read -p "first number: " firstnu6 read -p "second number: " secnu7 total=$( ($firstnu*$secnu) )8 echo -e "\nThe reault of $firstnu x $secnu is = $total"
在数值运算上,我们可以用 declare -i total=$firstnu*$secnu,也可以使用上面的方式来进行。
- var=$((运算内容))
容易记忆,而且也比较方便,因为两个小括号内可以加上空格符。将来可以用这种方式来计算。
至于数值运算上的处理,则有 + - * / %等。
1.3 脚本执行方式区别(source, sh script, ./script)
不同的脚本执行方式会造成不一样的结果。
- 利用直接执行的方式来执行脚本
当使用直接命令执行(不论是绝对路径/相对路径/还是$PATH内),或者是用bash(或sh)来执行脚本时,该脚本都会使用一个新的bash环境来执行脚本内的命令。
也就是说,使用这种执行方式时,其实脚本是子在子进程的bash内执行的,重点在于:当子进程完成后,子进程内的各项变量或操作将会结束而不会传回到父进程中。
举个例子:
1 [] # echo $firstname $lastname2 <==确认了,这两个变量并不存在3 [] # sh sh02.sh4 please input your first name: VBird5 please input your last name: Tsai6 7 Your full name is: VBird Tsai8 [] # echo $firstname $lastname9 <==事实上,这两个变量在父进程的bash中还是不存在的
- 利用source来执行脚本:在父进程中执行
如果你使用source来执行命令那就不一样了!同样的脚本我们来执行看看:
[] # source sh02.shplease input your first name: VBirdplease input your last name: TsaiYour full name is: VBird Tsai[] # echo $firstname $lastnameVBird Tsai <==有数据产生哦
1.4 善用判断式
1.4.1 $?(命令回传码)与&& 或||
$?:命令回传码,若前一个命令的执行结果为正确,在linux下面会传回一个$?=0的值。那么我们就可以利用这个配合&&和||的帮忙,来控制命令的执行
命令执行情况 | 说明 |
cmd1 && cmd2 | 若cmd1执行完毕且正确执行($?=0),则开始执行cmd2 若cmd1执行完毕且为错误($?!=0),则cmd2不执行 |
cmd1 || cmd2 | 若cmd1执行完毕且正确执行,则cmd2不执行 若cmd2执行完毕且错误,则cmd2开始执行 |
1.4.2 利用test命令的测试功能
当要检测系统上面某些文件或者是相关的属性时,利用test命令来工作。举例来说,要检查/dmtsai是否存在时,使用:
1 [~]# test -e /dmtsai
执行结果不会显示任何信息,但最后我们可以通过$?或&&及||来显示整个结果,例如:
1 [~]# test -e /dmtsai && echo "exist" || echo "not exist"
最终的结果可以告诉我们是”exist“还是”not exist“。下表展示哪些标志可以来判断。
测试的标志 | 代表意义 |
关于某个文件名的”文件类型“判断,如test -e filename表示文件存在与否 | |
-e | 该文件名是否存在(常用) |
-f | 该文件名是否存在且为文件(file)(常用) |
-d | 该文件名是否存在且为目录(directory)(常用) |
-b | 该文件名是否存在且为一个块设备 |
-c | 该文件名是否存在且为一个字符设备 |
-S | 该文件名是否存在且为一个Socket文件 |
-p | 该文件名是否存在且为一个管道文件FIFO(pipe) |
-L | 该文件名是否存在且为一个连接文件 |
关于文件的权限检测,如test -r filename表示可读否(但root权限常有例外) | |
-r | 检测该文件名是否存在且具有”可读“的权限 |
-w | 检测文件名是否存在且具有”可写“的权限 |
-x | 检测文件名是否存在且具有”可执行“的权限 |
-u | 检测文件名是否存在且具有”SUID“的属性 |
-g | 检测文件名是否存在且具有”SGID“的属性 |
-k | 检测文件名是否存在且具有”Sticky bit“的属性 |
-s | 检测文件名是否存在且为“非空白文件” |
两个文件之间的比较,如:test file1 -nt file 2 | |
-nt | (newer than)判断file1是否比file2新 |
-ot | (older than)判断file1是否比file2旧 |
-ef | 判断file1与file2是否为同一文件,可用在判断hard link(硬连接)的判定上。 主要意义在于判定两个文件是否均指向同一个inode |
关于两个整数之间的判定,例如test n1 -eq n2 | |
-eq | 两数值相等(equal) |
-ne | 两数值不等(not equal) |
-gt | n1大于n2 |
-lt | n1小于n2 |
-ge | n1大于等于n2 |
-le | n1小于等于n2 |
判定字符串的数据 | |
test -z string | 判定字符串是否为0,若string为空字符串,则为true |
test -n string | 判定字符串是否非为0,若string为空字符串,则为false 注:-n可以省略 |
test str1=str2 | 判定str1是否等于str2,若相等,则回传true |
test str1!=str2 | 判定str1是否等于str2,若相等,则回传false |
多重条件判定,例如:test -r filename -a -x filename | |
-a | 两个条件同时成立,例如 test -r file -a -x file,则file同时具有r与x权限时,才回传true |
-o | 任何一个条件成立,例如tes -r file -o -x file,则file具有r或x权限时,就可回传true |
! | 取反,例如test ! -x file,当file不具有x权限时,回传true |
现在我们就利用test来帮助我们写几个简单的例子。首先,判断一下,让用户输入一个文件名,我们判断:
1) 这个文件是否存在,若不存在则给予一个“Filename does not exist”的信息,并中断程序
2)若这个文件存在,则判断它是个文件或目录,结果输出“Filenamne is regular file”或“Filename is directory”
3)判断一下,执行者的身份对这个文件或目录所拥有的权限,并输出权限数据。
[~]# vi sh05.sh#!/bin/bash# 1. 让用户输入文件名,并且判断用户是否真的有输入字符串echo -e "Please input a filename, I will check the filename's type and permission. \n\n"read -p "Input a filename : " filenametest -z $filename && echo "You must input a filename." && exit 0# 2. 判断文件是否存在,若不存在则显示信息并结束脚本test ! -e $filename && echo "The filename '$filename' DO NOT exist" && exit 0#3. 开始判断文件类型与属性test -f $filename && filetype="regulare file"test -d $filename && filetype="directory"test -r $filename && perm="readable"test -w $filename && perm="$perm writable"test -x $filename && perm="$perm executable"# 4. 开始输出信息echo "The filename: $filename is a $filetype"echo "And the permissiions are : $perm"
需要注意的是,由于root在很多权限的限制上面都是无效的,所以使用root执行这个脚本时,常常会发现与ls -l观察到的结果并不相同。
1.4.3 利用判断符号[]
其实,我们还可以利用判断符号[]来进行数据的判断!举例来说,如果我想要知道$HOME这个变量是否为空的,可以这样做:
[~]# [ -z "$HOME" ];echo $?
使用中括号必须要特别注意,因为中括号用在很多地方,包括通配符与正则表达式等,多以如果要在bash的语法中使用中括号作为shell判断式时,
必须要注意中括号的两端有空格符来分隔,注意一下事项:
1)在中括号[]内的每个组件都需要有空格键来分隔;
2)在中括号内的变量,最好都以双引号括起来;
3)在中括号内的变量,最好都以单或双引号括号起来;
另外,中括号的使用方法与test 几乎一模一样。只是中括号比较常用在条件判断式if ... then ...fi的情况中就是了。
下面使用中括号的判断来做一个小案例:
1)当执行一个程序的时候,这个程序会让用户选择Y或N;
2)如果用户输入Y或y时,就显示“OK, continue”;
3)如果用户输入N或n时,就显示“Oh, interrupt”;
4)如果不是Y/y/N/n之内的其它字符,就显示“I don't know what your choice is”。
[~]# vi sh06.sh#!/bin/bashread -p "Please input (Y/N) : " yn[ "$yn" == "Y" -o "$yn" == "y" ] && echo "OK, continue" && exit 0[ "$yn" == "N" -o "$yn" == "n" ] && echo "Oh, interrupt!" && exit 0echo "I don't know what your choice is" && exit 0
1.4.4 shell script的默认变量($0, $1 ...)
shell 脚本是怎么带参数的呢?看下面的例子:
/path/to/script.sh opt1 opt2 opt3 opt4 $0 $1 $2 $3 $4
这样够清楚了吧?执行的脚本文件名为$0这个变量,第一个接的参数就是$1。所以,只要我们在脚本里面善用$1的话,就可以很简单的立即执行某些命令功能了。
除了这些数字的变量之外,还有一些较为特殊的变量可以在脚本内使用来调用这些参数。
1)$#:代表后接的参数 个数;
2)$@:代表 “$1“、”$2“、”$3”之意,每个变量都是独立的(用双引号括起来)
3)$*:代表"$1c$2c$3c$4",其中c为分隔字符,默认为空格键,所以本例中代表"$1 $2 $3 $4"
$@和$*基本上还是有所不同。不过,一般使用情况下可以直接记忆$@即可。下面做个练习:
假设我们要执行一个可以携带参数的脚本,执行该脚本后屏幕会显示如下的数据:
1)程序的文件名
2)共有几个参数
3)若参数的个数小于2则告知用户参数数量太少
4)全部的参数内容
5)第一个参数
6)第二个参数
[~]# vi sh07.sh#!/bin/bashecho "The script name is ==> $0"echo "Total parmameter is ==> $#"[ "$#" -lt 2 ] && echo "The number of parameter is less than 2. stop here." && exit 0echo "Your whole parameter is ==> '$@'"echo "Your 1st parameter is ==> $1"echo "Your 2st parameter is ==> $2"
- shift:参数变量号码偏移
直接看例子:
[~]# vi sh08.sh#!/bin/bashecho "Total param ==> $#"echo "param is ==> '$@'"shift # 进行一次 一个变量的偏移echo "Total param ==> $#"echo "param is ==> '$@'"shift 3 # 进行一次 三个变量的偏移echo "Total param ==> $#"echo "param is ==> '$@'"
1.5 条件判断式
1.5.1 利用if ... then
- 单层、简单条件判断式
如果只有一个判断式要进行,那么可以简单的这样看:
if [ 条件判断式 ]; then ....fi
至于条件判断式的判断方法,与前面讲的相同。较特别的是,如果有多个条件要判别时,除了sh06.sh那个案例所写的,也就是
将多个条件写入一个中括号中之外,还可以有多个中括号来隔开,而括号与括号之间,则以&& 或 ||隔开,他们的意义式:
1)&& 代表 and
2)|| 代表 or
所以,在使用中括号的判断式中,&&与||就与命令执行的状态不同了。举例来说,sh06.sh里的判断式可以这样修改:
[ "$yn" == "Y" -o "$yn" == "y" ] 可替换为 [ "$yn" == "Y" ] || [ "$yn" == "y" ]
[~] # vi sh06-2.sh#!/bin/bashread -p "Please input (Y/N): " ynif [ "$yn" == "Y" ] || [ "$yn" == "y" ]; then echo "OK, continue" exit 0fiif [ "$yn" == "N" ] || [ "$yn" == "n" ]; then echo "Oh, interrupt!" exit 0fiecho "I don't know what your choice is" && exit 0
- 多重、复杂条件判断式
if [ 条件判断式 ]; then ...else ...fiif [ 条件判断式1 ]; then ...elif [ 条件判断式2 ]; then ...else ...fi
我们可以将sh06-2.sh写成这样:
[~] # vi sh06-3.sh#!/bin/bashread -p "Please input (Y/N): " ynif [ "$yn" == "Y" ] || [ "$yn" == "y" ]; then echo "OK, continue" exit 0elif [ "$yn" == "N" ] || [ "$yn" == "n" ]; then echo "Oh, interrupt!" exit 0else echo "I don't know what your choice is"fi
接下来玩个大点的例子,结合grep和netstat。我们可以利用 netstat -tuln来取得目前主机有启动的服务。
重点是Local Address(本地主机的IP与端口对应)那个字段,它代表的是本机所启动的网络服务。几个常见的端口对应关系为:
80:www
22:ssh
25:mail
111:RPC
631:CUPS(打印服务功能)
假设我的主机有兴趣要检测的是比较常见的端口时,那如何通过netstat取检测主机是否有开启这四个主要的网络服务端端口呢?
[~] # vi sh10.sh#!/bin/bash# 1. 先做一些告知操作echo "Now, I will detect your Linux server's services!"echo -e "The www, ftp, ssh, and mail will be detect! \n"# 2. 开始进行一些测试工作,并且输出信息testing=$(netstat -tuln | grep ":80") # 检测port 80在否if [ "$testing" != "" ]; then echo "www is running in your system."fitesting=$(netstat -tuln | grep ":22") # 检测port 22在否if [ "$testing" != "" ]; then echo "SSH is running in your system."fitesting=$(netstat -tuln | grep ":21") # 检测port 21在否if [ "$testing" != "" ]; then echo "FTP is running in your system."fitesting=$(netstat -tuln | grep ":25") # 检测port 25在否if [ "$testing" != "" ]; then echo "MAIL is running in your system."fi
1.5.2 利用case ... esac判断
格式:
case $var in "第一个变量的内容") ... ;; "第二个变量的内容") ... ;; *) ... ;;esac
举个例子:
[~] # vi sh09.sh#!/bin/bash # read -p "Input your choice: " choice # case $choice incase $1 in "one") echo "Your choice is ONE" ;; "two") echo "your choice is TWO" ;; *) echo "Usage $0 {one|two}" ;;esac
1.5.3 利用function功能
什么是函数(function)?简单的说,函数可以在脚本中做出一个类似自定义执行命令的东西,简化代码。
function的语法如下:
function fname() { .... }
fname就是我们的自定义的执行命令名称。需要注意的是:因为脚本的执行方式是从上到下,从左到右的,所以在脚本当中function的设置一定要在
程序的最前面。看下面的例子,把sh09.sh脚本改成:
[~] # vi sh09-1.sh#!/bin/bashfunction printit(){ echo -n "Your choice is " # 加上-n可以不断行在同一行显示}case $1 in "one") printit; echo $1 ;; "two") printit; echo $1 ;; *) echo "Usage $0 {one|two} ;;esac
另外,function也是拥有内置变量的。它的内置变量和脚本很类似,函数名称用$0表示,后续的变量用$1, $2, ....等表示。举例:
[~] # vi sh09-2.sh#!/bin/bashfunction printit(){ echo "Your choice is $1"}case $1 in "one") printit 1 ;; "two") printit 2 ;; *) echo "Usage $0 {one|two}" ;;esac
1.6 循环(loop)
除了if...then...fi这种条件判断式之外,循环可能是程序中最重要的一环。循环有一种依据判断式达成与否的不定循环,还有另一种
已经固定要跑多少次的固定循环。
1.6.1 while do done,until do dne(不定循环)
一般来说,不定循环最常见的就是下面这两种状态了:
while [ condition ]do ...done
当condition条件成立时,就进入循环体中,直到condition的条件不成立才停止的意思。还有另外一种不定循环的方式:
until [ condition ]do ...done
这种方式正好与while相反,当condition条件成立时,就终止循环,否则就持续进行循环。
下面举个例子,假设我们要用户输入yes或者YES才结束程序的执行,否则就一直让用户输入字符串。
[~] # vi sh13.sh#!/bin/bashwhile [ "$yn" != "yes" -a "$yn" != "YES" ]do read -p "Please input yes/YES to stop this program: " yndoneecho "OK! you input the correct answer."
如果使用until,它的条件会变成这样:
[~] # vi sh13-2.sh#!/bin/bashuntil [ "$yn" == "yes -o "$yn" == "YES" ]do read -p "Please input yes/YES to stop this program: " yndoneecho "OK! you input the correct answer."
如果我们要来计算1+2+3+...+100的值,利用循环是这样的:
[~] # vi sh14.sh#!/bin/bashs=0i=0while [ "$i" != "100" ]do i=$(($i+1)) s=$(($s+$i))doneecho "The result of '1+2+3+...+100' is ==> $s"
如果想让用户输入一个数字让程序有1+2+..直到用户输入的数字为止,该如何编写?
1.6.2 for...do...done (固定循环)
相对于while,until的循环方式是必须要“符合某个条件”的状态,for这种语法则是“已经知道要进行几次循环”的状态,它的语法是:
for var in con1 con2 con3 ...do ...done
以上面的例子来说,这个$var变量内容在循环工作室:
1. 第一次循环时,$var的内容为con1;
2. 第二次循环时,$var的内容为con2;
3. 第三次循环时,$var的内容为con3;
做一个简单的练习。假设有三种动物,分别是 dog cat pig三种,想每一行都输出这样:“There are dogs ...”之类的字样:
[~] # vi sh15.sh#!/bin/bashfor animal in dog cat pigdo echo "There are ${animal}s..."done
再举个复杂点的例子,由于系统上面的各种账号都是写在/etc/passwd内的第一个字段,能不能通过管道命令的cut找出单纯的账号名称后,
以id及finger分别检查用户的标识符与特殊参数呢?程序如下:
[~] # vi sh16.sh#!/bin/bashusers=$(cut -d ':' -f1 /etc/passwd)for username in $usersdo id $username finger $usernamedone
再举个例子,我想要利用ping这个可以判断网络状态的命令。来进行网络状态的实际检测时,我想要检测的域是本机所在的192.168.1.1~192.168.1.100
由于有100台主机,总不会要我在for后面舒服1到100吧?此时可以这样做
[~] # vi sh17.sh#!/bin/bashnetwork="192.168.1"for sitenu in $(seq 1 100)do ping -c 1 -w 1 ${network}.${sitenu} &> /dev/null && result=0 || result=1 if [ "$result" == 0 ]; then echo "Server ${network}.${sitenu} is UP." else echo "Serve ${network}.${sitenu} is DOWN." fidone
最后,让我们来玩判断式加上循环的功能!我想要让用户输入某个目录文件名,然后找出某目录内的文件名的权限:
[~] # vi sh18.sh#!/bin/bash# 1. 先看看目录是否存在read -p "Please input a directory: " dirif [ "$dir" == "" -o ! -d "$dir" ]; then echo "The $dir is NOT exist in your system." exit 1fi# 2. 开始测试文件filelist=$(ls $dir)for filename in $filelistdo perm="" test -r "$dir/$filename" && perm="$perm readable" test -w "$dir/$filename" && perm="$perm writeable" test -x "$dir/$filename" && perm="$perm executable" echo "The file $dir/${filename}'s permission is $perm"done
1.6.3 for...do...done 的数值处理
除了上述的方法之外,for循环还有另外一种写法!语法如下:
for ((初始值;限制值;执行步长))do ...done
这种语法适合于数值方式的运算中,for后面的括号内的三串内容意义为:
- 初始值:某个变量在循环当中的初始值,直接以类似i=1设置好;
- 限制值:当变量的值在这个限制值的范围内,就继续循环,例如i<=100
- 执行步长:每做一次循环时的变化量。例如 i=i+1
[~] # vi sh19.sh#!/bin/bashread -p "Please input a number, I will count for 1+2+3..+your input: " nus=0for ((i=1; i<=$nu; i=i+1))do s=$(($s+$i))doneecho "The result of '1+2+3+...+$nu' is ==> $s"
2. 正则表达式
2.1 基础正则表达式
正则表达式是处理字符串的一种表达方式,需要支持工具来辅助才行。这里我们就先介绍一个最简单的字符串选取功能的工具程序--grep。
介绍完grep的功能之后,就进入正则表达式的特殊字符的处理能力了。
首先,先了解一些特殊的符号:
特殊符号 | 代表意义 |
[:alnum:] | 代表英文大小写字符及数字,即0-9,A-Z,a-z |
[:alpha:] | 代表任何英文大小写字符,即A-Z,a-z |
[:blank:] | 代表空格键与[tab]键 |
[:cntrl:] | 代表键盘上面的控制按键,即包括CR,LF,Tab,Del等 |
[:digit:] | 代表数字而已,即0-9 |
[:graph:] | 除了空格符(空格键与[tab]键)外的其它所有按键 |
[:lower:] | 代表小写字符,即a-z |
[:print:] | 代表任何可以被打印出来的字符 |
[:punct:] | 代表标点符号,即 " ' ? ! ; : # $ |
[:upper:] | 代表大写字符,即A-Z |
[:space:] | 任何会产生空白的字符,包括空格键[tab]CR等 |
[:xdigit:] | 代表十六进制的数字类型,包括0-9, A-F, a-f |
2.1.1 grep的一些高级参数
[~] # grep [-A] [-B] [--color=auto] '搜索字符串' filename参数:-A:后面可加数字,为after的意思,除了列出该行外,后续的n行也列出来-B:后面可加数字,为befer的意思,除了列出该行外,前面的n行也列出来--color=auto:可将正确的那个选取数据列出颜色范例一:用dmesg列出内核信息,再以grep找出内含eth的那行[~] # dmesg | grep 'eth'[~] # dmesg | grep -n --color=auto 'eth'[~] # dmesg | grep -n -A3 -B2 --color=auto 'eth'
grep最重要的功能就是进行字符串数据的对比,然后将符合用户需求的字符串打印出来。需要说明的是grep在数据中查找一个字符串时,是以整行为单位来
进行数据的选取的!也就是说,假如一个文件内有10行,其中有两行具有你所查找的字符串,则将那两行显示再屏幕上,其它的就丢弃了。
2.1.2 基础正则表达式练习
要了解正则表达式最简单的方法就是由实际练习去感受。所以在归纳正则表达式特殊符号前,我们先以下面这个文件的内容来进行正则表达式的理解。
先说明一下,下面的练习大前提是:
- 语系使用“export LANG=C"的设置值;
- grep已经使用alias设置成为"grep --color=auto”
先下载文件:http://linux.vbird.org/linux_basic/0330regularex/regular_express.txt
1)例题一:查找特定字符串
查找特定字符串很简单吧?假设我们要从刚才的文件当中取得the这个特定字符串,最简单的方式就是这样:
[~] # grep -n 'the' regular_express.txt
那如果想要反响选择呢?也就是说,当该行没有’the‘这个字符串时才显示在屏幕上,那就直接使用:
[~] # grep -vn 'the' regular_express.txt
接下来,如果想要取得不论大小写的'the'这个字符串,则:
[~] # grep -in 'the' regular_express.txt
2)例题二:利用中括号[]来查找集合字符
如果我想要查找test或taste这两个单词时,可以发现,其实他们有共同的't?st'存在。这个时候我可以这样来查找:
[~] # grep -n 't[ae]st' regular_express.txt
了解了吧?其实[]里面不论有几个字符,它都值代表着“一个”字符,所以,上面的例子说明了,需要的字符串时“tast“或”test“两个字符串而已!
而如果想要查找到所有oo的字符时,则使用:
[~] # grep -n 'oo' regular_express.txt
但是,如果我不想要oo前面有g的话呢?此时,可以利用在集合字符的反响选择[^]来完成:
[~] # grep -n '[^g]oo' regular_express.txt
再来,假设我oo前面不想要有小写字符,所以我可以这样写[^abcdef..z]oo,但是这样写太累了,可以简化成下面这样:
[~] # grep -n '[^a-z]oo' regular_express.txt
也就是说,当我们在一组集合字符中,如果该字符组是连续的,例如大写英文/小写英文/数字等,就可以使用
[a-z],[A-Z],[0-9]等方式来书写。但由于考虑到语系对于编码顺序的影响,因此除了连续编码使用'”-“之外,也可以使用如下的方法来取得前面两个测试的结果:
[~] # grep -n '[^[:lower:]]oo' regular_express.txt[~] # grep -n '[[:digit:]]' regular_express.txt
3)例题三:行首与行尾字符^$
我们在例题一中,可以查询到一行字符串里面有the的,那如果我想要让the只在行首列出呢?这个时候就要使用制符表了,我们可以这样做:
[~] # gep -n '^the' regular_express.txt
如果我想要开头是小写字符的那一行就列出呢?可以这样:
[~] # grep -n '^[a-z]' regular_express.txt [~] # grep -n '^[[:lower:]]' regular_express.txt
如果我不想要开头是英文字母,则可以这样:
[~] # grep -n '^[^a-zA-Z]' regular_express.txt[~] #grep -n '^[^[:alpha:]]' regular_express.txt
看到了吧,那个^符号在字符集符号(中括号[])之内与之外是不同的!在[]内代表”反向选择“,在[]之外则代表定位在行首的意义。
那如果我要找出行尾结束为小数点(.)的那一行,该如何处理?
[~] # grep -n '\.$' regular_express.txt
小数点有其它含义,下面会介绍到,所以需要用"\"来进行转义。还有就是5-9行没显示的问题,这就要考虑到linux和windows的断行字符的问题
用 cat -An regular_express.txt | head -n 10 | tail -n 6查看一下。
那怎么找出空白行呢?可以这样做:
[~] # grep -n '^$' regular_express.txt
再来,假设你已经知道一个程序脚本或者配置文件中,空白行与开头为#的那一行是批注,那怎么将这些行屏蔽掉呢?
[~] # cat -n /etc/syslog.conf# 在CentOS中,结果可以发现有空行和以#开头的行输出[~] # grep -v '^$' /etc/syslog.conf | grep -v '^#'
4)例题四:任意一个字符.,与重复字符*
.(小数点):代表一定有一个任意字符的意思;
*(星号):代表重复前一个0到无穷次的意思,为组合形态。
假设需要找出g??d的字符串,即共有四个字符,开头是g而结束是d,可以这样做:
[~] # grep -n 'g..d' regular_express.txt
如果我想要列出有oo,ooo, oooo等的数据,也就是说,至少要有两个(含两个)o以上,该怎么做?是o*还是oo*还是ooo*呢?
因为*代表的是重复0个或者多个前面的RE字符的意思,因此, ”o*“代表的是具有空字符或一个o以上的字符,特别注意,因为允许空字符(就是有没有字符都可以),所以
grep -n 'o*' regular_express.txt 将会把所有的数据都打印出来
如果是”oo*“呢?则第一个o肯定必须要存在,第二个o可有可无,所以是 o,oo,ooo等,都可以被列出来
同理,当需要至少两个以上的o字符串时,就需要ooo*,即,
[~] # grep -n 'ooo*' regular_express.txt
这样可以理解了把。现在出个练习,如果我想要字符串开头与结尾都是g,但是两个g之间仅能存在至少一个o,就是gog,goog, gooog等,该怎么做?
[~] # grep -n 'goo*g' regular_express.txt
再来一题,如果想要找出g开头与g结尾的字符串,当中的字符可有可无,那该如何是好?是g*g吗?
用g*g测试一下,结果会发现结果不是预期的那样,为什么? 因为g*g里面的g*代表空字符或一个以上的g再加上后面的g,因此,这个表达式的内容就是g,gg,
所以只要该行当中有一个以上的g就符合需求了。
那我们该怎么做呢?可以利用任意一个字符”.“,即”g.*g“的做法。”.*”代表零个或过个任意字符的意思
[~] # grep -n 'g.*g' regular_express.txt
5)例题五:限定连续RE字符范围{}
前面,我们可以利用.与*来这只0个到无限多个重复字符,那如果我想要限制一个范围区间内的重复字符数呢?举例来说,我想要找出2-5个o的连续字符串,
该怎么做?这个时候就要使用到限定范围的字符{}了,但因为{与}的符号在shell是有特殊意义的,因此,我们必须要使用转义字符\来让它失去特殊意义才行。
假设我要找到两个o的字符串,可以是:
[~] # grep -n 'o\{2\}' regular_express.txt
这样看似乎和ooo*的字符没有什么区别啊?那么换个查找的字符串,假设我们要找出g后面接2到5个o,然后再接一个g的字符串,是这样:
[~] # grep -n 'go\{2,5\}g' regular_express.txt
如果我想要2个以上的gooo...g呢? 除了可以是gooo*g,也可以这样:
[~] # grep -n 'go\{2,\}g' regular_express.txt
2.1.3 基础正则表达式字符
经过上面的几个简单例题,我们可以将基础的正则表达式特殊字符归纳成表,如下:
RE字符 | 意义与范例 |
^word | 意义:待查找的字符串(word)在行首 范例:查找行首为#开始的那一行,并列出行号 grep -n '^#' regular_express.txt |
word$ | 意义:待查找的字符串(word)在行尾 范例:将行尾为!的那一行打印出来,并列出行号 grep -n '!$' regular_express.txt |
. | 意义:代表一定有一个任意字符的字符 范例:查找的字符串可以是(eve)(eae)(eee)(ee) grep -n 'e.e' regular_express.txt |
\ | 意义:转义字符,将特殊符号的特殊意义去除 范例:查找含有单引号的那一行 grep -n \' regular_express.txt |
* | 意义:重复零个到无穷多个的前一个字符 范例:找出含有(es)(ess)等的字符串 grep -n 'ess*' regular_express.txt |
[list] | 意义:从字符集合的RE字符里面找出想要选取的字符 范例:查找含有(gl)(gd)的那一行 grep -n 'g[ld]' regular_express.txt |
[n1-n2] | 意义:从字符集合的RE字符里找出想要选取的字符范围 范例:找出含有任意数字的那一行 grep -n '[0-9]' regular_express.txt |
[^list] | 意义:从字符集合的RE字符里找出不要的字符串或范围 |
\{n,m\} | 意义:连续n到m个的前一个RE字符,若为\{n\}则是连续n个的前一个RE字符,若为\{n,\} 则是连续n个以上的前一个RE字符 范例:在g与g之间有2到3个的o存在的字符串 grep -n 'go\{2,3\}g' regular_express.txt |
再次强调:正则表达式的特殊字符与一般在命令行输入命令的“通配符”并不相同,例如,在通配符当中的*代表的是零都无限多个字符的意思,
但是在正则表达式中,*则是重复0到无穷多个的前一个RE字符的意思,使用的意义并不相同,不要搞混了!
2.1.4 sed工具
在了解了一些正则表达式的基础应用之后,有两个命令可以练习一下,就是sed与下面会介绍的awk了。这两个工具可是相当有用的。
先来谈一谈sed,sed本身也是一个管道命令,可以分析standard input的,而且sed还可以将数据进行替换,删除,新增,选取特定行等功能
用法如下:
[~] # sed [-nefr] [动作]参数:-n:使用安静模式。在一般sed的用法中,所有来自STDIN 的数据一般都会被列出到屏幕上。但如果加上-n参数后,则只有经过 sed特殊处理的那一行(或操作)才会被列出来-e:直接在命令模式上进行sed的动作编辑-f:直接将sed的动作写在一个文件内,-f filename则可以执行filename内的sed动作-r:sed的动作支持的是扩展型正则表达式的语法(默认是基础正则表达式语法)-i:直接修改读取的内容,而不是有屏幕输出动作说明:[n1[,n2]] functionn1,n2:不见得会存在,一般代表选择性动作的行数,举例来说,如果我的动作是需要在10到20行之间的,则“10,20[动作行为]”function 有下面这些参数:a:新增,a的后面可以接字符串,而这些字符串会在新的一行出现(目前的下一行)c:替换,c的后面可以接字符串,这些字符串可以替换n1 n2之间的行d:删除,因为是删除,所以d后面通常不接任何参数i:插入,i的后面可以接字符串,而这些字符串会在新的一行出现(目前的上一行)p:打印,也就是将某个选择的数据打印出来,通常p会与参数sed -n一起运行s:替换,可以直接进行替换的工作。通常这个s的动作可以搭配 正则表达式,例如1,20s/old/new/g就是
1)以行动单位的新增/删除功能
范例一:将 /etc/passwd的内容列出并且打印行号,同时,请将第2~5行删除[~] # nl /etc/passwd | sed '2,5d'
sed的动作为’2,5d',那个d就是删除。注意一下,原本应该是要执行sed -e才对,没有-e也行。同时也要注意的是
sed后面接的动作,必须以两个单引号括住
如果题型变换一下,举例来说,如果只要删除第2行,可以使用 nl /ec/passwd | sed '2d'
若是要删除第3行到最后一行,则是 nl /etc/passwd | sed '3,$d',那个$代表最后一行
范例二:承上例,在第二行后加上 drink tea[~] # nl /etc/passwd | sed '2a drink tea'
在a后面加上的字符串就已将出现在第二行后面。如果要在第二行前呢? nl /etc/passed | sed '2i drink tea'就对了,
范例三:在第二行后面加入两行字,例如 "Drank tea or ..."与“drink beer?”[~] # nl /etc/passwd | sed '2a Drink tea or ......\ >drink beer?'
这个范例的重点是我们可以新增不只一行,可以新增好几行,但是每一行之间都必须要以反斜杠\ 来进行新行的增加。
2)以行动单位的替换与显示功能
范例四:将第2~5行的内容替换成为 “No 2-5 number”[~] # nl /etc/passwd | sed '2,5c No 2-5 number'
以前要列出11-20行,还要通过head -n 20 | tail -n 10 之类的方法来处理,sed可以直接使用下面的方法:
[~] # nl /etc/passwd | sed -n '5,7p'
3)部分数据的查找并替换的功能
除了整行的处理模式之外,sed还可以用行为单位进行部分数据的查找并替换的功能,格式类似这样:
sed 's/要被替换的字符串/新的字符串/g'
下面看一个例子:
# 1. 先查看源信息,用ifconfig查询IP[~] # ifconfig eth0# 2. 利用关键字配合grep选取出关键的一行数据[~] # ifconfig eth0 | grep 'inet addr'# 3. 将IP前面的部分删除[~] # ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g'# 4. 将IP后面的部分删除[~] # ifconfig eth0 | grep 'inet addr' | sed 's/^.*addr://g' | sed 's/Bcase.*$//g'
做这种的思路就是一步一步的来,做一步查看一下,不对的就修改。
再看一个例子:
# 1.使用grep将关键字MAN所在的行取出来[~] # cat /etc/man.config | grep 'MAN'# 2. 删除批注之后的数据[~] # cat /etc/man.config | grep 'MAN' | sed 's/#.*$/g'# 从上面可以看出来,原本批注的数据都变成空行,所以,接下来要删除空行[~] # cat /etc/man.config | grep 'MAN' | sed 's/#.*$/g' | sed 's/^$/d'
4)直接修改文件内容(危险操作)
使用-i选项可直接修改文件。
例如想要将regular_express.txt文件中的每一行末尾的.换成!,可以这样 sed -i 's/\.$/\!/g' regular_express.txt
2.2 扩展正则表达式
使用范围更广的扩展型正则表达式的表示方式会更方便。举个简单的例子,如果我们要去除空白行与行首为#的行,使用的是
grep -v '^$' regular_express.txt | grep -v '^#'
需要使用到管道命令查找两次,如果通过扩展型正则表达式,可以简化为:
egrep -v '^$|^#' regular_express.txt
熟悉正则表达式后,到这个扩展型的正则表达式,其实就是多几个重要的特殊符号。如下表所示:
RE字符 | 意义与范例 |
+ | 意义:重复一个或一个以上的前一个RE字符 范例:查找(god)(g00d)(goood)等的字符串。那个o+代表一个以上的o,所有,下面的 执行结果会将第1,9,13行列出来。 egrep -n 'go+d' regular_express.txt |
? | 意义:零个或一个的前一个RE字符 范例:查找(gd)(dod)。 egrep -n 'go?d' regular_express.txt |
| | 意义:用或(or)的方式找出数个字符串 范例:查找gd或good这两个字符串,注意,是“或”,所以第1,9,14三行都可以被打印 出来。如果还想要找出dog呢? egrep -n 'gd|good' regular_express.txt egrep -n 'gd|good|dog' regular_express.txt |
() | 意义:找出“组”字符串 范例:查找(glad)或(good)这两个字符串,因为g与d是重复的,所以,我就可以将la与 oo列于()当中,并以|来分隔开来 egrep -n 'g(la|oo)d' regular_express.txt |
()+ | 意义:多个重复组的判别 范例:将“AxyzxyzxyzxyzC” 用echo显示,然后使用如下的方法查找一下! echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+C' 上面的例子意思是说,我要找开头是A结尾是C,中间有一个以上XYZ字符串的意思 |
2.3 awk:好用的数据处理工具
awk也是一个非常棒的数据处理工具。相比于sed常常作用与一整行的处理,awk则比较倾向与将一行分成数个字段来处理。
因此,awk相当适合处理小型的数据。awk通常运行的模式是这样的:
awk '条件类型 1{动作 1} 条件类型 2{动作 2} ...' filename
awk 后面接两个单引号并加上大括号{}来设置想要对数据进行的处理动作。awk可以处理后续接的文件,也可以读取来自前个命令的输出。
awk 主要还是处理每一行的字段内的数据,而默认的字段的分隔符为空格键或[tab]键。举例来说,我们用last可以将登陆者的数据取出来。若想
要取出账号与登陆者IP,且账号与IP之间以[tab]隔开,则会变成这样:
last -n 5 | awk '{print $1 "\t" $3}
上面是awk常用的动作。通过print的功能将字段数据列出来。字段的分隔则以空格键或[tab]按键来隔开。因为不论那一行我都要处理,因此,就
不需要有条件类型的限制。使用awk的时候,需要先确认一下数据源的格式,如果是连续性的数据,就不要有空格或[tab]在内,否则会发生误判。
每行的每个字段都是有变量名称的,$1表示的是第一列以此类推。$0表示整行数据。那么awk怎么知道我到底这个数据有几行几列呢,这就需要
awk的内置变量帮忙了,如下表所示:
变量名称 | 代表意义 |
NF | 每一行($0)拥有的字段总数 |
NR | 目前awk所处理的是第几行数据 |
FS | 目前的分隔字符,默认是空格 |
继续上面last -n 5的例子来说明,如果我想要:
- 列出每一行的账号(就是$1)
- 列出目前处理的行数(就是awk内置的NR变量)
- 且说明,该行有多少字段(就是awk内的NF变量)
则可以这样表示:
last -n 5 | awk '{print $1 "\t lines: " NR "\t columes: " NF}'# 注意!在awk内的NR,NF等变量要用大写,且不需要$
- awk的逻辑运算符
既然有需要用到条件的类别,自然就需要一些逻辑运算。例如下面这些:
运算符 | 代表意义 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
== | 等于 |
!= | 不等于 |
举例说明,在/etc/passwd当中是以冒号来作为字段的分隔,该文件中第一字段为账号,第三字段则是UID。那么假设我要查阅,第三列小于10以下的数据
并且仅列出账号与第三列,那么可以这样做:
[~] # cat /etc/passwd | awk '{FS=":"} $3 < 10 {print $1 "\t" $3}'
但是会发现第一行没有正确显示出来,这是因为我们读入第一行的时候,那些变量$1,$2,...默认还是以空格键为分隔的,所以虽然我们定义了FS=":",但是
却仅能在第二行后才开始生效。那怎么解决呢?我们可以预先设置awk的变量,利用BEGIN这个关键字,这样做:
[~] # cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t" $3}'
除了BEGIN外,还有END。另外,如果用awk来进行“计算功能”呢?一下例子来看,假设我有一个薪资数据表文件名为pay.txt,内容是这样的:
Name 1st 2nd 3thVBird 23000 24000 25000DMTsai 21000 20000 23000Bird2 43000 42000 41000
如何帮我计算每个人的总额呢?而且我还想要格式化输出,可以这样考虑:
1)第一行只是说明,所以第一行不要进行加总(NR==1时的处理)
2)第二行以后就会有加总的情况出现(NR>=2以后处理)
[~] # cat pay.txt | awk 'NR==1 {printf "%10s %10s %10s %10s %10s \n",$1, $2, $3, $4, "Total"} \ NR>=2 {total=$2+$3+$4; printf "%10s %10s %10s %10s %10.2f\n", $1, $2, $3, $4, total}'
针对以上例子有几个重要事项应该要先说明:
- 所有awk的动作,都是在{}内完成,如果需要多个命令辅助时,可以利用分号“;”间隔,或者直接以[Enter]按键来隔开每个命令
- 逻辑运算中,等于的情况,一定要使用 ==
- 格式化输出时,在printf的格式设置中,必须加上就\n才能分行
- 与bash、shell的变量不同,在awk中,变量可以直接使用,不需要加上$
awk也可以用if条件判断的。