正规表示法用在非常多地方!熟悉正规表示法好处多多!因为其他编程语言也会用到这玩意儿喔!
谈完了指令列的 bash shell 的操作,再来得要思考一下,如果管理员有一堆指令要循序进行,且这些指令可能具有相依性 (例如判断式), 或者是管理员需要写一些让用户『交互』的脚本时,应该如何处理?这时就得要通过 bash 的 shell script (程序化脚本) 来进行了。 此外,很多时候我们需要进行数据的截取,这时好用的正规表示法就得要派上用场了!
用户在操作邮件时,常常会发现到许多邮件被丢到垃圾桶或者是被判定为病毒邮件,这些判定的方式,很多就是通过『正规表示法』来处理的! 正规表示法 (Regular Expression) 就是处理字符串的方法,他是以行为单位来进行字符串的处理行为,正规表示法通过一些特殊符号的辅助, 可以让用户轻易的达到『搜索/删除/取代』某特定字符串的处理程序!
由于正规表示法牵涉到数据的截取,因此读者们先了解一下最简单的数据截取指令: grep 的高端用法。例如,找出 /etc/passwd 当中, 含有 student 的那行,且列出行号:
[student@localhost ~]$ grep -n student /etc/passwd
43:student:x:1000:1000::/home/student:/bin/bash
读者们会看到输出的信息中,最前面会多出一个行号的信息,就可以让用户知道该信息来自文件的那一行这样。 另外,当用户有观察开机流程所产生的信息时,例如想要查找开机过程产生的问题等等,可以使用 dmesg 这个指令。 只是这个指令输出的信息量非常庞大。若用户仅须知道 eth0 这张网络卡的相关信息,就可以使用如下的方式查找:
[student@localhost ~]$ dmesg | grep -n -i eth0
671:[ 4.334926] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready
承上,如果我们还需要知道该行之前的 4 行以及之后的 3 行,以了解这行前后文的话,则可以这样处理:
[student@localhost ~]$ dmesg | grep -n -A 3 -B 4 -i eth0 667-[ 4.071531] nf_conntrack version 0.5.0 (16384 buckets, 65536 max) 668-[ 4.087847] ip6_tables: (C) 2000-2006 Netfilter Core Team 669-[ 4.127769] Ebtables v2.0 registered 670-[ 4.138456] Bridge firewalling registered 671:[ 4.334926] IPv6: ADDRCONF(NETDEV_UP): eth0: link is not ready <==以这行为基准 672-[ 11.114627] tun: Universal TUN/TAP device driver, 1.6 673-[ 11.114631] tun: (C) 1999-2004 Max Krasnyansky <maxk@qualcomm.com> 674-[ 11.225880] device virbr0-nic entered promiscuous mode
正规表示法既然是通过一些字符来作为数据截取的判断,那么有哪些惯用的符号呢?大概有底下这些基本的符号:
RE 字符 | 意义与范例 |
^word | 意义:待搜索的字符串(word)在行首! 范例:搜索行首为 # 开始的那一行,并列出行号 grep -n '^#' regular_express.txt |
word$ | 意义:待搜索的字符串(word)在行尾! 范例:将行尾为 ! 的那一行打印出来,并列出行号 grep -n '!$' regular_express.txt |
. | 意义:代表『一定有一个任意字符』的字符! 范例:搜索的字符串可以是 (eve) (eae) (eee) (e e), 但不能仅有 (ee) !亦即 e 与 e 中间『一定』仅有一个字符,而空白字符也是字符! grep -n 'e.e' regular_express.txt |
\ | 意义:跳脱字符,将特殊符号的特殊意义去除! 范例:搜索含有单引号 ' 的那一行! grep -n \' regular_express.txt |
* | 意义:重复零个到无穷多个的前一个 RE 字符 范例:找出含有 (es) (ess) (esss) 等等的字符串,注意,因为 * 可以是 0 个,所以 es 也是符合带搜索字符串。另外,因为 * 为重复『前一个 RE 字符』的符号, 因此,在 * 之前必须要紧接着一个 RE 字符喔!例如任意字符则为 『.*』 ! grep -n 'ess*' regular_express.txt |
[list] | 意义:字符集合的 RE 字符,里面列出想要截取的字符! 范例:搜索含有 (gl) 或 (gd) 的那一行,需要特别留意的是,在 [] 当中『谨代表一个待搜索的字符』, 例如『 a[afl]y 』代表搜索的字符串可以是 aay, afy, aly 即 [afl] 代表 a 或 f 或 l 的意思! grep -n 'g[ld]' regular_express.txt |
[n1-n2] | 意义:字符集合的 RE 字符,里面列出想要截取的字符范围! 范例:搜索含有任意数字的那一行!需特别留意,在字符集合 [] 中的减号 - 是有特殊意义的,他代表两个字符之间的所有连续字符!但这个连续与否与 ASCII 编码有关,因此,你的编码需要设置正确(在 bash 当中,需要确定 LANG 与 LANGUAGE 的变量是否正确!) 例如所有大写字符则为 [A-Z] grep -n '[A-Z]' regular_express.txt |
[^list] | 意义:字符集合的 RE 字符,里面列出不要的字符串或范围! 范例:搜索的字符串可以是 (oog) (ood) 但不能是 (oot) ,那个 ^ 在 [] 内时,代表的意义是『反向选择』的意思。 例如,我不要大写字符,则为 [^A-Z]。但是,需要特别注意的是,如果以 grep -n [^A-Z] regular_express.txt 来搜索,却发现该文件内的所有行都被列出,为什么?因为这个 [^A-Z] 是『非大写字符』的意思, 因为每一行均有非大写字符,例如第一行的 "Open Source" 就有 p,e,n,o.... 等等的小写字 grep -n 'oo[^t]' regular_express.txt |
\{n,m\} | 意义:连续 n 到 m 个的『前一个 RE 字符』 意义:若为 \{n\} 则是连续 n 个的前一个 RE 字符, 意义:若是 \{n,\} 则是连续 n 个以上的前一个 RE 字符! 范例:在 g 与 g 之间有 2 个到 3 个的 o 存在的字符串,亦即 (goog)(gooog) grep -n 'go\{2,3\}g' regular_express.txt |
另外,由于字符截取通常会有大小写、数字、特殊字符等等的差异,因此我们也能够使用如下的符号来代表某些特殊字符:
特殊符号 | 代表意义 |
[: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:] | 代表标点符号 (punctuation symbol),亦即:" ' ? ! ; : # $... |
[:upper:] | 代表大写字符,亦即 A-Z |
[:space:] | 任何会产生空白的字符,包括空白键, [Tab], CR 等等 |
[:xdigit:] | 代表 16 进位的数字类型,因此包括: 0-9, A-F, a-f 的数字与字符 |
请操作者进行如下的例题来处理相关任务:
sed 也是支持正规表示法的一项工具软件,具有很多很好用的功能在内!过去我们曾经使用过 ifconfig 与 awk 来找到 IP , 现在让我们使用 sed 来处理 IP 的设置。最基础的 sed 功能为取代,如下所示:
[student@localhost ~]$ sed 's/旧字符串/新字符串/g' 文件内容
用户只要替换『新旧字符串』内容,即可处理相关的字符串修订。现在让我们来处理 IP 的截取。使用 ifconfig eth0 来输出网络数据, 之后以 grep 取出 inet 那一行:
[student@localhost ~]$ ifconfig eth0 | grep 'inet[[:space:]]'
inet 172.16.0.83 netmask 255.255.0.0 broadcast 172.16.255.255
使用 sed 取代开头到 inet 空白的项目:
[student@localhost ~]$ ifconfig eth0 | grep 'inet[[:space:]]' | \ > sed 's/^.*inet[[:space:]]*//g' 172.16.0.83 netmask 255.255.0.0 broadcast 172.16.255.255
再接着取消空白netmask 之后的消息
[student@localhost ~]$ ifconfig eth0 | grep 'inet[[:space:]]' | \ > sed 's/^.*inet[[:space:]]*//g' | sed 's/[[:space:]]*netmask.*$//g' 172.16.0.83
除了替换数据之外,sed 还可以截取出特定的关键行数,例如只想要取出 10-15 行的 /etc/passwd 内容时,可以这样做:
[student@localhost ~]$ cat -n /etc/passwd | sed -n '10,15p'
这个动作在处理一些脚本化程序时,相当有帮助!而如果想要直接修改文件内容时,例如想要将 .bashrc 内的 function 改成大写时, 也可以这样做:
[student@localhost ~]$ sed 's/function/FUNCTION/g' .bashrc ....... # User specific aliases and FUNCTIONs >==这里会变大写 [student@localhost ~]$ sed -i 's/function/FUNCTION/g' .bashrc
加上 -i 选项后,该改变直接写入文件,且不会在屏幕上输出了!因此使用上需要特别注意!
shell script 对管理员来说,是一项非常好用的工具!请读者们一定要自己手动设计过一次相关的脚本程序, 而且能够针对自己管理的服务器进行一些例行工作的优化,才会更有感觉。
shell script 的撰写其实没有很难,基本上需要注意到:
至于 shell script 的运行,例如有名为 /home/student/shell.sh 的 script 时,可以用底下的方法:
底下我们将使用 student 的身份,并在 ~/bin 底下创建多个 shell script 来作为练习。首先,如果操作者运行 myid.sh 时, 系统会输出这个帐号的 id 指令输出消息,并且输出用户的家目录 (${HOME})、以及历史命令纪录笔数 (${HISTSIZE}), 最后列出所有的命令别名 (alias) 时,我们可以这样做:
上述的结果一项一项撰写成为 myid.sh 的内容如下:
[student@localhost ~]$ mkdir bin [student@localhost ~]$ cd bin [student@localhost bin]$ vim myid.sh #!/bin/bash # This script will use id, echo to show account's messages # write by VBird 2016/04/27 echo "This script will show your accout messages." echo "The 'id' command output is: " id echo "your user's home is: ${HOME}" echo "your history record: ${HISTSIZE}" echo "your command aliases: " alias echo "your home dir's filenames: " ll ~
之前曾经谈过进程的观察,且进程之间是有相依性的,因此可以使用 pstree 来观察进程的相依行为。 那么使用 shell script 时,他与当前的 shell 有无关系呢?底下举例来瞧瞧。如果你有一个如下的脚本, 该如何进入到该目录去?
[student@localhost ~]$ cd ~/bin [student@localhost bin]$ vim gototmp.sh #!/bin/bash # this shell script will take you togo /tmp directory. # VBird 2016/05/02 cd /tmp [student@localhost bin]$ chmod a+x gototmp.sh
请问,当你运行 gototmp.sh 这个脚本之后,以及『运行期间』,你的工作目录会在哪里?为什么?
事实上,运行脚本有基本的两种方式:
上述的第二种就是通过 source 或 . 来处理的。现在,请使用『 source ~/bin/gototmp.sh 』指令, 再次查阅一下你的工作目录是否正确的进入到 /tmp 了?
在第二堂课我们曾经使用过 bc 来计算数学的 pi,亦即使用『echo "scale=10; 4*a(1)" | bc -lq』来计算 pi。 而如果需要输出更正确的 pi 值,可以将 scale 的参数放大,例如『echo "scale=20; 4*a(1)" | bc -lq』来计算。 我们是否能够使用一个变量,让该变量带入脚本后,让用户可以与系统对谈呢?这时有两种基本的方法可以达到这个目的:
读者可以先理解 read 的用法:
[student@localhost ~]$ read -p 'Input your name: ' name1 Input your name: VBird Tsai [student@localhost ~]$ echo ${name1} VBird Tsai
亦即 read 会将用户输入的数据变成变量内容,之后就可以轻易的进行变量设置的任务。因此,若需要让用户与程序交互来输入 pi 的计算精确度, 可以写下如下的脚本:
[student@localhost ~]$ mkdir bin [student@localhost ~]$ vim bin/mypi.sh #!/bin/bash # Program: # User input a scale number to calculate pi number. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH echo -e "This program will calculate pi value. \n" echo -e "You should input a float number to calculate pi value.\n" read -p "The scale number (10~10000) ? " num echo -e "Starting calculate pi value. Be patient." time echo "scale=${num}; 4*a(1)" | bc -lq [student@localhost ~]$ chmod a+x bin/mypi.sh [student@localhost ~]$ mypi.sh This program will calculate pi value. You should input a float number to calculate pi value. The scale number (10~10000) ? 50 Starting calculate pi value. Be patient. 3.14159265358979323846264338327950288419716939937508 real 0m0.001s user 0m0.000s sys 0m0.002s
此时,只要用户运行 mypi.sh ,就可以手动输入 10 到 10000 之间不等的数值,让系统直接进行运算工作!
在第 8 堂课的课程内容曾经短暂介绍过 shell 内有个名为 ${1} 的变量,即是 shell script 的外带参数。事实上,外带参数可以有多个, 相关的『数字变量』有底下的相关性:
/path/to/scriptname opt1 opt2 opt3 opt4
$0 $1 $2 $3 $4
运行的脚本文件名为 ${0} 这个变量,第一个接的参数就是 ${1}。所以,只要在 script 里面善用 ${1} ,就可以很简单的立即下达某些指令功能了! 除了这些数字的变量之外,尚有底下这些常见的变量可以在 shell script 内调用:
在某些时刻,运行脚本可能是在背景中,因此不可能跟用户交互 (记得 jobs, fg, bg 的情况下),此时就能够通过这种外带参数的方式来运行。 例如我们将 mypi.sh 修改成外带参数的 mypi2.sh ,读者可以这样尝试:
[student@localhost ~]$ vim bin/mypi2.sh #!/bin/bash # Program: # User input a scale number to calculate pi number. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH num=${1} echo -e "This program will calculate pi value. \n" echo -e "Starting calculate pi value. Be patient." time echo "scale=${num}; 4*a(1)" | bc -lq [student@localhost ~]$ chmod a+x bin/mypi2.sh [student@localhost ~]$ mypi2.sh 50 This program will calculate pi value. Starting calculate pi value. Be patient. 3.14159265358979323846264338327950288419716939937508 real 0m0.001s user 0m0.000s sys 0m0.002s
读者可以发现,mypi2.sh 将 mypi.sh 内的两行输出 (说明程序功能与 read 的功能) 取消,而在不修改其他代码的情况下, 让 num=${1} 来让精确度使用第一个外带参数的方式来处理。
读者可以思考 mypi.sh 这个脚本的运作,虽然指定用户应该要输入 10~10000 的数值,但是却没有在脚本中进行防呆, 因此,当用户输入 (1)非为数值的字符串及 (2)输入超过数值范围时,就可能发生程序误判的情况,如下:
[student@localhost ~]$ mypi.sh This program will calculate pi value. You should input a float number to calculate pi value. The scale number (10~10000) ? whoami Starting calculate pi value. Be patient. 0 real 0m0.001s user 0m0.001s sys 0m0.001s [student@localhost ~]$ mypi.sh This program will calculate pi value. You should input a float number to calculate pi value. The scale number (10~10000) ? <==这里直接按下 enter 就好 Starting calculate pi value. Be patient. (standard_in) 1: syntax error real 0m0.001s user 0m0.000s sys 0m0.002s
此时就会发生不可预期的错误。读者在设计程序脚本时,应该就用户可能会输入的字符或通常的运作方式进行分析, 先设计好防呆,在代码的运作上比较不容易出问题。想要达成这种防呆的机制,需要用到条件判断式的支持,一般 shell script 条件判断的语法为:
if [ 条件判断式 ]; then 当条件判断式成立时,可以进行的指令工作内容; fi <==将 if 反过来写,就成为 fi 啦!结束 if 之意!
相关条件设置的方式已经在第八堂课谈过,请自行前往参阅。若有多重条件判断,则使用下列方式:
# 一个条件判断,分成功进行与失败进行 (else) if [ 条件判断式 ]; then 当条件判断式成立时,可以进行的指令工作内容; else 当条件判断式不成立时,可以进行的指令工作内容; fi
如果考虑更复杂的情况,则可以使用这个语法:
# 多个条件判断 (if ... elif ... elif ... else) 分多种不同情况运行 if [ 条件判断式一 ]; then 当条件判断式一成立时,可以进行的指令工作内容; elif [ 条件判断式二 ]; then 当条件判断式二成立时,可以进行的指令工作内容; else 当条件判断式一与二均不成立时,可以进行的指令工作内容; fi
如果考虑两个以上的条件混合运行时,就需要使用 -a 或 -o 的协助。
# 两个条件都要成立才算成立的情况: if [ 条件判断式一 -a 条件判断二 ]; then 两个条件都成立,这时才运行 (and 的概念) fi # 两个条件中,任何一个条件成立都算 OK 的情况: if [ 条件判断式一 -o 条件判断二 ]; then 随便哪一个条件成立,都可以运行 (or 的概念) fi
以上面的语法来补足 mypi.sh 的防呆,大致防呆的思考可以是:
大致的防呆流程就像上面所叙述,接下来读者们可以使用语法来将这些流程加入 mypi.sh:
[student@localhost ~]$ vim bin/mypi.sh #!/bin/bash # Program: # User input a scale number to calculate pi number. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH echo -e "This program will calculate pi value. \n" echo -e "You should input a float number to calculate pi value.\n" read -p "The scale number (10~10000) ? " num if [ "${num}" == "" ]; then # check empty number echo "You must input a number..." echo "I will use this number '20' to calculate pi" num=20 else checking="$(echo ${num} | grep '[^0-9]')" # check if any non-number char if [ "${checking}" != "" ]; then # check non-number value echo "You must input number..." echo "I will use this number '20' to calculate pi" num=20 fi fi if [ "${num}" -lt 10 ]; then echo "I will use this number '10' to calculate pi" num=10 elif [ "${num}" -gt 10000 ]; then echo "I will use this number '10000' to calculate pi" num=10000 fi echo -e "Starting calculate pi value. Be patient." time echo "scale=${num}; 4*a(1)" | bc -lq
接下来请运行数次 mypi.sh ,并分别输入不同的数据 (Enter, 文本, 小于 10 的数字, 大于 10000 的数字等等), 以确认自己的处理方式应为可行。
若读者只想让 mypi.sh 的操作者体验一下 pi 的计算,因此只想给予 20, 100, 1000 三个数值, 当用户不是输入此类数值,则告知对方仅能输入这三个数值。若以 if ... then 的方式来说,需要填写的判断式稍嫌多了些。 此时可以使用 case ... esac 来做设计。
case $变量名称 in <==关键字为 case ,还有变量前有钱字号 "第一个变量内容") <==每个变量内容建议用双引号括起来,关键字则为小括号 ) 程序段 ;; <==每个类别结尾使用两个连续的分号来处理! "第二个变量内容") 程序段 ;; *) <==最后一个变量内容都会用 * 来代表所有其他值 不包含第一个变量内容与第二个变量内容的其他程序运行段 exit 1 ;; esac <==最终的 case 结尾!『反过来写』思考一下!
请使用上述的语法,搭配仅能输入 20, 100, 1000 三个数值来撰写 mypi3.sh 脚本:
[student@localhost ~]$ vim bin/mypi3.sh #!/bin/bash # Program: # User input a scale number to calculate pi number. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH echo -e "This program will calculate pi value. \n" echo -e "You should input a float number to calculate pi value.\n" read -p "The scale number (20,100,1000) ? " num case ${num} in "20") echo "Your input is 20" ;; "100") echo "Your input is 100" ;; "1000") echo "Your input is 1000" ;; *) echo "You MUST input 20|100|1000" echo "I stop here" exit 0 ;; esac echo -e "Starting calculate pi value. Be patient." time echo "scale=${num}; 4*a(1)" | bc -lq [student@localhost ~]$ chmod a+x mypi3.sh [student@localhost ~]$ mypi3.sh This program will calculate pi value. You should input a float number to calculate pi value. The scale number (20,100,1000) ? 30 You MUST input 20|100|1000 I stop here [student@localhost ~]$ mypi3.sh This program will calculate pi value. You should input a float number to calculate pi value. The scale number (20,100,1000) ? 100 Your input is 100 Starting calculate pi value. Be patient. 3.141592653589793238462643383279502884197169399375105820974944592307\ 8164062862089986280348253421170676 real 0m0.003s user 0m0.003s sys 0m0.000s
前置动作:请使用 unit09 的硬盘进入作业环境,并请先以 root 身分运行 vbird_book_setup_ip 指令设置好你的学号与 IP 之后,再开始底下的作业练习。
请使用 root 的身份进行如下实做的任务。直接在系统上面操作,操作成功即可,上传结果的程序会主动找到你的实做结果。
作业结果传输:请以 root 的身分运行 vbird_book_check_unit 指令上传作业结果。 正常运行完毕的结果应会出现【XXXXXX;aa:bb:cc:dd:ee:ff;unitNN】字样。若需要查阅自己上传数据的时间, 请在操作系统上面使用: http://192.168.251.250 检查相对应的课程文件。