取自《Linux Shell 核心编程指南》 丁明一

书写格式

  脚本文件第一行要求使用 (#!) 符号指定一个脚本的解释器,如 #!/bin/bash、 #!/bin/sh、 #!/usr/bin/env python 等,该行被 # 注释,所以不会被当作命令来执行,但计算机通过该注释信息得知应该使用什么解释器来解释整个脚本文件中的所有有效代码
  单行注释: #
  多行注释: << 符号后面的关键词可以是任意字符串,但前面使用什么关键词,结束注释时必须使用相同的关键词。如果从 <<ABC 开始注释,则结束注释信息时也必须使用 ABC(字母区分大小写)。

#!/bin/bash
<<COMMENT
文章作者:纹笑
个人博客:feifeitan.cn
书名:《Linux Shell 核心编程指南》
COMMENT

# echo命令后给什么字串屏幕将回显什么字串
echo "Hello World"

执行方法

  如果暂时还没有给脚本文件可执行的权限,那么默认脚本是无法直接执行,但 bash 或 sh 这样的解释器,可以将脚本文件作为参数(读取脚本文件中的内容)来执行脚本文件。举例如下:

./first.sh
# 报错,权限不够

# 一、
bash first.sh
# 二、
sh first.sh

提供可执行权限

chmod +x first.sh    # 分配可执行权限
./first.sh           # 使用相对路径执行(当前工作目录)
/root/first.sh       # 使用绝对路径执行

非子进程执行: pstree 命令来查看进程树,了解进程之间的关系。通过该指令输出,可以看到计算机启动的第一个进程是 systemd,然后在这个进程下启动了 N 个子进程,如 NetworkManager、 atd、 chronyd、 sshd 这些都是 systemd 的子进程。而在 sshd 进程下又有 2 个 sshd 的子进程,在 2 个 sshd 子进程下又开启了 bash 解释器子进程,而且在其中一个 bash 进程下面还执行了一条 pstree 命令。不管是直接执行脚本,还是使用 bash 或 sh 这样的解释器执行脚本,都是会开启子进程的。不开启子进程运行脚本方法如下:

. exit.sh

source exit.sh

  使用 source 执行和使用 ./ 执行的区别如下:
    执行进程不同:使用 source 或 "." 命令执行脚本时,是在当前进程中执行该脚本,因此脚本中定义的变量、函数等内容会直接导入当前 shell 环境中。而直接执行脚本文件则是启动一个新的子进程来执行脚本文件,执行完毕后该子进程退出,对父进程的环境没有任何影响。
    影响环境不同:由于 source 命令在当前进程中执行,因此脚本中定义的变量、函数等内容会导入当前 shell 环境中,可被之后执行的命令继续使用。而直接执行则启动新的子进程来执行脚本,脚本中定义的变量、函数等内容只能在该子进程内部使用,对父进程的环境没有任何影响。

  因此,如果希望脚本中定义的变量、函数等能在当前 shell 环境中保留,那么可以使用 source 或 "." 来执行;如果只需要执行脚本,不需要对当前 shell 环境造成影响,那么可以直接运行脚本文件。

输出

echo:

# 直接打印
echo "Hello World"

# 如果不带 -e ,则无法识别转义字符:
echo "\t" 
# 会输出 \t

# 带有 -e ,则可以识别转义符
echo -e "hello\tworld"
符号功能描述
\b退格键(Backspace)
\f换行但光标仍停留在原来的位置
\n换行且光标移至行首
\r光标移至行首,但不换行
\t插入Tab键
\打印 \
\033 或 \e设置终端属性,如字体颜色、背景颜色、定位光标等

   \033 或 \e 举例如下:

echo -e "\033[1mOK\033[0m"

<<COMMENT
\033 或\e 后面跟不同的代码可以设置不同的终端属性, 1m 是让终端粗体显示字符串,后面
的 OK 就是需要显示的字符串内容,最后\033[0m 是在加粗输出 OK 后,关闭终端的属性设置。
 如果最后没有使用 0m 关闭属性设置,则之后终端中所有的字符串都使用粗体显示。
COMMENT

# 简单举例
echo -e "\033[1mOK"          # 加粗显示 OK 后没关闭属性设置
echo -e "\e[1mOK\e[0m"       # 使用\e 和\033 的效果相同
echo -e "\e[4mOK\e[0m"       # 加下画线后输出 OK
echo -e "\e[5mOK\e[0m"       # 闪烁显示 OK
echo -e "\e[30mOK\e[0m"      # 黑色显示 OK
echo -e "\e[31mOK\e[0m"      # 红色显示 OK
echo -e "\e[32mOK\e[0m"      # 绿色显示 OK
echo -e "\e[33mOK\e[0m"      # 棕色显示 OK
echo -e "\e[34mOK\e[0m"      # 蓝色显示 OK
echo -e "\e[35mOK\e[0m"      # 紫色显示 OK
echo -e "\e[36mOK\e[0m"      # 蓝绿色显示 OK
echo -e "\e[37mOK\e[0m"      # 亮灰色显示 OK
echo -e "\e[1;33mOK\e[0m"    # 亮黄色显示 OK
echo -e "\e[42mOK\e[0m"      # 绿色背景显示 OK
echo -e "\e[44mOK\e[0m"      # 蓝色背景显示 OK
echo -e "\e[32;44mOK\e[0m"   # 绿色字体,蓝色背景显示 OK

# 除了可以定义终端的字体颜色、样式、背景,还可以使用 H 定义位置属性。例如,可
# 以通过下面的命令在屏幕的第 3 行、第 10 列显示 OK。
echo -e "\033[3;10HOK"

# 在第 3 行开头位置显示 OK
echo -e "\033[3HOK" 

printf用法:printf [格式] 参数

格式字符功能描述
%d或%i十进制整数
%o八进制整数
%x十六进制整数
%u无符号十进制整数
%f浮点数(小数点数)
%s字符串
\b退格键(Backspace)
\f换行但光标仍停留在原来的位置
\n换行且光标移至行首
\r光标移至行首,但不换行
\tTab键
# 屏幕显示整数 12
printf "%d" 12 

# printf "%d" jacob #jacob 不是整数,所以会报错
printf: jacob: 无效数字

# 该命令的格式%5d 设置了打印宽度为 5,以右对齐的方式显示整数 12。注意,该命令
# 的输出信息 12 前面有 3 个空格。 3 个空格 +2 个数字一起是 5 个字符的宽度。
printf "%5d" 12

# 左对齐,使用%-5d 实现效果
printf "%-5d" 12

# printf 命令输出信息后,默认是不换行的!如果需要换行则可以使用\n 命令符。
# 为了让左右对齐清晰,这里用两个 | 作为参照
printf "|%-10d|\n" 12

# 默认后面跟的数字为 10 进制,0x11 表示的是十六进制的 11, 
# printf 命令将十六进制的11转换为十进制整数输出(17)
printf "%d\n" 0x11

# 第一个数字为 0 表示八进制,将八进制的 11 转换为十进制整数输出(9)
# 在 C 语言第一个数字为 0 也表示为八进制。
printf "%d\n" 011

# 当使用\d 打印比较大的整数时,系统提示超出范围,并提示可以打印的最大数是
# 9223372036854775807。如果需要打印这样的大整数,则需要使用\u 命令符,但\u 命令符也
# 有最大的显示值(18446744073709551615), 当大于最大值时则无法打印。
printf "%u\n" 9223372036854775808

# 打印小数
printf "%f\n" 3.88 

# 小数点后保留 3 位
printf "%.3f\n" 3.88 

# 右对齐,占用 8 位宽度
printf "|%8.3f|\n" 3.88 

# 左对齐,占用 8 位宽度
printf "|%-8.3f|\n" 3.88 

# 打印字符串 hello
printf "%s\n" "hello" 

# 右对齐,占用 10 位宽度
printf "|%10s|\n" "hello" 

# 左对齐,占用 10 位宽度
printf "|%-10s|\n" "hello" 

# 打印2个字符串和tab
printf "%s\t\t%s\n" "hello" "world" 

输入

read 用法:read [选项] [变量名],如果未指定变量名,则默认变量名称为 REPLY。
  功能描述: read 命令可以从标准输入读取一行数据。

选项功能
-p显示提示信息
-t设置读入数据的超时时间
-n设置读取n个字符后结束,而默认会读取标准输入的一整行内容
-r支持读取\,而默认read命令理解\为特殊符号(转义字符)
-s静默模式,不显示标准输入的内容(Silent mode)

   从标准输入中读取数据,这里通过键盘输入了 123, read 命令则从标准输入读取这个 123,并将该字符串赋值给变量 key1,对于 key1 这个变量,我们可以使用 echo $key1 显示该变量
的值。

echo $key1
read key1
echo $key1
read key1 key2 key3                   # 从标准输入读取 3 组字符串
echo $key1
echo $key2
echo $key3
read -p "请输入用户名:" user           # 设置一个提示信息
echo $user
read -t 3 -p "请输入用户名:" user      # 使用-t 设置超时时间, 3 秒后 read 命令自动退出。
read -n1 -p "按任意键:" key            # 仅读取一个字符
read key                              # 默认 read 命令不支持\
echo $key                             # 所以 key 的值没有\字符
read -r key                           # 设置 read 命令支持读取\
echo $key                             # 查看结果,\被保留
read -p "请输入密码:" pass
#!/bin/bash
#Read User's name and password from standard input.
read -p "请输入用户名:" user
read -s -p "请输入密码:" pass
useradd "$user"
echo "$pass" | passwd --stdin "$user"

   这个脚本通过 read 命令读取用户输入的用户名和密码,并且在读取用户输入的密码时,不直接在屏幕上显示密码的内容,这样更安全。用户输入的用户名和密码分别保存在 user和 pass 这两个变量中,下面就通过$调用变量中的值,使用 useradd 命令创建一个系统账户,使用 passwd 命令给用户配置密码。直接使用 passwd 修改密码默认采用人机交互的方式配置密码,需要人为手动输入密码,并且要重复输入两次。这里我们使用了一个 | 符号,这个符号就像管道,它的作用是将前一个命令的输出结果,通过管道传给后一个命令,作为后一个命令的输入。

管道符 | : 介绍在前面

# who 命令可以查看有哪些账户在什么时间登录了计算机。但是条数可能会非常多
#  wc 命令可以统计行数
who
who | wc -l

# ss 命令可以查看 Linux 系统中所有服务监听的端口列表。但是 ss 命令自身没有灵活的过滤功能
# grep 命令有比较强大灵活的过滤功能
# grep sshd表示在前面输出结果中寻找带有字符串 sshd 的行并打印
ss -nutlp
ss -nutlp | grep sshd

# echo 命令默认会把输出结果显示在屏幕上,而有了管道后, echo 命令
# 可以把输出的 123456 存储到管道中, passwd 再从管道中读取 123456,
# 来修改系统账户 jacob 的密码
echo "123456" | passwd --stdin jacob

输出重定向

  Linux 输出分为标准输出和标准错误输出。标准输出文件的描述符为 1,标准错误输出的文件描述符为 2。而标准输入的文件描述符则为0。大部分情况下默认将输出信息显示在屏幕上,如果希望改变输出信息的方向,可以用 > 或 >> 符号将输出信息重定向到文件中。
  使用 1> 或 1>> 可以将标准输出信息重定向到文件中( 1 可以忽略不写,默认值为 1 ),也可以使用 2> 或 2>> 将错误的输出信息重定向到文件中。这里的 > 符号将输出信息重定向到文件,若文件不存在则创建。若文件存在则会覆盖。而 >> 的区别是,若文件存在则会追加到结尾。

ehco "Hello" > test.txt 
cat test.txt

echo "World" > test.txt        # 覆盖
cat test.txt

echo "Hello" >> test.txt       # 追加
cat test.txt
ls /etc/tests > test.txt       # 将标准输出重定向到文件
cat test.txt
# 删除文件夹后
ls /etc/tests > test.txt       # 错误信息会直接显示在屏幕上
ls /etc/tests 2>> test.txt     # 错误重定向,追加到文件中

  这两个方法,只将需要重定向的内容写入文件,不需要重定向的内容打印在屏幕上。如果想分别重定向到不同的文件,用以下指令:

ls -l /etx/hosts/ /nofile > ok.txt 2> error.txt
cat ok.txt
cat error.txt

  使用 &> 符号可以同时将标准输出和错误输出都重定向到一个文件(覆盖),也可以使用 &>> 符号实现追加重定向。

ls -l /etc/hosts /nofile &> test.txt
cat test.txt

  还可以使用 2>&1 将错误输出重定向到标准正确输出,也可以使用 1>&2 将标准正确输出重定向到错误输出。

  Linux 系统中有一个特殊的设备 /dev/null 。这个文件写入多少数据都会被丢弃,并且无法找回。如果有些输出信息是我们不需要的,则可以使用重定向将输出信息导入到该设备文件中。

echo "Hello" > /dev/null

输入重定向

  默认标准输入为键盘鼠标,这就必须用户进行交互,可以通过输入重定向来实现非交互操作。

mail -s warning root@localhost                   # -s 设置邮件标题,收件人为 root
# 必须用户输入才行
mail -s warning root@localhosts < /etc/hosts     # 非交互发送邮件

  使用 < 符号进行输入重定向。 < 符号后面需要跟一个文件名,这样可以让程序不再从键盘读取输入数据,而从文件中读取数据。

  使用 << 符号可以将数据内容重定向传递给前面的一个命令,作为命令的输入。
  语法:

命令 << 分隔符
内容
分隔符

# 举例:
cat > /tmp/test.txt << HERE
该文件为测试文件。
测试完后,记得将该文件删除。
Welcome to Earth.
HERE

  系统会自动将两个分隔符之间的内容重定向传递给前面的命令,作为命令的输入
注意:分隔符是什么都可以,但前后的分隔符必须一致。推荐使用 EOF(end of file)使用 << 将数据导入程序时,如果内容里面有缩进, 则连同缩进的内容都会传递给程序。而此时的 Tab 键仅仅起缩进的作用,我们并不希望传递给程序。如果需要,可以使用 <<- 符号重定向输入的方式实现,这样系统会忽略掉所有数据内容及分隔符(EOF)前面的 Tab 键。

引号

双引号表示是一个整体

touch a b c     # 创建 3 个文件,分别为 a、b、c
touch "a b c"   # 创建 1 个文件,空格是文件名的一部分

单引号引用一个整体,同时单引号还可以屏蔽特殊符号(将特殊符号的特殊含义屏蔽,转化为字符表面的名义)

# # 是特殊符号,不会显示
echo # 
# 可以显示
echo '#' 
echo '$a$b'

\ 符号也能实现屏蔽转义,但是只能屏蔽后面的第一个符号,单引号可以屏蔽所有特殊符号。

echo '$a$b'
echo \$a\$b
 
echo '#'         # # 符号被理解为注释,需要屏蔽
echo '$$'        # $$ 显示当前进程的进程号,需要屏蔽
echo '&'         # & 符号默认为后台进程,需要屏蔽
echo *           # * 符号代表当前目录下的所有文件,需要屏蔽
echo ~           # ~ 符号默认代表用户的根目录,需要屏蔽
echo '()'

`` 反引号是一个命令替换符号,可是使用命令的输出结果替代命令

# 把/var/log 目录下的所有数据备份到/root 目录下,但是备份的文件名是固定的。 
# 如果需要系统执行计划任务,实现在每周星期五备份一次数据,然后新的备份就会
# 把原有的备份文件覆盖(因为文件名是固定的)
tar -czf /root/log.tar.gz /var/log/

# 因为使用了``符号实现命令替换, 所以这里备份的文件名不再是 date,
# 而是 date 命令执行后的输出结果, 即使用命令的输出结果替
# 换 date 命令本身的字符串,最后备份的文件名类似 log-20230624.tar.gz。 
tar -czf /root/log-`date +%Y%m%d`.tar.gz /var/log/

  举例如下:

echo "当前系统账户登录数量:`who | wc -l`"
cat /var/run/atd.pid               # 查看 atd 进程的进程号
kill `cat /var/run/atd.pid`        # 杀死 atd 进程
rpm -ql at                         # 查看 at 软件的文件列表
ls -l `rpm -ql at`                 # 查看文件列表的详细信息
ls /etc/*.conf                     # 查看/etc/目录下的所有以 conf 结尾的文件
tar -czf x.tar.gz `ls /etc/*.conf` # 将多个文件压缩打包为一个文件
tar -tf x.tar.gz                   # 查看压缩包中的文件列表

  反引号虽然好用,但是还是有自身缺陷:
   1、容易和单引号混淆
   2、不支持嵌套(反引号中再使用反引号)
  为了解决这些问题,便有了 $() 组合符号,功能也是命令替换,而且支持嵌套。举例:

echo "当前系统账户登录数量:$(who | wc -l)"
ping -c2 $(hostname)
touch $(date +%Y%m%d).txt
echo "当前系统进程数量: $(ps aux | wc -l)"
echo $(echo 我是 1 级嵌套 $(echo 我是 2 级嵌套))

变量

  在 Linux 系统中, 自定义变量的定义格式为 变量名=变量值,变量名仅是用来找到变量值的一个标识而已身没有其他功能。
变量名仅可以使用字母(大小写都可以)、数字和下画线(_) 组合,而且不可以使用数字开头。
当需要读取变量值时,需在变量名前加一个“$”; 而当变量名与其他非变量名的字符混在一起时, 需使用{}分隔。
如需取消变量定义, 则使用 unset 命令删除变量。

hello = 123       # 错误定义,等号两边不可以有空格
test=123          # 定义变量,变量名为 test,值为 123
echo $test        # 调用变量,提取变量的值
echo $testRMB     # 返回值为空,因为没有定义一个名称是 testRMB 的变量
echo ${test}RMB   # 正确返回 123RMB

echo $test-yuan
echo $test:yuan
echo $test yuan

<<COMMENT
虽然这三条命令都没有使用{}分隔变量名与其他字符,但最后返回值也不为空白,因
为 Shell 变量名称仅可以由字母、数字、下画线组成,不可能包括特殊符号(如横线、冒号、
空格等),所以系统不会把特殊符号当作变量名的一部分,系统会理解变量名为 test 。
COMMENT 

unset test        # 取消变量定义
echo $test        # 返回的结果为空

  tr -s 后面使用引号引用了一个空格,作用是将管道传送的数据中连续的多个空格合并为一个空格。
  如果-s 选项后面使用引号引用其他的字符,则效果也一样,可以把多个连续的特定字符合并为一个字符。
  cut 命令,可以获取数据的特定列(使用-f 选项指定需要获取的列数),并可以通过-d 选项设置以什么字符为列的分隔符。

echo "aaa bbb" | tr -s "a"     # 将多个连续的 a 合并为一个 a
echo "a---b---c" | tr -s "-"   # 将多个连续的-合并为一个-
echo "A B C" | cut -d" " -f2   # 以空格为分隔符,获取第二列,-d 中间不用加空格
echo "A-B-C" | cut -d"-" -f3   # 以-为分隔符,获取第三列
echo "AcBcC" | cut -d"c" -f2   # 以 c 为分隔符,获取第二列

预设变量

变量名描述
UID当前账户的账户ID号
USER当前账户的账户名称
HISTSIZE当前终端的最大历史命令条目数量(最多可以记录多少条历史命令)
HOME当前账户的根目录
LANG当前环境使用的语言
PATH命令搜索路径
PWD返回当前工作目录
RANDOM随机返回0至32767的整数
$0返回当前命令的名称($0、 $1、 $2、等, 也叫位置变量)
$n返回位置参数, 如$1(第一个位置参数) 、 $2等,数字大于9时必须使用${n}
$#命令参数的个数
$*命令行的所有参数,“$*”所有的参数作为一个整体
$@命令行的所有参数,“$@”所有的参数作为独立的个体
$?返回上一条命令退出时的状态代码(一般来说, 0代表正确,非0代表失败)
$$返回当前进程的进程号
$!返回最后一个后台进程的进程号

  举例 1:

#!/bin/bash
#描述信息:本脚本主要目的是获取主机的数据信息(内存、网卡 IP、 CPU 负载)
localip=$(ifconfig eth0 | grep netmask | tr -s " " | cut -d" " -f3)
mem=$(free |grep Mem | tr -s " " | cut -d" " -f7)
cpu=$(uptime | tr -s " " | cut -d" " -f13)
echo "本机 IP 地址是:$localip"
echo "本机内存剩余容量为:$mem"
echo "本机 CPU 15min 的平均负载为:$cpu"

  举例 2:

#!/bin/bash
echo "当前账户是:$USER,当前账户的 UID 是:$UID"
echo "当前账户的根目录是:$HOME"
echo "当前工作目录是:$PWD"
echo "返回 0~32767 的随机数:$RANDOM"
echo "当前脚本的进程号是:$$"
echo "当前脚本的名称为:$0"
echo "当前脚本的第 1 个参数是:$1"
echo "当前脚本的第 2 个参数是:$2"
echo "当前脚本的第 3 个参数是:$3"
echo "当前脚本的所有参数是:$*"
echo "准备创建一个文件..."
touch "$*"
echo "准备创建多个文件..."
touch "$@"
ls /etc/passwd
echo "我是正确的返回状态码:$?,因为上一条命令执行结果没有问题"
ls /etc/pas
echo "我是错误的返回状态码:$?,因为上一条命令执行结果有问题,提示无此文件"

  当前登录的账户是 root,所以 $USER 的值为 root。 默认 root 的账户 ID 号为 0,所以 $UID 的值为 0。管理员根目录为 /root。因此, $HOME 的值为 /root。执行脚本时当前工作目录为管理员根目录, $PWD 的值也是 /root。 $RANDOM1 每次执行脚本都可能返回不同的值,这里返回的值为 11951。每次执行脚本时进程的进程号也是随机的,这里 $$ 的结果是 29772。$0 显示当前脚本名称为 sys_var.sh, $1 是执行脚本的第 1 个参数(这里执行脚本时给了 4 个参数: A C 8 D),因此 $1 的值为 A; $2 是执行脚本的第 2 个参数,也就是 C,其他位置变量依此类推。 $* 会显示所有参数的内容。因为 “$*” 将所有参数视为一个整体,因此创建了一个名称为 “A C 8 D” 的文件,空格也是文件名的一部分。而 “$@” 将所有参数视为独立的个体,因为 touch 名称创建了 4 个文件,分别是 A、 C、 8、 D。,使用 ls -l 命令可以查看得更清楚。

[root@centos7 ~]# ls -l
-rw-r--r-- 1 root root 0 8 月 1 23:45 8
-rw-r--r-- 1 root root 0 8 月 1 23:45 A
-rw-r--r-- 1 root root 0 8 月 1 23:45 A C 8 D
-rw-r--r-- 1 root root 0 8 月 1 23:45 C
-rw-r--r-- 1 root root 0 8 月 1 23:45 D

  “$?” 返回上一条命令的退出状态代码,脚本中先执行 ls /etc/passwd,当这个命令被正确地执行后,“$0” 返回的结果为 0。而当执行 ls /etc/pass 命令时,因为 pass 文件不存在,所以该命令报错无法找到该文件。此时,“$?” 返回的退出状态码为 2(正确为 0,错误为非 0,但根据错误的情况不同, 每个程序返回的具体数字也会有所不同)。

数据过滤

grep
  描述: grep 命令可以查找关键词并打印匹配的行。
  用法: grep [选项] 匹配模式 [文件]。
  常用选项:
    -i 忽略字母大小写。
    -v 取反匹配。
    -w 匹配单词。
    -q 静默匹配,不将结果显示在屏幕上。

grep th test.txt             # 在 test.txt 文件中过滤包含 th 关键词的行
grep -i the test.txt         # 过滤包含 the 关键词的行(不区分字母大小写)
grep -w num test.txt         # 仅过滤 num 关键词(不会过滤 number 关键词)
grep -v the test.txt         # 过滤不包含 the 关键词的行
grep -q root /etc/passwd     # 不在屏幕上显示过滤的结果

正则表达式

  基本正则表达式:正则表达式在每个不同的系统里都有些许区别。

字符含义
c匹配字母c
.匹配任意单个字符
*匹配前一个字符出现零次或多次
.*匹配多个任意字符
[]匹配集合中的任意单个字符,括号可以是任意数量字符的集合
[x-y]匹配连续的字符串范围
^匹配字符串的开头
$匹配字符串的结尾
[^]匹配否定,对括号中的集合取反
\匹配转义后的字符串
{n,m}匹配前一个字符重复n到m次
{n,}匹配前一个字符重复至少n次
{n}匹配前一个字符重复n次
()将(与)之间的内容存储在“保留空间”,最多可存储9个
\n通过\1至\9调用保留空间中的内容

  举例:

[root@centos7 ~]# cp /etc/passwd /tmp/            #复制素材模板文件

[root@centos7 ~]# grep "root" /tmp/passwd
#查找包含 root 的行(双引号内不是要匹配的内容,以下案例相同)
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin

[root@centos7 ~]# grep ":..0:" /tmp/passwd
#查找:与“0:”之间包含任意两个字符的字符串,并显示该行
root:x:0:0:root:/root:/bin/bash                   # :0:0:
sync:x:5:0:sync:/sbin:/bin/sync                   # :5:0:
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown      # :6:0:
halt:x:7:0:halt:/sbin:/sbin/halt                  # :7:0:
games:x:12:100:games:/usr/games:/sbin/nologin     # :100:
avahi-autoipd:x:170:170:Avahi IPv4LL Stack:/var/lib/avahi-autoipd:/sbin/nologin # :170:

[root@centos7 ~]# grep "00*" /tmp/passwd
#查找包含至少一个 0 的行(第一个 0 必须出现,第二个 0 可以出现 0 次或多次)
root:x:0:0:root:/root:/bin/bash #该行有两处匹配
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin #匹配 0 出现 2 次
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
avahi-autoipd:x:170:170:Avahi IPv4LL
Stack:/var/lib/avahi-autoipd:/sbin/nologin
avahi:x:70:70:Avahi mDNS/DNS-SD Stack:/var/run/avahi-daemon:/sbin/nologin
-
[root@centos7 ~]# grep "o[os]t" /tmp/passwd
#查找包含 oot 或 ost 的行
root:x:0:0:root:/root:/bin/bash
operator:x:11:0:operator:/root:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin

[root@centos7 ~]# grep "[0-9]" /tmp/passwd
#查找包含 0~9 数字的行(输出内容较多,这里为部分输出)
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync

[root@centos7 ~]# grep "[f-q]" /tmp/passwd
#查找包含 f~q 字母的行(f 到 q 之间的任意字母都可以,输出内容较多,这里为部分输出)
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

[root@centos7 ~]# grep "^root" /tmp/passwd
#查找以 root 开头的行
root:x:0:0:root:/root:/bin/bash

[root@centos7 ~]# grep "bash$" /tmp/passwd
#查找以 bash 结尾的行
root:x:0:0:root:/root:/bin/bash

[root@centos7 ~]# grep "sbin/[^n] " /tmp/passwd
#查找 sbin/后面不跟 n 的行
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt

[root@centos7 ~]# grep "0\{1,2\}" /tmp/passwd
#查找数字 0 出现最少 1 次、 最多 2 次的行
root:x:0:0:root:/root:/bin/bash
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
avahi-autoipd:x:170:170:Avahi IPv4LL
Stack:/var/lib/avahi-autoipd:/sbin/nologin
avahi:x:70:70:Avahi mDNS/DNS-SD Stack:/var/run/avahi-daemon:/sbin/nologin

[root@centos7 ~]# grep "\(root\).*\1" /tmp/passwd
#查找两个 root 之间可以是任意字符的行。注意,这里使用\(root\)将 root 保留,后面的\1 再
# 次调用 root,类似于前面复制 root,后面粘贴 root
root:x:0:0:root:/root:/bin/bash       # root:x:0:0:root:/root  不对劲???

[root@centos7 ~]# grep "^$" /tmp/passwd
#过滤文件的空白行

[root@centos6 test]# grep -v "^$" /tmp/passwd
#过滤文件的非空白行

扩展正则表达式

字符含义
{n,m}等同于基本正则表达式的{n,m}
+匹配前的字符出现一次或多次
?匹配前的字符出现零次或一次
竖线匹配逻辑或,即匹配竖线前或后的字串(这里打不出来,markdown 在这里不能转义)
()匹配正则集合,同时也有保留的意思,等同于基本正则表达式的()

  grep 命令默认不支持扩展正则表达式,需要使用 grep -E 或者使用 egrep 命令进行扩展正则表达式的过滤。

egrep "0{1,2}" /tmp/passwd
#查找数字 0 出现最少 1 次最多 2 次的行
egrep "0+" /tmp/passwd
#查找包含至少一个 0 的行
egrep " (root|admin) " /tmp/passwd
#查找包含 root 或者 admin 的行

POSIX 规范的正则表达式

   如果需要匹配的对象是中文或是像 “ن” 这样的阿拉伯语字符,则 POSIX 其实是由一系列规范组成的,这里仅介绍 POSIX 正则表达式规范。 POSIX 正则表达式规范帮助我们解决语系问题。

字符集含义字符集含义
[:alpha:]字母字符[:graph:]非空格字符
[:alnum:]字母与数字字符[:print:]任意可以显示的字符
[:cntrl:]控制字符[:space:]任意可以产生空白的字符
[:digit:]数字字符[:blank:]空格与Tab键字符
[:xdigit:]十六进制数字字符[:lower:]小写字符
[:punct:]标点符号[:upper:]大写字符

   Linux 允许通过方括号使用 POSIX 标准规范,如[[:alnu:]]将匹配任意单个字母数字字符,下面通过几个简单的例子来说明用法。由于过滤输出的内容较多,以下仅为部分输出。

[root@centos7 ~]# grep "[[:digit:]]" /tmp/passwd
root:x:0:0:root:/root:/bin/bash           # 匹配了下面所有的数字
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

[root@centos7 ~]# grep "[[:alpha:]]" /tmp/passwd
root:x:0:0:root:/root:/bin/bash           # 匹配了所有的字母
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

[root@centos7 ~]# grep "[[:punct:]]" /tmp/passwd
root:x:0:0:root:/root:/bin/bash           # 匹配了所有的标点
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

[root@centos7 ~]# grep [[:space:]] /tmp/passwd
ftp:x:14:50:FTP_User:/var/ftp:/sbin/nologin   # 这里的下划线都被匹配了
vcsa:x:69:69:virtual_console_memory_owner:/dev:/sbin/nologin

GNU 规范

   Linux 中的 GNU 软件一般支持转义元字符,这些转义元字符有: \b(边界字符,匹配单词的开始或结尾), \B(与\b 为反义词, \Bthe\B 不会匹配单词 the,仅会匹配 the 在中间的单词,如 atheist), \w(等同于[_[:alnum:]]), \W(等同于1])。另外有部分软件支持使用\d 表示任意数字, \D 表示任意非数字。 \s 表示任意空白字符(空格、制表符等), \S 表示任意非空白字符。

[root@centos7 ~]# grep "i\b" /tmp/passwd             # 匹配 i 结尾的单词
avahi-autoipd:x:170:170:Avahi IPv4LL Stack:/var/lib/avahi-autoipd:/sbin/nologin
avahi:x:70:70:Avahi mDNS/DNS-SD Stack:/var/run/avahi-daemon:/sbin/nologin

[root@centos6 ~]# grep "\W" /tmp/passwd
# 匹配所有非字母、数字及下画线组合的内容
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

[root@centos7 ~]# grep "\w" /tmp/passwd
# 匹配所有字母、数字及下画线组合的内容(内容太多,这里不再显示输出内容)

[root@centos7 ~]# /usr/bin/grep -P --color "\d" /etc/passwd
# 默认 grep 仅支持基本正则表达式,使用-P 让 grep 支持 perl 兼容的正则表达式(下面的结果仅
为部分输出内容)
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin

[root@centos7 ~]# /usr/bin/grep -P --color "\D" /etc/passwd

运算符

Shell 支持多种算术运算,可以使用$((表达式))、 $[表达式]、 let 表达式进行整数的算术运算,注意这些命令无法执行小数运算; 使用 bc 命令可以进行小数运算。

运算符号含义描述
++自加1
--自减1
+加法
-减法
*乘法
/除法
**求幂
%取余(求模)
+=自加任意数
-=自减任意数
*=自乘任意数
/=自除任意数
%=对任意数取余
&&逻辑与
两条竖线逻辑或
>大于
>=大于或等于
<小于
<=小于或等于
表达式?表达式:表达式根据表达式的结果,返回特定的值
[root@centos7 ~]# echo $((2+4))
6
[root@centos7 ~]# echo $((2-4))
-2
[root@centos7 ~]# echo $((2*4))
8
[root@centos7 ~]# echo $((2**4)) #2 的 4 次幂
16
[root@centos7 ~]# echo $((10%3)) #10 除以 3 后返回余数
1
[root@centos7 ~]# x=2 #定义变量并赋值
[root@centos7 ~]# echo $((x+=2)) #x=x+2 (x=2+2)
4
[root@centos7 ~]# echo $((x*=3)) #x=x×3 (x=4×3)
12
[root@centos7 ~]# echo $((x%=2)) #x=x%2 (x=8%2)
0
[root@centos7 ~]# x=2 ; y=3 #定义 2 个变量并赋值
[root@centos7 ~]# echo $((x*y))
6
[root@centos7 ~]# echo $x $y #x,y 自身的值不变
2 3
[root@centos7 ~]# echo $((3>2&&5>3)) #仅当&&两边的表达式都为真时,返回 1
1
[root@centos7 ~]# echo $((3>2&&5>9)) #当&&两边的表达式任意为假时,返回 0
0
[root@centos7 ~]# echo $((3>8&&5>9))
0
[root@centos7 ~]# echo $((1>2||5>8)) #仅当||两边的表达式都为假时,返回 0
0
[root@centos7 ~]# echo $((3>2||5>9)) #当||两边的表达式任意一个为真时,返回 1
1
[root@centos7 ~]# echo $((1>2||5>2))
1
[root@centos7 ~]# echo $[2+8]
10
[root@centos7 ~]# echo $[2**8]
256
[root@centos7 ~]# x=3 ; y=5
[root@centos7 ~]# echo $[x+y]
8
[root@centos7 ~]# echo $[x*y]
15
[root@centos7 ~]# echo $[1+2*3] #先计算乘除法,再计算加减法
7
[root@centos7 ~]# echo $[(1+2)*3] #使用()让计算机先计算加减法,再计算乘除法
9
[root@centos7 ~]# echo $[x>y?2:3] #如果 x 大于 y,返回 2,否则返回 3
3
[root@centos7 ~]# echo $[y>x?2:3] #如果 y 大于 x,返回 2,否则返回 3
2
[root@centos7 ~]# echo $[y>x?2+2:3*5]
4
[root@centos7 ~]# echo $[x>y?2+2:3*5]
15

let
  使用 let 命令计算时,默认不会输出运算的结果,一般需要将运算的结果赋值给变量,通过变量查看运算结果。另外,使用 let 命令对变量进行计算时,不需要在变量名前添加$符号。

[root@centos7 ~]# let 1+2 #无任何输出结果
[root@centos7 ~]# x=5 #变量赋初始值
[root@centos7 ~]# let x++ #x=x+1 (x=5+1)
[root@centos7 ~]# echo $x
6
[root@centos7 ~]# let x*=2 ; echo $x #x=x×2 (x=6×2)
12
[root@centos7 ~]# let i=1+2*3;echo $i
7
[root@centos7 ~]# let i=(1+2)*3;echo $i
9
[root@centos7 ~]# let 2.2+5.5 #注意,let 无法进行小数运算
invalid arithmetic operator

   最后,注意在使用 ++ 或 -- 运算符号时, x++和++x 的结果是不同的, x-- 和 --x 的结果也不同。 x++ 是先调用 x 再对 x 自加 1, ++x 是先对 x 自加 1 再调用 x; x-- 是先调用 x 再对 x 自减 1, --x 是先对 x 自减 1 再调用 x。

[root@centos7 ~]# x=1           # 变量赋初始值
[root@centos7 ~]# echo $[x++]   # 先调用 x,屏幕显示 1,再对 x 自加 1
1
[root@centos7 ~]# echo $x       # 此时 x 的值已经为 2
2
[root@centos7 ~]# x=6           # 变量赋初始值
[root@centos7 ~]# echo $[x--]   # 先调用 x,屏幕显示 6,再对 x 自减 1
6
[root@centos7 ~]# echo $x       # 此时 x 的值已经为 5
5
[root@centos7 ~]# x=1
[root@centos7 ~]# echo $[++x]   # 先对 x 自加 1,再显示,结果为 2
2
[root@centos7 ~]# x=6
[root@centos7 ~]# echo $[--x]   # 先对 x 自减 1,再显示,结果为 5
5

bc
   bc支持对任意精度的小数进行运算甚至编写计算函数, 则可以使用 bc 计算器实现。 bc 计算器支持交互和非交互两种执行方式。

   交互式举例:

[root@centos7 ~]# bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation,
Inc.
1.5+3.2
4.7
# 2 除以 10 的结果,默认仅显示整数部分的值
2/10
0 #
通过 bc 计算器中的内置变量 scale,可以指定需要保留的小数点位数
scale=2
2/10
.20
#在 bc 计算器中使用^计算幂运算
2^3
8 #
使用 quit 命令退出 bc 计算器
quit

   非交互式:
   bc 计算器的另外两个内置变量 ibase(in)和 obase(out)可以进行进制转换, ibase 用来指定输入数字的进制, obase 用来设置输出数字的进制,默认输入和输出的数字都是十进制的。

[root@centos7 ~]# x=$(echo "(1+2)*3" | bc)
[root@centos7 ~]# echo $x
9
[root@centos7 ~]# echo "2+3;scale=2;8/19" | bc
5
.42
[root@centos7 ~]# echo "obase=2;10" | bc #输入十进制的 10,输出对应的二进制
1010
[root@centos7 ~]# echo "obase=8;10" | bc #十进制转八进制
12
[root@centos7 ~]# echo "obase=16;10" | bc #十进制转十六进制
A
[root@centos7 ~]# echo "ibase=2;11" | bc #输入二进制,输出对应的十进制
3
[root@centos7 ~]# echo "ibase=16;FF" | bc #十六进制转十进制
255
[root@centos7 ~]# echo "obase=2;2+8" | bc #输入十进制,输出二进制结果
1010
[root@centos7 ~]# echo "length(22833)" | bc #统计数字的长度
5

  举例:

#!/bin/bash
#计算 1+2+3,...,+n 的和,可以使用 n*(n+1)/2 公式快速计算结果
read -p "请输入一个正整数:" num
sum=$[num*(num+1)/2]
echo -e "\033[32m$num 以内整数的总和是:$sum\033[0m"
#使用三角形的底边和高计算面积:A=1/2bh
read -p "请输入三角形底边长度:" bottom
read -p "请输入三角形高度:" hight
A=$(echo "scale=1;1/2*$bottom*$hight" | bc)
echo -e "\033[32m 三角形面积是:$A\033[0m"
#梯形面积:(上底边长度+下底边长度)*高/2
read -p "请输入梯形上底边长度:" a
read -p "请输入梯形下底边长度:" b
read -p "请输入梯形高度:" h
A=$(echo "scale=2;($a+$b)*$h/2" | bc)
echo -e "\033[32m 梯形面积是:$A\033[0m"
#使用 A=πr^2 公式计算圆的面积,取 2 位小数点精度, π=3.14
read -p "请输入圆的半径:" r
A=$(echo "scale=2;3.14*$r^2" | bc)
echo -e "\033[32m 圆的面积是:$A\033[0m"
echo "3282820KiB 等于多少 GiB?"
G=$(echo "32828920/1024/1024" | bc)
echo -e "\003[32m 答案${G}G\033[0m"
#注意使用{}防止变量名歧义
#时间格式转化
read -p "请输入秒数:" sec
ms=$[sec*1000]
echo -e "\033[32m$sec 秒=$ms 毫秒\033[0m"
us=$[sec*1000000]
echo -e "\033[32m$sec 秒=$us 微秒\033[0m"
hour=$(echo "scale=2;$sec/60/60"|bc)
echo -e "\033[32m$sec 秒=$hour 小时\033[0m"
  1. _[:alnum: