Linux 基础学习训练教材 - RockyLinux 9.x

第 9 堂课:正规表示法与 shell script 初探

正规表示法用在非常多地方!熟悉正规表示法好处多多!因为其他编程语言也会用到这玩意儿喔!

最近更新时间: 2023/05/10

谈完了指令列的 bash shell 的操作,再来得要思考一下,如果管理员有一堆指令要循序进行,且这些指令可能具有相依性 (例如判断式), 或者是管理员需要写一些让用户『交互』的脚本时,应该如何处理?这时就得要通过 bash 的 shell script (程序化脚本) 来进行了。 此外,很多时候我们需要进行数据的截取,这时好用的正规表示法就得要派上用场了!

9.1:正规表示法的应用

用户在操作电子邮件时,常常会发现到许多邮件被丢到垃圾桶或者是被判定为病毒邮件,这些判定的方式,很多就是通过『正规表示法』来处理的! 正规表示法 (Regular Expression) 就是处理字符串的方法,他是以行为单位来进行字符串的处理行为,正规表示法通过一些特殊符号的辅助, 可以让用户轻易的达到『搜索/删除/取代』某特定字符串的处理程序!

9.1.1:grep 指令的应用

由于正规表示法牵涉到数据的截取,因此读者们先了解一下最简单的数据截取指令: grep 的高端用法。例如,找出 /etc/passwd 当中, 含有 student 的那行,且列出行号:

[student@localhost ~]$ grep -n student /etc/passwd
37:student:x:1000:1000:student:/home/student:/bin/bash

读者们会看到输出的信息中,最前面会多出一个行号的信息,就可以让用户知道该信息来自文件的那一行这样。 另外,当用户有观察开机流程所产生的信息时,例如想要查找开机过程产生的问题等等,可以使用 dmesg 这个指令。 只是这个指令输出的信息量非常庞大。若用户仅须知道某张网络卡的相关信息,那该如何处理呢? 先让我们来查找一下网络卡的代号:

# 单纯使用 dmesg 的情况下,数据量相当庞大
[student@localhost ~]$ dmesg
......
[    2.142807] libata version 3.00 loaded.
[    2.149320] ata_piix 0000:00:01.1: version 2.13
[    2.153260]  vda: vda1 vda2 vda3
[    2.157591] scsi host0: ata_piix
[    2.166390] scsi host1: ata_piix
......

# 使用底下的方法可以找到网络卡的代号
[student@localhost ~]$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:ba:0b:fc brd ff:ff:ff:ff:ff:ff
    altname enp0s3

上面输出的 ens3 就是网卡代号,而 lo 则是内部循环测试网卡,那个可以暂时略过不理。 现在,让我们从 dmesg 的开机消息中,找出网络卡相关的消息吧:

[student@localhost ~]$ dmesg | grep -n -i ens3
548:[    2.191080] virtio_net virtio0 ens3: renamed from eth0

大概在 548 行是比较重要的项目。那如果我们还需要知道该行之前 (before) 的 4 行以及之后 (after) 的 3 行, 以了解这行前后文的话,则可以这样处理:

[student@localhost ~]$ dmesg | grep -n -A 3 -B 4 -i ens3
544-[    2.157591] scsi host0: ata_piix
545-[    2.166390] scsi host1: ata_piix
546-[    2.166428] ata1: PATA max MWDMA2 cmd 0x1f0 ctl 0x3f6 bmdma 0xc000 irq 14
547-[    2.166430] ata2: PATA max MWDMA2 cmd 0x170 ctl 0x376 bmdma 0xc008 irq 15
548:[    2.191080] virtio_net virtio0 ens3: renamed from eth0  <==以这行为基准
549-[    2.200898] cirrus 0000:00:02.0: vgaarb: deactivate vga console
550-[    2.270108] Console: switching to colour dummy device 80x25
551-[    2.270401] [drm] Initialized cirrus 2.0.0 2019 for 0000:00:02.0 on minor 0

例题 9.1.1-1:使用反向的方式,取得非关键字的那几行
  1. 使用 man grep ,找到反向 (invert) 这个关键字,找出其意义与选项参数功能
  2. 输入 df 后,将 tmpfs 相关的那几行取消,让屏幕仅输出一般的文件系统,方便查阅。
  3. 针对上面的功能,写一只名为 df2 的命令别名,未来可以直接使用 df2 显示出这个效果

9.1.2:正规表示法的符号意义

正规表示法既然是通过一些字符来作为数据截取的判断,那么有哪些惯用的符号呢?大概有底下这些基本的符号:

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 的数字与字符

请操作者进行如下的例题来处理相关任务:

例题 9.1.2-1: 使用 grep 熟悉正规表示法的应用
  1. 找出 /etc/services 内含 http 关键字的那几行
  2. 承上,若仅须『开头含有 http 』字样的那几行?
  3. 承上,若仅须『开头含有 http 或 https 』字样的那几行?
  4. 承上,若仅须『开头含有 http 或 https 』字样之外,且后面仅能接空白字符或 [tab] 字符的那几行?
  5. 承上,若仅须『开头含有 http 且后续接有 80 』字样的那几行
  6. 找出 /etc/services 内含有星号 (*) 的那几行
  7. 找出 /etc/services 内含有星号,且星号前为英文(不论大小写)的那几行
  8. 找出 /etc/services 含有一个数字且后面紧邻一个大写字符的那几行
  9. 找出 /etc/services 开头是一个数字紧邻一个大写字符的那几行
  10. 使用 find /etc 找出文件名,并找出结尾含有『 .conf 』的文件名数据
  11. 承上,且含有『大写字符或数字』文件名在内的那几个

9.1.3:sed 工具的使用

  • 利用 sed 进行数据的取代

sed 也是支持正规表示法的一项工具软件,具有很多很好用的功能在内!过去我们曾经使用过 ifconfig 与 awk 来找到 IP , 现在让我们使用 sed 来处理 IP 的设置。最基础的 sed 功能为取代,如下所示:

[student@localhost ~]$ sed 's/旧字符串/新字符串/g' 文件内容

用户只要替换『新旧字符串』内容,即可处理相关的字符串修订。现在让我们来处理 IP 的截取。使用 ifconfig ens3 来输出网络数据, 之后以 grep 取出 inet 那一行 (注意: ens3 请依据你的系统网卡来调整):

[student@localhost ~]$ ifconfig ens3 | grep 'inet[[:space:]]'
        inet 172.16.5.237  netmask 255.255.0.0  broadcast 172.16.255.255

使用 sed 取代开头到 inet 空白的项目:

[student@localhost ~]$ ifconfig ens3 | grep 'inet[[:space:]]' | \
> sed 's/^.*inet[[:space:]]//g'
172.16.5.237  netmask 255.255.0.0  broadcast 172.16.255.255

再接着取消空白netmask 之后的消息

[student@localhost ~]$ ifconfig ens3 | grep 'inet[[:space:]]' | \
> sed 's/^.*inet[[:space:]]//g' | \
> sed 's/[[:space:]]*netmask.*$//g'
172.16.5.237
  • 以 sed 进行数据的输出

除了替换数据之外,sed 还可以截取出特定的关键行数,例如只想要取出 10-15 行的 /etc/passwd 内容时,可以这样做:

[student@localhost ~]$ cat -n /etc/passwd | sed -n '10,15p' 
请使用 man sed 查找一下 -n 这个选项的功能!此外,亦请分别就上面这个指令,列出有加 -n 与没加 -n 的差别!

上面这个动作在处理一些脚本化程序时,相当有帮助!而如果想要直接修改文件内容时,例如想要将 .bashrc 内的 function 改成大写时, 也可以这样做:

[student@localhost ~]$ grep -n 'function' ~/.bashrc
18:# User specific aliases and functions

[student@localhost ~]$ sed 's/function/FUNCTION/g' ~/.bashrc | cat -n
.......
    18  # User specific aliases and FUNCTIONs >==看第 18 行,这里会变大写

[student@localhost ~]$ sed -i 's/function/FUNCTION/g' .bashrc

加上 -i 选项后,该改变直接写入文件,且不会在屏幕上输出了!因此使用上需要特别注意!

例题 9.1.3-1: 使用 sed 进行正规表示法的练习 (特别注意到 / 与 \ 的搭配应用!)
  1. 应该要用 grep 去找出 /etc/passwd 里面,结尾是 bash 的那几行
  2. 承上,通过 sed 将 /bin/bash 改成 /sbin/nologin 显示到屏幕上
  3. 承上,通过 tr 这个指令,将全部的英文都变成大写字符

9.2:学习 shell script

shell script 对管理员来说,是一项非常好用的工具!请读者们一定要自己手动设计过一次相关的脚本程序, 而且能够针对自己管理的服务器进行一些例行工作的优化,才会更有感觉。

9.2.1:基础 shell script 的撰写与运行

shell script 的撰写其实没有很难,基本上需要注意到:

  • 指令的运行是从上而下、从左而右的分析与运行 (这点跟传统编程语言差很大!);
  • 指令的下达中,指令、选项与参数间的多个空白都会被忽略掉;
  • 空白行也将被忽略掉,并且 [tab] 按键所推开的空白同样视为空白键;
  • 如果读取到一个 Enter 符号 (CR) ,就尝试开始运行该行 (或该串) 命令;
  • 至于如果一行的内容太多,则可以使用『 \[Enter] 』来延伸至下一行;
  • 『 # 』可做为注解!任何加在 # 后面的数据将全部被视为注解文本而被忽略!

至于 shell script 的运行,例如有个文件名为 /home/student/shell.sh 的 script 时,可以用底下的方法:

  • 直接指令下达: shell.sh 文件必须要具备可读与可运行 (rx) 的权限,然后:
    • 绝对路径:使用 /home/student/shell.sh 来下达指令;
    • 相对路径:假设工作目录在 /home/student/ ,则使用 ./shell.sh 来运行
    • 变量『PATH』功能:将 shell.sh 放在 PATH 指定的目录内,例如: ~/bin/
  • 以 bash 程序来运行:通过『 bash shell.sh 』或『 sh shell.sh 』来运行

底下我们将使用 student 的身份,并在 ~/bin 底下创建多个 shell script 来作为练习。首先,如果操作者运行 myid.sh 时, 系统会输出这个帐号的 id 指令输出消息,并且输出用户的家目录 (${HOME})、以及历史命令纪录笔数 (${HISTSIZE}), 最后列出所有的命令别名 (alias) 时,我们可以这样做:

  1. 请先声明使用的 script 为 bash ;
  2. 请先说明程序的功能 (需要用 # 注解)
  3. 请先说明程序的撰写者 (需要用 # 注解)
  4. 秀出『 This script will show your account messages 』
  5. 秀出『 The 'id' command output is: 』
  6. 秀出 id 这个指令的结果
  7. 秀出『 your user's home is: ${HOME} 』的结果
  8. 秀出『 your history record: ${HISTSIZE} 』的结果
  9. 秀出『 your command aliases: 』
  10. 显示出 alias 的结果
  11. 显示出家目录的文件名结果

上述的结果一项一项撰写成为 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 ~

例题 9.2.1-1: 基础 shell script 的运行与相关环境参数的功能 (包括使用 source 运行脚本)
  1. 如何直接以 bash 或 sh 去运行这只脚本 (指的是直接用 bash 指令去运行,而不是运行 myid.sh)?
  2. 承上,运行过程中,如果还需要输出代码之后才运行,可以加上哪个选项?(此功能相当有用!可用以检测代码的错误, debug)
  3. 若需要直接输入 myid.sh 就能运行时,需要有什么设置 (包括权限、路径等等的数据) ?
  4. 如何使用绝对路径来运行?
  5. 若你刚好在工作目录下看到这个脚本,但你不确定工作目录有没有在 PATH 环境中,该如何下达指令运行该脚本?
  6. 为何 alias 的结果没有输出?若运行该脚本时还要输出目前的 alias ,该如何运行?为什么?

9.2.2:shell script 的运行环境

之前曾经谈过进程的观察,且进程之间是有相依性的,因此可以使用 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
pwd
[student@localhost bin]$ chmod a+x gototmp.sh
例题 9.2.2-1:请问:
  1. 当你运行 gototmp.sh 这个脚本之后,以及『运行期间』,你的工作目录会在哪里?
  2. 如果使用 source 去运行,最终工作目录又是在哪里?
  • 使用 source 或 . 来运行脚本

事实上,运行脚本有基本的两种方式:

  • 直接产生一个新的进程 (process) 来运行,例如 bash script.sh、 ./script.sh 都属于这一种;
  • 将 script.sh 的指令,调用进来目前的这个 bash process 运行,而不是产生一个新的 process

上述的第二种就是通过 source 或 . 来处理的。现在,请使用『 source ~/bin/gototmp.sh 』指令, 再次查阅一下你的工作目录是否正确的进入到 /tmp 了?

[student@localhost bin]$ source gototmp.sh
/tmp
[student@localhost tmp]$ pwd
/tmp
例题 9.2.2-2:你在操作 Linux 系统的过程中,可能会切换到许多不同的 shell (例如 bash 转到 csh 等等)。不过这些操作环境中,均需要使用到底下的变量, 且这些变量是在有需要时才加载的 (不是写入到 .bashrc),因此需要额外撰写成 myenv.sh。内容需要:
  1. 设计所需要的环境操作参数:
    1. 设计 MYIP 的变量为目前系统的 IP (假设网络卡为 ens3 时)
    2. 设计 mywork 的变量为指定到 /usr/local/libexec 目录中
    3. 设计 megacli 命令别名为 /opt/mega/cli/command 这个指令(此指令并不存在,仅作为范例用)
  2. 设计完毕之后,若要使用这个文件内的数据,该如何运行?

9.2.3:以对谈式脚本及外带参数计算 pi

在第二堂课我们曾经使用过 bc 来计算数学的 pi,亦即使用『echo "scale=10; 4*a(1)" | bc -lq』来计算 pi。 而如果需要输出更正确的 pi 值,可以将 scale 的参数放大,例如『echo "scale=20; 4*a(1)" | bc -lq』来计算。 我们是否能够使用一个变量,让该变量带入脚本后,让用户可以与系统对谈呢?这时有两种基本的方法可以达到这个目的:

  • 以 read 作为对谈式脚本的设计依据
  • 以 ${1} 等作为外带参数的设计依据
  • 使用 read 让用户输入参数:

读者可以先理解 read 的用法:

[student@localhost ~]$ read -p 'Input your name: ' name1
Input your name: VBird Tsai

[student@localhost ~]$ echo ${name1}
VBird Tsai

亦即 read 会将用户输入的数据变成变量内容,之后就可以轻易的进行变量设置的任务。因此,若需要让用户与程序交互来输入 pi 的计算精确度, 可以写下如下的脚本:

[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 之间不等的数值,让系统直接进行运算工作!此外你得要注意, 在 echo 显示信息时,可以使用 \t, \n 等方法来处理断行、[tab] 按钮的样式~只是需要加上 -e 的选项才行!你可以『 man echo 』查找可用的变量!

例题 9.2.3-1:创建一个名为 /usr/local/bin/listcmd.sh 的脚本,该脚本可以完成底下的各项工作:
  1. 先制作这个脚本:
    1. 第一行一定要声明 shell
    2. 显示出这个脚本的目的 (中英文均可,例如: This shell script will list your command's full path name and permissions.)
    3. 开始通过 read 让用户输入指令名称
    4. 通过上一步骤取得指令名称后,通过 which 找到这个指令的完整路径
    5. 利用 ls -l 列出这个指令的完整权限
    6. 利用 getfacl 列出这个指令的完整权限
    7. 离开 shell script,并回传 0 的数值。
  2. 最后将该指令的权限修订成全部成员均可运行,并运行一次确认状态。运行的结果会有点像这样:
    [student@localhost ~]$ listcmd.sh
    
    This shell script will list your command's full path name and permissions.
    
    Please input a command name: ls
    -rwxr-xr-x. 1 root root 166448  5月 12  2019 /usr/bin/ls
    getfacl: Removing leading '/' from absolute path names
    # file: usr/bin/ls
    # owner: root
    # group: root
    user::rwx
    group::r-x
    other::r-x
    
  • 通过外带参数的功能运行:

在第 8 堂课的课程内容曾经短暂介绍过 shell 内有个名为 ${1} 的变量,即是 shell script 的外带参数。事实上,外带参数可以有多个, 相关的『数字变量』有底下的相关性:

/path/to/scriptname  opt1  opt2  opt3  opt4 
       $0             $1    $2    $3    $4

运行的脚本文件名为 ${0} 这个变量,第一个接的参数就是 ${1}。所以,只要在 script 里面善用 ${1} ,就可以很简单的立即下达某些指令功能了! 除了这些数字的变量之外,尚有底下这些常见的变量可以在 shell script 内调用:

  • $# :代表后接的参数『个数』,以上表为例这里显示为『 4 』;
  • "$@" :代表『 "$1" "$2" "$3" "$4" 』之意,每个变量是独立的(用双引号括起来);
  • "$*" :代表『 "$1c$2c$3c$4" 』,其中 c 为分隔字符,缺省为空白键, 所以本例中代表『 "$1 $2 $3 $4" 』之意。

在某些时刻,运行脚本可能是在背景中,因此不可能跟用户交互 (记得 jobs, fg, bg 的情况下),此时就能够通过这种外带参数的方式来运行。 例如我们将 mypi.sh 修改成外带参数的 mypi2.sh ,读者可以这样尝试:

[student@localhost ~]$ cp ~/bin/mypi.sh ~/bin/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"
#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/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} 来让精确度使用第一个外带参数的方式来处理。

例题 9.2.3-2:创建一个名为 /usr/local/bin/listcmd2.sh 的脚本,该脚本可以完成底下的各项工作:
  1. 先修改这个脚本,处理前,请先将 listcmd.sh 拷贝成为 listcmd2.sh 喔!
    1. 增加一行功能,让指令名称由外带参数来处理 (${1})
    2. 注解取消,或删除 read 的用户打字输入功能。
  2. 最后将该指令的权限修订成全部成员均可运行,并运行一次确认状态,指令运行有点像这样:
    [root@station5-237 bin]# listcmd2.sh pwd
    
    This shell script will list your command's full path name and permissions.
    
    -rwxr-xr-x. 1 root root 46840  5月 12  2019 /usr/bin/pwd
    getfacl: Removing leading '/' from absolute path names
    # file: usr/bin/pwd
    # owner: root
    # group: root
    user::rwx
    group::r-x
    other::r-x
    

9.2.4:通过 if .. then 设计条件判断

读者可以思考 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

事实上,还有另一种复合条件判断的指令语法,有点像这样:

# 两个条件都要成立才算成立的情况:
if [ 条件判断式一 ] && [ 条件判断二 ]; then
	两个条件都成立,这时才运行 (and 的概念)
fi

# 两个条件中,任何一个条件成立都算 OK 的情况:
if [ 条件判断式一 ] || [ 条件判断二 ]; then
	随便哪一个条件成立,都可以运行 (or 的概念)
fi

以上面的语法来补足 mypi.sh 的防呆,大致防呆的思考可以是:

  1. 用户输入的数据非为数值的状态:
    • 不可以是空白也不可以有非数字的字符存在
    • 若有上述状况,则数值改为缺省的 20
  2. 用户输入的数据不在规范的范围内:
    • 若小于 10 则以 10 取代;
    • 若大于 10000 则以 10000 取代;

大致的防呆流程就像上面所叙述,接下来读者们可以使用语法来将这些流程加入 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

# 判断是否有非为数字的字符,若有,则会出现,若无则是空白
checking=$( echo ${num} | grep '[^0-9]' )
if [ "${num}" == "" -o "${checking}" != "" ]; then
        num=20
fi

if [ "${num}" -le 10 ]; then
        num=10
elif [ "${num}" -ge 10000 ]; then
        num=10000
fi
echo "Use scale number: ${num}"

echo -e "Starting calculate pi value.  Be patient."
time echo "scale=${num}; 4*a(1)" | bc -lq

接下来请运行数次 mypi.sh ,并分别输入不同的数据 (Enter, 文本, 小于 10 的数字, 大于 10000 的数字等等), 以确认自己的处理方式应为可行。

例题 9.2.4-1:进行防呆测试
请通过相同的方法来修改 mypi2.sh ,让该脚本也能够防呆。

9.2.5:以 case .. esac 设计条件判断

若读者只想让 mypi.sh 的操作者体验一下 pi 的计算,因此只想给予 20, 100, 1000 三个数值, 当用户不是输入此类数值,则告知对方仅能输入这三个数值。若以 if ... then 的方式来说,需要填写的判断式稍嫌多了些。 此时可以使用 case ... esac 来做设计。

case  $变量名称 in   <==关键字为 case ,还有变量前有钱字号
  "第一个变量内容")   <==每个变量内容建议用双引号括起来,关键字则为小括号 )
	程序段
	;;            <==每个类别结尾使用两个连续的分号来处理!
  "第二个变量内容")
	程序段
	;;
  *)                  <==最后一个变量内容都会用 * 来代表所有其他值
	不包含第一个变量内容与第二个变量内容的其他程序运行段
	exit 1
	;;
esac                  <==最终的 case 结尾!『反过来写』思考一下!

请使用上述的语法,搭配仅能输入 20, 100, 1000 三个数值来撰写 mypi3.sh 脚本:

[student@localhost ~]$ cp ~/bin/mypi.sh ~/bin/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 ~/bin/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

这样就可以缩减用户输入的参数了!

例题 9.2.5-1:
通过 case ... esac 的方法,修改 mypi2.sh 变成 mypi4.sh,以外带参数的方式,让 mypi4.sh 只能支持 20|100|1000 的数值, 若用户外带参数不是这三个,则显示『 Usage: mypi4.sh 20|100|1000 』的屏幕提示,否则就直接计算 pi 值并输出结果。

9.3:延伸正规表示法的字符

事实上,一般读者只要了解基础型的正规表示法大概就已经相当足够了,不过,某些时刻为了要简化整个指令操作, 了解一下使用范围更广的延伸型正规表示法的表达式会更方便呢!举个简单的例子,一般我们要去除信息内的 (1)空白行与 (2)注解行时, 可能需要使用两次基本正规表示法,如下所示:

[student@localhost ~]$ grep -v '^$' /etc/crontab  | grep -v '^#'

延伸型正规表示法,可以通过『群组』的功能,将需要截取的关键字写在一起,整个指令就会变得更清爽:

[student@localhost ~]$ egrep -v '^$|^#' /etc/crontab

grep 支持基本正规表示法,若要使用到延伸正规表示法,就得要使用 egrep 这个指令来替换才行。

熟悉了正规表示法之后,到这个延伸型的正规表示法,你应该也会想到,不就是多几个重要的特殊符号吗? 是的~所以,我们就直接来说明一下,延伸型正规表示法有哪几个特殊符号?

RE 字符意义与范例
+意义:重复『一个或一个以上』的前一个 RE 字符
范例:搜索 (god) (good) (goood)... 等等的字符串。 那个 o+ 代表『一个以上的 o 』所以,底下的运行成果会将第 1, 9, 13 行列出来。
egrep -n 'go+d' regular_express.txt
?意义:『零个或一个』的前一个 RE 字符
范例:搜索 (gd) (god) 这两个字符串。 那个 o? 代表『空的或 1 个 o 』所以,上面的运行成果会将第 13, 14 行列出来。 有没有发现到,这两个案例( 'go+d' 与 'go?d' )的结果集合与 'go*d' 相同? 想想看,这是为什么喔! ^_^
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" 字符串的意思~

以上这些就是延伸型的正规表示法的特殊字符。另外,要特别强调的是,那个 ! 在正规表示法当中并不是特殊字符, 所以,如果你想要查出来文件中含有 ! 与 > 的字行时,可以这样:

[student@localhost ~]$ grep -n '[!>]' regular_express.txt

另外,延伸正规表示法对于一般用户来说,大致上最重要的就是群组 (|) 这种用法~所以,目前只要知道 egrep 这个指令的使用时机, 以及通过 | 来分隔要同时截取的关键字即可!

9.4:课后练习操作

  • 上课的课后练习,非作业:
  1. 正规表示法的应用-截取重要的设置项目 for /etc/man_db.conf
    1. 先确认 /etc/man_db.conf 里面有几行?
    2. 再确认这个文件里面不是空白行也不是注解开头的,总共有几行?
    3. 最终只列出开头是『 MANPATH_MAP 』的那几行即可。
  2. 正规表示法的应用-截取重要的设置项目 for /etc/rsyslog.conf
    1. 根据前一题的作法,请将 /etc/rsyslog.conf 当中,非空白行、非注解行取出 (注意喔, # 在最前面才是全注解!)
    2. 承上,输出的数据中,还是含有 # ,请将每一行的 # 到最后面的数据,全部删除,只保留需要的设置部份。
    3. 从 /etc/rsyslog.conf 当中,找出含有 tcp 或 udp 那一行,包括行号列出
    4. 根据上面的输出结果,你觉得 rsyslog 这个服务,可能会启动那一个网络端口口号码?
  3. 撰写一只名为 lm 的指令,这个指令会取得后面所有的参数 ( "$@" ),然后使用『 ls -al ${@} | more 』的样式, 进行一页一页翻动的功能。这个指令放在大家都能运行的 /usr/local/bin 里面,请设置好权限之后, 以 student 的身份运行看看喔!
  • 作业 (不提供学生答案,仅提供教师参考答案)

作业硬盘一般操作说明:

  • 打开云端虚拟机前,请务必确认你打开的硬盘是『unit09』,否则就会做错题目
  • 若要使用图形界面,请务必使用 student 身份登录,若需要切换身份,再激活终端机处理。
  • 若有简答题需要使用中文,请自行以第一堂课的动作自行处理输入法安装。
  • 每部虚拟机均有独特的网卡地址,请勿使用他人硬盘上传,否则计分为 0 分。
  • 每位同学均有自己的 IP 尾数,请先向老师询问您的 IP 尾数,才可以进行作业上传。
  • 最终上传作业结果,请务必使用 root 身份上传。
  • 进入作业硬盘后,先用 root 身份运行 vbird_book_setup_ip , 运行流程请参考:vbird_book_setup_ip

作业当中,某些部份可能为简答题~若为简答题时,请将答案写入 /home/student/ans.txt 当中,并写好正确题号,方便老师订正答案。 请注意,文件名写错将无法上传!

请使用 root 的身份进行如下实做的任务。直接在系统上面操作,操作成功即可,上传结果的程序会主动找到你的实做结果。

  1. (20%)分析『本日』注册表信息的相关设置,重点在实做与练习正规表示法:
    1. 写一只名为 /usr/local/sbin/getmesg 的脚本,内容会使用 grep 取得 /var/log/messages 在『本日』的登录信息。 要注意的是,我们假设系统语系为英文,抓日期的方式可以从『 date 』这个指令搭配相关参数去处理。 当你用 root 的身份运行 getmesg 时,屏幕会显示类似如下的信息:
      [root@localhost ~]# getmesg
      ....
      May 10 19:57:16 localhost chronyd[771]: System clock was stepped by -28797.191834 seconds
      May 10 19:57:16 localhost chronyd[771]: System clock TAI offset set to 37 seconds
      May 10 19:57:16 localhost rsyslogd[748]: imjournal: journal files changed, reloading... 
      May 10 19:57:16 localhost rsyslogd[748]: imjournal: journal files changed, reloading... 
      ....
      # 月份名称会出现在行首!
      
    2. 写一只名为 /usr/local/sbin/chsel 的脚本,这个脚本会修改 /etc/selinux/config 文件的内容, 会将行首出现『SELINUX=??? 』那一行(一整行喔)数据,强制替换成『SELINUX=enforcing』或『SELINUX=permissive』, 且该文件会被直接修改。运行方式会像这样 (需要 root 权限):
      [root@localhost ~]# chsel 0
      # 此时 /etc/selinux/config 内的那行会被改成 SELINUX=permissive
      
      [root@localhost ~]# chsel 1
      # 此时 /etc/selinux/config 内的那行会被改成 SELINUX=enforcing
      
    3. 写一只名为 /usr/local/bin/chupper 的脚本,当运行该脚本之后,会把 /etc/hosts 抓出来变成大写字符的内容。 使用一般帐号也可以运行这个脚本,运行的结果会有点像这样:
      [student@localhost ~]$ cat /etc/hosts
      127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
      ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
      
      [student@localhost ~]$ chupper
      127.0.0.1   LOCALHOST LOCALHOST.LOCALDOMAIN LOCALHOST4 LOCALHOST4.LOCALDOMAIN4
      ::1         LOCALHOST LOCALHOST.LOCALDOMAIN LOCALHOST6 LOCALHOST6.LOCALDOMAIN6
      # 可以发现英文全部变大写字符了!
      
  2. (10%)创建一只名为 /usr/local/bin/myprocess 的脚本,脚本内容主要为:
    1. 第一行一定要声明 shell 为 bash 才行;
    2. 主要仅运行『 /bin/ps -Ao pid,user,cpu,tty,args 』
    3. 这只脚本必须要让所有人都可以运行才行!
    4. 运行结果会有点像这样:
      [student@localhost ~]$ myprocess
          PID USER     CPU TT       COMMAND
            1 root       - ?        /usr/lib/systemd/systemd rhgb --switched-root --system --deserialize 31
            2 root       - ?        [kthreadd]
            3 root       - ?        [rcu_gp]
      ....
      
  3. (10%)写一只名为 /usr/local/bin/mydate.sh 的脚本,运行后可以输出如下的数据:
    1. 第一行一定要声明 shell 才对!
    2. 以 西元年/月/日 显示出目前的日期
    3. 以 小时:分钟:秒钟 显示出目前的时间
    4. 输出从 1970/01/01 到目前累计的秒数
    5. 列出这个月的月历,且依据台湾的习惯,输出时,以星期一为一周的开始。
    6. 这只脚本必须要让所有人都可以运行才行!
    7. 运行结果会有点像这样:
      [student@localhost ~]$ mydate.sh
      2023/05/10
      20:33:10
      1683721990
            May 2023
      Mo Tu We Th Fr Sa Su
       1  2  3  4  5  6  7
       8  9 10 11 12 13 14
      15 16 17 18 19 20 21
      22 23 24 25 26 27 28
      29 30 31
      
  4. (20%)写一只 /usr/local/bin/listcmd.sh 的脚本,该脚本运行后,会告知底下相关的事宜:
    1. 脚本的运行方式为『 listcmd.sh passwd 』,其中 passwd 可以使用任何文件名来取代
    2. 第一行一定要声明 shell
    3. 判断是否有外带参数,若没有外带参数,请 (1)屏幕显示『 Usage: ${0} cmd_name 』,(2)并以回传值 2 离开程序
    4. 这阶段运行结果会有点像这样:
      [student@localhost ~]$ listcmd.sh
      Usage: /usr/local/bin/listcmd.sh cmd_name
      
      [student@localhost ~]$ echo $?
      2
      
    5. 使用『 which ${1} 2> /dev/null 』的结果,判断该字符串是否为指令,你可以将这个指令的结果带入成为某个变量, 然后分析这个变量值。基本上,这个字符串值若存在,则代表是指令,若为空值,则非指令。
    6. 若该字符串为指令,则依序输出:
      • 输出指令的完整路径
      • 用 ls -l 列出这个指令的完整权限
      • 利用 getfacl 列出这个指令的完整权限
      • 离开 shell script,并回传 0 的数值。
    7. 这阶段运行结果会有点像这样:
      [student@localhost ~]$ listcmd.sh passwd
      /usr/bin/passwd
      -rwsr-xr-x. 1 root root 32656 May 15  2022 /usr/bin/passwd
      getfacl: Removing leading '/' from absolute path names
      # file: usr/bin/passwd
      # owner: root
      # group: root
      # flags: s--
      user::rwx
      group::r-x
      other::r-x
      
      [student@localhost ~]$ echo $?
      0
      
    8. 若该字符串不为指令,则使用 locate 后面加 /${1}$ 的正规表示法 (locate 要支持正规表示法,必须要输入特定的选项 请自行 man locate 查到正确的选项支持),然后依据 locate 之后的回传值处理后续工作
      • 若回传值 (为 0) 显示该字符串其实具有相同的文件名,则使用 ls -ld 将文件名全部列出,然后以回传值 0 离开程序
      • 若回传值 (不为 0) 显示该字符串并不为文件名,则显示『no this filename』,然后以回传值 10 离开程序
    9. 运行结果会有点像这样:
      [student@localhost ~]$ listcmd.sh shadow
      ----------. 1 root root 1159 Feb 16 18:19 /etc/shadow
      
      [student@localhost ~]$ echo $?
      0
      
      [student@localhost ~]$ listcmd.sh nonfile
      no this filename
      
      [student@localhost ~]$ echo $?
      10
      
  5. (10%)写一只名为 /usr/local/bin/myheha 的脚本,建议使用 case 这种语法,不要使用 if 语法。这只脚本的运行结果会这样:
    1. 脚本内第一行一定要声明 shell 为 bash
    2. 当运行 myheha hehe 时,屏幕会输出『 I am haha 』
    3. 当运行 myheha haha 时,屏幕会输出『 You are hehe 』
    4. 当外带参数不是 hehe 也不是 haha 时,屏幕会输出『 Usage: myheha hehe|haha 』
    5. 运行结果会有点像这样:
      [student@localhost ~]$ myheha hehe
      I am haha
      
      [student@localhost ~]$ myheha haha
      You are hehe
      
      [student@localhost ~]$ myheha nono
      Usage: /usr/local/bin/myheha hehe|haha
      
  6. (20%)写一只判断生日的脚本,名称为 /usr/local/bin/yourbday.sh,内容为:
    1. 脚本内第一行一定要声明 shell 为 bash
    2. 指令运行的方式为『 yourbday.sh YYYY-MM-DD 』
    3. 当用户没有输入外带参数时,屏幕显示『 Usage: yourbday.sh YYYY-MM-DD 』,并且离开程序
    4. 以正规表示法的方式来查找生日的格式是否正常,若不正常,重新显示上面的消息,并且离开程序
    5. 以 date --date="YYYY-MM-DD" +%s 的回传值确认时间格式是否正确?若不正确请显示『 invalid date 』后,离开程序
    6. 分别取得生日与现在的累积秒数,根据两者的差异,同时假设一年 365.25 天,然后:
      • 如果生日比现在的总累积秒数还要大,代表来自未来,请输出『You are not a real human..』,之后离开程序
      • 如果所有问题都排除了,那请搭配 bc 来显示出你的岁数,岁数计算到小数点第二位, 例如『 You are 22.35 years old 』的样式。
    7. 运行结果会有点像这样:
      [student@localhost ~]$ yourbday.sh
      Usage: /usr/local/bin/yourbday.sh YYYY-MM-DD
      
      [student@localhost ~]$ yourbday.sh 2001-03-53
      invalid date format
      
      [student@localhost ~]$ yourbday.sh 2001-03-21
      You are 22.13 old
      
  7. (10%)系统上面已经有一只脚本名为: /usr/local/sbin/examcheck ,当运行时,应该要出现如下的画面, 但是程序开发人员写错了某些地方,请你将应该有问题的代码订正,使得脚本得以展示如下的结果:
    1. 运行 examcheck ok 时,显示『Yes! You are right!』
    2. 运行 examcheck false 时,显示『So sad... your answer is wrong..』
    3. 运行 examcheck otherword 时,显示『 Usage: examcheck ok|false 』(otherword 为任意字符,随便不是 ok 与 false 的其他字符之意)
    4. 运行结果会有点像这样:
      [student@localhost ~]$ examcheck ok
      
      Yes! You are right!
      
      [student@localhost ~]$ examcheck false
      
      So sad...Your answer is wrong...
      
      [student@localhost ~]$ examcheck qqq
      
      Usage: examcheck ok|false
      
      

作业结果传输:请以 root 的身分运行 vbird_book_check_unit 指令上传作业结果。 正常运行完毕的结果应会出现【XXXXXX_aa:bb:cc:dd:ee:ff_unitNN】字样。若需要查阅自己上传数据的时间, 请在操作系统上面使用浏览器查找: http://192.168.251.254 检查相对应的课程文件。 相关流程请参考: vbird_book_check_unit

修改历史:
  • 2023/05/01:整体变化不大,但是 if then 判断式,增加了 && 与 || 的使用!作业硬盘仍需要时间处理就是了!
  • 2023/05/10:作业的内容变化中,第一题的变化很大!从简答题变成写小脚本...有点难啊!
2023/05/01 以来统计人数
计数器
其他链接
环境工程模式篇
鸟园讨论区
鸟哥旧站

今日 人数统计
昨日 人数统计
本月 人数统计
上月 人数统计