Shell编程

一、Shell概述

1.1 Shell的含义

Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言。Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务。Ken Thompson 的 sh 是第一种 Unix Shell,Windows Explorer 是一个典型的图形界面 Shell。

它不仅是操作系统内核与用户之间的绝缘层,同时也是一种功能相当强大的编程语言。一个Shell程序,通常称为脚本,它是一个由系统调用,命令工具,软件包和已编译的二进制包"粘合"起来的极易使用的工具。事实上,整个UNIX以及类UNIX系统命令,软件包和工具都能由一个shell脚本调用。Shell脚本在系统管理任务表现非常的出色,并且对于日常反复性的处理工作避免了使用那些结构过于复杂的程序语言。

1.2 Shell的分类

Shell类别易学性可移植性编辑性快捷性
Bourne Shell (sh)容易较差较差
Korn Shell (ksh)较难较好较好
Bourne Again (Bash)较好
POSIX Shell (psh)较难较好
C Shell (csh)较难较好较好
TC Shell (tcsh)

Shell 的两种主要语法类型有 Bourne 和 C,这两种语法彼此不兼容。Bourne 家族主要包括 sh、ksh、Bash、psh、zsh;C 家族主要包括:csh、tcsh (Bash 和 zsh 在不同程度上支持 csh 的语法)。

可以通过/etc/shells 文件来查询 Linux 支持的 Shell。命令如下:

**注意:**Linux的标准Shell为Bash。

二、Shell 脚本的执行方式

2.1 echo 命令

echo命令格式如下:

[root@localhost ~]# echo [选项] [输出内容]
选项:
-e: 支持反斜线控制的字符转换
-n: 取消输出后行末的换行符号(就是内容输出后不换行)

在 echo 命令中如果使用了“-e”选项,则可以支持控制字符,如下表所示:

控制字符作用
\\输出\本身
\a输出警告音
\b退格键,也就是向左删除键
\c取消输出行末的换行符。和“-n”选项一致
\eESCAPE 键
\f换页符
\n换行符
\r回车键
\t制表符,也就是 Tab 键
\v垂直制表符
\0nnn按照八进制 ASCII 码表输出字符。其中 0 为数字零,nnn 是三位八进制数
\xhh按照十六进制 ASCII 码表输出字符。其中 hh 是两位十六进制数

echo 命令还可以进行一些比较有意思的东西,比如:

例如:

2.2 Shell 脚本的创建

打开文本编辑器(可以使用 vi/vim 命令来创建文件),新建一个文件 hello.sh,扩展名为 sh(sh代表shell),扩展名并不影响脚本执行,见名知意就好。

[root@localhost sh]# vi hello.sh
#!/bin/Bash
echo "Hello Shell !"

2.3 Shell 脚本的执行

Shell 脚本写好了,那么这个脚本该如何运行呢?在 Linux 中脚本的执行主要有这样两种种方法:

第一种:赋予执行权限,直接运行

这种方法是最常用的 Shell 脚本运行方法,也最为直接简单。就是赋予执行权限之后,直接运行。当然运行时可以使用绝对路径,也可以使用相对路径运行。命令如下:

[root@localhost sh]# chmod 755 hello.sh 
#赋予执行权限
[root@localhost sh]# /root/sh/hello.sh 
Hello Shell!
#使用绝对路径运行
[root@localhost sh]# ./hello.sh
Hello Shell!
#因为已经在/root/sh 目录当中,所以也可以使用相对路径运行

第二种:通过 Bash 调用执行脚本

这种方法也非常简单,命令如下:

[root@localhost sh]# bash hello.sh 
Hello Shell!

三、Bash 的基本功能

3.1 历史命令

3.1.1 历史命令的查看

[root@localhost ~]# history [选项] [历史命令保存文件]
选项:
-c: 清空历史命令
-w: 把缓存中的历史命令写入历史命令保存文件。如果不手工指定历史命令保存文件,则放入默认历史命令保存文件~/.bash_history 中

可以通过 /etc/profile文件来查看或者修改history命令保存条数。

可以通过查看.bash_history文件来查看历史命令。

我们使用 history 命令查看的历史命令和~/.bash_history 文件中保存的历史命令是不同的。那是因为当前登录操作的命令并没有直接写入~/.bash_history 文件,而是保存在缓存当中的。需要等当前用户注销之后,缓存中的命令才会写入~/.bash_history 文件。

如果我们需要把内存中的命令直接写入~/.bash_history 文件,而不等用户注销时再写入,就需要使用“-w”选项了。命令如下:

[root@localhost ~]# history -w 
#把缓存中的历史命令直接写入~/.bash_history

这时再去查询~/.bash_history 文件,历史命令就和 history 命令查询的一致了。

如果需要清空历史命令,需要执行:

[root@localhost ~]# history -c 
#清空历史命令

3.1.2 历史命令的调用

使用原先的历史命令有这样几种方法:

  • 使用上、下箭头调用以前的历史命令
  • 使用“!n”重复执行第 n 条历史命令
  • 使用“!!”重复执行上一条命令
  • 使用“!字串”重复执行最后一条以该字串开头的命令
  • 使用“!$”重复上一条命令的最后一个参数

3.2 命令别名

命令格式:

[root@localhost ~]# alias
#查询命令别名
[root@localhost ~]# alias 别名='原命令' 
#设定命令别名
例如:
[root@localhost ~]# alias 
#查询系统中已经定义好的别名
alias cp='cp -i'
alias l.='ls -d .* --color=auto'
alias ll='ls -l --color=auto'
alias ls='ls --color=auto'
alias mv='mv -i'
alias rm='rm -i'
alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
[root@localhost ~]# alias vi='vim'
#定义 vim 命令的别名是 vi

别名的优先级比命令高,那么命令执行时具体的顺序是什么呢?命令执行时的顺序是这样的:

1、 第一顺位执行用绝对路径或相对路径执行的命令。

2、 第二顺位执行别名。

3、 第三顺位执行 Bash 的内部命令。

4 第四顺位执行按照$PATH 环境变量定义的目录查找顺序找到的第一个命令。

为了让这个别名永久生效,可以把别名写入环境变量配置文件“~/.bashrc”。命令如下:

[root@localhost ~]# vi /root/.bashrc

如果想要环境变量马上生效,可以使用source命令。source命令可用于:

  • 刷新当前的Shell环境
  • 在当前环境使用source执行Shell脚本]
  • 从脚本中导入环境中一个Shell函数
  • 从另一个Shell脚本中读取变量
[root@localhost ~]# source .bashrc

3.3 Bash 常用快捷键

快捷键作用
ctrl+A把光标移动到命令行开头。
ctrl+E把光标移动到命令行结尾。
ctrl+C强制终止当前的命令。
ctrl+L清屏,相当于 clear 命令。
ctrl+U删除或剪切光标之前的内容。
ctrl+K删除或剪切光标之后的内容。
ctrl+Y粘贴 ctrl+U 或 ctrl+K 剪切的内容。
ctrl+R在历史命令中搜索,按下 ctrl+R 之后,就会出现搜索界面,只要输入搜索内容,就会从历史命令中搜索。
ctrl+D退出当前终端。
ctrl+Z暂停,并放入后台。
ctrl+S暂停屏幕输出。
ctrl+Q恢复屏幕输出。

3.4 输入输出重定向

3.4.1 Bash的标准输入输出

设备设备文件名文件描述符类型
键盘/dev/stdin0标准输入
显示器/dev/stdout1标准输出
显示器/dev/stderr2标准错误输出

3.4.2 输出重定向

标准输出重定向

符号作用
命令 > 文件以覆盖的方式,把命令的正确输出输出到指定的文件或设备当中。
命令 >> 文件以追加的方式,把命令的正确输出输出到指定的文件或设备当中。

标准错误输出重定向

符号作用
错误命令 2>文件以覆盖的方式,把命令的错误输出输出到指定的文件或设备当中。
错误命令 2>>文件以追加的方式,把命令的错误输出输出到指定的文件或设备当中。

正确输出和错误输出同时保存

符号作用
命令 > 文件 2>&1以覆盖的方式,把正确输出和错误输出都保存到同一个文件当中。
命令 >> 文件 2>&1以追加的方式,把正确输出和错误输出都保存到同一个文件当中。
命令 &>文件以覆盖的方式,把正确输出和错误输出都保存到同一个文件当中。
命令 &>>文件以追加的方式,把正确输出和错误输出都保存到同一个文件当中。
命令>>文件 1 2>>文件 2把正确的输出追加到文件 1 中,把错误的输出追加到文件 2 中。

3.4.3 输入重定向

[root@localhost ~]# wc [选项] [文件名]
选项:
-c 统计字节数
-w 统计单词数
-l 统计行数

3.5 多命令顺序执行

多命令执行符格式作用
;命令 1 ;命令 2多个命令顺序执行,命令之间没有任何逻辑联系
&&命令 1 && 命令 2当命令 1 正确执行($?=0),则命令 2 才会执行
当命令 1 执行不正确($? ≠ 0),则命令 2 不会执行
||命令 1 ||命令 2当命令 1 执行不正确($? ≠ 0),则命令 2 才会执行
当命令 1 正确执行($?=0),则命令 2 不会执行

3.6 管道符

3.6.1 行提取命令 grep

[root@localhost ~]# grep [选项] "搜索内容" 文件名
选项:
-A 数字: 列出符合条件的行,并列出后续的 n 行 
-B 数字: 列出符合条件的行,并列出前面的 n 行 
-c: 统计找到的符合条件的字符串的次数
-i: 忽略大小写
-n: 输出行号
-v: 反向查找
--color=auto 搜索出的关键字用颜色显示

3.6.2 find 和 grep 的区别

find 命令是在系统当中搜索符合条件的文件名,如果需要模糊查询,使用通配符进行匹配,搜索时文件名是完全匹配。grep 命令是在文件当中搜索符合条件的字符串,如果需要模糊查询,使用正则表达式进行匹配,搜索时字符串是包含匹配。

注意:find 命令是可以通过-regex 选项识别正则表达式规则的,也就是说 find 命令可以按照正则表达式规则匹配,而正则表达式是模糊匹配。

3.6.3 管道符

[root@localhost ~]# netstat -an | grep "ESTABLISHED"
#查询下本地所有网络连接,提取包含 ESTABLISHED(已建立连接)的行
#就可以知道服务器上有多少已经成功连接的网络连接

3.7 通配符

通配符作用
匹配一个任意字符
*匹配 0 个或任意多个任意字符,也就是可以匹配任何内容
[]匹配中括号中任意一个字符。例如:[abc]代表一定匹配一个字符,或者是 a,或者是 b,或者是 c
[-]匹配中括号中任意一个字符,-代表一个范围。例如:[a-z]代表匹配一个小写字母。
[^]逻辑非,表示匹配不是中括号内的一个字符。例如:[^0-9]代表匹配一个不是数字的字符

3.8 Bash 中其他特殊符号

符号作用
''单引号。在单引号中所有的特殊符号,如“$”和“`”(反引号)都没有特殊含义。
""双引号。在双引号中特殊符号都没有特殊含义,但是“$”、“`”和“\”是例外,拥有“调用变量的值”、“引用命令”和“转义符”的特殊含义。
``反引号。反引号括起来的内容是系统命令,在 Bash 中会先执行它。和$()作用一样,不过推荐使用$(),因为反引号非常容易看错。
$()和反引号作用一样,用来引用系统命令。
()用于一串命令执行时,()中的命令会在子 Shell 中运行
{}用于一串命令执行时,{}中的命令会在当前 Shell 中执行。也可以用于变量变形与替换。
[]用于变量的测试。
#在 Shell 脚本中,#开头的行代表注释。
$用于调用变量的值,如需要调用变量 name 的值时,需要用$name 的方式得到变量的值。
\转义符,跟在\之后的特殊符号将失去特殊含义,变为普通字符。如\$将输出“$”符号,而不当做是变量引用。

四、Bash 变量

4.1 变量的定义

在定义变量时,有一些规则需要遵守:

  • 变量名称可以由字母、数字和下划线组成,但是不能以数字开头。如果变量名是“2name”则是错误的。
  • 在 Bash 中,变量的默认类型都是字符串型,如果要进行数值运算,则必修指定变量类型为数值型。
  • 变量用等号连接值,等号左右两侧不能有空格。
  • 变量的值如果有空格,需要使用单引号或双引号包括。如:“test="hello world!”。其中双引号括起来的内容“$”“\”和反引号都拥有特殊含义,而单引号括起来的内容都是普通字符。
  • 在变量的值中,可以使用“\”转义符。
  • 如果需要增加变量的值,那么可以进行变量值的叠加。不过变量需要用双引号包含"$变量名"或用${变量名}包含变量名。例如:
[root@localhost ~]# test=123
[root@localhost ~]# test="$test"456
[root@localhost ~]# echo $test
123456
#叠加变量 test,变量值变成了 123456
[root@localhost ~]# test=${test}789
[root@localhost ~]# echo $test 
123456789
#再叠加变量 test,变量值编程了 123456789

变量值的叠加可以使用两种格式:“$变量名”${变量名}

  • 如果是把命令的结果作为变量值赋予变量,则需要使用反引号或$()包含命令。例如:
[root@localhost ~]# test=$(date)
[root@localhost ~]# echo $test
20210612 日 星期六 11:27:50 CST
  • 环境变量名建议大写,便于区分。

4.2 变量的分类

  • 用户自定义变量:这种变量是最常见的变量,由用户自由定义变量名和变量的值。
  • 环境变量:这种变量中主要保存的是和系统操作环境相关的数据,比如当前登录用户,用户的家目录,命令的提示符等。不是太好理解吧,那么大家还记得在 Windows 中,同一台电脑可以有多个用户登录,而且每个用户都可以定义自己的桌面样式和分辨率,这些其实就是Windows 的操作环境,可以当做是 Windows 的环境变量来理解。环境变量的变量名可以自由定义,但是一般对系统起作用的环境变量的变量名是系统预先设定好的。
  • 位置参数变量:这种变量主要是用来向脚本当中传递参数或数据的,变量名不能自定义,变量作用是固定的。
  • 预定义变量:是 Bash 中已经定义好的变量,变量名不能自定义,变量作用也是固定的。

4.3 用户自定义变量

4.3.1 变量定义

[root@localhost ~]# 2name="test"
-bash: 2name=test: command not found
#变量名不能用数字开头
[root@localhost ~]# name = "test"
-bash: name: command not found
#等号左右两侧不能有空格
[root@localhost ~]# name=test bash
-bash: test bash: command not found
#变量的值如果有空格,必须用引号包含

4.3.2 变量调用

[root@localhost ~]# name="hello bash"
#定义变量 name
[root@localhost ~]# echo $name
hello bash
#输出变量 name 的值

4.3.3 变量查看

[root@localhost ~]# set [选项]
选项:
-u: 如果设定此选项,调用未声明变量时会报错(默认无任何提示)
-x: 如果设定此选项,在命令执行之前,会把命令先输出一次
+<参数>: 取消某个set曾启动的参数。

直接使用 set 命令,会查询系统中所有的变量,包含用户自定义变量和环境变量

当设置了-u 选项后,如果调用没有设定的变量会有报错。默认是没有任何输出的。

如果设定了-x 选项,会在每个命令执行之前,先把命令输出一次

4.3.4 变量删除

[root@localhost ~]# unset 变量名

4.4 环境变量

环境变量分为系统环境变量和用户环境变量。环境变量对所有shell环境起作用,而普通变量只对当前shell环境起作用。

4.4.1 环境变量设置

[root@localhost ~]# export age="18"
#使用 export 声明的变量即是环境变量

4.4.2 环境变量查询

env 命令和 set 命令的区别是,set 命令可以查看所有变量,而 env 命令只能查看环境变量。

[root@localhost ~]# env | grep age

4.4.3 环境变量删除

[root@localhost ~]# unset age

4.4.4 系统默认环境变量

env 命令可以查询到所有的环境变量,可是还有一些变量虽然不是环境变量,却是和 Bash 操作接口相关的变量,这些变量也对 Bash 操作终端起到了重要的作用。这些变量就只能用 set 命令来查看,下面只列出重要的内容:

[root@localhost ~]# set
BASH=/bin/bash 
#Bash 的位置
BASH_VERSINFO=([0]="4" [1]="1" [2]="2" [3]="1" [4]="release"
[5]="i386-redhat-linux-gnu") 
#Bash 版本
BASH_VERSION='4.1.2(1)-release' 
#bash 的版本
COLORS=/etc/DIR_COLORS 
#颜色记录文件
HISTFILE=/root/.bash_history 
#历史命令保存文件
HISTFILESIZE=1000 
#在文件当中记录的历史命令最大条数
HISTSIZE=1000 
#在缓存中记录的历史命令最大条数
LANG=zh_CN.UTF-8 
#语系环境
MACHTYPE=i386-redhat-linux-gnu 
#软件类型是 i386 兼容类型
MAILCHECK=60 
#每 60 秒去扫描新邮件
PPID=2166 
#父 shell 的 PID。我们当前 Shell 是一个子 shell
PS1='[\u@\h \W]\$ ' 
#命令提示符
PS2='> ' 
#如果命令一行没有输入完成,第二行命令的提示符
UID=0 
#当前用户的 UID

4.4.5 PATH 变量

PATH 变量:系统查找命令的路径

先查询下 PATH 环境变量的值:

PATH 变量的值是用“:”分割的路径,这些路径就是系统查找命令的路径。也就是说当我们输入了一个程序名,如果没有写入路径,系统就会到 PATH 变量定义的路径中去寻找,是否有可以执行的程序。如果找到则执行,否则会报“命令没有发现”的错误。

把脚本拷贝到 PATH 变量定义的路径中,也可以不输入路径而直接运行。

[root@localhost ~]# cp /root/sh/hello.sh /bin/
#拷贝 hello.sh 到/bin 目录
[root@localhost ~]# hello.sh 
hello bash!
#hello.sh 可以直接执行了

可以修改 PATH 变量的值,而不是把程序脚本复制到/bin/目录中。通过变量的叠加实现:

[root@localhost ~]# PATH="$PATH":/root/sh
#在变量 PATH 的后面,加入/root/sh 目录
[root@localhost ~]# echo $PATH
/usr/lib/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/root/sh
#查询 PATH 的值,变量叠加生效了

当然这样定义的 PATH 变量只是临时生效,一旦重启或注销就会消失,如果想要永久生效,需要写入环境变量配置文件,后面讲解环境变量配置文件。

4.4.6 PS1 变量

PS1 变量:命令提示符设置

PS1 是一个很有意思的变量,是用来定义命令行的提示符的,可以按照我们自己的需求来定义自己喜欢的提示符。PS1 可以支持以下这些选项:

  • \d:显示日期,格式为“星期 月 日”
  • \H:显示完整的主机名。如默认主机名“localhost.localdomain”
  • \h:显示简写主机名。如默认主机名“localhost”
  • \t:显示 24 小时制时间,格式为“HH:MM:SS”
  • \T:显示 12 小时制时间,格式为“HH:MM:SS”
  • \A:显示 24 小时制时间,格式为“HH:MM”
  • @:显示 12 小时制时间,格式为“HH:MM am/pm”
  • \u:显示当前用户名
  • \v:显示 Bash 的版本信息
  • \w:显示当前所在目录的完整名称
  • \W:显示当前所在目录的最后一个目录
  • \#:执行的第几个命令
  • \$:提示符。如果是 root 用户会显示提示符为“#”,如果是普通用户会显示提示符为“$”

这些选项该怎么用啊?我们先看看 PS1 变量的默认值吧:

默认的提示符是显示“[用户名@简写主机名 最后所在目录]提示符”

在 PS1 变量中,如果是可以解释的符号,如“\u”“\h”等,则显示这个符号的作用。如果是不能解释的符号,如“@”或“空格”,则原符号输出。那么我们修改下 PS1 变量,看看会出现什么情况吧:

[root@localhost ~]# PS1='[\u@\t \w]\$ ' 
#修改提示符为‘[用户名@当前时间 当前所在完整目录]提示符’
[root@04:46:40 ~]#cd /usr/local/src/
#切换下当前所在目录,因为家目录是看不出来区别的
[root@04:47:29 /usr/local/src]#
#看到了吗?提示符按照我们的设计发生了变化

这里要小心,PS1 变量的值要用单引号包含,否则设置不生效。再举个例子吧:

[root@04:50:08 /usr/local/src]#PS1='[\u@\@ \h \# \W]\$'
[root@04:53 上午 localhost 31 src]#
#提示符又变了。\@:时间格式是 HH:MM am/pm;\#:会显示执行了多少个命令

4.4.7 LANG 语系变量

LANG 变量定义了 Linux 系统的主语系环境,这个变量的默认值是:

这是因为我们 Linux 安装时,选择的是中文安装,所以默认的主语系变量是“zh_CN.UTF-8”。那么 Linux 中到底支持多少语系呢?可以使用以下命令查询:

支持这么多的语系,当前系统到底是什么语系呢?使用 locale 命令直接查询:

我们还要通过文件/etc/sysconfig/i18n 定义系统的默认语系,查看下这个文件的内容:

这又是当前系统语系,又是默认语系,有没有快晕倒的感觉。解释下吧,可以这样理解,默认语系是下次重启之后系统所使用的语系,而当前系统语系是当前系统使用的语系。如果系统重启,会从默认语系配置文件/etc/sysconfig/i18n 中读出语系,然后赋予变量 LANG 让这个语系生效。也就是说,LANG 定义的语系只对当前系统生效,要想永久生效就要修改/etc/sysconfig/i18n 文件了。

4.5 位置参数变量

位置参数变量作用
$nn 为数字,$0 代表命令本身,$1-$9 代表第一到第九个参数,十以上的参数需要用大括号包含,如${10}.
$*这个变量代表命令行中所有的参数,$*把所有的参数看成一个整体
$@这个变量也代表命令行中所有的参数,不过$@把每个参数区分对待
$#这个变量代表命令行中所有参数的个数
[root@localhost sh]# vi count.sh
#!/bin/bash

num1=$1
#给 num1 变量赋值是第一个参数
num2=$2
#给 num2 变量赋值是第二个参数
sum=$(( $num1 + $num2))
#变量 sum 的和是 num1 加 num2
echo $sum
#打印变量 sum 的值

那么还有几个位置参数变量是干嘛的呢?我们在写个脚本来说明下

[root@localhost sh]# vi parameter.sh
#!/bin/bash

echo "A total of $# parameters"
#使用$#代表所有参数的个数
echo "The parameters is: $*"
#使用$*代表所有的参数
echo "The parameters is: $@"
#使用$@也代表所有参数

那么“$*”和“$@”有区别吗?还是有区别的,$*会把接收的所有参数当成一个整体对待,而$@则会区分对待接收到的所有参数。还是举个例子:

[root@localhost sh]# vi parameter2.sh
#!/bin/bash

for i in "$*"
#定义 for 循环,in 后面有几个值,for 会循环多少次,注意“$*”要用双引号括起来
#每次循环会把 in 后面的值赋予变量 i
#Shell 把$*中的所有参数看成是一个整体,所以这个 for 循环只会循环一次
 do
 echo "The parameters is: $i"
#打印变量$i 的值
 done
x=1
#定义变量 x 的值为 1
for y in "$@"
#同样 in 后面的有几个值,for 循环几次,每次都把值赋予变量 y #可是 Shell 中把“$@”中的每个参数都看成是独立的,所以“$@”中有几个参数,就会循环几次
 do
 echo "The parameter$x is: $y"
#输出变量 y 的值
 x=$(( $x +1 )) 
#然变量 x 每次循环都加 1,为了输出时看的更清楚
 done

4.6 预定义变量

预定义变量作用
$?最后一次执行的命令的返回状态。如果这个变量的值为 0,证明上一个命令正确执行;如果这个变量的值为非 0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了
$$当前进程的进程号(PID)
$!后台运行的最后一个进程的进程号(PID)

我们先来看看“$?”这个变量,看起来不好理解,我们还是举个例子:

[root@localhost sh]# ls
count.sh hello.sh parameter2.sh parameter.sh
#ls 命令正确执行
[root@localhost sh]# echo $?
0
#预定义变量“$?”的值是 0,证明上一个命令执行正确
[root@localhost sh]# ls install.log
ls: 无法访问 install.log: 没有那个文件或目录
#当前目录中没有 install.log 文件,所以 ls 命令报错了
[root@localhost sh]# echo $?
2
#变量“$?”返回一个非 0 的值,证明上一个命令没有正确执行
#至于错误的返回值到底是多少,是在编写 ls 命令时定义好的,如果碰到文件不存在就返回数值 2

接下来我们来说明下“$$”和“$!”这两个预定义变量,我们写个脚本吧:

[root@localhost sh]# vi variable.sh
#!/bin/bash

echo "The current process is $$"
#输出当前进程的 PID;
#这个 PID 就是 variable.sh 这个脚本执行时,生成的进程的 PID
find /root -name hello.sh &
#使用 find 命令在 root 目录下查找 hello.sh 文件
#符号&的意思是把命令放入后台执行,工作管理在系统管理章节详细介绍
echo "The last one Daemon process is $!"
#输出这个后台执行命令的进程的 PID,也就是输出 find 命令的 PID

4.7 接收键盘输入

[root@localhost ~]# read [选项] [变量名]
选项:
-p “提示信息”: 在等待 read 输入时,输出提示信息
-t 秒数: read 命令会一直等待用户输入,使用此选项可以指定等待时间
-n 字符数: read 命令只接受指定的字符数,就会执行
-s: 隐藏输入的数据,适用于机密信息的输入
变量名:
变量名可以自定义,如果不指定变量名,会把输入保存入默认变量 REPLY
如果只提供了一个变量名,则整个输入行赋予该变量
如果提供了一个以上的变量名,则输入行分为若干字,一个接一个地赋予各个变量,而命令行上的最后一个变量取得剩余的所有字

写个例子来解释下 read 命令:

[root@localhost sh]# vi read.sh 
#!/bin/bash

read -t 30 -p "Please input your name: " name
#提示“请输入姓名”并等待 30 秒,把用户的输入保存入变量 name 中
echo "Name is $name"
#看看变量“$name”中是否保存了你的输入
read -s -t 30 -p "Please enter your age: " age
#提示“请输入年龄”并等待 30 秒,把用户的输入保存入变量 age 中 
#年龄是隐私,所以我们用“-s”选项隐藏输入
echo -e "\n" 
#调整输出格式,如果不输出换行,一会的年龄输出不会换行
echo "Age is $age"
read -n 1 -t 30 -p "Please select your gender[M/F]: " gender
#提示“请选择性别”并等待 30 秒,把用户的输入保存入变量 gender
#使用“-n 1”选项只接收一个输入字符就会执行(都不用输入回车)
echo -e "\n"
echo "Sex is $gender"

五、变量类型

既然所有变量的默认类型是字符串型,使用 declare 命令就可以实现声明变量的类型。命令如下:

[root@localhost ~]# declare [+/-][选项] 变量名
选项:
-: 给变量设定类型属性
+: 取消变量的类型属性
-a: 将变量声明为数组型
-i: 将变量声明为整数型(integer) 
-r: 讲变量声明为只读变量。注意,一旦设置为只读变量,既不能修改变量的值,也不能删除变量,甚至不能通过+r 取消只读属性
-x: 将变量声明为环境变量
-p: 显示指定变量的被声明的类型

5.1 整数变量类型

只要把变量声明为整数型就可以运算了。

[root@localhost ~]# aa=11
[root@localhost ~]# bb=22
#给变量 aa 和 bb 赋值
[root@localhost ~]# declare -i cc=$aa+$bb
#声明变量 cc 的类型是整数型,它的值是 aa 和 bb 的和
[root@localhost ~]# echo $cc
33
#这下终于可以相加了

5.2 数组变量类型

那么数组是什么呢?所谓数组,就是相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,然后用编号区分他们的变量的集合,这个名字称为数组名,编号称为下标。组成数组的各个变量成为数组的分量,也称为数组的元素,有时也称为下标变量。

[root@localhost ~]# name[0]="111"
#数组中第一个变量
[root@localhost ~]# name[1]="222"
#数组中第二个变量
[root@localhost ~]# name[2]="333" 
 #数组第三个变量
[root@localhost ~]# echo ${name}
111
#输出数组的内容,如果只写数组名,那么只会输出第一个下标变量
[root@localhost ~]# echo ${name[*]}
111 222 333
#输出数组所有的内容

**注意:**数组的下标是从 0 开始的,在调用数组值时,需要使用${数组[下标]}的方式来读取。不过好像在刚刚的例子中,并没有把 name 变量声明为数组型,其实在定义变量时采用了“变量名[下标]”的格式,这个变量就会被系统认为是数组型了,不用强制声明。

5.3 环境变量类型

可以使用 declare 命令把变量声明为环境变量,和 export 命令的作用是一样的:

[root@localhost ~]# declare -x test=123
#把变量 test 声明为环境变量

5.4 只读变量类型

注意,一旦给变量设定了只读属性,那么这个变量既不能修改变量的值,也不能删除变量,甚至不能使用“+r”选项取消只读属性。命令如下:

[root@localhost ~]# declare -r test
#给 test 赋予只读属性
[root@localhost ~]# test=456
-bash: test: readonly variable
#test 变量的值就不能修改了
[root@localhost ~]# declare +r test 
-bash: declare: test: readonly variable
#也不能取消只读属性
[root@localhost ~]# unset test
-bash: unset: test: cannot unset: readonly variable
#也不能删除变量

不过还好这个变量只是命令行声明的,所以只要重新登录或重启,这个变量就会消失了。

5.5 查询变量属性和取消变量属性

变量属性的查询使用“-p”选项,变量属性的取消使用“+”选项。命令如下:

[root@localhost ~]# declare -p cc
declare -i cc="33"
#cc 变量是 int 型
[root@localhost ~]# declare -p name
declare -a name='([0]="111" [1]="222" [2]="333")'
#name 变量是数组型
[root@localhost ~]# declare -p test
declare -rx test="123"
#test 变量是环境变量和只读变量
[root@localhost ~]# declare +x test
#取消 test 变量的环境变量属性
[root@localhost ~]# declare -p test
declare -r test="123"
#注意,只读变量属性是不能取消的

六、数值运算

6.1 数值运算

只要把变量声明为整数型就可以运算了。

[root@localhost ~]# aa=11
[root@localhost ~]# bb=22
#给变量 aa 和 bb 赋值
[root@localhost ~]# declare -i cc=$aa+$bb
#声明变量 cc 的类型是整数型,它的值是 aa 和 bb 的和
[root@localhost ~]# echo $cc
33
#这下终于可以相加了

6.2 使用 expr 或 let 数值运算工具

要想进行数值运算的第二种方法是使用 expr 命令,这种命令就没有 declare 命令复杂了。使用 expr 命令进行运算时,要注意“+”号左右两侧必须有空格,否则运算不执行。命令如下:

[root@localhost ~]# aa=11
[root@localhost ~]# bb=22
#给变量 aa 和变量 bb 赋值
[root@localhost ~]# dd=$(expr $aa + $bb)
#dd 的值是 aa 和 bb 的和。注意“+”号左右两侧必须有空格
[root@localhost ~]# echo $dd 
33

let 命令和 expr 命令基本类似,都是 Linux 中的运算命令,命令如下:

[root@localhost ~]# aa=11
[root@localhost ~]# bb=22
#给变量 aa 和变量 bb 赋值
[root@localhost ~]# let ee=$aa+$bb
[root@localhost ~]# echo $ee
33
#变量 ee 的值是 aa 和 bb 的和
[root@localhost ~]# n=20
#定义变量 n
[root@localhost ~]# let n+=1
#变量 n 的值等于变量本身再加 1
[root@localhost ~]# echo $n
21

expr 命令和 let 命令大家可以按照习惯使用,不过 let 命令对格式要求要比 expr 命令宽松,所以推荐使用 let 命令进行数值运算。

6.3 “$((运算式))”或“$[运算式]”

其实这是一种方式“$(())”“$[]”这两种括号按照个人习惯使用即可。命令如下:

[root@localhost ~]# aa=11
[root@localhost ~]# bb=22
[root@localhost ~]# ff=$(( $aa+$bb ))
[root@localhost ~]# echo $ff
33
#变量 ff 的值是 aa 和 bb 的和
[root@localhost ~]# gg=$[ $aa+$bb ]
[root@localhost ~]# echo $gg
33
#变量 gg 的值是 aa 和 bb 的和

七、Shell 运算符

7.1 Shell运算符类别

Shell 和其他编程语言一样,支持多种运算符,包括:

  • 算数运算符
  • 关系运算符
  • 布尔运算符
  • 字符串运算符
  • 文件测试运算符

原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用。

7.2 常用运算符

优先级运算符说明
1=,+=,-=,*=,/=,%=,&=, ^=, |=, <<=, >>=赋值、运算且赋值
2||逻辑或
3&&逻辑与
4|按位或
5^按位异或
6&按位与
7== , !=等于、不等于
8< =, > =, < , >小于或等于、大于或等于、小于、大于
9<< , >>按位左移、按位右移
10+, -加、减
11* , / , %乘、除、取模
12!, ~逻辑非、按位取反或补码
13-, +单目负、单目正

运算符优先级表明在每个表达式或子表达式中哪一个运算对象首先被求值,数值越大优先级越高,具有较高优先级级别的运算符先于较低级别的运算符进行求值运算。

7.3 算术运算符

下表列出了常用的算术运算符,假定变量 a 为 10,变量 b 为 20:

运算符说明举例
+加法expr $a + $b 结果为 30。
-减法expr $a - $b 结果为 -10。
*乘法expr $a \* $b 结果为 200。
/除法expr $b / $a 结果为 2。
%取余expr $b % $a 结果为 0。
=赋值a=$b 将把变量 b 的值赋给 a。
==相等。用于比较两个数字,相同则返回 true。[ $a == $b ] 返回 false。
!=不相等。用于比较两个数字,不相同则返回 true。[ $a != $b ] 返回 true。

注意:

  • 条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]

  • 乘号(*)前边必须加反斜杠(\)才能实现乘法运算;

  • 在 MAC 中 shell 的 expr 语法是:$((表达式)),此处表达式中的 "*" 不需要转义符号 "" 。

7.4 关系运算符

系运算符只支持数字,不支持字符串,除非字符串的值是数字。

下表列出了常用的关系运算符,假定变量 a 为 10,变量 b 为 20:

运算符说明举例
-eq检测两个数是否相等,相等返回 true。[ $a -eq $b ] 返回 false。
-ne检测两个数是否不相等,不相等返回 true。[ $a -ne $b ] 返回 true。
-gt检测左边的数是否大于右边的,如果是,则返回 true。[ $a -gt $b ] 返回 false。
-lt检测左边的数是否小于右边的,如果是,则返回 true。[ $a -lt $b ] 返回 true。
-ge检测左边的数是否大于等于右边的,如果是,则返回 true。[ $a -ge $b ] 返回 false。
-le检测左边的数是否小于等于右边的,如果是,则返回 true。[ $a -le $b ] 返回 true。

7.5 布尔运算符

下表列出了常用的布尔运算符,假定变量 a 为 10,变量 b 为 20:

运算符说明举例
!非运算,表达式为 true 则返回 false,否则返回 true。[ ! false ] 返回 true。
-o或运算,有一个表达式为 true 则返回 true。[ $a -lt 20 -o $b -gt 100 ] 返回 true。
-a与运算,两个表达式都为 true 才返回 true。[ $a -lt 20 -a $b -gt 100 ] 返回 false。

7.6 逻辑运算符

以下介绍 Shell 的逻辑运算符,假定变量 a 为 10,变量 b 为 20:

运算符说明举例
&&逻辑的 AND[[ $a -lt 100 && $b -gt 100 ]] 返回 false
||逻辑的 OR[[ $a -lt 100 || $b -gt 100 ]] 返回 true

7.7 字符串运算符

下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":

运算符说明举例
==检测两个字符串是否相等,相等返回 true。[ $a == $b ] 返回 false。
!=检测两个字符串是否不相等,不相等返回 true。[ $a != $b ] 返回 true。
-z检测字符串长度是否为0,为0返回 true。[ -z $a ] 返回 false。
-n检测字符串长度是否不为 0,不为 0 返回 true。[ -n "$a" ] 返回 true。
$检测字符串是否为空,不为空返回 true。[ $a ] 返回 true。

7.8 文件测试运算符

文件测试运算符用于检测 Unix 文件的各种属性。属性检测描述如下:

操作符说明举例
-b file检测文件是否是块设备文件,如果是,则返回 true。[ -b $file ] 返回 false。
-c file检测文件是否是字符设备文件,如果是,则返回 true。[ -c $file ] 返回 false。
-d file检测文件是否是目录,如果是,则返回 true。[ -d $file ] 返回 false。
-f file检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。[ -f $file ] 返回 true。
-g file检测文件是否设置了 SGID 位,如果是,则返回 true。[ -g $file ] 返回 false。
-k file检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。[ -k $file ] 返回 false。
-p file检测文件是否是有名管道,如果是,则返回 true。[ -p $file ] 返回 false。
-u file检测文件是否设置了 SUID 位,如果是,则返回 true。[ -u $file ] 返回 false。
-r file检测文件是否可读,如果是,则返回 true。[ -r $file ] 返回 true。
-w file检测文件是否可写,如果是,则返回 true。[ -w $file ] 返回 true。
-x file检测文件是否可执行,如果是,则返回 true。[ -x $file ] 返回 true。
-s file检测文件是否为空(文件大小是否大于0),不为空返回 true。[ -s $file ] 返回 true。
-e file检测文件(包括目录)是否存在,如果是,则返回 true。[ -e $file ] 返回 true。

其他检查符:

  • -S: 判断某文件是否 socket。
  • -L: 检测文件是否存在并且是一个符号链接。

7.9 变量的测试与内容置换

变量置换方式变量 y 没有设置变量 y 为空值变量 y 设置值
x=${y-新值}x=新值x 为空x=$y
x=${y:-新值}x=新值x=新值x=$y
x=${y+新值}x 为空x=新值x=新值
x=${y:+新值}x 为空x 为空x=新值
x=${y=新值}x=新值 y=新值x 为空 y值不变x=$y y值不变
x=${y:=新值}x=新值 y=新值x=新值 y=新值x=$y y值不变
x=${y?新值}新值输出到标准错误输出(屏幕)x 为空x=$y
x=${y:?新值}新值输出到标准错误输出新值输出到标准错误输出x=$y
  • 如果大括号内是“-”或“+”,则在改变变量 x 值的时候,变量 y 是不改变的;如果大括号内是“=”,则在改变变量 x 值的同时,变量 y 的值也会改变。

  • 如果大括号内是“?”,则当变量 y 不存在或为空时,会把“新值”当成报错输出到屏幕上。

八、环境变量配置文件

8.1 source 命令

source命令也称为“点命令”,也就是一个点符号(.),是bash的内部命令。source命令使Shell读入指定的Shell程序文件并依次执行文件中的所有语句,通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。

source命令用法如下:

[root@localhost ~]# source 配置文件
或者
[root@localhost ~]# . 配置文件

8.2 环境变量配置文件

8.2.1 登录时生效的环境变量配置文件

在 Linux 系统登录时主要生效的环境变量配置文件有以下五个:

  • /etc/profile
  • /etc/profile.d/*.sh
  • ~/.bash_profile
  • ~/.bashrc
  • /etc/bashrc

放在/etc目录下的是对所有用户生效的,写在用户家目录下的只对当前用户生效。

8.2.1.1 环境变量配置文件调用过程

8.2.1.2 在用户登录过程先调用/etc/profile 文件

在这个环境变量配置文件中会定义这些默认环境变量:

  • USER 变量:根据登录的用户,给这个变量赋值(就是让 USER 变量的值是当前用户)。

  • LOGNAME 变量:根据 USER 变量的值,给这个变量赋值。

  • MAIL 变量:根据登录的用户,定义用户的邮箱为/var/spool/mail/用户名。

  • PATH 变量:根据登录用户的 UID 是否为 0,判断 PATH 变量是否包含/sbin、/usr/sbin和/usr/local/sbin 这三个系统命令目录。

  • HOSTNAME 变量:更加主机名,给这个变量赋值。

  • HISTSIZE 变量:定义历史命令的保存条数。

  • umask:定义 umask 默认权限。注意/etc/profile 文件中的 umask 权限是在“有用户登录过程(也就是输入了用户名和密码)”时才会生效。

  • 调用/etc/profile.d/*.sh 文件,也就是调用/etc/profile.d/目录下所有以.sh 结尾的文件

8.2.1.3 由/etc/profile 文件调用/etc/profile.d/*.sh 文件

这个目录中所有以.sh 结尾的文件都会被/etc/profile 文件调用,这里最常用的就是 lang.sh 文件,而这个文件又会调用/etc/sysconfig/i18n 文件。/etc/sysconfig/i18n 这个文件眼熟吗?就是前面讲过的默认语系配置文件。

8.2.1.4 由/etc/profile 文件调用~/.bash_profile 文件

~/.bash_profile 文件就没有那么复杂了,这个文件主要实现了两个功能:

  • 调用~/.bashrc 文件。
  • 在 PATH 变量后面加入了“:$HOME/bin”这个目录。那也就是说,如果我们在自己的家目录中建立 bin 目录,然后把自己的脚本放入“~/bin”目录,就可以直接执行脚本,而不用通过目录执行了。
8.2.1.5 由~/.bash_profile 文件调用~/.bashrc 文件

在~/.bashrc 文件中主要实现了:

  • 定义默认别名。
  • 调用/etc/bashrc
8.2.1.6 由~/.bashrc 调用了/etc/bashrc 文件

在/etc/bashrc 文件中主要定义了这些内容:

  • PS1 变量:也就是用户的提示符,如果我们想要永久修改提示符,就要在这个文件中修改
  • umask:定义 umask 默认权限。这个文件中定义的 umask 是针对“没有登录过程(也就是不需要输入用户名和密码时,比如从一个终端切换到另一个终端,或进入子 Shell)”时生效的。如果是“有用户登录过程”,则是/etc/profile 文件中的 umask 生效。
  • PATH 变量:会给 PATH 变量追加值,当然也是在“没有登录过程”时才生效。
  • 调用/etc/profile.d/*.sh 文件,这也是在“没有用户登录过程”是才调用。在“有用户登录过程”时,/etc/profile.d/*.sh 文件已经被/etc/profile 文件调用过了 。

**注意:**这样这五个环境变量配置文件会被依次调用,那么如果是我们自己定义的环境变量应该放入哪个文件呢?如果你的修改是打算对所有用户生效的,那么可以放入/etc/profile 环境变量配置文件;如果你的修改只是给自己使用的,那么可以放入~/.bash_profile 或~/.bashrc 这两个配置文件中的任一个。

如果误删除了这些环境变量,比如删除了/etc/bashrc 文件,或删除了~/.bashrc 文件,那么这些文件中配置就会失效(~/.bashrc 文件会调用/etc/bashrc 文件)。那么提示符就会变成:

-bash-4.1#

8.2.2 注销时生效的环境变量配置文件

在用户退出登录时,只会调用一个环境变量配置文件,就是~/.bash_logout。这个文件默认没有写入任何内容,可是如果我们希望再退出登录时执行一些操作,比如清除历史命令,备份某些数据,就可以把命令写入这个文件。

8.2.3 其他配置文件

还有一些环节变量配置文件,最常见的就是~/bash_history 文件,也就是历史命令保存文件。

九、Shell 登录信息

9.1 /etc/issue

在登录 tty1-tty6 这六个本地终端时,会有几行的欢迎界面。这些欢迎信息是保存在哪里的?可以修改吗?当然可以修改,这些欢迎信息是保存在/etc/issue 文件中,我们查看下这个文件:

[root@localhost ~]# cat /etc/issue
CentOS release 6.8 (Final)
Kernel \r on an \m

支持的转义符可以通过 man agetty 命令查询,在表中列出常见的转义符作用:

转义符作用
\d显示当前系统日期
\s显示操作系统名称
\l显示登录的终端号
\m显示硬件体系结构,如 i386、i686 等
\n显示主机名
\o显示域名
\r显示内核版本
\t显示当前系统时间
\u显示当前登录用户的序列号

9.2 /etc/issue.net

/etc/issue 是在本地终端登录是显示欢迎信息的,如果是远程登录(如 ssh 远程登录,或 telnet远程登录)需要显示欢迎信息,则需要配置/etc/issue.net 这个文件了。使用这个文件时由两点需要

注意:

  • 首先,在/etc/issue 文件中支持的转义符,在/etc/issue.net 文件中不能使用。

  • 其次,ssh 远程登录是否显示/etc/issue.net 文件中的欢迎信息,是由 ssh 的配置文件决定的。

如果我们需要 ssh 远程登录可以查看/etc/issue.net 的欢迎信息,那么首先需要修改 ssh 的配置文件/etc/ssh/sshd_config,加入如下内容:

[root@localhost ~]# cat /etc/ssh/sshd_config 
…省略部分输出…
# no default banner path
#Banner none
Banner /etc/issue.net …省略部分输出…

这样在 ssh 远程登录时,也可以显示欢迎信息,只是不再可以识别“\d”和“\l”等信息。

9.3 /etc/motd

/etc/motd文件中也是显示欢迎信息的,这个文件和/etc/issue及/etc/issue.net文件的区别是:/etc/issue 及/etc/issue.net 是在用户登录之前显示欢迎信息,而/etc/motd 是在用户输入用户名和密码,正确登录之后显示欢迎信息。在/etc/motd 文件中的欢迎信息,不论是本地登录,还是远程登录都可以显示。

9.4 定义 Bash 快捷键

[root@localhost ~]# stty -a 
#查询所有的快捷键

更改快捷键,执行:

[root@localhost ~]# stty 关键字 快捷键
例如:
[root@localhost ~]# stty intr ^p
#定义 ctrl+p 快捷键为强制终止,“^”字符只要手工输入即可
[root@localhost ~]# stty -a
speed 38400 baud; rows 21; columns 104; line = 0;
intr = ^P; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch 
= <undef>;
start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 
1; time = 0;
#强制终止变成了 ctrl+p 快捷键

十、正则表达式

之前讲过正则表达式和通配符的区别(正则表达式用来在文件中匹配符合条件的字符串,通配符用来匹配符合条件的文件名),其实这种区别只在 Shell 当中适用,因为用来在文件当中搜索字符串的命令,如 grep、awk、sed 等命令可以支持正则表达式,而在系统当中搜索文件的命令,如 ls、find、cp 这些命令不支持正则表达式,所以只能使用 shell 自己的通配符来进行匹配了。

正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

我们在写用户注册表单时,只允许用户名包含字符、数字、下划线和连接字符(-),并设置用户名的长度,我们就可以使用以下正则表达式来设定。

10.1 普通字符

普通字符包括没有显式指定为元字符的所有可打印和不可打印字符。这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

字符说明
[ABC]匹配 [...] 中的所有字符,例如 [aeiou] 匹配字符串 "google runoob taobao" 中所有的 e o u a 字母。
[^ABC]匹配除了 [...] 中字符的所有字符,例如 [^aeiou] 匹配字符串 "google runoob taobao" 中除了 e o u a 字母的所有字母。
[A-Z][A-Z] 表示一个区间,匹配所有大写字母,[a-z] 表示所有小写字母。
.匹配除换行符(\n、\r)之外的任何单个字符,相等于 [^\n\r]。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\d匹配一个数字字符。等价于 [0-9]。
\D匹配一个非数字字符。等价于 [^0-9]。
\w匹配字母、数字、下划线。等价于'[A-Za-z0-9_]'。
\W匹配非字母、数字、下划线。等价于 '[^A-Za-z0-9_]'。

10.2 非打印字符

非打印字符也可以是正则表达式的组成部分。下表列出了表示非打印字符的转义序列:

字符描述
\cx匹配由x指明的控制字符。例如, \cM 匹配一个 Control-M 或回车符。x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
\f匹配一个换页符。等价于 \x0c 和 \cL。
\n匹配一个换行符。等价于 \x0a 和 \cJ。
\r匹配一个回车符。等价于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。
\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]。
\t匹配一个制表符。等价于 \x09 和 \cI。
\v匹配一个垂直制表符。等价于 \x0b 和 \cK。

10.3 特殊字符

所谓特殊字符,就是一些有特殊含义的字符,如上面说的 runoo*b 中的 *,简单的说就是表示任何字符串的意思。如果要查找字符串中的 ***** 符号,则需要对 ***** 进行转义,即在其前加一个 \,runo\*ob 匹配字符串 runo*ob。

许多元字符要求在试图匹配它们时特别对待。若要匹配这些特殊字符,必须首先使字符"转义",即,将反斜杠字符\放在它们前面。下表列出了正则表达式中的特殊字符:

字符描述
$匹配输入字符串的结尾位置。如果设置了 RegExp 对象的 Multiline 属性,则 $ 也匹配 '\n' 或 '\r'。要匹配 $ 字符本身,请使用 \$。
( )标记一个子表达式的开始和结束位置。子表达式可以获取供以后使用。要匹配这些字符,请使用 \( 和\)。
*匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*。
+匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+。
.匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \. 。
[标记一个中括号表达式的开始。要匹配 [,请使用\[。
?匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 ?。
\将下一个字符标记为或特殊字符、或原义字符、或向后引用、或八进制转义符。例如, 'n' 匹配字符 'n'。'\n' 匹配换行符。序列 '\' 匹配 "",而 '(' 则匹配 "("。
^匹配输入字符串的开始位置,除非在方括号表达式中使用,当该符号在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。要匹配 ^ 字符本身,请使用 ^。
{标记限定符表达式的开始。要匹配 {,请使用 \{。
|指明两项之间的一个选择。要匹配 |,请使用 |。

10.4 限定符

限定符用来指定正则表达式的一个给定组件必须要出现多少次才能满足匹配。有 ***** 或 +?{n}{n,}{n,m} 共6种。

正则表达式的限定符有:

字符描述
*匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。* 等价于{0,}。
+匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
?匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 、 "does" 中的 "does" 、 "doxy" 中的 "do" 。? 等价于 {0,1}。
{n}n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
{n,}n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
{n,m}m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。

注意*+ 限定符都是贪婪的,因为它们会尽可能多的匹配文字,只有在它们的后面加上一个 ? 就可以实现非贪婪或最小匹配。

10.5 定位符

定位符使您能够将正则表达式固定到行首或行尾。它们还使您能够创建这样的正则表达式,这些正则表达式出现在一个单词内、在一个单词的开头或者一个单词的结尾。

定位符用来描述字符串或单词的边界,^$ 分别指字符串的开始与结束,\b 描述单词的前或后边界,\B 表示非单词边界。正则表达式的定位符有:

字符描述
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与 \n 或 \r 之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与 \n 或 \r 之前的位置匹配。
\b匹配一个单词边界,即字与空格间的位置。
\B非单词边界匹配。

注意

  • 不能将限定符与定位符一起使用。由于在紧靠换行或者单词边界的前面或后面不能有一个以上位置,因此不允许诸如 ^\* 之类的表达式。

  • 若要匹配一行文本开始处的文本,请在正则表达式的开始使用 ^ 字符。不要将 ^ 的这种用法与中括号表达式内的用法混淆。

  • 若要匹配一行文本的结束处的文本,请在正则表达式的结束处使用 $ 字符。

  • 若要在搜索章节标题时使用定位点,下面的正则表达式匹配一个章节标题,该标题只包含两个尾随数字,并且出现在行首:

10.6 选择

用圆括号 () 将所有选择项括起来,相邻的选择项之间用 | 分隔。() 表示捕获分组,() 会把每个分组里的匹配的值保存起来, 多个匹配值可以通过数字 n 来查看(n 是一个数字,表示第 n 个捕获组的内容)。

但用圆括号会有一个副作用,使相关的匹配会被缓存,此时可用 ?: 放在第一个选项前来消除这种副作用。

其中 ?: 是非捕获元之一,还有两个非捕获元是 ?=?!,这两个还有更多的含义,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串。

以下列出 ?=、?<=、?!、?<! 的使用区别:

  • exp1(?=exp2):查找 exp2 前面的 exp1。
  • (?<=exp2)exp1:查找 exp2 后面的 exp1。
  • exp1(?!exp2):查找后面不是 exp2 的 exp1。
  • (?<!exp2)exp1:查找前面不是 exp2 的 exp1。

10.6 修饰符

标记也称为修饰符,正则表达式的标记用于指定额外的匹配策略。标记不写在正则表达式里,标记位于表达式之外。

下表列出了正则表达式常用的修饰符:

修饰符含义描述
iignore - 不区分大小写将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。
gglobal - 全局匹配查找所有的匹配项。
mmulti line - 多行匹配使边界字符 ^$ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。
s特殊字符圆点 . 中包含换行符 \n默认情况下的圆点 . 是 匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。

10.7 运算符优先级

正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式非常类似。

相同优先级的从左到右进行运算,不同优先级的运算先高后低。下表从最高到最低说明了各种正则表达式运算符的优先级顺序:

运算符描述
\转义符
(), (?😃, (?=), []圆括号和方括号
*, +, ?, {n}, {n,}, {n,m}限定符
^, $, \任何元字符、任何字符定位点和序列(即:位置和顺序)
|替换,"或"操作 字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food"。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"。

十一、字符串处理

11.1 cut 列提取命令

[root@localhost ~]# cut [选项] 文件名
选项:
-f 列号: 提取第几列
-d 分隔符: 按照指定分隔符分割列
-c 字符范围: 不依赖分隔符来区分列,而是通过字符范围(行首为 0)来进行字段提取。“n-”表示从第 n 个字符到行尾;“n-m”从第 n 个字符到第 m个字符;“-m”表示从第 1 个字符到第 m 个字符。

cut 命令的默认分隔符是制表符,也就是“tab”键,不过对空格符支持的不怎么好。我们先建立一个测试文件,然后看看 cut 命令的作用吧:

[root@localhost ~]# vi student.txt
ID Name gender Mark
1 Liming M 86
2 Sc M 90
3 Tg M 83

[root@localhost ~]# cut -f 2 student.txt
#提取第二列内容

那如果想要提取多列呢?只要列号直接用“,”分开,命令如下:

[root@localhost ~]# cut -f 2,3 student.txt

cut 可以按照字符进行提取,需要注意“8-”代表的是提取所有行的第八个字符开始到行尾,而“10-20”代表提取所有行的第十个字符到第二十个字符,而“-8”代表提取所有行从行首到第八个字符:

[root@localhost ~]# cut -c 8- student.txt 
#提取第八个字符开始到行尾,好像很乱啊,那是因为每行的字符个数不相等

[root@localhost ~]# cut -d ":" -f 1,3 /etc/passwd 
#以“:”作为分隔符,提取/etc/passwd 文件的第一列和第三列

如果我想用 cut 命令截取 df 命令的第一列和第三列,就会出现这样的情况:

[root@localhost ~]# df -h | cut -d " " -f 1,3

11.2 printf 格式化输出

[root@localhost ~]# printf ‘输出类型输出格式’ 输出内容
输出类型:
%ns: 输出字符串。n 是数字指代输出几个字符
%ni: 输出整数。n 是数字指代输出几个数字
%m.nf: 输出浮点数。m 和 n 是数字,指代输出的整数位数和小数位数。如%8.2f代表共输出 8 位数,其中 2 位是小数,6 位是整数。
输出格式:
\a: 输出警告声音
\b: 输出退格键,也就是 Backspace 键 \f: 清除屏幕
\n: 换行
\r: 回车,也就是 Enter 键 \t: 水平输出退格键,也就是 Tab 键 \v: 垂直输出退格键,也就是 Tab 键

使用 printf 命令输出下这个文件的内容:

如果不指定输出格式,则会把所有输出内容连在一起输出。其实文本的输出本身就是这样的,cat 等文本输出命令之所以可以按照格式漂亮的输出,那是因为 cat 命令已经设定了输出格式。那么为了用 printf 输出合理的格式,应该这样做:

格式化字符串输出(sprintf使用)

其中格式化字符串包括两部分内容: 一部分是正常字符,这些字符将按原样输出; 另一部分是格式化规定字符, 以 % 开始, 后跟一个或几个规定字符,用来确定输出内容格式。 需要特别注意的是使用 printf 时默认是不会换行的,而 print 函数默认会在每行后面加上 \n 换行符。

格式符说明
%d十进制有符号整数
%u十进制无符号整数
%f浮点数
%s字符串
%c单个字符
%p指针的值
%e指数形式的浮点数
%x%X 无符号以十六进制表示的整数
%o无符号以八进制表示的整数
%g自动选择合适的表示法
$ awk 'BEGIN{n1=124.113;n2=-1.224;n3=1.2345; printf("%.2f,%.2u,%.2g,%X,%o\n",n1,n2,n3,n1,n1);}'
124.11,4294967295,1.2,7C,174
$ awk 'BEGIN{n1=124.113;n2=-1.224;n3=1.2645; printf("%.2f,%.2u,%.2g,%X,%o\n",n1,n2,n3,n1,n1);}'
124.11,4294967295,1.3,7C,174

**注:**看上面的 n3 输出值会发现,在使用 printf 处理时一个比较智能的功能是可以进行四舍五入保留小数点位的。

十二、awk 编程

12.1 awk 概述

awk(名字来源于三个创始人姓氏首字母)是linux系统下文本编辑工具,是linux下的一个命令,他对其他命令的输出,对文件的处理都十分强大,其实他更像一门编程语言,他可以自定义变量,有条件语句,有循环,有数组,有正则,有函数等。他读取输出,或者文件的方式是一行,一行的读,根据你给出的条件进行查找,并在找出来的行中进行操作。他有三种形势,awk,gawk,nawk,平时所说的awk其实就是gawk。

awk则比较倾向于将一行分成数个字段来处理。awk将输入数据视为一个文本数据库,像数据库一样,它也有记录和字段的概念。默认情况下,记录的分隔符是回车,字段的分隔符是空白符(空格,\t),所以输入数据的每一行表示一个记录,而每一行中的内容被空白分隔成多个字段。利用字段和记录,awk可以非常灵活地处理文件。对于日志、CSV 那样的每行格式相同的文本文件,awk可能是最方便的工具。

awk命令用法一:

awk [选项参数] 'pattern {action}' 文件名
# 示例
$ awk '{print $0}' demo.txt

awk脚本是由模式和操作组成的,两者是可选的,如果没有pattern,则action应用到全部记录,如果没有action,则输出匹配全部记录。

awk命令用法二:

awk -f {awk脚本} {文件名}
# 示例
$ awk -f cal.awk demo.txt

12.1.1 选项参数

  • -F fs or --field-separator fs 指定输入文件折分隔符,fs是一个字符串或者是一个正则表达式。
  • -v var=value or --asign var=value 赋值一个用户定义变量。
  • -f scripfile or --file scriptfile 从脚本文件中读取awk命令。
  • -mf nnn and -mr nnn 对nnn值设置内在限制,-mf选项限制分配给nnn的最大块数目;-mr选项限制记录的最大数目。这两个功能是Bell实验室版awk的扩展功能,在标准awk中不适用。
  • -W compact or --compat, -W traditional or --traditional 在兼容模式下运行awk。所以gawk的行为和标准的awk完全一样,所有的awk扩展都被忽略。
  • -W copyleft or --copyleft, -W copyright or --copyright 打印简短的版权信息。
  • -W help or --help, -W usage or --usage 打印全部awk选项和每个选项的简短说明。
  • -W lint or --lint 打印不能向传统unix平台移植的结构的警告。
  • -W lint-old or --lint-old 打印关于不能向传统unix平台移植的结构的警告。
  • -W posix 打开兼容模式。但有以下限制,不识别:/x、函数关键字、func、换码序列以及当fs是一个空格时,将新行作为一个域分隔符;操作符=不能代替^和^=;fflush无效。
  • -W re-interval or --re-inerval 允许间隔正则表达式的使用,参考(grep中的Posix字符类),如括号表达式[[:alpha:]]。
  • -W source program-text or --source program-text 使用program-text作为源代码,可与-f命令混用。
  • -W version or --version 打印bug报告信息的版本。

12.1.1 pattern 条件

条件说明
BEGIN在 awk 程序一开始时,尚未读取任何数据之前执行。BEGIN 后的动作只在程序开始时执行一次
END在 awk 程序处理完所有数据,即将结束时执行。END 后的动作只在程序结束时执行一次
>大于
<小于
>=大于等于
<=小于等于
==等于
!=不等于
A~B判断字符串 A 中是否包含能匹配 B 表达式的子字符串
A!~B判断字符串 A 中是否不包含能匹配 B 表达式的子字符串
/正则/如果在“//”中可以写入字符,也可以支持正则表达式

12.1.2 action 操作

操作由一个或多个命令、函数、表达式组成,之间由换行符或分号隔开,并位于大括号内,主要有四个部分:

  • 变量或数组赋值
  • 输出命令
  • 内置函数
  • 控制流命令

12.2 awk 中的记录,域,分割符

当我们读取输出时,或者读取文件时,读取一行就是一个记录。记录分割符是默认是回车符,保存在RS,ORS中。我们从记录中分割出我们要单词,或者是词组等,我们称他为域,域分割符,默认的是空格和TAB銉,保存在内建变量ORS中。举个例子:

aaaa:bbbb:ccccccc
1111:2343:5t43343

上面有二行,这二行就是二个记录,每行后面的回车就是记录分割符,里面冒号就是域分割符,分割出来的,aaaa,1111这类东西就是域了。

12.3 awk 内置变量

变 量描述
$n当前记录的第n个字段,字段间由 FS分隔。
$0完整的输入记录。
ARGC命 令行参数的数目。
ARGIND命令行中当前文件的位置(从0开始算)。
ARGV包 含命令行参数的数组。
CONVFMT数字转换格式(默认值为%.6g)
ENVIRON环 境变量关联数组。
ERRNO最后一个系统错误的描述。
FIELDWIDTHS字 段宽度列表(用空格键分隔)。
FILENAME当前文件名。
FNR同 NR,但相对于当前文件。
FS字段分隔符(默认是任何空格)。
IGNORECASE如 果为真,则进行忽略大小写的匹配。
NF当前记录中的字段数。
NR当前记录数。
OFMT数字的输出格式(默认值是%.6g)。
OFS输 出字段分隔符(默认值是一个空格)。
ORS输出记录分隔符(默认值是一个换行符)。
RLENGTH由 match函数所匹配的字符串的长度。
RS记录分隔符(默认是一个换行符)。
RSTART由 match函数所匹配的字符串的第一个位置。
SUBSEP数组下标分隔符(默认值是\034)。

12.4 awk 运算符

运算符描述
= += -= *= /= %= ^= **=赋值
?:C条件表达式
||逻辑或
&&逻辑与
~ ~!匹配正则表达式和不匹配正则表达式
< <= > >= != ==关系运算符
空格连接
+ -加,减
* / &乘,除与求余
+ - !一元加,减和逻辑非
^ ***求幂
++ --增加或减少,作为前缀或后缀
$字段引用
in数组成员

12.5 awk 正则

匹配符描述
\Y匹配一个单词开头或者末尾的空字符串
\B匹配单词内的空字符串
<匹配一个单词的开头的空字符串,锚定开始
>匹配一个单词的末尾的空字符串,锚定末尾
\W匹配一个非字母数字组成的单词
\w匹配一个字母数字组成的单词
'匹配字符串末尾的一个空字符串
\‘匹配字符串开头的一个空字符串

如果要想让 awk 识别正则,必须使用“//”包含,例如:

12.6 awk 函数

12.6.1 字符串函数

函数名描述
sub匹配记录中最大、最靠左边的子字符串的正则表达式,并用替换字符串替换这些字符串。如果没有指定目标字符串就默认使用整个记录。替换只发生在第一次匹配的 时候
gsub整个文档中进行匹配
index返回子字符串第一次被匹配的位置,偏移量从位置1开始
substr返回从位置1开始的子字符串,如果指定长度超过实际长度,就返回整个字符串
split可按给定的分隔符把字符串分割为一个数组。如果分隔符没提供,则按当前FS值进行分割
length返回记录的字符数
match返回在字符串中正则表达式位置的索引,如果找不到指定的正则表达式则返回0。match函数会设置内建变量RSTART为字符串中子字符串的开始位 置,RLENGTH为到子字符串末尾的字符个数。substr可利于这些变量来截取字符串
toupper和tolower可用于字符串大小间的转换,该功能只在gawk中有效

12.6.2 数学函数

函数名返回值
atan2(x,y)y,x 范围内的余切
cos(x)余弦函数
exp(x)求 幂
int(x)取整
log(x)自然对 数
rand()随机数
sin(x)正弦
sqrt(x)平 方根
srand(x)x是rand()函数的种子
int(x)取 整,过程没有舍入
rand()产生一个大于等于0而小于1的随机数

12.6 awk语法

典型的awk语法如下:

awk '{ 

     BEGIN{stat1} 
     BEGIN{stat2} 
     pattern1{action1} 
     pattern2{action2} 
     ... 
     patternn{actionn} 
     {默认动作,无条件,始终执行} 

     END{stat1} 
     END{stat2} 
}'

12.7 awk 脚本

关于 awk 脚本,我们需要注意两个关键词 BEGIN 和 END。

  • BEGIN{ 这里面放的是执行前的语句 }
  • END {这里面放的是处理完所有的行后要执行的语句 }
  • {这里面放的是处理每一行时要执行的语句}

假设有这么一个文件(学生成绩表):

$ cat score.txt
Marry   2143 78 84 77
Jack    2321 66 78 45
Tom     2122 48 77 71
Mike    2537 87 97 95
Bob     2415 40 57 62

我们的 awk 脚本如下:

$ cat cal.awk
#!/bin/awk -f
#运行前
BEGIN {
    math = 0
    english = 0
    computer = 0
 
    printf "NAME    NO.   MATH  ENGLISH  COMPUTER   TOTAL\n"
    printf "---------------------------------------------\n"
}
#运行中
{
    math+=$3
    english+=$4
    computer+=$5
    printf "%-6s %-6s %4d %8d %8d %8d\n", $1, $2, $3,$4,$5, $3+$4+$5
}
#运行后
END {
    printf "---------------------------------------------\n"
    printf "  TOTAL:%10d %8d %8d \n", math, english, computer
    printf "AVERAGE:%10.2f %8.2f %8.2f\n", math/NR, english/NR, computer/NR
}

我们来看一下执行结果:

$ awk -f cal.awk score.txt
NAME    NO.   MATH  ENGLISH  COMPUTER   TOTAL
---------------------------------------------
Marry  2143     78       84       77      239
Jack   2321     66       78       45      189
Tom    2122     48       77       71      196
Mike   2537     87       97       95      279
Bob    2415     40       57       62      159
---------------------------------------------
  TOTAL:       319      393      350
AVERAGE:     63.80    78.60    70.00

12.8 awk 流程控制

各种实例的所有指令是顺序执行的,即一个接一个地执行。但在某些情况下,我们可能希望基于一些条件进行文本过滤操作,即流程控制语句允许的那些语句。

在 awk 编程中,因为命令语句非常长,在输入格式时需要注意以下内容:

  • 多个条件{动作}可以用空格分割,也可以用回车分割。
  • 在一个动作中,如果需要执行多个命令,需要用“;”分割,或用回车分割。
  • 在 awk 中,变量的赋值与调用都不需要加入“$”符。
  • 条件中判断两个值是否相同,请使用“==”,以便和变量赋值进行区分。

在 awk 编程中有各种各样的流程控制语句,其中包括:

  • if-else 语句
  • for 语句
  • while 语句
  • do-while 语句
  • break 语句
  • continue 语句
  • next 语句
  • nextfile 语句
  • exit 语句

12.8.1 if 指令

语法:

if (表达式) {语句1}

else if (表达式) {语句2}

else {语句3}

范例 :

# awk 'BEGIN {
T=79;
if (T >=80) {
    print "Excellent!";
}                     
else if (T >=60) {
    print "Pass!";
}                 
else {           
    print "Fail";
}               
}'               
Pass!1.2.3.4.5.6.7.8.9.10.11.12.13.

说明:

\1. 备注:中括号内的语句均可包含多条。

2.设置变量T=79

3.如果T>=80,打印Excellent!

4.如果60<=T<80,打印Pass!

5.如果T小于60,则打印Fail!

12.8.2 while 指令

语法 :

while( 表达式 ) {语句}

范例 :求100以内的和

# awk 'BEGIN {
max=100;
i=0;
while ( i <= max ) {
    j += i
    i++
}     
    print j;
}'
50501.2.3.4.5.6.7.8.9.10.

范例:打印出变量T中所有连续的数字

# awk 'BEGIN {
T="p12-p34 p56-p78 p9 0";
while(match(T,/[0-9]+/) > 0) {
    print substr(T,RSTART,RLENGTH);
    sub(/[0-9]+/,"",T);
}                     
}'
12
34
56
78
9
01.2.3.4.5.6.7.8.9.10.11.12.13.

说明:

1.设置变量“T”等于“ p12-p34 p56-p78”。

2.执行while指令,用match()函数去匹配出现一次以上的数字。

3.如果匹配到数字,则打印出其“起始位置”和“长度”,也就是打印出匹配到的数字。并从变量中删除数字,开始下轮循环。直到while循环结束。

12.8.3 do-while 指令

语法 :

do {语句} while(表达式)

范例 :

# awk 'BEGIN {
i = 0;
j = 0;
do {
    j += i;
    i++;
}
while ( i <= 100 );
print j;
}'1.2.3.4.5.6.7.8.9.10.

说明:

1.设定变量 i = 0, j = 0

2.把变量 j 的值加变量 i 的值的结果赋予变量 j,变量 i 累加1

3.一直循环到变量 i >100,则打印变量 j 的值

12.8.4 for 指令(一)

语法 :

for( 变量;条件;表达式) {statement}

范例:计算100以内整数的和

# awk 'BEGIN {
j = 0;
for ( i=0;i<=100;i++ ) {
    j += i;
}
print j;
}'
50501.2.3.4.5.6.7.8.

说明 :

变量 : 用于设定 for 循环的起始条件, 如上例中的 i=0

条件 : 用于设定该循环的停止条件, 如上例中的 i <= 100

表达式: 用于改变 counter 值, 如上例中的 i++

12.8.5 for 指令(二)

语法:

for(变量 in 函数 ) {statement}

范例:

# awk 'BEGIN {
array["a"]="apple";array["b"]="banana";array[3]="three";array[4]=40;
for ( i in array ) {
printf ( "array[%s] = %s\n",i,array[i] )
}
}'
array[4] = 40
array[a] = apple
array[b] = banana
array[3] = three1.2.3.4.5.6.7.8.9.10.

12.8.6 break 指令

break 指令用以跳出循环(for, while, do-while 等循环).

范例 :

# awk 'BEGIN {
for ( i = 1; i != 0; i++ ) {
    if ( i == 101 ) {
        break
    }
    j += i
}
print j;
}'
50501.2.3.4.5.6.7.8.9.10.

12.8.7 continue 指令

循环中的 statement 进行到一半时, 执行 continue 指令来略过循环中尚未执行的statement.

范例 :求整数3以内的和。并打印大于3小于等于5的整数,且在其后加上字符串“hello”。

# awk 'BEGIN {
for (i=0;i<=5;i++) {
    if (i <=3) {
        j+=i;
        continue;
    }
    print i,"hello"
}
print j;
}'
4 hello
5 hello
61.2.3.4.5.6.7.8.9.10.11.12.13.

说明:

break的作用是跳出循环。

continue的作用是略过其后的语句,但并未跳出循环。

上例中 i <= 3的值省略了其后的print语句。

12.8.8 next 指令

执行 next 指令时, awk 将略过该指令之后的所有指令; 继续读取下一笔数据行,继续从第一个 Pattern {Actions} 开始执行。

范例 :

# cat tmp.txt
web01[192.168.2.100]
httpd            ok
tomcat               ok
sendmail               ok
web02[192.168.2.101]
httpd            ok
postfix               ok
web03[192.168.2.102]
mysqld            ok
httpd               ok

# cat tmp.txt | awk '/^web/ {
T=$0;next};
/^[^$]/ {
printf("%s %s %5s\n",T":\t",$1,$2);
}'
web01[192.168.2.100]:     httpd    ok
web01[192.168.2.100]:     tomcat    ok
web01[192.168.2.100]:     sendmail    ok
web02[192.168.2.101]:     httpd    ok
web02[192.168.2.101]:     postfix    ok
web03[192.168.2.102]:     mysqld    ok
web03[192.168.2.102]:     httpd    ok1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.

12.8.9 exit 指令

执行 exit 指令时, awk将立刻停止运行。

登录后复制

# awk 'BEGIN {
for(i=0;i<=100;i++) {
    if(i==50) { print "exit program!"
        exit
        }
    j += i
    }
print j
}'
exit program!

12.9 awk 数组

AWK 可以使用关联数组这种数据结构,索引可以是数字或字符串。

AWK关联数组也不需要提前声明其大小,因为它在运行时可以自动的增大或减小。

数组使用的语法格式:

array_name[index]=value
  • array_name:数组的名称
  • index:数组索引
  • value:数组中元素所赋予的值

12.9.1 创建数组

接下来看一下如何创建数组以及如何访问数组元素:

$ awk 'BEGIN {
sites["runoob"]="www.runoob.com";
sites["google"]="www.google.com"
print sites["runoob"] "\n" sites["google"]
}'

执行以上命令,输出结果为:

www.runoob.com
www.google.com

在上面的例子中,我们定义了一个站点(sites)数组,该数组的索引为网站英文简称,值为网站访问地址。可以使用如下格式访问数组元素:

array_name[index] 

12.9.2 删除数组元素

我们可以使用 delete 语句来删除数组元素,语法格式如下:

delete array_name[index]

下面的例子中,数组中的 google 元素被删除(删除命令没有输出):

$ awk 'BEGIN {
sites["runoob"]="www.runoob.com";
sites["google"]="www.google.com"
delete sites["google"];
print fruits["google"]
}'

12.9.3 多维数组

AWK 本身不支持多维数组,不过我们可以很容易地使用一维数组模拟实现多维数组。

如下示例为一个 3x3 的二维数组:

100 200 300
400 500 600
700 800 900

以上实例中,array[0][0] 存储 100,array[0][1] 存储 200 ,依次类推。为了在 array[0][0] 处存储 100, 我们可以使用如下语法: array["0,0"] = 100。

我们使用了 0,0 作为索引,但是这并不是两个索引值。事实上,它是一个字符串索引 0,0。

下面是模拟二维数组的例子:

$ awk 'BEGIN {
array["0,0"] = 100;
array["0,1"] = 200;
array["0,2"] = 300;
array["1,0"] = 400;
array["1,1"] = 500;
array["1,2"] = 600;
# 输出数组元素
print "array[0,0] = " array["0,0"];
print "array[0,1] = " array["0,1"];
print "array[0,2] = " array["0,2"];
print "array[1,0] = " array["1,0"];
print "array[1,1] = " array["1,1"];
print "array[1,2] = " array["1,2"];
}'

执行上面的命令可以得到如下结果:

array[0,0] = 100
array[0,1] = 200
array[0,2] = 300
array[1,0] = 400
array[1,1] = 500
array[1,2] = 600

在数组上可以执行很多操作,比如,使用 asort 完成数组元素的排序,或者使用 asorti 实现数组索引的排序等等。

12.10 awk 自定义函数

一个程序包含有多个功能,每个功能我们可以独立一个函数。函数可以提高代码的复用性。

用户自定义函数的语法格式为:

function function_name(argument1, argument2, ...)
{
    function body
}

解析:

  • function_name 是用户自定义函数的名称。函数名称应该以字母开头,其后可以是数字、字母或下划线的自由组合。AWK 保留的关键字不能作为用户自定义函数的名称。
  • 自定义函数可以接受多个输入参数,这些参数之间通过逗号分隔。参数并不是必须的。我们也可以定义没有任何输入参数的函数。
  • function body 是函数体部分,它包含 AWK 程序代码。

以下实例我们实现了两个简单函数,它们分别返回两个数值中的最小值和最大值。我们在主函数 main 中调用了这两个函数。 文件 functions.awk 代码如下:

# 返回最小值
function find_min(num1, num2)
{
  if (num1 < num2)
    return num1
  return num2
}

# 返回最大值
function find_max(num1, num2)
{
  if (num1 > num2)
    return num1
  return num2
}

# 主函数
function main(num1, num2)
{
  # 查找最小值
  result = find_min(10, 20)
  print "Minimum =", result

  # 查找最大值
  result = find_max(10, 20)
  print "Maximum =", result
}

# 脚本从这里开始执行
BEGIN {
  main(10, 20)
}  

执行 functions.awk 文件,可以得到如下的结果:

$ awk -f functions.awk 
Minimum = 10
Maximum = 20

十三、sed命令

13.1 sed概述

sed 主要是用来将数据进行选取、替换、删除、新增的命令。sed 可依照脚本的指令来处理、编辑文本文件,主要用来自动编辑一个或多个文件、简化对文件的反复操作、编写转换程序等。我们看看命令的语法:

13.2 sed 语法

sed [-hnV][-e<script>][-f<script文件>][文本文件]
sed [选项][动作]’ 文件名

参数说明

  • -e 允许对输入数据应用多条 sed 命令编辑。
  • -f<script文件>或--file=<script文件> 以选项中指定的script文件来处理输入的文本文件。
  • -h 或--help 显示帮助。
  • -n 一般 sed 命令会把所有数据都输出到屏幕,如果加入此选择,则只会把经过 sed 命令处理的行输出到屏幕。
  • -V 或--version 显示版本信息。
  • -i 用 sed 的修改结果直接修改读取数据的文件,而不是由屏幕输出
  • -r 在 sed 中支持扩展正则表达式。

动作说明

  • a :新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行),添加多行时,除最后 一行外,每行末尾需要用“\”代表数据未完结。
  • c :取代, c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行,替换多行时,除最后一行外,每行末尾需用“\”代表数据未完结。
  • d :删除,因为是删除,所以 d 后面通常为空
  • i :插入, i 的后面可以接字符串,而这些字符串会在新的一行出现(目前的上一行),插入多行时,除最后 一行外,每行末尾需要用“\”代表数据未完结。
  • p :打印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行
  • s :取代,可以直接进行取代的工作,通常这个 s 的动作可以搭配正则,例如 1,20s/old/new/g

注意:

对 sed 命令大家要注意,sed 所做的修改并不会直接改变文件的内容(如果是用管道符接收的命令的输出,这种情况连文件都没有),而是把修改结果只显示到屏幕上,除非使用“-i”选项才会直接修改文件。

13.3 行数据操作

13.3.1 查看数据

查看下student.txt的第二行,如果想指定输出某行数据,就需要“-n”选项。

[root@localhost ~]# sed -n '2p' student.txt

13.3.2 删除数据

删除第二行到第四行的数据,文件本身不会修改。

[root@localhost ~]# sed '2,4d' student.txt

13.3.3 插入数据

在第二行后加入 hello

[root@localhost ~]# sed '2a hello' student.txt

“a”会在指定行后面追加入数据,如果想要在指定行前面插入数据,则需要使用“i”动作:

[root@localhost ~]# sed '2i hello > world' student.txt

如果是想追加或插入多行数据,除最后一行外,每行的末尾都要加入“\”代表数据未完结。

13.3.4 替换数据

再来看看如何实现行数据替换,假设李明老师的成绩太好了,我实在是不想看到他的成绩刺激我,那我就可以这样:

[root@localhost ~]# cat student.txt | sed '2c No such person'

sed 命令默认情况是不会修改文件内容的,如果我确定需要让 sed 命令直接处理文件的内容,可以使用“-i”选项。不过要小心啊,这样非常容易误操作,在操作系统文件时请小心谨慎。可以使用这样的命令:

[root@localhost ~]# sed -i '2c No such person' student.txt

13.4 字符串替换

“c”动作是进行整行替换的,如果仅仅想替换行中的部分数据,就要使用“s”动作了。s 动作的格式是:

[root@localhost ~]# sed ‘s/旧字串/新字串/g’ 文件名

十四、字符串处理

14.1 排序命令 sort

[root@localhost ~]# sort [选项] 文件名
选项:
-f: 忽略大小写
-b: 忽略每行前面的空白部分
-n: 以数值型进行排序,默认使用字符串型排序
-r: 反向排序
-u: 删除重复行。就是 uniq 命令
-t: 指定分隔符,默认是分隔符是制表符
-k n[,m]: 按照指定的字段范围排序。从第 n 字段开始,m 字段结束(默认到行尾)

sort 命令默认是用每行开头第一个字符来进行排序的,比如:

[root@localhost ~]# sort /etc/passwd
#排序用户信息文件

如果想要反向排序,请使用“-r”选项:

[root@localhost ~]# sort -r /etc/passwd
#反向排序

如果想要指定排序的字段,需要使用“-t”选项指定分隔符,并使用“-k”选项指定字段号。加入我想要按照 UID 字段排序/etc/passwd 文件:

[root@localhost ~]# sort -t ":" -k 3,3 /etc/passwd
#指定分隔符是“:”,用第三字段开头,第三字段结尾排序,就是只用第三字段排序

sort 默认是按照字符排序,要想按照数字排序,请使用“-n”选项:

[root@localhost ~]# sort -n -t ":" -k 3,3 /etc/passwd

当然“-k”选项可以直接使用“-k 3”,代表从第三字段到行尾都排序(第一个字符先排序,如果一致,第二个字符再排序,直到行尾)。

14.2 uniq

uniq 命令是用来取消重复行的命令,其实和“sort -u”选项是一样的。命令格式如下:

[root@localhost ~]# uniq [选项] 文件名
选项:
-i: 忽略大小写

14.3 wc

统计命令 wc

[root@localhost ~]# wc [选项] 文件名
选项:
-l: 只统计行数
-w: 只统计单词数
-m: 只统计字符数

十五、条件判断

15.1 按照文件类型进行判断

测试选项作用
-b 文件判断该文件是否存在,并且是否为块设备文件(是块设备文件为真)
-c 文件判断该文件是否存在,并且是否为字符设备文件(是字符设备文件为真)
-d 文件判断该文件是否存在,并且是否为目录文件(是目录为真)
-e 文件判断该文件是否存在(存在为真)
-f 文件判断该文件是否存在,并且是否为普通文件(是普通文件为真)
-L 文件判断该文件是否存在,并且是否为符号链接文件(是符号链接文件为真)
-p 文件判断该文件是否存在,并且是否为管道文件(是管道文件为真)
-s 文件判断该文件是否存在,并且是否为非空(非空为真)
-S 文件判断该文件是否存在,并且是否为套接字文件(是套接字文件为真)
[root@localhost ~]# [ -e /root/sh/ ]
[root@localhost ~]# echo $?
0
#判断结果为 0,/root/sh/目录是存在的
[root@localhost ~]# [ -e /root/test ]
[root@localhost ~]# echo $? 
1
#在/root/下并没有 test 文件或目录,所以“$?”的返回值为非零

15.2 按照文件权限进行判断

测试选项作 用
-r 文件判断该文件是否存在,并且是否该文件拥有读权限(有读权限为真)
-w 文件判断该文件是否存在,并且是否该文件拥有写权限(有写权限为真)
-x 文件判断该文件是否存在,并且是否该文件拥有执行权限(有执行权限为真)
-u 文件判断该文件是否存在,并且是否该文件拥有 SUID 权限(有 SUID 权限为真)
-g 文件判断该文件是否存在,并且是否该文件拥有 SGID 权限(有 SGID 权限为真)
-k 文件判断该文件是否存在,并且是否该文件拥有 SBit 权限(有 SBit 权限为真)
[root@localhost ~]# ll student.txt 
-rw-r--r--. 1 root root 97 67 07:34 student.txt
[root@localhost ~]# [ -w student.txt ] && echo "yes" || echo "no" 
yes
#判断文件是拥有写权限的

15.3 两个文件之间进行比较

测试选项作 用
文件 1 -nt 文件 2判断文件 1 的修改时间是否比文件 2 的新(如果新则为真)
文件 1 -ot 文件 2判断文件 1 的修改时间是否比文件 2 的旧(如果旧则为真)
文件 1 -ef 文件 2判断文件 1 是否和文件 2 的 Inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法
[root@localhost ~]# ln /root/student.txt /tmp/stu.txt
#创建个硬链接吧
[root@localhost ~]# [ /root/student.txt -ef /tmp/stu.txt ] && echo "yes" || echo "no"
yes
#用 test 测试下,果然很有用

15.4 两个整数之间比较

测试选项作 用
整数 1 -eq 整数 2判断整数 1 是否和整数 2 相等(相等为真)
整数 1 -ne 整数 2判断整数 1 是否和整数 2 不相等(不相等位置)
整数 1 -gt 整数 2判断整数 1 是否大于整数 2(大于为真)
整数 1 -lt 整数 2判断整数 1 是否小于整数 2(小于位置)
整数 1 -ge 整数 2判断整数 1 是否大于等于整数 2(大于等于为真)
整数 1 -le 整数 2判断整数 1 是否小于等于整数 2(小于等于为真)
[root@localhost ~]# [ 23 -ge 22 ] && echo "yes" || echo "no" 
yes
#判断 23 是否大于等于 22,当然是了
[root@localhost ~]# [ 23 -le 22 ] && echo "yes" || echo "no" 
no
#判断 23 是否小于等于 22,当然不是了

15.5 字符串的判断

测试选项作 用
-z 字符串判断字符串是否为空(为空返回真)
-n 字符串判断字符串是否为非空(非空返回真)
字串 1 ==字串 2判断字符串 1 是否和字符串 2 相等(相等返回真)
字串 1 != 字串 2判断字符串 1 是否和字符串 2 不相等(不相等返回真)
[root@localhost ~]# name=sc
#给 name 变量赋值
[root@localhost ~]# [ -z "$name" ] && echo "yes" || echo "no" 
no
#判断 name 变量是否为空,因为不为空,所以返回 no

15.6 多重条件判断

测试选项作 用
判断 1 -a 判断 2逻辑与,判断 1 和判断 2 都成立,最终的结果才为真
判断 1 -o 判断 2逻辑或,判断 1 和判断 2 有一个成立,最终的结果就为真
!判断逻辑非,使原始的判断式取反
[root@localhost ~]# aa=11
#给变量 aa 赋值
[root@localhost ~]# [ -n "$aa" -a "$aa" -gt 23 ] && echo "yes" || echo "no"
no
#判断变量 aa 是否有值,同时判断变量 aa 的是否大于 23
#因为变量 aa 的值不大于 23,所以虽然第一个判断值为真,返回的结果也是假
[root@localhost ~]# [ ! -n "$aa" ] && echo "yes" || echo "no" 
no
#本来“-n”选项是变量 aa 不为空,返回值就是真。
#加入!之后,判断值就会取反,所以当变量 aa 有值时,返回值是假
#注意:“!”和“-n”之间必须加入空格,否则会报错

十六、流程控制

16.1 if 条件判断

16.1.1 单分支 if 条件语句

单分支条件语句最为简单,就是只有一个判断条件,如果符合条件则执行某个程序,否则什么事情都不做。语法如下:

if [ 条件判断式 ];then
程序
fi

单分支条件语句需要注意几个点:

  • if 语句使用 fi 结尾,和一般语言使用大括号结尾不同

  • [ 条件判断式 ]就是使用 test 命令判断,所以中括号和条件判断式之间必须有空格

  • then 后面跟符合条件之后执行的程序,可以放在[]之后,用“;”分割。也可以换行写入,就不需要“;”了,比如单分支 if 语句还可以这样写:

    if [ 条件判断式 ]
    then
    程序
    fi
    

例子1:我们写一个磁盘使用率告警的例子,来看看单分支 if 条件语句。

[root@localhost ~]# vi sh/if1.sh
#!/bin/bash
#统计根分区使用率
rate=$(df -h | grep "/dev/sda3" | awk '{print $5}' | cut -d "%" -f1)
#把根分区使用率作为变量值赋予变量 rate
if [ $rate -ge 80 ]
#判断 rate 的值如果大于等于 80,则执行 then 程序
then
 echo "Warning! /dev/sda3 is full!!"
#打印警告信息。在实际工作中,也可以向管理员发送邮件。
fi

16.1.2 双分支 if 条件语句

if [ 条件判断式 ]
then
条件成立时,执行的程序
else
条件不成立时,执行的另一个程序
fi

例子 1:我们写一个数据备份的例子,来看看双分支 if 条件语句。

例子 1:备份 mysql 数据库
[root@localhost ~]# vi sh/bakmysql.sh
#!/bin/bash
#备份 mysql 数据库。
ntpdate asia.pool.ntp.org &>/dev/null
#同步系统时间
date=$(date +%y%m%d)
#把当前系统时间按照“年月日”格式赋予变量 date
size=$(du -sh /var/lib/mysql)
#统计 mysql 数据库的大小,并把大小赋予 size 变量
if [ -d /tmp/dbbak ]
#判断备份目录是否存在,是否为目录
then
#如果判断为真,执行以下脚本
 echo "Date : $date!" > /tmp/dbbak/dbinfo.txt
#把当前日期写入临时文件
 echo "Data size : $size" >> /tmp/dbbak/dbinfo.txt
#把数据库大小写入临时文件
 cd /tmp/dbbak
 #进入备份目录
 tar -zcf mysql-lib-$date.tar.gz /var/lib/mysql dbinfo.txt &>/dev/null
#打包压缩数据库与临时文件,把所有输出丢入垃圾箱(不想看到任何输出)
 rm -rf /tmp/dbbak/dbinfo.txt
#删除临时文件
else
 mkdir /tmp/dbbak
#如果判断为假,则建立备份目录
 echo "Date : $date!" > /tmp/dbbak/dbinfo.txt
 echo "Data size : $size" >> /tmp/dbbak/dbinfo.txt
#把日期和数据库大小保存如临时文件
 cd /tmp/dbbak
 tar -zcf mysql-lib-$date.tar.gz dbinfo.txt /var/lib/mysql &>/dev/null
#压缩备份数据库与临时文件
 rm -rf /tmp/dbbak/dbinfo.txt
#删除临时文件
fi

例子 2:再举个例子,在工作当中,服务器上的服务经常会宕机。如果我们对服务器监控不好,就会造成服务器中服务宕机了,而管理员却不知道的情况,这时我们可以写一个脚本来监听本机的服务,如果服务停止或宕机了,可以自动重启这些服务。我们拿 apache 服务来举例:

例子 2:判断 apache 是否启动,如果没有启动则自动启动
[root@localhost ~]# vi sh/autostart.sh
#!/bin/bash
#判断 apache 是否启动,如果没有启动则自动启动
port=$(nmap -sT 192.168.4.210 | grep tcp | grep http | awk '{print $2}')
#使用 nmap 命令扫描服务器,并截取 apache 服务的状态,赋予变量 port
if [ "$port" == "open" ]
#如果变量 port 的值是“open”
then
echo$(date) httpd is ok!>> /tmp/autostart-acc.log
#则证明 apache 正常启动,在正常日志中写入一句话即可
else
 /etc/rc.d/init.d/httpd start &>/dev/null
#否则证明 apache 没有启动,自动启动 apache
 echo "$(date) restart httpd !!" >> /tmp/autostart-err.log
#并在错误日志中记录自动启动 apche 的时间
fi

以我们使用 nmap 端口扫描命令,nmap 命令格式如下:

[root@localhost ~]# nmap -sT 域名或 IP
选项:
-s 扫描
-T 扫描所有开启的 TCP 端口

这条命令的执行结果如下:

[root@localhost ~]# nmap -sT 192.168.4.210
#可以看到这台服务器开启了如下的服务
Starting Nmap 5.51 ( http://nmap.org ) at 2018-11-25 15:11 CST
Nmap scan report for 192.168.4.210
Host is up (0.0010s latency).
Not shown: 994 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http apache 的状态是 open
111/tcp open rpcbind
139/tcp open netbios-ssn
445/tcp open microsoft-ds
3306/tcp open mysql
Nmap done: 1 IP address (1 host up) scanned in 0.49 seconds

知道了 nmap 命令的用法,我们在脚本中使用的命令就是为了截取 http 的状态,只要状态是“open”就证明 apache 启动正常,否则证明 apache 启动错误。来看看脚本中命令的结果:

[root@localhost ~]# nmap -sT 192.168.4.210 | grep tcp | grep http | awk '{print $2}'
#扫描指定计算机,提取包含 tcp 的行,在提取包含 httpd 的行,截取第二列
open
#把截取的值赋予变量 port

16.1.3 多分支 if 条件语句

if [ 条件判断式 1 ]
then
当条件判断式 1 成立时,执行程序 1
elif [ 条件判断式 2 ]
then
当条件判断式 2 成立时,执行程序 2 …省略更多条件…
else
当所有条件都不成立时,最后执行此程序
fi

那我们再写一个例子,用 if 多分支条件语句来判断一下用户输入的是一个文件,还是一个目录:

例子:判断用户输入的是什么文件
[root@localhost ~]# vi sh/if-elif.sh
#!/bin/bash
#判断用户输入的是什么文件
read -p "Please input a filename: " file
#接收键盘的输入,并赋予变量 file
if [ -z "$file" ]
#判断 file 变量是否为空
then
 echo "Error,please input a filename"
#如果为空,执行程序 1,也就是输出报错信息
 exit 1
#退出程序,并返回值为 1(把返回值赋予变量$?)
elif [ ! -e "$file" ]
#判断 file 的值是否存在
then
 echo "Your input is not a file!"
#如果不存在,则执行程序 2
 exit 2
#退出程序,把并定义返回值为 2
elif [ -f "$file" ]
#判断 file 的值是否为普通文件
then
 echo "$file is a regulare file!"
#如果是普通文件,则执行程序 3
elif [ -d "$file" ]
#判断 file 的值是否为目录文件
then
 echo "$file is a directory!"
#如果是目录文件,则执行程序 4
else
 echo "$file is an other file!"
#如果以上判断都不是,则执行程序 5
fi

16.2 多分支 case 条件语句

case 语句和 if…elif…else 语句一样都是多分支条件语句,不过和 if多分支条件语句不同的是,case 语句只能判断一种条件关系,而 if 语句可以判断多种条件关系。case 语句语法如下:

case $变量名 in
"值 1")
如果变量的值等于值 1,则执行程序 1
;;
"值 2")
如果变量的值等于值 2,则执行程序 2
::
…省略其他分支
*)
如果变量的值都不是以上的值,则执行此程序
;;
esac

这个语句需要注意以下内容:

  • case 语句,会取出变量中的值,然后与语句体中的值逐一比较。如果数值符合,则执行对应的程序,如果数值不符,则依次比较下一个值。如果所有的值都不符合,则执行“)”(“”代表所有其他值)中的程序。
  • case 语句以“case”开头,以“esac”结尾。
  • 每一个分支程序之后要通过“;;”双分号结尾,代表该程序段结束(千万不要忘记,超哥每次写 case语句,都会忘记双分号,有点“囧”)。

我们写一个判断是“yes/no”的例子:

[root@localhost ~]# vi sh/case.sh
#!/bin/bash
#判断用户输入
read -p "Please choose yes/no: " -t 30 cho
#在屏幕上输出“请选择 yes/no”,然后把用户选择赋予变量 cho
case $cho in
#判断变量 cho 的值
 "yes")
#如果是 yes
 echo "Your choose is yes!"
#执行程序 1
 ;;
 "no")
#如果是 no
 echo "Your choose is no!"
#执行程序 2
 ;;
 *)
#如果既不是 yes,也不是 no
 echo "Your choose is error!"
#则执行此程序
 ;;
esac

16.3 for 循环

for 循环是固定循环,也就是在循环时已经知道需要进行几次的循环,有时也把 for 循环称为计数循环。for 的语法有如下两种:

语法一:
for 变量 in123do
		程序
	done

这种语法中 for 循环的次数,取决于 in 后面值的个数(空格分隔),有几个值就循环几次,并且每次循环都把值赋予变量。也就是说,假设 in 后面有三个值,for 会循环三次,第一次循环会把值1 赋予变量,第二次循环会把值 2 赋予变量,以此类推。

语法二:
for (( 初始值;循环控制条件;变量变化 ))
	do
		程序
	done

语法二中需要注意

  • 初始值:在循环开始时,需要给某个变量赋予初始值,如 i=1;
  • 循环控制条件:用于指定变量循环的次数,如 i<=100,则只要 i 的值小于等于 100,循环就会继续;
  • 变量变化:每次循环之后,变量该如何变化,如 i=i+1。代表每次循环之后,变量 i 的值都加 1。

16.3.1 语法一举例

批量解压缩脚本应该这样写:

例子 2:批量解压缩
[root@localhost ~]# vi sh/auto-tar.sh
#!/bin/bash
#批量解压缩脚本
cd /lamp
#进入压缩包目录
ls *.tar.gz > ls.log
#把所有.tar.gz 结尾的文件的文件覆盖到 ls.log 临时文件中
for i in $(cat ls.log)
#读取 ls.log 文件的内容,文件中有多少个值,就会循环多少次,每次循环把文件名赋予变量 i
 do
 tar -zxvf $i &>/dev/null
#加压缩,并把所有输出都丢弃
 done
rm -rf /lamp/ls.log
#删除临时文件 ls.log

16.3.2 语法二举例

批量添加指定数量的用户:

[root@localhost ~]# vi useradd.sh
#!/bin/bash
#批量添加指定数量的用户
read -p "Please input user name: " -t 30 name
#让用户输入用户名,把输入保存入变量 name
read -p "Please input the number of users: " -t 30 num 
#让用户输入添加用户的数量,把输入保存入变量 num
read -p "Please input the password of users: " -t 30 pass
#让用户输入初始密码,把输入保存如变量 pass
if [ ! -z "$name" -a ! -z "$num" -a ! -z "$pass" ]
#判断三个变量不为空
 then
 y=$(echo $num | sed 's/[0-9]//g')
 #定义变量的值为后续命令的结果
#后续命令作用是,把变量 num 的值替换为空。如果能替换为空,证明 num 的值为数字
#如果不能替换为空,证明 num 的值为非数字。我们使用这种方法判断变量 num 的值为数字
 if [ -z "$y" ]
#如果变量 y 的值为空,证明 num 变量是数字
 then
 for (( i=1;i<=$num;i=i+1 ))
#循环 num 变量指定的次数
 do 
 /usr/sbin/useradd $name$i &>/dev/null
#添加用户,用户名为变量 name 的值加变量 i 的数字
 echo $pass | /usr/bin/passwd --stdin $name$i &>/dev/null
#给用户设定初始密码为变量 pass 的值
 done
 fi 
fi

批量删除用户:

[root@localhost ~]# vi sh/userdel.sh
#!/bin/bash
#批量删除用户
# Author: shenchao (E-mail: shenchao@atguigu.com)
user=$(cat /etc/passwd | grep "/bin/bash"|grep -v "root"|cut -d ":" -f 1)
#读取用户信息文件,提取可以登录用户,取消 root 用户,截取第一列用户名
for i in $user
#循环,有多少个普通用户,循环多少次
 do 
  userdel -r $i
  #每次循环,删除指定普通用户
 done

16.4 while 循环

while [ 条件判断式 ]
 do
  程序
 done

对 while 循环来讲,只要条件判断式成立,循环就会一直继续,直到条件判断式不成立,循环才会停止。好吧,我们还是写个 1 加到 100 的例子吧,这种例子虽然对系统管理帮助不大,但是对理解循环非常有帮助:

例子:1 加到 100
#!/bin/bash
#从 1 加到 100
i=1
s=0
#给变量 i 和变量 s 赋值
while [ $i -le 100 ]
#如果变量 i 的值小于等于 100,则执行循环
 do
 s=$(( $s+$i ))
 i=$(( $i+1 ))
 done
echo "The sum is: $s"

16.5 until 循环

再来看看 until 循环,和 while 循环相反,until 循环时只要条件判断式不成立则进行循环,并执行循环程序。一旦循环条件成立,则终止循环。语法如下:

until [ 条件判断式 ]
 do
  程序
 done

还是写从 1 加到 100 这个例子,注意和 while 循环的区别:

例子:从 1 加到 100
[root@localhost ~]# vi sh/until.sh 
#!/bin/bash
#从 1 加到 100
s=0
#给变量 i 和变量 s 赋值
until [ $i -gt 100 ]
#循环直到变量 i 的值大于 100,就停止循环
 do
 s=$(( $s+$i ))
 i=$(( $i+1 ))
 done
echo "The sum is: $s"

16.6 函数

function 函数名 () {
程序
}

那我们写一个函数吧,还记得从 1 加到 100 这个循环吗?这次我们用函数来实现它,不过不再是从 1 加到 100 了,我们让用户自己来决定加到多少吧:

例子:
[root@localhost ~]# vi sh/function.sh
#!/bin/bash
#接收用户输入的数字,然后从 1 加到这个数字
function sum () {
#定义函数 sum
 s=0 
 for (( i=0;i<=$1;i=i+1 ))
#循环直到 i 大于$1 为止。$1 是函数 sum 的第一个参数
#在函数中也可以使用位置参数变量,不过这里的$1 指的是函数的第一个参数
 do 
 s=$(( $i+$s ))
 done
 echo "The sum of 1+2+3...+$1 is : $s"
#输出 1 加到$1 的和
}
read -p "Please input a number: " -t 30 num 
#接收用户输入的数字,并把值赋予变量 num
y=$(echo $num | sed 's/[0-9]//g')
#把变量 num 的值替换为空,并赋予变量 y
if [ -z "$y" ]
#判断变量 y 是否为空,以确定变量 num 中是否为数字
 then
 sum $num
#调用 sum 函数,并把变量 num 的值作为第一个参数传递给 sum 函数
else
 echo "Error!! Please input a number!"
#如果变量 num 的值不是数字,则输出报错信息
fi

16.7 特殊流程控制语句

16.7.1 exit 语句

系统是有 exit 命令的,用于退出当前用户的登录状态。可是在 Shell 脚本中,exit 语句是用来退出当前脚本的。也就是说,在 Shell 脚本中,只要碰到了 exit 语句,后续的程序就不再执行,而直接退出脚本。exit 的语法如下:

exit [返回值]

如果 exit 命令之后定义了返回值,那么这个脚本执行之后的返回值就是我们自己定义的返回值。可以通过查询$?这个变量,来查看返回值。如果 exit 之后没有定义返回值,脚本执行之后的返回值是执行 exit 语句之前,最后执行的一条命令的返回值。写一个 exit 的例子:

[root@localhost ~]# vi sh/exit.sh
#!/bin/bash
#演示 exit 的作用
read -p "Please input a number: " -t 30 num 
#接收用户的输入,并把输入赋予变量 num
y=$(echo $num | sed 's/[0-9]//g')
#如果变量 num 的值是数字,则把 num 的值替换为空,否则不替换
#把替换之后的值赋予变量 y
[ -n "$y" ] && echo "Error! Please input a number!" && exit 18
#判断变量 y 的值如果不为空,输出报错信息,退出脚本,退出返回值为 18
echo "The number is: $num"
#如果没有退出加班,则打印变量 num 中的数字

这个脚本中,大家需要思考,如果我输入的不是数字,那么“echo "The number is: $num"”这个脚本是否会执行?当然不会,因为如果输入的不是数字,“[ -n "$y" ] && echo "Error! Please input a number!" && exit 18”这句脚本会执行,exit 一旦执行脚本就会终止。执行下这个脚本:

[root@localhost ~]# chmod 755 sh/exit.sh
#给脚本服务执行权限
[root@localhost ~]# sh/exit.sh 执行脚本
Please input a number: test 
#输入值不是数字,而是 test
Error! Please input a number! 
#输出报错信息,而不会输出 test
[root@localhost ~]# echo $? 
#查看下返回值
18 
#返回值居然真是 18 啊
[root@localhost ~]# sh/exit.sh 
Please input a number: 10 
#输入数字 10
The number is: 10 
#输出数字 10

16.7.2 break 语句

再来看看特殊流程控制语句 break 的作用,当程序执行到 break 语句时,会结束整个当前循环。而 continue 语句也是结束循环的语句,不过 continue 语句单次当前循环,而下次循环会继续。画个示意图解释下 break 语句。

举个例子:

[root@localhost ~]# vi sh/break.sh
#!/bin/bash
#演示 break 跳出循环
for (( i=1;i<=10;i=i+1 ))
#循环十次
 do 
 if [ "$i" -eq 4 ] 
#如果变量 i 的值等于 4
 then
 break
#退出整个循环
 fi 
 echo $i
#输出变量 i 的值
 done

执行下这个脚本,因为一旦变量 i 的值等于 4,整个循环都会跳出,所以应该只能循环三次:

[root@localhost ~]# chmod 755 sh/break.sh
[root@localhost ~]# sh/break.sh 
1
2
3

16.7.3 continue 语句

再来看看 continue 语句,continue 也是结束流程控制的语句。如果在循环中,continue 语句只会结束单次当前循环,也画个示意图来说明下 continue 语句。

还是用刚刚的脚本,不过退出语句换成 continue 语句,看看会发生什么情况:

[root@localhost ~]# vi sh/continue.sh
#!/bin/bash
#演示 continue 语句
for (( i=1;i<=10;i=i+1 ))
 do 
 if [ "$i" -eq 4 ] 
 then
 continue
#退出语句换成 continue
 fi 
 echo $i
done

运行下这个脚本:

[root@localhost ~]# chmod 755 sh/continue.sh 
#赋予执行权限
[root@localhost ~]# sh/continue.sh 
1
2
3
5 
#少了 4 这个输出
6
7
8
9
10

continue 只会退出单次循环,所以并不影响后续的循环,所以只会少 4 的输出。