Shell命令自查笔记
Shell学习笔记
什么是 shell
Shell 是一个应用程序,它连接了用户和 Linux 内核,让用户能够更加高效、安全、低成本地使用 Linux 内核,这就是 Shell 的本质。换句话说,图形界面和命令行要达到的目的是一样的,都是让用户控制计算机。

Shell 是如何连接用户和内核的?
其实,Shell 程序本身的功能是很弱的,比如文件操作、输入输出、进程管理等都得依赖内核。我们运行一个命令,大部分情况下 Shell 都会去调用内核暴露出来的接口,这就是在使用内核(操作系统内核(Kernel)),只是这个过程被 Shell 隐藏了起来,它自己在背后默默进行,我们看不到而已。接口其实就是一个一个的函数,使用内核就是调用这些函数。比如,我们都知道在 Shell 中输入cat log.txt命令就可以查看 log.txt 文件中的内容,然而,log.txt 放在磁盘的哪个位置?分成了几个数据块?在哪里开始?在哪里终止?如何操作探头读取它?这些底层细节 Shell 统统不知道的,它只能去调用内核提供的 open() 和 read() 函数,告诉内核我要读取 log.txt 文件,请帮助我,然后内核就乖乖地按照 Shell 的吩咐去读取文件了,并将读取到的文件内容交给 Shell,最后再由 Shell 呈现给用户(其实呈现到显示器上还得依赖内核)。整个过程中 Shell 就是一个“中间商”,它在用户和内核之间“倒卖”数据,只是用户不知道罢了。
Shell命令的本质到底是什么?
Shell 内置命令的本质是一个自带的函数,执行内置命令就是调用这个自带的函数。因为函数代码在 Shell 启动时已经被加载到内存了,所以内置命令的执行速度很快。
Shell 命令分为两种:
- Shell 自带的命令称为内置命令,它在 Shell 内部可以通过函数来实现,当 Shell 启动后,这些命令所对应的代码(函数体代码)也被加载到内存中,所以使用内置命令是非常快速的。
- 更多的命令是外部的应用程序,一个命令就对应一个应用程序。运行外部命令要开启一个新的进程,所以效率上比内置命令差很多。
- 用户输入一个命令后,Shell 先检测该命令是不是内置命令,如果是就执行,如果不是就检测有没有对应的外部程序:有的话就转而执行外部程序,执行结束后再回到 Shell;没有的话就报错,告诉用户该命令不存在。
内置命令
- 内置命令不宜过多,过多的内置命令会导致 Shell 程序本身体积膨胀,运行 Shell 程序后就会占用更多的内存。
- Shell 是一个常驻内存的程序,占用过多内存会影响其它的程序。只有那些最常用的命令才有理由成为内置命令,比如 cd、kill、echo 等;
- 可以使用 type 来确定一个命令是否是内建命令

外部命令
一个外部的应用程序究竟是如何变成一个 Shell 命令的呢?
应用程序就是一个文件,只不过这个文件是可以执行的。既然是文件,那么它就有一个名字,并且存放在文件系统中。用户在 Shell 中输入一个外部命令后,只是将可执行文件的名字告诉了 Shell就可以。
Shell 在启动文件中增加了一个叫做 PATH 的环境变量,该变量就保存了 Shell 对外部命令的查找路径,如果在这些路径下找不到同名的文件,Shell 也不会再去其它路径下查找了,它就直接报错。

Shell命令的选项和参数在本质上到底是什么?
不管是内置命令还是外部命令,它后面附带的数据最终都以参数的形式传递给了函数,实现一个命令的一项重要工作就是解析传递给函数的参数。
- 注意,命令后面附带的数据并不是被合并在一起,作为一个参数传递给函数的;这些数据是由空格分隔的,它们被分隔成了几份,就会转换成几个参数。
脚本的创建过程
如果写出自己的第一个Shell脚本?
- 分析任务
- 自然语言:步骤拆分,顺序化整理
- 编写可执行语句
- 脚本语言:各步骤如何实现
- 完善脚本
- 界面友好/结构规范/代码的优化
- 代码简洁,执行效率高
第一个Shell脚本
新建一个 test.sh,扩展名sh代表 shell。
#!/bin/bash # 这是一条注释 echo 'hello world !';
- 第 1 行的
#!是一个约定的标记(读作 sha-bang),它告诉系统这个脚本需要什么解释器来执行,即要使用哪一种 Shell;后面的/bin/bash就是指明了解释器的具体位置(/bin/bash 后面不要有分号(;))。 - 第 2 行的#及其后面的内容是注释。Shell 脚本中所有以#开头的都是注释(当然以#!开头的除外)。写脚本的时候,多写注释是非常有必要的,以方便其他人能看懂你的脚本,也方便后期自己维护时看懂自己的脚本——实际上,即便是自己写的脚本,在经过一段时间后也很容易忘记。
- 第 3 行的 echo 命令用于向标准输出文件(Standard Output,stdout,一般就是指显示器)输出文本。在.sh文件中使用命令与在终端直接输入命令的效果是一样的。
如何运行Shell脚本
1. 将 Shell 脚本作为程序运行
通过这种方式运行脚本,脚本文件第一行的
#!/bin/bash一定要写对,好让系统查找到正确的解释器。

chmod +x表示给 test.sh 增加执行权限../表示当前目录,整条命令的意思是执行当前目录下的 test.sh 脚本。如果不写./,Linux 会到系统路径(由 PATH 环境变量指定)下查找 test.sh,而系统路径下显然不存在这个脚本,所以会执行失败。
2. 将 Shell 脚本作为参数传递给 Bash 解释器
bash 是一个外部命令,Shell 会在 /bin 目录中找到对应的应用程序,也即 /bin/bash。
将脚本文件的名字作为参数传递给 Bash (Bash | sh 都是外部命令)
不需要给脚本赋予执行权限,也不需要在第一行指定解释器信息
bash 可以简写 sh
echo 'hello world !';

3. 新的命令——source 命令
source 是 Shell 内置命令的一种,它会读取脚本文件中的代码,并依次执行所有语句。你也可以理解为,source 命令会强制执行脚本文件中的全部命令,而忽略脚本文件的权限。
使用 source 命令不用给脚本增加执行权限,并且写不写./都行
也不需要在脚本文件的第一行指定解释器信息
- 用法:
source fileName. fileName- 两种写法的效果相同。对于第二种写法,注意点号.和文件名中间有一个空格。

检测是否开启了新进程
Linux 中的每一个进程都有一个唯一的 ID,称为 PID,使用
$$变量就可以获取当前进程的 PID。$$是 Shell 中的特殊变量
- 新建一个 check.sh
#!/bin/bash # 输出当前进程 PID echo $$;

echo $$获取当前线程ID
总结
- 需要在新进程中运行 Shell 脚本,使用
bash test.sh这种写法。本质上等于给脚本赋予执行权限再运行 ./test.sh; - 在当前进程中运行 Shell 脚本,使用
source ./test.sh这种写法。 - bash test.sh === sh test.sh === 给脚本赋予权限再执行。
- source test.sh ==== . test.sh
Shell四种运行方式
- 交互式的登录 Shell
- 交互式的非登录 Shell
- 非交互式的登录 Shell
- 非交互式的非登录 Shell
判断 Shell 是否是交互式
- 查看变量
-的值,如果值中包含了字母i,则表示交互式(interactive)。 - 查看变量PS1的值,如果非空,则为交互式,否则为非交互式,因为非交互式会清空该变量。
- 终端输出
-

- 脚本中输出
-,必须在 新进程 中运行 Shell 脚本。

判断 Shell 是否为登录式
- 在 CentOS GNOME 桌面环境自带的终端下查看 login_shell 选项:

- 虚拟终端,输入用户名和密码登录后,再查看 login_shell 选项:

- 在 Shell 脚本文件中查看 login_shel 选项:

同时判断交互式、登录式
echo $PS1;shopt login_shell
或者
echo $-;shopt login_shell
常见的 Shell 启动方式
- 通过 Linux 控制台(不是桌面环境自带的终端)或者 ssh 登录 Shell 时(这才是正常登录方式),为交互式的登录 Shell。

- 执行 bash 命令时默认是非登录,非交互的,增加--login选项(简写为-l)后变成登录式。

- 使用由()包围的组命令或者命令替换进入子 Shell 时,子 Shell 会继承父 Shell 的交互和登录属性

- 在 Linux 桌面环境下打开终端时,为交互式的非登录 Shell。
Shell配置文件(配置脚本)的加载
无论是否是交互式,是否是登录式,Bash Shell 在启动时总要配置其运行环境,例如初始化环境变量、设置命令提示符、指定系统命令路径等。这个过程是通过加载一系列配置文件完成的,这些配置文件其实就是 Shell 脚本文件。
与 Bash Shell 有关的配置文件主要有 /etc/profile、~/.bash_profile、~/.bash_login、~/.profile、~/.bashrc、/etc/bashrc、/etc/profile.d/*.sh,不同的启动方式会加载不同的配置文件。
~表示用户主目录。*是通配符,/etc/profile.d/*.sh 表示 /etc/profile.d/ 目录下所有的脚本文件(以.sh结尾的文件)。

登录式的 Shell
如果是登录式的 Shell,首先会读取和执行
/etc/profiles,这是所有用户的全局配置文件,接着会到用户主目录中寻找~/.bash_profile,~/.bash_login或者~/.profile,它们都是用户个人的配置文件。
如果三个文件同时存在的话,到底应该加载哪一个呢?它们的优先级顺序是 ~/.bash_profile > ~/.bash_login > ~/.profile 。
如果 ~/.bash_profile 存在,那么一切以该文件为准,并且到此结束,不再加载其它的配置文件。
如果 ~/.bash_profile 不存在,那么尝试加载 ~/.bash_login。~/.bash_login 存在的话就到此结束,不存在的话就加载 ~/.profile。
- 注意,
/etc/profiles文件还会嵌套加载/etc/profile.d/*.sh,请看下面的代码:

- 同样,
~/.bash_profile也使用类似的方式加载~/.bashrc:

非登录的 Shell
如果以非登录的方式启动 Shell,那么就不会读取以上所说的配置文件,而是直接读取
~/.bashrc。
~/.bashrc 文件还会嵌套加载 /etc/bashrc,请看下面的代码:

如何编写自己的Shell配置文件(配置脚本)?
Shell 在登录和非登录时都会加载哪些配置文件了。对于普通用户来说,也许
~/.bashrc才是最重要的文件,因为不管是否登录都会加载该文件。
我们可以将自己的一些代码添加到 ~/.bashrc,这样每次启动 Shell 都可以个性化地配置。如果你有代码洁癖,也可以将自己编写的代码放到一个新文件中(假设叫 myconf.sh),只要在 ~/.bashrc 中使用类似. ./myconf.sh的形式将新文件引入进来就行了
实例1:给 PATH 变量增加新的路径
Shell 是怎样知道去哪里找到我们输入的命令的?例如,当我们输入 ls 后,Shell 不会查找整个计算机系统,而是在指定的几个目录中检索(最终在 /bin/ 目录中找到了 ls 程序),这些目录就包含在 PATH 变量中。
当用户登录 Shell 时,PATH 变量会在 /etc/profile 文件中设置,然后在 ~/.bash_profile 也会增加几个目录。如果没有登录 Shell,PATH 变量会在 /etc/bashrc 文件中设置。
如果我们想增加自己的路径,可以将该路径放在 ~/.bashrc 文件中,
PATH=$PATH:$HOME/addon
实例2:修改命令提示符的格式
修改 PS1 变量的值就可以修改命令提示符的格式,在
~/.bashrc文件中修改 PS1 变量的值就可以持久化,每个使用 Shell 的用户都会看见新的命令提示符。


调试 Shell 脚本
- 直接观察执行过程
- 执行中命令输出,报错信息
- 与用户的交互
- 开启调试模式
- sh -x 脚本文件
- 插入echo 断点
- echo '提示信息...'

#! /bin/bash
echo "1.创建名为 scapp 的用户账号";
# 如果用户已经存在会输出错误日志到 error.log
# 而不会出现到控制台
path="/home/logs";
file="error.log";
if [ ! -d $path ]; then
mkdir -p $path && cd $path && touch $file;
fi
adduser scapp 2>> /home/logs/error.log
echo "2.初始化密码为 123456 ";
echo 123456 | passwd --stdin scapp &> /dev/null
免交互及输出处理
需要交互的可执行操作
- passwd改密码
- ssh远程登录
- vim文本编辑器
- 图形化的安装/配置过程
- ...
passwd 改名的免交互
- 选项 --stdin
- 从标准输入读取密码字串
- 可以从键盘,也可以由另一个命令给出
echo 123456 | passwd --stdin scapp &> /dev/null
忽略无关输出
- 黑洞设备 /dev/null
- 相当于只能写入,不能读出的单向文件
- 存放到其中数据都会丢失
- 用法: 可以执行语句 &> /dev/null
echo 123456 | passwd --stdin scapp &> /dev/null
只记录错误输出
- 根据需求,可以将出错的信息保存在指定文件
- 针对后台脚本的有效排错手段
- 适用于不便于交互但又需要查看报错的情况
- 用法: 可以执行语句 2> /path/file.log
# 如果用户已经存在会输出错误日志到 error.log adduser scapp 2>> /home/logs/error.log
命令组合方式
顺序分隔
- 使用分号
- 命令1;命令2;命令3
- 依次执行,只有先后,没有逻辑关系
x=134;y=456 mkdir -p /home/logs;cd /home/logs
- 典型应用
systemctl restart vsftpd; systemctl eable vsftpd
逻辑 "与" 分隔
- 使用 &&
- 命令1 && 命令2 && 命令3
- 逻辑关系为 '而且' (and),期待所有命令都能执行成功
- 一旦某个命令出现失败,后续命令不再执行
echo "hello" && echo "world"
hello
world
- 典型应用:
- 源代码编译安装软件,编译,编译过程
make && make install
逻辑 "或" 分隔
- 使用 ||
- 命令1 || 命令2 || 命令3
- 逻辑关系为 '或者' (or),任何一条命令成功都符合期望
- 只在前面的命令失败时,后续才会执行
echo "hello" || echo "world"
hello
- 典型应用
- 针对前置命令失败的情况下,执行补救任务
id scapp 2>/dev/null || (adduser scapp && id scapp)
组合逻辑分隔
- 命令1 && 命令2 || 命令3
- 当命令1执行成功时,会继续执行命令2,忽略命令3
- 当命令1执行失败时,会执行命令3,忽略命令2
# 判断用户是否存在 id scapp &> /dev/null && echo YES || echo NO # 判断CPU是否支持虚拟化 grep -qe 'vmx|svm' /proc/cpuinfo && echo YES || echo NO
管道操作
-
使用 |
-
将命令的屏幕输出交给另一端的命令处理
- 命令1 | 命令2 | 命令3
- 后续命令要能正常处理传来的 文本 ,否则无意义
-
典型应用
- 分屏浏览所有网络设备信息
- 将列出的信息交给less命令进行分页查看
ifconfig -a | less
- 应用实例
- 使用 find 在 /etc 目录下递归查找,列出所有普通文件(find 的结果是一行一行的)
- 将查找结果交给 wc 进行统计行数
find /etc -type f | wc -l
- 应用实例
- 统计正处于监听状态的TCP端口数
- 使用 netstat 命令列出所有的TCP连接数
- 将查找的结果交给 grep 过滤,计算状态为 LISTEN 的连接数
netstat -anpt | grep -c "LISTEN"
命令组合运用
标准输入输出,一切皆是文件。
一切皆是文件
- Unix/Linxu的基本思想
- 普通文件,目录,键盘,鼠标,... 都是以文件的形式存在
- 要访问硬件设备,必须找对应的设备文件
I/O 交互设备
- 标准输入设备 : 从此设备接受用户的输入的数据
- 标准输出设备 : 通过此设备向用户报告正常的命令输出结果
- 标准错误设备 : 通过此设备报给执行中的出错信息
| 类型 | 设备文件 | 文件描述号 | 默认设备 |
|---|---|---|---|
| 标准输入 | /dev/stdin | 0 | 键盘 |
| 标准输出 | /dev/stdout | 1 | 显示器 |
| 标准错误 | /dev/stderr | 2 | 显示器 |
stdout 与 stderr
- 运行成功(正常),其屏幕信息报给 stdout 设备
- 运行失败(异常),其屏幕信息报给 stderr 设备
重定向操作
什么是重定向?
- 重新指定命令执行时 I/O 设备的方向
- 不使用默认的键盘,显示器
- 改用指定文本文件
| 类型 | 操作符 | 用途 |
|---|---|---|
| 重定向输入 | < | 将文本输入来源键盘改为指定文件 |
| 重定向输出 | > | 将命令行的正常执行输出保存到文件,而不是直接显示在显示器 |
| 重定向输出 | >> | 与 > 类似,但操作是追加而不是覆盖 |
| 重定向错误 | 2> | 将命令行的执行出错信息保存到文件,而不是直接显示在显示器 |
| 重定向错误 | 2>> | 与 2> 类似,但操作是追加而不是覆盖 |
| 混合重定向 | &> | 相当于 >和 2>,覆盖到同一文件 |
| 混合重定向 | &>> | 2>&1 >> | 相当于 >>和 2>>,追加到同一文件 |
重定向输入
- 键盘输入 ---> 文件输入
- 比如,使用 mail 命令发电子邮件时
# 提前写好邮件内容 vim /root/mail.txt # 读取邮件发送 mail -s "A Test Mail" root@localhost < /root/mail.txt
重定向输出
- 输出到显示器 ---> 输出到文件
- 比如,需要保存某个命令输出的结果时
# 覆盖目标文件 echo 'search example.com' > /etc/resolv.conf # 追加到目标文件 echo 'nameserver 8.8.8.8' >> /etc/resolv.conf
重定向错误
- 输出到显示器 ---> 输出到文件
- 比如,需要保存某个命令输出的结果时
id scapp 2> /home/logs/error.log
混合重定向
- 将正常,错误输出分别/合并重定向
- 比如,需要区分命令输出,或者合并输出的信息时
# 分别记入不同文件 ls -ld /rootx /root > /root/ls.log 2> /root/err.log # 同时追加记入一个文件 # 2>&1 把错误信息合并到标准输出 在追加或是覆盖文件 ls -ld /rootx /root &>> /root/info.log ls -ld /rootx /root >> /root/info.log 2>&1 ls -ld /rootx /root 2>&1 >> /root/info.log
变量
什么是变量?
- 以固定名称存放的可能变化的值
- 提高脚本对任务需求,运行环境变化的适应能力
- 在脚本执行中方便重复使用某个值
在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。这意味着,Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串,这一点和大部分的编程语言不同。当然,如果有必要,你也可以使用 Shell
declare关键字显式定义变量的类型。
定义/赋值变量
- 基本格式
- 变量名=变量值
- 相关注意事项
- 若指定的变量名已经存在,相当于为此变量重新赋值
- 等号两边不要有空格
- 变量名由字母/数字/下划线组成,区分大小写
- 变量名不能以数字开头,不要使用关键字和特殊字符
x=123 var1=CentOS user_name=scapp VAR1=7.2 a=10;b=30 c=20 d=40
查看/引用变量
- 基本格式
- 引用变量值: $变量名
- 查看变量值:
echo $变量名|echo ${变量名} - 推荐给所有变量加上花括号{ },这是个良好的编程习惯。
echo $x
echo $var1
echo $user_name
echo $VAR1
echo ${var1}7.2
echo $a $b
echo $c,$d
取消变量
- 自定义变量的失效
- 退出定义变量的shell环境,变量会自动失效
- 在环境内也可以手动取消 : unset 变量名
unset x
变量的分类角度
- 存储类型
- 整数型,浮点型,双精度浮点型,字符型,...
- Shell 不作为高级编程语言,对存储类型的要求比较松散
使用类型
| 类型 | 说明 |
|---|---|
| 环境变量 | 变量名通常都大写,由系统维护,用来设置工作环境,其中之只有个别变量用户可以直接更改 |
| 位置变量 | 由bash内置,用来存储在执行脚本时提供命令行参数 |
| 预定义变量 | 由bash内置,一般有特殊用途的变量,可以直接调用,但不能赋值或修改 |
| 自定义变量 | 由用户自主设置,修改及使用 |
环境变量
-
配置文件
- /etc/profile
- /etc/profile.d/*.sh
- ~/.bash_profile
- 推荐使用 在 /etc/profile.d/ 下创建Shell脚本来定义环境变量
-
相关配置
- env 显示与当前用户相关的环境变量,还可以让命令在指定环境中运行。
- set 显示当前 Shell 所有变量,包括其内建环境变量(与 Shell 外观等相关),用户自定义变量及导出的环境变量。
- export 显示从 Shell 中导出成环境变量的变量,也能通过它将自定义变量导出为环境变量。
# 使用 vimdiff 工具比较一下它们之间的差别: temp=this_temp_flag export temp_env=this_env_temp_flag env|sort > ent.txt set|srot > set.txt export|sort > export.txt vimdiff ent.txt set.txt
- 常见的环境变量
- PWD,PATH,USER,LOGNAME,UID,LANG,RANDOM
- SHELL,HOME,PS1,PS2

-
PS1
- 命令提示字符格式。就是我们命令行最前面的命令提示符,可以修改。不同的Linux版本与系统可能内容会不同。
字符 说明 \d 可显示出【星期\月\日】这种格式,例如:Mon Feb 2 \h 仅取主机名在第一个小数点之前的名字 \H 完整的主机名 \t 显示时间。为24小时格式HH:MM:SS \T 显示时间,为12小时格式HH::MM:SS \A 显示时间,为24小时格式的HH:MM @ 显示时间,为12小时格式的am/pm格式 \u 目前用户的账号名称,如dongshao \v BASH的版本信息。例如:4.2.46版本的,仅取4.2显示 \w 完整的工作目录 \W 利用basename函数取得工作目录名称,所以仅会列出最后一个目录名 # 会显示一个数字,表示当前执行的命令为第几次执行的命令 $ 提示字符,如果是root时,提示字符为#,否则就为$
位置变量
- 在执行脚本的时候提供命令行参数
- 表示为 $n,n为序号
$1,$2,$3, ... ${10},${11}, ...

- 在运行添加用户脚本,指定用户名

预定义变量
- 用来保存脚本程序执行信息
- 直接使用这些变量
- 不能直接为为这些变量赋值
| 变量名 | 含义 |
|---|---|
| $0 | 当前所在的进程或脚本名 |
| $$ | 当前运行进程的PID号 |
| $? | 命令执行后的返回状态值,0表示正常,1或其他值异常 |
| $# | 已加载的位置变量的个数 |
| $* | 以一个单字符串显示所有向脚本传递的参数 |
| $@ | 与以上相同,但是使用时加引号,并在引号中返回每个参数 |
$* 与 $@ 的区别
- 相同的
- 都是引用所有参数
- 不同的
- 只有在双引号中体现出来。假设在脚本运行中写了三个参数 1,2,3 ,则"*"等价于"1 2 3" (传递了一个参数),而 "@" 等价于 "1" "2" "3" (传到了三个参数)

$? 获取上一个命令的退出状态
新建 exit_test.sh
#!/bin/bash if [ "$1" == 100 ] then # 参数正确,退出状态为0; exit 0; else # 参数错误,退出状态为1; exit 1; fi

exit 表示退出当前 Shell 进程,我们必须在新进程中运行 test.sh,否则当前 Shell 会话(终端窗口)会被关闭,我们就无法取得它的退出状态了。
$? 获取函数的返回值
新建 sum.sh
function sum() {
return $[$1+$2]
}
sum 30 50
echo $?

Shell 函数中的 return 关键字用来表示函数的退出状态,而不是函数的返回值;Shell 不像其它编程语言,没有专门处理返回值的关键字。
双引号与单引号
- "字符串..."
- 在双引号内允许 $ 扩展,可调用其他变量的值
- 出现特殊字符时,可采用 \ 符号转义
- 当变量值不包括空格,制表符时,双引号通常省略
- '字符串'
- 所有字符均为该字符串本身(无特殊含义)
- 不允许 \ 转移
利用双引号赋值
将变量A的赋值给变量B

有空格时,双引号不可以省略

常见的转义字符
- 普通字符 <--> 特殊字符
- \n 换行
- \t 制表位
\\,\",\#,\$
添加 -e 选项解析特殊转义字符

利用单引号赋值
- 有利于保留特殊字符

变量值及范围控制
read 取值的用法
- 基本格式
- read 变量名 ...
- read -p '提示信息' 变量名 ...

- 静默取值
- 启用 read -s 选项
- 关闭输入回显,(从键盘输入信息时,不显示在屏幕上)
- 适合读入敏感信息,提高安全性
read -s -p 'Pass_word:' your_pass
变量的作用范围
Shell 变量的作用域可以分为三种:
- 有的变量只能在函数内部使用,这叫做局部变量(local variable);
- 有的变量可以在当前 Shell 进程中使用,这叫做全局变量(global variable);
- 而有的变量还可以在子进程中使用,这叫做环境变量(environment variable)。
局部变量
Shell 也支持自定义函数,在 Shell 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果。
- 新建一个 func_.sh
#!/bin/bash
function localVar() {
a=99
}
localVar
echo $a;

输出结果:99
a 是在函数内部定义的,但是在函数外部也可以得到它的值,证明它的作用域是全局的,而不是仅限于函数内部。
要想变量的作用域仅限于函数内部,可以在定义时加上local命令,此时该变量就成了局部变量。
#!/bin/bash
function localVar() {
local a=99
}
localVar
echo $a;

输出结果为空,表明变量 a 在函数外部无效,是一个局部变量。
全局变量
所谓全局变量,就是指变量在当前的整个 Shell 进程中都有效。(global variable);每个 Shell 进程都有自己的作用域,彼此之间互不影响。 在 Shell 中定义的变量,默认就是全局变量。
想要实际演示全局变量在不同 Shell 进程中的互不相关性,可在图形界面下同时打开两个 Shell,或使用两个终端远程连接到服务器(SSH)。


- 一个 Shell 窗口,定义一个变量 a 并赋值为 100,然后打印,Shell 窗口中是可正确打印变量 a 的值的。
- 然后再打开一个新的 Shell 窗口,同样打印变量 a 的值,但结果却为空;
- 这说明全局变量 a 仅仅在定义它的第一个 Shell 进程中有效,对新的 Shell 进程没有影响。
这很好理解,就像小王家和小徐家都有一部电视机(变量名相同),但是同一时刻小王家和小徐家的电视中播放的节目可以是不同的(变量值不同)。
需要强调的是,全局变量的作用范围是当前的 Shell 进程,而不是当前的 Shell 脚本文件,它们是不同的概念。在一个 Shell 进程中可以使用 source 命令执行多个 Shell 脚本文件,此时全局变量在这些脚本文件中都有效。
- 新建 a.sh 文件,内容以下
echo $a; b=200
- 新建 b.sh 文件,内容以下
echo $b
- 在同一个进程中执行

- 在当前线程中定义a的变量值为99。
- 在a脚本里获取a变量的值,新建b变量赋值200。
- 在b的脚本里获取b的变量的值。
这三条命令都是在一个进程中执行的,从输出结果可以发现,在 Shell 窗口中以命令行的形式定义的变量 a,在 a.sh 中有效;在 a.sh 中定义的变量 b,在 b.sh 中也有效,变量 b 的作用范围已经超越了 a.sh。
- 在不同进程中运行

这三条命令都是在不同进程中执行的,a.sh 无法输出全局变量 a 的值,b.sh 无法输出全局变量 b 的值.
环境变量
全局变量只在当前 Shell 进程中有效,对其它 Shell 进程和子进程都无效;使用export命令将全局变量导出,那么它就在所有的子进程中也有效了,这称为"环境变量" (environment variable)
环境变量被创建时所处的 Shell 进程称为父进程,如果在父进程中再创建一个新的进程来执行 Shell 命令,那么这个新的进程被称作 Shell 子进程。当 Shell 子进程产生时,它会继承父进程的环境变量为自己所用,所以说环境变量可从父进程传给子进程。不难理解,环境变量还可以传递给孙进程。
注意,两个没有父子关系的 Shell 进程是不能传递环境变量的,并且环境变量只能向下传递而不能向上传递,即“传子不传父”。
创建 Shell 子进程最简单的方式是运行 bash 命令:

通过 exit 命令可以一层一层的退出 Shell;
演示一下环境变量的使用:

可以发现,默认情况下,a 在 Shell 子进程中是无效的;使用 export 将 a 导出为环境变量后,在子进程中就可以使用了。
通过 export 导出的环境变量只对当前 Shell 进程以及所有的子进程有效,如果最顶层的父进程被关闭了,那么环境变量也就随之消失了,其它的进程也就无法使用了,所以说环境变量也是临时的。
让一个变量在所有 Shell 进程中都有效,不管它们之间是否存在父子关系,该怎么办呢?
只有将变量写入 Shell 配置文件中才能达到这个目的!Shell 进程每次启动时都会执行配置文件中的代码做一些初始化工作,如果将变量放在配置文件中,那么每次启动进程都会定义这个变量。
发布 环境变量
- export 局部变量名[=变量值] ...
- 直接定义/赋值指定的变量,作为环境变量发布
- 对已有的局部变量,只需发布不要赋值

取消 环境变量
- export -n 全局变量名
- 取消变量的全局属性
- 此操作对局部变量无实际意义

数值运算及处理
整数运算操作
| 算术运算符 | 说明/含义 |
|---|---|
| +、- | 加法(或正号)、减法(或负号) |
| *、/、% | 乘法、除法、取余(取模) |
| ** | 幂运算 |
| ++、-- | 自增和自减,可以放在变量的前面也可以放在变量的后面 |
| !、&&、|| | 逻辑非(取反)、逻辑与(and)、逻辑或(or) |
| <、<=、>、>= | 比较符号(小于、小于等于、大于、大于等于) |
| ==、!=、= | 比较符号(相等、不相等;对于字符串,= 也可以表示相当于) |
| <<、>> | 向左移位、向右移位 |
| ~、|、 &、^ | 按位取反、按位或、按位与、按位异或 |
| =、+=、-=、*=、/=、%= | 赋值运算符,例如 a+=1 相当于 a=a+1,a-=1 相当于 a=a-1 |
数学计算命令
| 运算命令 | 说明 |
|---|---|
| (( )) | 用于整数运算,效率很高,推荐使用。 |
| let | 用于整数运算,和 (()) 类似。 |
| $[] | 用于整数运算,不如 (()) 灵活。 |
| expr | 可用于整数运算,也可以处理字符串。比较麻烦,需要注意各种细节,不推荐使用。 |
| bc | Linux下的一个计算器程序,可以处理整数和小数。Shell 本身只支持整数运算,想计算小数就得使用 bc 这个外部的计算器。 |
| declare -i | 将变量定义为整数,然后再进行数学运算时就不会被当做字符串了。功能有限,仅支持最基本的数学运算(加减乘除和取余),不支持逻辑运算、自增自减等,所以在实际开发中很少使用。 |
对整数进行数学运算
Shell (( )) 的用法 (推荐使用)
- 将数学运算表达式放在括号里,
((表达式)) - 表达式可以只有一个,也可以有多个,多个表达式之间以逗号,分隔。对于多个表达式的情况,以最后一个表达式的值作为整个 (( )) 命令的执行结果。
- 可以使用 \$ 获取 (()) 命令的结果,这和使用$获得变量值是类似的
echo $x,echo $((x+y))。
#可以在计算完成后给变量赋值 #使用变量时不用加$前缀,(()) 会自动解析变量名 ((a=10+20)); echo $a; ((b=20+30)); echo $b; ((c=a+b)); echo $c; #可以在 (()) 前面加上$符号获取 (()) 命令的执行结果,也即获取整个表达式的值 d=$((1+2)); echo $d; e=$((2+3)); echo $e; f=$((d+e)); echo $f; # (()) 也可以进行逻辑运算,在 if 语句中常会使用逻辑运算 if ((a>15 && b<60)); then echo 'true'; else echo 'false'; fi # 需要立即输出表达式的运算结果时 echo $((c+f)); # 对多个表达式同时进行计算 ((i=3+7,j=2,j+=i,k=i+j)) echo $i; echo $j; echo $k; #进行稍微复杂一些的综合算术运算 ((s=1+2**3-4%3)) echo $s;
实例演示
利用 (()) 进行逻辑运算。
if ((8>7&&5<6)); then echo 'true'; else echo 'false'; fi
用 (( )) 进行稍微复杂一些的综合算术运算
a=$((100*(100+1)/2)); echo $a;
利用 (( )) 进行自增(++)和自减(--)运算。
a=10; # 10 echo $((a++)); b=10; # 11 echo $((++b)); c=10; # 10 echo $((c--)); d=10; # 9 echo $((--c));
- 前自增(前自减)和后自增(后自减)的区别也非常清楚。
- 执行
echo $((a++))和echo $((a--))命令输出整个表达式时,输出的值即为 a 的值,表达式执行完毕后,会再对 a 进行 ++、-- 的运算。 - 而执行
echo $((++a))和echo $((--a))命令输出整个表达式时,会先对 a 进行 ++、-- 的运算,然后再输出表达式的值,即为 a 运算后的值。
使用 let 命令操作变量
- 操作变量运算,并保存新结果
- 适用于不需要输出的情况,循环使用
- 若要查看结果,可结合 echo 命令
Shell let 命令实例演示
给变量 i 加 8
i=2 let i+=8 echo $i

let 后面可以跟多个表达式
a=10 b=35 let a+=6 c=a+b echo $a $c c=10;d=35 let c+=6,e=c+d echo $c,$e

计算并获取结果
使用 expr 命令工具
- 格式 : expr n1 运算符 n2 ...
- 乘法操作应采用
\*转义,避免被作为Shell通配符 - 计算结果直接显示在屏幕上
- 混合运算,乘除优先
- 运算符和变量之间必须有一个空格

算式替换 $[]
- 使用
$[]表达式 - 格式 :
$[整数1 运算符 整数2 ...] - 乘法操作无需转义,运算符两侧可以无空格,引用变量可省略
$符号 - 计算结果替换表达式本身,结合
echo命令才能显示到屏幕上 - 不能单独使用
$[],必须能够接收$[]的计算结果。 - 还支持乘方运算
$[10**3]10的三次方
echo $[3*5]

自增表达式
- 先运算,将运算结果保存到这个变量
- 后取值,以这个变量的新值作为整个表达式的值
| 运算类别 | 对应的表达式 | 应用示例 |
|---|---|---|
| 加法 | i+=2 或 i=i+2 | echo $[i+=2] |
| 减法 | i-=2 或 i=i-2 | echo $[i-=2] |
| 乘法 | i=2 或 i=i2 | echo $[i*=2] |
| 除法 | i/=2 或 i=i/2 | echo $[i/=2] |
| 求模 | i%=2 或 i=i%2 | echo $[i%=2] |
i+=1的极简表示
- 当变量自增/减的数量为1时
- 数量1可以省略,+= 可以写成 ++ ,-= 可以写成 --
- 比如: ++i 相当于 i+=1,--i 相当于 i-=1

++i 与 i++ 的区别 (主要是表达式的值)
- 运算符在前,++i 或 --i
- 先运算,将运算结果保存到这个变量
- 后取值,调用这个变量的新值

- 运算符在后,i++ 或 i--
- 先取值,调用这个变量的旧值
- 后运算,将运算结果保存在这个变量

使用随机整数
环境变量 RANDOM
- 32767在计算机中的存储形式:0111 1111 1111 1111
- 在
/bin/bash环境可直接使用 - 每次调用时,随机生成0~32767之间的一个整数
- 结合 echo 命令可查看结果
扩大随机数范围
- 生成2个随机数,并计算相乘的结果
- 乘积结果的范围: 0 ~ 32767*32767
- 结合
$[]

减少随机数范围
- 将随机数与指定的阈值相除(取模运算是求两个数相除的余数),取余数(余数的取值范围 0 <= 0 < 除数)
- 比喻:%1000 ,则求模结果的范围:0 ~ 999
- 结合
$[]

限制随机数区间
- 将求模减小后的随机数与指定基数相加,取最终结果
- 比如 0 ~ 999 的随机数,再加1,则结果范围:1 ~ 1000
- 结合
$[]

整数序列
使用 seq 命令
- 根据指定条件输出一组整数
- 默认从整数 1 开始
- 缺省的增量步长也是1
- 格式:
- seq 末值
- seq 首值 末值
- seq 首值 步长 末值
- -f, --format=格式 默认格式为'%g'
- -s, --separator=字符串,使用指定的字符串分割数字(默认使用 换行符(\n)分割)
- -w, --sequal-width 在前面添加 0 使用等宽相同
seq 5 seq 1 5 seq 1 2 10 seq -s '|' 5 seq -s '#' 5 seq -s '+' 5 seq -s "`echo -e "\t"`" 5 seq -f '%g' 10 seq -f '%02g' 6 10 seq -f '%03g' 6 10 seq -f 'dir_%03g' 6 10 mkdir $(seq -f 'dir%03g' 1 10)


数值运算的局限性
- Bash 内建机制仅支持整数运算
expr命令,$[]表达式不支持小数运算

使用 bc 计算器的免交互
- 结合管道向
bc发动表达式 - 通过
echo命令 + 管道传递要计算的表达式

小数值的比较
- 基本用法
- echo "数值 比较符 数值2" | bc
- 如果表达式成立,则返回的计算结果为1,否则返回0
- 常见比较: >,>=,<,<=,==,!=

字符串处理
获取字符串长度
- 基本用法
${#String_name}
#!/bin/bash
str='hello world';
echo ${#str}

字符串拼接
#!/bin/bash
name='shell';
url='http://i.cnblogs.com/'
# 中间不可以有空格
str1=$name$url;
# 如果被双引号包围,那么中间可以有空格
str2="$name $url";
# 中间可以出现其他字符串
str3=$name":"$url;
str4="$name : $url";
# 这个时候需要给变量加上大括号
str5="${name}Script:${url}index.html";
echo $str1;
echo $str2;
echo $str3;
echo $str4;
echo $str5;

截取子字符串
使用 expr 命令
- 基本用法
- expr substr $var1 起始位置 截取长度
var1="CentOS7.2" expr substr $var1 1 6 expr substr $var1 7 3

使用 cut 命令
- 基本用法
- 命令输出 | cut -c 起始位置-结束位置
- 命令输出 | cut -d '分割符' -f 字段编号
var1="CentOS7.2" echo $var1 | cut -c 5-6 # 缺省从 1 开始 echo $var1 | cut -c -6 echo $var1 | cut -d "t" -f 2

使用 ${} 表达式
- 基本用法
- 从左边计数
- ${var1:起始位置:截取长度}(左边起始位置的编号为 0)
- $
- 从右边计数
- ${var1:0-start:length}(右边起始位置编号为 1)

指定字符(子字符串)开始截取
- 使用 # 号截取右边字符
- 基本用法:
${string#chars}${string#*chars}${string##*chars}
string 表示要截取的字符,chars 是指定的字符(或者子字符串),是通配符的一种,表示任意长度的字符串。chars连起来使用的意思是:忽略左边的所有字符,直到遇见 chars。如果没有通配符,必须精确匹配字符串。
如果希望直到最后一个指定字符(子字符串)再匹配结束,那么可以使用##。
url='https://www.cnblogs.com/shareeverything/p/15864186.html';
# 指定字符串 https://,精确匹配字符串
echo ${url#https://};
# * 通配符 表示任意长度的字符串
echo ${url#*//};
# 以 / 分隔
echo ${url#*/};
# 以上写法遇到第一个匹配的字符(子字符串)就结束了
echo ${url##*/}

- 使用 % 截取左边字符
- 基本用法:
${string%chars}${string%chars*}${string%%chars*}
url='https://www.cnblogs.com/shareeverything/p/15864186.html';
echo ${url%/*}
echo ${url%%/*}
echo ${url%%/s*}
请注意的位置,因为要截取 chars 左边的字符,而忽略 chars 右边的字符,所以应该位于 chars 的右侧。其他方面%和#的用法相同

字符串替换
使用 ${} 表达式
- 基本用法
- ${var1/old/new} (第一个匹配到的被替换)
- ${var1//old/new} (整个字符匹配到的替换)
var1="CentOS7.2 Server"
echo ${var1/CentOS/RHEL}
echo ${var1/e/#}
echo ${var1//e/#}

tr 单字替换工具
- 基本用法
- 命令输出 | tr 'abc' 'ABC' (将字符 a,b,c 替换成 A,B,C)
- 命令输出 | tr -d 'abc' (-d:删除字符)
# 将 o,r 字符替换成 大写 head -1 passwd.txt | tr 'or' 'OR' # 删除所有的 :字符 head -1 passwd.txt | tr -d ':'

Shell 数组
Shell 数组的定义
在 Shell 中,用括号( )来表示数组,数组元素之间用空格来分隔
- 一般形式
- arr_name=(ele1 ele2 ele3 ...)
- 注意,赋值号=两边不能有空格,必须紧挨着数组名和数组元素。
- Shell 是弱类型的,它并不要求所有数组元素的类型必须相同
- Shell 数组元素的下标也是从 0 开始计数
nums=(29 30 31 32 100 33);
- Shell 是弱类型的,它并不要求所有数组元素的类型必须相同
arr=(11 33 "www" 'https' "//" );
获取数组长度
nums=(29 30 31 32 100 33);
echo ${#nums[@]};
echo ${#nums[*]};

- Shell 数组的长度不是固定的,定义之后还可以增加元素
nums=(29 30 31 32 100 33);
echo ${#nums[@]};
echo ${#nums[*]};
nums[6]=99;
nums[11]=99;
echo ${#nums[@]};
echo ${#nums[*]};

获取数组元素或修改元素(下标从零开始)
nums=(29 30 31 32 100 33);
# 31
echo ${nums[2]};
item=${nums[3]}
# 32
echo $item
# 修改
nums[2]=55;
echo ${nums[2]};
# 获取所有元素
echo ${nums[@]};
echo ${nums[*]};

删除数组元素
nums=(29 30 31 32 100 33);
unset nums[1];
echo ${nums[@]};
# 删除整个数组
unset nums;

Shell数组合并
arr1=(1 3 5);
arr2=("2" "4" "6");
newArr1=(${arr1[@]} ${arr2[@]});
newArr2=(${arr1[@]} ${arr2[@]});

Shell关联数组(键值对(key-value))
- 关联数组必须使用带有 -A 选项的 declare 命令创建
declare -A member;
member["name"]="echo";
member["age"]="27";
member["gender"]="男";
member["hobby"]="?";
echo '---------获取长度--------------------------------';
echo ${#member[@]};
echo ${#member[*]};
echo '---------获取所有元素--------------------------------';
echo ${member[@]};
echo ${member[*]};
echo '---------获取所有下标--------------------------------';
echo ${!member[@]};
echo ${!member[*]};
echo '---------获取指定下标元素-------------------------';
# 获取指定下标元素
echo ${member["name"]};
echo ${member["age"]};
echo ${member["gender"]};
echo '--------------------------------';
for val in ${member[@]}
do
echo $val;
done
echo '--------------------------------';
for val in ${!member[@]}
do
echo $val;
done

路径分割
- 文件路径 /etc/passwd 的含义
- 文件所在位置:/etc
- 文件名称: passwd
- 目录路径 /boot/grub 的含义
- 目录所在位置:/boot
- 目录名称: grub
dirname 获取目录名称
- 基本用法
- dirname "字符串"
var1="/etc/passwd" dirname $var1

basename 获取基本名称
- 基本用法
- basename "字符串"

随机字符串
- 常见的随机工具
- 随机数变量:RANDOM
- 特殊设备文件:/dev/urandom | /dev/random (会阻塞)
- UUID生成命令:uuidgen
- od 以指定格式查看文件内容。
echo $RANDOM head -1 /dev/urandom | od -tx

/dev/urandom 与 /dev/random 区别
/dev/random和/dev/urandom是Linux系统中提供的随机伪设备,这两个设备的任务,是提供永不为空的随机字节数据流。很多解密程序与安全应用程序(如SSH Keys,SSL Keys等)需要它们提供的随机数据流。
这两个设备的差异
-
/dev/random 的 random pool 依赖于系统中断,因此在系统的中断数不足时,/dev/random设备会一直封锁,尝试读取的进程就会进入等待状态,直到系统的中断数充分够用, /dev/random 设备可以保证数据的随机性。
-
/dev/urandom 不依赖系统的中断,也就不会造成进程忙等待,但是数据的随机性也不高。
-
使用cat 命令可以读取/dev/random 和/dev/urandom的数据流(二进制数据流,很难阅读),可以用od命令转换为十六进制后查看.
-
推荐使用 :别问,问就是用 /dev/urandom
随机信息的转换
md5sum校验工具- 根据输入文本计算出256位(32字符)的MD5编码值
- 输入文本有任何差异,编码结果都会不相同
echo $RANDOM | md5sum head -1 /dev/urandom | md5sum

截取随机字符串
- 使用 cut 命令
- 直接利用随机信息的md5sum 转换结果
- 任意截取32以内的连续字符串
# 取8位 echo $RANDOM | md5sum | cut -c -8 # 16 echo $RANDOM | md5sum | cut -c -16 # 32 echo $RANDOM | md5sum | cut -c -32 uuidgen | tr -d "-" | cut -c -8 uuidgen | tr -d "-" | cut -c -16 uuidgen | tr -d "-" | cut -c -32

命令替换
提取命令输出
- 简化运维任务,在命令行嵌入另一条命令的输出结果
- 将命令的输出保存到指定的变量
使用 `` 反撇号
- 基本用法:
- `可执行命令`

使用 $() 表达式
- 基本用法
- $(可执行命令)
- 可嵌套使用

应用示例
- 列出/boot/下超过3M的文件详细属性
ll -ah | $(find /boot -size +3M)
- 记录一个8位的随机密码,并为用户stu01设置此密码
stu01_pwd=$(uuidgen | cut -c -8) echo $stu01_pwd adduser stu01 echo $stu01_pwd | passwd --stdin stu01
条件测试
脚本的识别能力
- 为命令执行提供的最直接的识别依据
- 前一条命令是否成功
- 文件或目录是否存在
- 数值大小
- 字符串是否匹配
- 是否在某个时间点
- 当前用户是否是 root 用户
- ...
返回状态值 $?
- 判断前一天命令是否执行成功?
- 查看 $? 的值,如果是 0,表示前一天命令正常完成
- 若 $? 的值为1或者其他数值,表示前一条命令失败(或异常)
$? 状态值的应用
- 判断当前系统的内核版本
- 利用grep 过滤,如果符合条件则 $? 的状态值为0
- 否者 $? 的值不为 0
uname -r | grep -q ^3.1;echo $? uname -r | grep -q ^3.0;echo $?

- 作为逻辑分隔 &&,|| 的依据
uname -r | grep -q ^3.1 && echo YES || echo NO

专用测试工具 test
- 语法格式
- test 选项 参数
- [ 选项 参数 ... ]
文件测试
- 基本用法
- test 选项 文件或目录
- [ 选择 文件或目录 ](括号两侧必须有空格)
文件类型测试
| 参数 | 说明 |
|---|---|
| -e(Exist) 文件名 | 如果文件存在则为真 |
| -c(Charater type) filename | 如果文件存在且为字符型特殊文件则为真 |
| -d(Directory) filename | 如果文件存在且为目录则为真 |
| -b(Block) filename | 如果文件存在且为块特殊文件则为真 |
| -L (link) filename | 判断文件是否存在,并且是否为符号链接文件 |
| -S (socket) filename | 判断该文件是否存在,并且是否为套接字文件 |
| -p (pipeline) filename | 判断文件是否存在,并且是否为管道文件。 |
| -s(Single Character) filename | 如果文件存在且至少有一个字符则为真 |
| -f(File) filename | 如果文件存在且为普通文件则为真 |
文件权限测试
| 参数 | 说明 |
|---|---|
| -r(Read) filename | 如果文件存在且可读则为真 |
| -w(Write) filename | 如果文件存在且可写则为真 |
| -x(Excute) filename | 如果文件存在且可执行则为真 |
| -u filename | 判断文件是否存在,并且是否拥有 SUID 权限 |
| -g filename | 判断文件是否存在,并且是否拥有 SGID 权限 |
| -k filename | 判断该文件是否存在,并且是否拥有 SBIT 权限 |
文件比较
| 参数 | 说明 |
|---|---|
| filename1 -nt filename2 | 判断 filename1 的修改时间是否比 filename2 的新 |
| filename -ot filename2 | 判断 filename1 的修改时间是否比 filename2 的旧 |
| filename1 -ef filename2 | 判断 filename1 是否和 filename2 的 inode 号一致,可以理解为两个文件是否为同一个文件。这个判断用于判断硬链接是很好的方法 |
// 测试成功,表示存在此文件 test -f /etc/hosts;echo $? // 测试失败,表示没有可以执行权限 test -x /etc/hosts;echo $? [ -f /etc/hosts ];echo $? [ -x /etc/hosts ];echo $?

- 检测对目标用户是否有写入权限
- root用户比较特殊,与文件的 W 权限无直接关系,因为root 有能力更改权限
- 所以检测一个文件的 W 权限最好不要用 root 用户

数值测试
- 基本用法
- test 数值1 选项 数值2
- [ 数值1 选项 数值2 ]
| 参数 | 说明 |
|---|---|
| -eq(Equal) | 等于则为真 |
| -ne(Not Equal) | 不等于则为真 |
| -gt(Greater Than) | 大于则为真 |
| -ge(Greater or Equal) | 大于等于则为真 |
| -lt(Lesser Than) | 小于则为真 |
| -le(Lesser or Equal) | 小于等于则为真 |
用法示例
- 检查开启的进程数是否超过100个
// 将进程数保存到变量 pnum=$(pgrep '' | wc -l) echo $pnum // 将变量与阈值比较 [ $pnum -ge 100 ] && echo YES || echo NO

- 检查登录用户是否少于10个
who | wc -l [ $(who | wc -l) -lt 10 ] && echo YES || echo NO

字符串测试
| 参数 | 说明 |
|---|---|
| = | 等于则为真 |
| != | 不相等则为真 |
| -z 字符串 | 字符串的长度为零则为真 |
| -n 字符串 | 字符串的长度不为零则为真 |
#!/bin/bash var1="CentOS7.2"; var2=""; #空格 var3=" "; echo '测试字符串 var1 ' [ -z $var1 ] && echo "ZORE" || echo "NOT ZORE"; echo '测试字符串 var2 ' [ -z $var2 ] && echo "ZORE" || echo "NOT ZORE"; echo '测试字符串 var3 ' [ -z "$var3" ] && echo "ZORE" || echo "NOT ZORE"; echo '测试字符串 var3 ' [ -n "$var3" ] && echo "NOT ZORE" || echo "ZORE"; # = 左右必须有空格,$SHELL 全局变量 [ $SHELL = "/bin/sh" ] && "相等" || echo "不相等" #

另外Shell还提供了三个逻辑操作符将条件连接起来
| 参数 | 说明 |
|---|---|
| -a | 或 |
| -o | 与 |
| ! | 非(取反) |
test -e ./notFile -o -e ./bash # ! 取反为真 [ ! 100 -gt 200 ] && echo YES # -o 或 有一个为真就是真 [ 100 -gt 200 -o 100 -lt 200 ] && echo YES # -a 与 两个条件都为真 [ 100 -gt 200 -a 100 -lt 200 ] && echo YES || echo NO

专用测试工具 [[]]
[[ ]]是 Shell 内置关键字,它和 test 命令类似,也用来检测某个条件是否成立。- 当
[[ ]]判断 expression 成立时,退出状态为 0,否则为非 0 值。 - 注意
[[ ]]和 expression 之间的空格,这两个空格是必须的,否则会导致语法错误。 - 不需要把变量名用双引号""包围起来,即使变量是空值,也不会出错
- 不需要、也不能对 >、< 进行转义,转义后会出错。
read str1; read str2; if [[ -z $st1 || -z $str2 ]]; then echo "字符串不能为空" elif [[ $str1 > $str2 ]]; then echo "str1 > str2"; else echo "str1 < str2"; fi
[[ ]] 支持逻辑运算符
test 或 [] |
Boolean | [[ ]] |
Boolean |
|---|---|---|---|
[ -z "$str1" ] || [ -z "$str2" ] |
√ |
[[ -z $str1 ]] || [[ -z $str2 ]] |
√ |
[ -z "$str1" -o -z "$str2" ] |
√ |
[[ -z $str1 -o -z $str2 ]] |
× |
[ -z $str1 || -z $str2 ] |
× |
[[ -z $str1 || -z $str2 ]] |
√ |
流程控制
if选择的分支结构
if 单分支结构
- 当 '条件成立' 时执行命令序列(命令可以时多个,用分号隔开
- 否则,不执行任何操作
if 单分支案例
- 检测是否存在文件夹,并创建文件。
#! /bin/bash if [ ! -d "/home/www/logs" ];then mkdir -p /home/www/logs && cd /home/www/logs && touch err.log; fi
if 双分支结构
- 当条件成立时执行命令序列1
- 否之,执行命令序列2
if 双分支案例
- 检测并判断指定主机是否可ping通
- 主机地址从命令行提供
#! /bin/bash ping -c 3 -i 0.2 -W 3 $1 &> /dev/null if [ $? -eq 0 ];then echo "Host $1 is up." else echo "Host $1 is down." fi

if多分支结构
- 相当于if语句的嵌套
- 针对多个条件分别执行不同的操作
if 多分支案例
- 从键盘输入一个分数,判读成绩分档
- 区分出优秀(85-100),合格(70-84),不及格(低于70分)
#! /bin/bash read -p '请输入分数 : ' scale if [ $scale -ge 85 ];then echo '优秀'; elif [ $scale -ge 70 -a $scale -lt 84 ];then echo '合格'; else echo '不及格' fi
case分支
case分支应用
- 判断用户输入的字符类型
#! /bin/bash read -p '请输入一个字符:' key; case "$key" in [a-Z]) echo '字母'; ;; [0-9]) echo '数字'; ;; *) echo '空格,功能键盘或其他控制字符'; ;; esac
for 循环
for循环案例
- 批量添加用户
- 这里有很多问题(用户已存在,密码被修改)
#! /bin/bash userList=$(cat ./users.txt) for item in $userList do adduser $item echo "123456" | passwd --stdin $item &> /dev/null done
- 批量删除
#! /bin/bash userList=$(cat ./users.txt) for item in $userList do userdel -r $item done
for + if 组合应用
- 批量添加用户
- 用户不存在,才执行添加及修改密码
#! /bin/bash userList=$(cat ./users.txt) for item in $userList do id $item &> /dev/null if [ $? -ne 0 ];then adduser $item echo "123456" | passwd --stdin $item &> /dev/null fi done
while 循环
- 条件式循环,反复测试条件,只要成立就执行命令序列
while循环案例
- 批量添加50个用户
#! bin/bash
i=1
while [ $i -le 50 ]
do
id tuser$i &> /dev/null;
if [ $? -ne 0 ]; then
useradd tuser$i;
echo "123456" | passwd --stdin tuser$i &> /dev/null;
fi;
let i++;
done
- 批量删除50个用户
#! /bin/bash i=1; while [ $i -le 50 ] do id tuser$i &> /dev/null; if [ $? -eq 0 ];then userdel -r tuser$i &> /dev/null; fi; let i++; done
中断及退出控制
在必要的时候放弃执行一部分代码
- 中断循环体
- 中断本次循环操作
| 类型 | 含义 |
| ---- | ---- |
| break | 跳出当前所在循环体,执行循环体后的代码块 |
| continue | 跳过循环体内余下的代码,重新判断条件,执行下一次循环 |
break示例
- 从键盘循环输入数字,0表示求和,输出最终结果
#! /bin/bash
sum=0;
while read -p '请输入需要累加的数字(0表示结束):' x;
do
[ $x -eq 0 ] && break;
sum=$((sum+x));
done
echo $sum;
continuue示例
- 跳过1~20以内非6的倍数,输出其他数的值。
#! /bin/bash
i=0;
while [ $i -le 20 ]
do
let i++;
[ $[i%6] -ne 0 ] && continue;
echo "6的倍数是:"$i"平方数:"$[i*i];
done
退出控制
退出整个shell脚本,并返回状态值,默认的返回值0;可以用使用
$?获取返回值。
一般用于出现错误处理方式。
| 类型 | 含义 |
|---|---|
| exit | 退出整个shell脚本 |
exit示例
- 利用位置参数获取2个整数,并计算出这两个整数的和
- 如果参数不够2个,则提正确用法并退出脚本。
#! /bin/bash
if [ $# -ne 2 ];then
echo "用法:$0 num1 num2";
exit 10;
fi
echo $[$1+$2];
服务脚本
函数的认识,什么是函数:
在shell环境中,将一些需要重复使用的额操作,定义为公共的语言块,既可称为函数。
使代码整洁,已读,便于维护,提高执行效率。
如何定义函数:
函数可以定义到一个文件里,在一个sh脚本里引用: . 文件路径/文件名
参考 /etc/init.d/functions
参考 /etc/init.d/network
## 格式一
function 函数名{
#代码块
...
}
## 格式二
函数名(){
#代码块
...
}
nginx.sh 示例
#!/bin/bash
. /etc/init.d/functions
arg=$1
function outParamter(){
[ $? -eq 0 ] && action "Nginx parameter is $arg" /bin/true || action "Nginx parameter is null. Please enter a parameter" /bin/false
}
function nginxStatus(){
[ $? -eq 0 ] && action "Nginx $1 " /bin/true || action "Nginx $1" /bin/false
}
case $1 in
start)
netstat -lntup|grep ":80\b" &>/dev/null
if [ $? -eq 0 ]
then
echo "Nginx is running..."
else
/usr/local/nginx/sbin/nginx
outParamter
fi
;;
stop)
/usr/local/nginx/sbin/nginx -s stop
outParamter
;;
reload)
/usr/local/nginx/sbin/nginx -s reload
outParamter
;;
restart)
netstat -lntp|grep ":80\b" &> /dev/null
if [ $? -ne 0 ]
then
/usr/local/nginx/sbin/nginx
nginxStatus start
else
/usr/local/nginx/sbin/nginx -s stop
nginxStatus stop
sleep 2
/usr/local/nginx/sbin/nginx
outParamter
fi
;;
status)
netstat -lntup | grep ":80\b" &>/dev/null
if [ $? -eq 0 ]
then
echo "Nginx is running..."
else
echo "Nginx is not running..."
fi
;;
*)
echo "Usage:$0 {start|stop|status|restart|reload}"
exit
esac
调用及参数传递
直接函数名,如果有参数:函数名 arg1 arg2
函数示例
求和
#! /bin/bash
function add1 {
echo $[$1+$2]
}
add2(){
echo $[$1+$2]
}
add1 $1 $2
add2 $1 $2

系统服务管理
- 查看服务列表,自启状态
- chkconfig --list
- chkconfig --list network
- 启动,停止,重启服务
- sevice 服务名 start|stop|restart
- /etc/init.d/服务名 start|stop|restart

编写服务脚本
- 任务目标
- 建立一个服务脚本 /etc/init.d/myprog
- 能够响应start|stop|restart|status参数
- 采用sleep作为测试程序(sleep 秒速)
- 控制参数有误或未提供,提示正确用法后退出。
#! /bin/bash case $1 in start) echo 'running' sleep 24h & ;; stop) echo 'stop' pkill -9 sleep ;; restart) $0 stop; $0 start; ;; status) pgrep -l sleep &> /dev/null && echo 'running' || echo 'stoped' ;; *) echo "用法: $0 [start|stop|restart|status]" exit 1 esac


交给chkconfig 工具管理
- 设置适用级别,启动,停止顺序,服务说明
- 添加系统服务 chkconfig --add myprog
- 查看服务 chkconfig --list myprog

文本的排序及统计
ls 列表排序
- 默认字母升序排列
- -S:按照文档大小降序排列
- -t:按照文档修改时间降序排序
- -r:反序排列
- -h:以人类看的懂格式显示大小
文件从大到小排序

文件从小到大排序

排序工具sort
- -u 去除重复行
- -n 按数字顺序升序排列
- -r 反向排序
- -t <分隔字符> 指定排序时所用的栏位分隔字符。
- -k 优先对第几列内容排序
- -o <输出文件> 将排序后的结果存入指定的文件。
- 排序时,默认是按每行/每个域的首字符排序,数字的优先级要大于字符的优先级
- 不指定升序还是降序时,默认是升序
- 重点掌握:-t -n -o -u - k,尤其是-k。
sort 示例
- vim salary.txt
google 110 5000 baidu 100 5000 guge 50 3000 sohu 100 4500 sohu 30 1500
- 规则的空格
- vim tt.txt
dell 100 10020 ee lenovo 200 12202 a huawei 500 13004 ff cisco 300 24005 bb
- 不规则的空格,分析-t的局限性
- vim t.txt
dell 100 10020 ee lenovo 200 12202 a huawei 500 13004 ff cisco 300 24005 bb
- 数值排序
- vim num.txt
tom 10 su 15 john 2 han 20 bell 25 jom 7
-r 排序
-
根据每行第一字符排序,默认升序
-
sort salary.txt
-
sort -r salary.txt

-
根据第三列排序
-
sort -k 3 salary.txt
-
sort -k 3r salary.txt

-
根据第三列倒序,第二列倒序和正序。
-
sort -k 3r -k 2r salary.txt
-
sort -k 3r -k 2 salary.txt

-
根据第一列去重
-
sort -u -k 1,1 salary.txt

sort排序的时候,-u不配合其他任何参数时,只有完全相同的重复行才会被消除。
这里隐含了一个知识点,那就是start 第一个域的第一个字符~end 最后一个域的最后一个字符,如果完全相同,仅保留第一次出现的行,后面出现的相同行都会被消除。-K M,N (为了便于理解,这里的M,N仅代表两个不同的整数)
M的意思是,从哪个域开始作为比较的依据;N截止到哪个域比较结束。M,N是一个(域的)范围,如果这个M,N结合-u使用。那么-u 的比较范围就是M域和N域之间的这段内容。如果数字N被省略,那么-u比较的范围就是从M域开始,一直到每行的最后一个域的最后一个字符。
- -k 的高级用法

总结
-k M.m,N.n
-k的语法总体是以逗号为分隔的,前面是M是start部分,后面的N是end部分。其中M.m,逗点.意思是从第M个域的第m个字符开始进行排序;N.n,逗点.意思是截止到第N个域的第n个字符截止,不再用n后面的字符排序。
去重工具uniq
只会去重连续的重复的文字,所以必须先要排好序,再去重。
- -c 统计重复次数


tac 命令工具
反序输出文本行,与cat命令的效果相反

rev 命令工具
以字符为单位反序,与echo命令效果相反

tee 整合重定向
- 命令输出的局限性:标准输出,管道,只能在当前终端上显示,无法保存结果
- 重定向输出操作:只能保存在指定文件,无法在终端显示结果
- -a 附加到既有文件的后面

tee 可以显示在终端,而且还可以重定向到文件
unix2dos | dos2unix 文本转换
- 格式兼容性问题
- 换行标记区别
- Windows系统 \r,\n
- Linxu系统 \n
- unix2dos 转换 linux文件
- dos2unix 转换 window文件
xargs 参数处理
unix命令都带参数,有些命令可以接受 标准输入 stdin 作为参数
cat /etc/passwd | grep root
上面的代码使用管道命令(|),管道命令的作用,是将左侧的命令(cat /etc/passwd ) 的标准输出转换为标准输入,再提供给右侧命令(grep root)作为参数。
因为 grep 命令可以接受标准输入作为参数,所以上面的代码同于下面代码
grep root /etc/passwd
但是,大多数命令都不接受标准输入作为参数,只能直接在命令行输入参数,这导致无法用管道命令传递参数。举例来说,echo命令就不接受管道传参。
echo 'hello world' | echo
上面的代码不会有输出。因为管道右侧的echo不接受管道传来的标准输入作为参数。
xargs命令的作用,是将标准输入转为命令行参数。
echo 'hello world' | xargs echo
上面的代码将管道左侧的标准输入,转为命令行参数hello world,传给第二个echo命令。
xargs的作用在于,大多数命令(比如rm、mkdir、ls)与管道一起使用时,都需要xargs将标准输入转为命令行参数。
-d 参数与分隔符
echo 'one two three' | xargs mkdir
默认情况下,xargs将换行符和空格作为分隔符,把标准输入分解成一个个命令行参数。
-d参数可以更改分隔符。
echo 'a:b:c' | xargs mkdir
-0 参数与 find 命令
由于xargs默认将空格作为分隔符,所以不太适合处理文件名,因为文件名可能包含空格。
find命令有一个特别的参数-print0,指定输出的文件列表以null分隔。然后,xargs命令的-0参数表示用null当作分隔符。
find /scott -type f -pringt0 | xargs rm -ef
上面命令删除/path路径下的所有文件。由于分隔符是null,所以处理包含空格的文件名,也不会报错。还有一个原因,使得xargs特别适合find命令。有些命令(比如rm)一旦参数过多会报错"参数列表过长",而无法执行,改用xargs就没有这个问题,因为它对每个参数执行一次命令
find . -name '*.txt' | xargs grep abc
-I 参数
如果xargs要将命令行参数传给多个命令,可以使用-I参数。
-I指定每一项命令行参数的替代字符串。

上面代码中,foo.txt是一个三行的文本文件。我们希望对每一项命令行参数,执行两个命令(echo和mkdir),使用-I file表示file是命令行参数的替代字符串。执行命令时,具体的参数会替代掉echo file; mkdir file里面的两个file。
正则表达式
awk文件过滤
awk是处理文本文件的一个应用程序,几乎所有 Linux 系统都自带这个程序,它依次处理文件的每一行,并读取里面的每一个字段。对于日志、CSV 那样的每行格式相同的文本文件,awk可能是最方便的工具。
awk其实不仅仅是工具软件,还是一种编程语言。不过,这里只介绍它的命令行用法,对于大多数场合,应该足够用了。
基本用法
- 格式一:前置命令 | awk [选项] '[条件]{编辑指令}'
- 格式二:awk [选项] '[条件]{编辑指令}' 文件

上面代码中,print $0就是把标准输入this is a test,重新打印了一遍。
awk默认会根据空格和制表符,将每一行分成若干字段,依次用$1、$2、$3代表第一个字段、第二个字段、第三个字段等等。

上面示例中,pwd.txt是awk所要处理的文本文件。前面单引号内部有一个大括号,里面就是每一行的处理动作print $0。其中,print是打印命令,$0代表当前行,因此上面命令的执行结果,就是把每一行原样打印出来。

用-F参数指定分隔符为冒号,便于提取第一字字段
awk内置变量
| 变量 | 用途 |
|---|---|
| FS | 保存或设置字段分隔符,例如:FS=':' |
| $n | 指定分隔符的第n个字段,如:$1,\(2,...\)n |
| $0 | 当前读入的整行文本内容 |
| NF | 记录当前处理行的字段列数 |
| NR | 记录当前已读入行的行数 |
| FNR | 保存当前处理行在原文本的序号 |
| FILENAME | 当前处理的文件名 |
| ENVIRON | 保存或设置字段分隔符,例如:FS=':' |



awk 处理时机
- 行前处理,BEGIN{},读入第一行文本之前执行,一般用来初始化操作
- 逐行处理,{},逐步读入文本执行相应的处理,是最常见的编辑指令块
- 行后处理,END{},处理完最后一行文本之后执行,一般用来输出处理结果。
- 可单独使用,也可以同时一起使用。
预处理不需要数据文件,可以进行小数运算。
awk 'BEGIN{a=3.14;print a+3.14}'

统计非交互结尾的用户个数
awk 'BEGIN{x=0} /nologin$/{x++;} END{print x}' /etc/passwd

统计总的行数
awk 'BEGIN{print NR} END{print NR}' /etc/passwd

文本过滤条件
使用正则
匹配 以ro开头的行并输出
awk -F: '/^ro/{print}' /etc/passwd

匹配不以base结尾的行
awk -F: '$NF !~ /bash$/ {print $1,$7}' /etc/passwd

使用数值比较
- ==,!=,>,>=,<,<=
输出奇数行
awk -F: 'NR % 2 == 1 {print NR":)",$1}' pwd.txt

输出第二行以后的行
awk -F: 'NR>2 {print $1}' pwd.txt

多个条件组合
- 逻辑与 && ,期望多个条件同时成立
- 逻辑或 || ,期望只要一个条件成立立即满足要求、
列出UID小于2的用户信息
=0&&$3<2{print $1}' /etc/passwd

列出uid为1或7的用户
awk -F: '$3==1||$3==7{print $1}' pwd.txt

变量运算
输出奇数行
awk -F: 'NR % 2 == 1 {print NR":)",$1}' pwd.txt

计算同时能被3或13整除的整数个数
seq 200 | awk 'BEGIN{i=0} ($0 %3 == 0)&&($0 % 13 == 0){i++} END{print i}'

if分支结构
- 格式
- {if(){}};
- {if(){}else{}};
- {if(){}else if(){}else
统计UID小于等于500用户个数
统计UID大于500用户个数
awk -F: 'BEGIN{i=0;j=0}{if($3<=500){i++}else{j++}}END{print i,j}' /etc/passwd
awk -F: 'BEGIN{i=0;j=0;k=0}{if($3<=50){i++}else if($3<=200){j++}else{k++}}END{print i,j,k}' /etc/passwd

while,for循环
统计/etc/passwd文件内 root 出现次数
利用 -F [😕] 表示分隔符为 :或 /
awk -F[:/] '{i=1}{while(i<=NF){if($i ~/nologin/){j++};i++}} END{print j}' /etc/passwd

浙公网安备 33010602011771号