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脚本?

  1. 分析任务
  • 自然语言:步骤拆分,顺序化整理
  1. 编写可执行语句
  • 脚本语言:各步骤如何实现
  1. 完善脚本
  • 界面友好/结构规范/代码的优化
  • 代码简洁,执行效率高

第一个Shell脚本

新建一个 test.sh,扩展名sh代表 shell。

#!/bin/bash
# 这是一条注释
echo 'hello world !';
  1. 第 1 行的 #! 是一个约定的标记(读作 sha-bang),它告诉系统这个脚本需要什么解释器来执行,即要使用哪一种 Shell;后面的/bin/bash就是指明了解释器的具体位置(/bin/bash 后面不要有分号(;))。
  2. 第 2 行的#及其后面的内容是注释。Shell 脚本中所有以#开头的都是注释(当然以#!开头的除外)。写脚本的时候,多写注释是非常有必要的,以方便其他人能看懂你的脚本,也方便后期自己维护时看懂自己的脚本——实际上,即便是自己写的脚本,在经过一段时间后也很容易忘记。
  3. 第 3 行的 echo 命令用于向标准输出文件(Standard Output,stdout,一般就是指显示器)输出文本。在.sh文件中使用命令与在终端直接输入命令的效果是一样的。

如何运行Shell脚本

1. 将 Shell 脚本作为程序运行

通过这种方式运行脚本,脚本文件第一行的 #!/bin/bash 一定要写对,好让系统查找到正确的解释器。

  1. chmod +x 表示给 test.sh 增加执行权限.
  2. ./ 表示当前目录,整条命令的意思是执行当前目录下的 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

总结

  1. 需要在新进程中运行 Shell 脚本,使用 bash test.sh 这种写法。本质上等于给脚本赋予执行权限再运行 ./test.sh;
  2. 在当前进程中运行 Shell 脚本,使用 source ./test.sh 这种写法。
  3. bash test.sh === sh test.sh === 给脚本赋予权限再执行。
  4. source test.sh ==== . test.sh

Shell四种运行方式

  • 交互式的登录 Shell
  • 交互式的非登录 Shell
  • 非交互式的登录 Shell
  • 非交互式的非登录 Shell

判断 Shell 是否是交互式

  1. 查看变量 - 的值,如果值中包含了字母i,则表示交互式(interactive)。
  2. 查看变量PS1的值,如果非空,则为交互式,否则为非交互式,因为非交互式会清空该变量。
  • 终端输出 -

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

判断 Shell 是否为登录式

  1. 在 CentOS GNOME 桌面环境自带的终端下查看 login_shell 选项:

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

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

同时判断交互式、登录式

echo $PS1;shopt login_shell

或者

echo $-;shopt login_shell

常见的 Shell 启动方式

  1. 通过 Linux 控制台(不是桌面环境自带的终端)或者 ssh 登录 Shell 时(这才是正常登录方式),为交互式的登录 Shell。

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

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

  1. 在 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

  1. 注意,/etc/profiles 文件还会嵌套加载 /etc/profile.d/*.sh,请看下面的代码:

  1. 同样,~/.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 脚本

  1. 直接观察执行过程
  • 执行中命令输出,报错信息
  • 与用户的交互
  1. 开启调试模式
  • sh -x 脚本文件
  1. 插入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 函数中定义的变量默认也是全局变量,它和在函数外部定义变量拥有一样的效果。

  1. 新建一个 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 脚本文件,此时全局变量在这些脚本文件中都有效。

  1. 新建 a.sh 文件,内容以下
echo $a;
b=200
  1. 新建 b.sh 文件,内容以下
echo $b
  1. 在同一个进程中执行

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

这三条命令都是在不同进程中执行的,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)

指定字符(子字符串)开始截取

  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##*/}

  1. 使用 % 截取左边字符
  • 基本用法:
  • ${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
posted @ 2022-02-05 14:18  BEJSON  阅读(144)  评论(0)    收藏  举报