【操作系统笔记十三】Shell脚本编程

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

什么是 shell

shell 就是命令解释器用于解释用户对操作系统的操作比如当我们在终端上执行 ls 然后回车这个时候会由 shell 来解释这个命令并且执行解释后的命令进而对操作系统进行操作。

在 Centos 操作系统中支持多种 shell我们可以通过下面的命令来查看一个操作系统支持的 shell

cat /etc/shells

但是 Centos 7 默认的 shell 是 bash也是在 Centos 系统中常用的 shell。

当我们在终端上执行命令这个命令就是通过 bash shell 来解释执行的实际上当我们打开一个终端实际上也就是打开一个 bash 进程可以通过 ps 来验证如下

ps -f

## 输出如下
UID         PID   PPID  C STIME TTY          TIME CMD
root       2531   2527  0 14:58 pts/1    00:00:00 -bash
root       2566   2531  0 15:14 pts/1    00:00:00 ps -f

以上 PID 为 2531 就是进程 bash 的 pid。所以我们在这个终端上执行的任何命令都会被 bash shell 进行解释执行。

什么是 shell 编程

在 Linux 操作系统中有一个非常重要的原则那就是一个命令只做一件事情。比如

## 进入到指定的目录
cd /var

## 查看当前目录下的所有文件
ls

## 查看当前目录所在的文件路径
pwd

## 我们还可以再统计出当前目录的大小
du -sh 

以上说明了一个命令只做一件事情

那么现在假设我们需要经常依次执行上面的命令我们如果每次都去输入上面的 4 个命令的话那就显的太麻烦了那么我们可以按照如下的 2 步来解决这个问题

  1. 将上面的 4 个命令写到一行中
cd /var; ls; pwd; du -sh
  1. 将上面的一行命令保存在一个文件中一般放 linux 命令的文件的后缀使用 .sh执行
vi 1.sh

## 将下面的内容保存到 1.sh 文件名中
cd /var; ls; pwd; du -sh

这样的话如果你想重复执行上面的 4 个命令的时候你只需要执行 1.sh 文件就可以了你也可以将这个文件给别人执行。

现在要执行上面的文件我们需要先给脚本赋予执行权限因为默认的时候文件是没有执行权限的如下

chmod u+x 1.sh

我们可以使用 bash 命令来执行上面的 1.sh 文件如下

bash 1.sh

那么执行上面的命令的结果和上面执行 4 条命令的结果是一样的。

那么上面的 1.sh 就是一个 shell 脚本编写 shell 脚本其实就是 shell 编程。

在上面的 1.sh 中可以将分号去掉然后一个命令一行如下

cd /var 
ls
pwd
du -sh

这样效果是一样的。

总结

  1. shell 就是命令解释器对用户输入的命令解释并执行
  2. shell 脚本就是用户为了完成某一件事而将多个命令放在一个文件中然后直接执行脚本文件就可以达到目的。

shell 的执行方式

执行一个 shell 脚本的方式有 4 种

  • bash script.sh
  • ./script.sh
  • source script.sh
  • . script.sh

我们先写一个名为 2.sh 的脚本来对比上面执行脚本的方式的异同点脚本内容如下

cd /tmp
pwd

bash 命令执行脚本

当我们使用 bash 来执行 2.sh :

bash 2.sh

输出如下

在这里插入图片描述

可以看出

  1. bash 命令可以执行没有执行权限的脚本
  2. 当 bash 执行脚本后当前的目录还是在 /root 主目录下并没有切换到 /tmp 目录中。
  3. 这个是因为当执行 bash 命令的时候会启动一个新的进程来执行脚本中的命令所以脚本执行的结果对当前进程是没有影响的

我们在终端上执行 bash 命令然后使用 ps -f 查看也能验证这点

在这里插入图片描述
以上的进程是执行 bash 命令后出现的进程。而且还能看出这个 bash 进程的父亲进程的 ID 是第一个 bash 进程的 PID。

./ 执行脚本

当我们使用 ./ 来执行 2.sh :

./2.sh

结果输出

-bash: ./2.sh: Permission denied

发现权限不够使用这种方式来执行脚本的时候要求这个脚本要有可执行权限如下更改脚本的权限

chmod u+x 2.sh

再次执行脚本输出如下

在这里插入图片描述
这个输出和使用 bash 命令执行脚本的输出是一样的

  • 同样会创建一个新的进程来执行脚本中的内容
  • 而且使用这种方式来执行的话脚本要求有可执行权限

source 和 . 来执行脚本

如下使用 source 来执行脚本

source 2.sh

输出如下

在这里插入图片描述

如下使用 . 来执行脚本

. 2.sh

输出如下

在这里插入图片描述

可以看出以上两种方式执行结果是一样的而且对当前终端是有影响的两种执行结果都进入了 /tmp 目录这个可以说明

  • 使用 source. 执行脚本的时候不会启动先的进程只是在当前的 bash 进程中执行脚本内容
  • 所以这两种执行脚本的方式在执行脚本的时候会影响当前终端。

变量

变量的定义

在 shell 编程中有很多时候我们想将数据先临时保存起来供后续使用我们可以先将数据保存到某个变量中如下是将字符串 jeffy 赋值给变量 name

name=jeffy

注意等号两边不能有空格

以上就是定义了一个名字为 name 的变量我们可以通过如下的方式来访问这个变量

## 在控制台中输出变量 name 的值
echo ${name}

## 以上访问 name 变量也可以简写为
echo $name

在定义变量的时候变量名的定义需要遵循下面的规则

  1. 由字母、数字、下划线组成
  2. 不以数字开头
  3. 变量名一般要求有一定的意义

比如下面的变量名是不推荐使用的

## 存在非法组成字符
*name=twq
## 以数字开头了
12name=twq
## a 这个变量名没啥意义
a=twq

现在如果我们想将 10+20 的结果赋值给一个名为 result 的变量如下

result=10+20

echo $result

## 以上输出为
10+20

上面的程序并没有达到我们的预期我们可以在定义变量之前加上一个关键字 let 如下

let result=10+20

echo $result

## 以上输出为
30

在有些场景下变量值中可能会出现空格等其他的特殊字符我们可以通过双引号或者单引号来解决特殊字符的问题如下

## 定义一个名为 content 的变量将 tom's cat 赋值给 content 变量
## 以下定义是错误的因为变量值中有空格
content=tom's cat

## 我们在变量值中加上双引号如下
content="tom's cat"

echo $content

## 如果变量值中有一个双引号的话则需要使用单引号比如
content='tom"s cat'

echo $content

变量和命令

除了数据我们也可以将一个命令赋值给一个变量如下

## 将 ls 命令赋值给变量 l
l=ls

## 这样使用
$l

以上的做法意义不大在实际生产上我们一般不会将一个命令赋值给一个变量

为了提高性能我们一般会将一个命令执行的结果赋值给一个变量如下

## 将 ls -l /etc 执行的结果赋值给变量 lsetc 
lsetc=$(ls -l /etc)

## 以后想使用 ls -l /etc 的结果的话我们只需要访问 lsetc 这个变量就可以了
echo $lsetc

## 我们也可以使用 `` 来代替上面的 $()
lsetc=`ls -l /etc`
echo lsetc

变量的拼接

比如我们先定义一个名为 tmp 的临时变量我们将字符串 test 赋值给 tmp

tmp=test

如果我们想将 tmp 中的变量值和字符串 twq 进行拼接

echo $tmptwq

## 输出为空

使用上面的方式是不能进行变量拼接的以上 tmptwq 被看成了一个变量然而这个变量并没有赋值所以输出为空我们可以通过如下的方式进行变量的拼接

echo ${tmp}twq

## 输出为
testtwq

## 我们也可以将拼接之后的数据再次赋值给变量 tmp 
tmp=${tmp}twq

echo $tmp

实际上我们可以在使用变量的时候在变量后面加上一个非数字、非字母、非下划线的字符就可以实现变量的拼接如下

echo $tmp:jeffy

## 输出
testtwq:jeffy

我们再定义一个变量

age=20

然后将 tmpage 变量使用 : 拼接起来如下

echo $tmp:$age

## 输出为
testtwq:20

变量的范围

我们现在先定义一个名为 demo_var 的变量这个变量的值是 hello shell如下

demo_var="hello shell"

## 在当前的 bash 进程中是可以访问的如下
echo $demo_var

## 如果我们再打开一个 bash 进程
bash

## 然后就访问不了了变量了如下输出为空
echo $demo_var

## 我们再在 bash 子进程中定义一个变量如下
demo_var="hello subshell"

## 然后退出当前的子 bash 进程
exit

## 在父进程中也访问不了子进程中定义的变量
echo $demo_var

## 以上输出为
hello shell

## 如果我们重新打开一个终端那也是访问不了上面的进程中的变量

从上面的演示可以得出结论

  • 变量默认的访问范围是当前的 bash 进程

那么我们如果在某个脚本中能不能访问上面的变量呢我们先创建一个名为 3.sh 的脚本内容就是访问并打印变量 demo_var 的值如下

vi 3.sh

echo $demo_var

## 保存以上的 2.sh 的脚本文件
## 给 2.sh 脚本赋予执行权限
chmod u+x 2.sh

然后我们分别使用 bash 2.sh./2.shsource 2.sh 以及 . 2.sh 四种方式来执行上面的脚本发现

  • 使用 bash 2.sh./2.sh 这两种方式访问不到变量的值这个是因为这两种方式都会启动一个子 bash 进程来执行脚本
  • 而使用 source 2.sh. 2.sh 这两种方式是可以访问到变量的值这个是因为这两种方式都是在当前的 bash 进程中执行脚本的

那么我们怎么样使得一个变量可以在子 bash 进程中访问呢我们可以使用 export 关键词将变量导出然后其他的进程就可以访问这个变量了如下

## 在定义 demo_var 的 bash 进程中 export 这个变量
export demo_var

## 然后使用四种方式执行 2.sh 脚本时都是可以访问变量的
## 也就是说变量通过 export 导出后就可以在子进程中进行访问了。

## 如果你想取消导出的变量可以使用关键字 unset 来实现如下
unset demo_var

环境变量及其配置文件

环境变量

所谓的环境变量就是每一个 shell 打开都可以获得到的变量只要一打开一个终端就可以访问环境变量。

我们可以通过下面的命令获取到当前用户下当前默认的所有的环境变量

env

输出为

XDG_SESSION_ID=8
HOSTNAME=master
SELINUX_ROLE_REQUESTED=
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
SSH_CLIENT=192.168.126.1 59094 22
SELINUX_USE_CURRENT_RANGE=
SSH_TTY=/dev/pts/0
USER=root
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
MAIL=/var/spool/mail/root
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
PWD=/root
LANG=en_US.UTF-8
SELINUX_LEVEL_REQUESTED=
HISTCONTROL=ignoredups
SHLVL=1
HOME=/root
LOGNAME=root
SSH_CONNECTION=192.168.126.1 59094 192.168.126.133 22
LESSOPEN=||/usr/bin/lesspipe.sh %s
XDG_RUNTIME_DIR=/run/user/0
_=/usr/bin/env

上面显示的都是 root 用户下的环境变量变量名都是使用大写来表示的。在以上的环境变量中有几个需要我们关心的环境变量

  • USER
## 输出当前用户
echo $USER
  • UID
## 输出当前用户 id
echo $UID
  • PATH
## 输出当前用户下的命令搜索路径 PATH 的变量值
echo $PATH

输出为

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

以上路径称为命令搜索路径当我们执行一个命令比如 ls、cd 等系统会从上面的路径下分别搜索这个命令对应的脚本文件

  • 先搜索 /usr/local/sbin
  • 再搜索 /usr/local/bin
  • 再搜索 /usr/sbin
  • 再搜索 /usr/bin
  • 最后搜索 /root/bin

如果第一次搜索到了就执行那个目录下的脚本命令比如当执行 ls 命令的时候系统会依次看

  • /usr/local/sbin 下面是否有 ls 脚本文件
  • /usr/local/bin 下面是否有 ls 脚本文件
  • /usr/sbin 下面是否有 ls 脚本文件
  • /usr/bin 下面是否有 ls 脚本文件
  • /root/bin 下面是否有 ls 脚本文件

发现 ls/usr/bin 下面所以最终执行的就是 /usr/bin/ls 脚本文件

  • 实际上执行 ls/usr/bin/ls 的效果是一样的
  • 就是因为 /usr/binPATH 变量中所以执行 ls 命令的时候可以省去完整的路径
  • 而且你可以在任何的位置上都可以执行 ls 命令

比如我们在 /tmp 目录下创建一个名为 4.sh 的脚本内容如下

echo "hello bash"
du -sh

然后赋予 4.sh 这个文件的执行权限

chmod u+x /tmp/4.sh

我们在 /root 目录下可以直接执行 ./4.sh 可以执行成功但是如果我们直接使用 4.sh 来运行的话是执行不成功的这个是因为在命令搜索路径中搜索不到 /tmp 目录下的 4.sh如果我们想直接 4.sh 执行成功的话需要将 4.sh 所在的目录拼接到环境变量 PATH 中如下

PATH=$PATH:/tmp

echo $PATH

输出为

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin:/tmp

现在就可以在任意的目录中都可以直接执行 4.sh 了。

以上更改之后的 PATH 对子进程是有效的但是对于新打开的终端是无效的。

bash shell 的格式

在我们执行 bash 命令的时候实际上也需要从命令搜索路径中搜索这个 bash 命令所在的路径然后执行这个命令我们可以通过 which 命令来查看 bash 命令是哪一个路径下的

which bash

## 输出如下
/usr/bin/bash

说明 bash 命令默认使用的是 /usr/bin/bash

那么如果使用 bash 命令执行脚本的时候默认就是使用 /usr/bin/bash 这个命令来执行的那么当我们使用其他方式来执行脚本的时候到底是使用什么命令来执行脚本的呢

当然默认的话还是使用 /usr/bin/bash 来执行的但是如果你想指定使用 /bin/bash 来执行脚本的话可以在脚本的前面加上一个声明

#!/bin/bash

加了上面的声明后

  • 如果使用 bash 命令来执行的话那么上面的声明就成了注释使用的还是系统默认的 bash
  • 如果使用其他方式来执行脚本的话那么上面的声明就是告诉使用 /bin/bash 来执行这个脚本中的内容

环境变量配置文件

在 Centos 7 中和环境变量相关的配置文件有如下几个

  • /etc/profile
  • ~/.bash_profile
  • ~/.bashrc
  • /etc/bashrc

一般的话我们的环境变量都保存在以上的 4 个配置文件中那么保存在 /etc 下面的配置文件中的环境变量是所有的用户都可以访问到的而保存在用户主目录下配置文件中的环境变量只有当前的用户可以访问的

那么对于一些通用的环境变量的话一般保存在 /etc 文件目录下的配置文件而对于某个用户特用的则保存在这个用户下的家目录下的配置文件中。

除了以上的区别之外我们还可以看出以上 4 个文件主要分为两类一类是 profile一类是 bashrc。那么这个 profile 主要是用于 login shell 的而 bashrc 主要是用于 nologin shell 的接下来我们分别在以上 4 个配置文件中使用 echo 打印输出一句话然后看看 4 个配置文件的执行顺序。

在文件 /etc/profile 中加入

echo "/etc/profile starting"

在文件 /etc/bashrc 中加入

echo "/etc/bashrc starting"

root 家目录下 ~/.bashrc 下加入

echo "~/.bashrc starting"

root 家目录下 ~/.bash_profile 中加入

echo "~/.bash_profile starting"

user1 家目录下 ~/.bashrc 下加入

echo "user1 ~/.bashrc starting"

user1 家目录下 ~/.bash_profile 中加入

echo "user1 ~/.bash_profile starting"

现在我们做以下的实验

  1. 重启客户端重新连接终端发现执行顺序是
/etc/profile starting
~/.bash_profile starting
~/.bashrc starting
/etc/bashrc starting
  1. su - user1
/etc/profile starting
user1 ~/.bash_profile starting
user1 ~/.bashrc starting
/etc/bashrc starting
  1. su - root
/etc/profile starting
~/.bash_profile starting
~/.bashrc starting
/etc/bashrc starting
  1. su user1
user1 ~/.bashrc starting
/etc/bashrc starting
  1. su root
~/.bashrc starting
/etc/bashrc starting

接下来对上面进行总结

  1. login shell 的执行顺序/etc/profile -> ~/.bash_profile -> ~/.bashrc -> /etc/bashrc
  2. nologin shell 的执行顺序~/.bashrc -> /etc/bashrc

现在为了使得在任何的终端上都可以执行 4.sh 我们重新打开一个终端然后将环境变量写入到配置文件 ~/.bash_profile 中如下

PATH=$PATH:$HOME/bin:/tmp

修改配置后还不能直接执行 4.sh 因为更改后的配置在当前的 bash 进程不生效如果我们想使得配置在当前的 bash 进程中生效的话可以执行命令

source .bash_profile

这样就可以在 root 的任意目录下执行 4.sh 了如果你想在其他的用户的任意目录中执行 4.sh 的话可以在 /etc/profile 的配置中增加配置

PATH=$PATH:/tmp

预定义变量和位置变量

预定义变量

常见的预定义变量包括$?$$$0

  • $? 表示判断上一条执行的命令是否正常执行如果 $? 返回的值是 0 的话则表示上一条命令是正常执行成功否则表示上一条命令执行失败
ifconfig

echo $?
## 返回 0

## 查询一个不存在的网卡
ifconfig em
echo $?
## 返回 1
  • $$ 表示返回当前进程的 PID
echo $$
  • $0 表示返回当前进程的名称
echo $0

创建一个 5.sh 的脚本脚本内容如下

#!/bin/bash

# 显示 PID 和 PName
echo $$
echo $0

给脚本赋值权限

chmod u+x 5.sh

## 分别使用两种方式来执行脚本
## 1. 使用 bash
bash 5.sh

## 2. 使用 . 来执行
. 5.sh

## 第二种方式输出的进程号和进程名是当前的 bash 进程的

位置变量

位置变量用于给脚本传递参数的变量。我们可以给一个脚本传递任意多的参数

  • $1 表示第一个参数
  • $2 表示第二个参数
  • $3 表示第三个参数
  • ${10} 表示第十个参数
  • ${11} 表示第十一个参数

我们现在写一个脚本 6.sh 如下

#!/bin/bash
# Program:
# Program shows the script name, parameters...

first=${1}
second=${2}
echo "The script name is ==> ${0}"
echo "The 1st parameter ==> $first"
echo "The 2nd parameter ==> $second"

然后给 5.sh 执行权限 chmod u+x 6.sh

使用下面的命令执行上面的脚本

bash 6.sh -a -l

输出如下

The script name is ==> 6.sh
The 1st parameter ==> -a
The 2nd parameter ==> -l

如果我们我们只传递了一个参数

bash 6.sh -a

输出为

The script name is ==> 6.sh
The 1st parameter ==> -a
The 2nd parameter ==> 

可以发现第二个参数的值为空了如果没有传递参数的话我们可以使用默认值来代替脚本内容修改后如下

#!/bin/bash
# Program:
# Program shows the script name, parameters...

first=${1}
second=${2-_}
echo "The script name is ==> ${0}"
echo "The 1st parameter ==> $first"
echo "The 2nd parameter ==> $second"

再此执行脚本

bash 6.sh -a

输出为

The script name is ==> 6.sh
The 1st parameter ==> -a
The 2nd parameter ==> _

传递 2 个参数来执行脚本

bash 6.sh -a -l

输出如下

The script name is ==> 6.sh
The 1st parameter ==> -a
The 2nd parameter ==> -l

在给脚本传递参数的时候还有两个预定义变量

  1. $# 表示参数的个数
  2. $@ 表示拿到所有的参数

修改 6.sh 脚本如下

#!/bin/bash
# Program:
# Program shows the script name, parameters...
# History:
# 2018/03/20 twq
first=${1}
second=${2-_}
echo "The script name is ==> ${0}"
echo "Total parameter number is ==> $#"
echo "Your whole parameter is ==> '$@'"
echo "The 1st parameter ==> $first"
echo "The 2nd parameter ==> $second"

传递 2 个参数来执行脚本

bash 6.sh -a -l

输出如下

The script name is ==> 6.sh
Total parameter number is ==> 2
Your whole parameter is ==> '-a -l'
The 1st parameter ==> -a
The 2nd parameter ==> -l

数组

变量可以存储任意的字符串数据如果需要存储任意多个具有相同意思的字符串数据的话我们可以使用数组来存储。

比如我们定义一个数组用于存储 3 个 ip

ips=(10.0.0.1 10.0.0.2 10.0.0.3)

如果我们想访问数组中所有的元素的话可以

echo ${ips[@]}

如果想显示数组的个数的话可以

echo ${#ips[@]}

如果想访问指定下标的元素的话可以

## 访问第一个元素的值
echo ${ips[0]}

## 访问第二个元素的值
echo ${ips[1]}

数字运算

在 Linux 脚本中支持

  1. 赋值运算符
  2. 算术运算符

赋值运算符很简单就是使用 = 来进行赋值操作。

算术运算符支持 + - * / % 这 5 种运算符我们接下来详细讲解下在 Linux 中怎么来完成算术运算。

如果我们想在 shell 脚本中完成将 4 + 5 的结果赋值给一个名为 result 的变量我们该怎么做呢

## 直接这样做是不行的
result=4+5
echo result

## 需要使用 expr 这个命令来实现运算
## 这里需要注意的是 4 + 5 之间必须要有空格
expr 4 + 5

## 将 expr 命令计算出来的结果赋值给 result 的变量
result=`expr 4 + 5`
echo $result

expr 命令不支持小数的运算

除了使用 expr 命令还可以使用双圆括号来进行运算

(( result=4+5 ))
echo $result

(( result++ ))
echo $result

test 比较

exit

exit 命令用于退出脚本的如果是正常退出的话exit 会返回 0如果是异常退出的话则 exit 会返回非 0

现在我们编写一个 7.sh 的脚本脚本的内容如下

#!/bin/bash

pwd
exit

执行上面的脚本发现加不加 exit 都是一样的我们执行

## 查看脚本返回的值为 0
echo $?

但是当脚本发生错误的时候比如执行一个不存在的命令

#!/bin/bash

ppwd
exit

这个时候执行脚本报错了

## 查看脚本返回的值为 127
echo $?

我们在执行 exit 命令的时候也可以指定脚本退出返回的值如下

#!/bin/bash

pwd
exit 127

这个时候不管脚本有没有报错都返回 127

## 查看脚本返回的值为 127
echo $?

test 命令

我们使用 man 来获取 test 命令的帮助文档

man test

发现我们使用 test 主要可以做如下的事情

  1. 测试两个字符串是否相等
  2. 测试两个整数之间的大小关系
  3. 测试一个文件是否存在
测试两个字符串是否相等
[root@master ~]# test "abc" = "abc"
[root@master ~]# echo $?
0
[root@master ~]# test "abc" = "abcd"
[root@master ~]# echo $?
1
[root@master ~]# test "abc" != "abcd"
[root@master ~]# echo $?
0

注意0 表示 true0 表示 false

以上的写法我们还可以这样写

[root@master ~]# [ "abc" = "abc" ]
[root@master ~]# echo $?
0
[root@master ~]# [ "abc" = "abcd" ]
[root@master ~]# echo $?
1
[root@master ~]# [ "abc" != "abcd" ]
[root@master ~]# echo $?
0
整数大小判断

在判断两个整数的大小的时候我们使用

  • -eq 表示判断是否等于 (equal)
  • -ge 表示判断是否大于等于 (great than or equal)
  • -gt 表示判断是否大于 (great than)
  • -le 表示判断是否小于等于 (less than or equal)
  • -lt 表示判断是否小于 (less than)
  • -ne 表示判断是否不等于 (not equal)
[root@master ~]# test 5 -gt 4
[root@master ~]# echo $?
0
[root@master ~]# test 5 -eq 4
[root@master ~]# echo $?
1
[root@master ~]# test 5 -lt 4
[root@master ~]# echo $?
1

以上的判断还可以这样写

[root@master ~]# [ 5 -gt 4 ]
[root@master ~]# echo $?
0
[root@master ~]# [ 5 -eq 4 ]
[root@master ~]# echo $?
1
[root@master ~]# [ 5 -lt 4 ]
[root@master ~]# echo $?
1

如果你想用 >、< 、= 等这种比较符号的话需要使用双中括号来实现

[root@master ~]# [[ 5 > 4 ]]
[root@master ~]# echo $?
0
[root@master ~]# [[ 5 = 4 ]]
[root@master ~]# echo $?
1
[root@master ~]# [[ 5 < 4 ]]
[root@master ~]# echo $?
1
测试一个文件是否存在
  1. 测试一个文件是否存在
[root@master ~]# test -e /etc/passwd
[root@master ~]# echo $?
0
[root@master ~]# test -e /etc/passwd2
[root@master ~]# echo $?
1

也可以写成

[root@master ~]# [ -e /etc/passwd ]
[root@master ~]# echo $?
0
[root@master ~]# [ -e /etc/passwd2 ]
[root@master ~]# echo $?
1
  1. 测试一个文件是否存在并且这个文件是目录
[root@master ~]# test -d /etc
[root@master ~]# echo $?
0
[root@master ~]# test -d /etc/passwd
[root@master ~]# echo $?
1

也可以写成

[root@master ~]# [ -d /etc ]
[root@master ~]# echo $?
0
[root@master ~]# [ -d /etc/passwd ]
[root@master ~]# echo $?
1
  1. 测试一个文件是否存在并且这个文件是普通文件
[root@master ~]# test -f /etc/passwd
[root@master ~]# echo $?
0
[root@master ~]# test -f /etc
[root@master ~]# echo $?
1

也可以写成

[root@master ~]# [ -f /etc/passwd ]
[root@master ~]# echo $?
0
[root@master ~]# [ -f /etc ]
[root@master ~]# echo $?
1
多个测试进行逻辑与和逻辑或
[root@master ~]# [[ -f /etc && 5 -gt 4 ]]
[root@master ~]# echo $?
1
[root@master ~]# [[ -f /etc || 5 -gt 4 ]]
[root@master ~]# echo $?
0

也可以写成

[root@master ~]# [ -f /etc ] && [ 5 -gt 4 ]
[root@master ~]# echo $?
1
[root@master ~]# [ -f /etc ] || [ 5 -gt 4 ]
[root@master ~]# echo $?
0

条件语句 if case while for

if…then

if [ 条件判断式 ]; then
    当条件判断式返回 0 的时候可以进行的命令工作内容
fi

写一个脚本来判断当前的用户是否是 root 用户脚本名为 8.sh 内容如下

#!/bin/bash

if [ $USER = 'root' ]; then 
    echo "the current user is root"
fi

if…then…else

if [ 条件判断式 ]; then
    当条件判断式返回 0 的时候可以进行的命令工作内容
else
    当条件判断式返回非 0 的时候可以进行的命令工作内容
fi

修改脚本 8.sh 内容如下

#!/bin/bash

if [ $USER = 'root' ]; then 
    echo "the current user is root"
else 
    echo "the current user is not root"
fi

if…then…elif…

if [ 条件判断式一 ]; then
    当条件判断式一返回 0 的时候可以进行的命令工作内容
elif [ 条件判断式二 ]; then
    当条件判断式二返回 0 的时候可以进行的命令工作内容
else
    当所有条件判断式都返回非 0 的时候可以进行的命令工作内容
fi

新增 9.sh 脚本脚本内容如下

#!/bin/bash

read -p "Please input (Y/N)" yn

if [[ "$yn" = "Y" || "$yn" = "y" ]]; then
    echo "Ok, continue"
elif [[ "$yn" = "N" || "$yn" = "n" ]]; then
    echo "Oh, interrupt!"
else
    echo "not support!!"
fi

编写一个名为 10.sh 的 shell 脚本shell 脚本中的内容设计为

  1. 判断传递给脚本的第一个参数的值如果这个值等于 hello 的话就显示 “Hello, how are you ?”
  2. 如果没有任何参数的话就提示用户必须要使用的参数
  3. 如果传递的参数不是 hello 就提醒用户仅能使用 hello 作为参数
#!/bin/bash

if [ "$1" = "hello" ]; then
    echo "Hello, how are you ?"
elif [ "$1" = "" ]; then
    echo "You must input parameters, for exapme > {$0 someword}"
else
    echo "The only parameter is 'hello', for example > {$0 hello}"
fi

case…esac

case…esac 用来进行选择执行语法如下

case $变量名称 in
    "第一个值")
        变量内容等于第一个值的时候执行的程序段
        ;;
    "第二个值")
        变量内容等于第二个值的时候执行的程序段
        ;;
    *)
        不等于第一个值且不等于第二个值的其他程序执行内容
        exit 1
        ;;
esac

我们可以将上一节课的 10.sh 脚本使用 case 来实现新建一个名为 11.sh 的脚本内容如下

#!/bin/bash

case $1 in
    "hello")
        echo "Hello, how are you ?"
        ;;
    "")
        echo "You must input parameters, for exapme > {$0 someword}"
        ;;
    *)
       echo "The only parameter is 'hello', for example > {$0 hello}"
       ;;
esac

我们在前面管理系统服务的时候一般我们都使用 service start|stop|restart 等来对服务来管理我们现在也可以自己来写一个 service.sh 脚本来默认对服务的启停等管理脚本内容如下

#!/bin/bash

case $1 in
    "start")
        echo "service start"
        ;;
    "stop")
        echo "service stop"
        ;;
    "restart")
        echo "service restart"
        ;;
    "status")
        echo "service status"
        ;;
    *)
       echo "Usage : {$0 start|stop|restart|status}"
       ;;
esac

while

while 的语法是

while [ condition ]
do
    程序段落
done

以上的说法是当 condition 成立时就进行循环直到 condition 的条件不成立才停止。

我们现在来写一个名为 12.sh 的脚本让用户输入 yes 或者 YES 的时候才结束程序否则就一直进行告知用户输入字符串

#!/bin/bash

while [[ "$yn" != "yes" && "$yn" != "YES" ]]
do
    read -p "Please input yes/YES to stop this program" yn
done
echo "Ok! program stopping"

还有一个循环和 while 刚好是相反的那就是 until

until [ condition ]
do
    程序段落
done

以上的说法是当 condition 不成立时就进行循环直到 condition 的条件成立才停止。

我们现在来写一个名为 13.sh 的脚本让用户输入 yes 或者 YES 的时候才结束程序否则就一直进行告知用户输入字符串

#!/bin/bash

until [[ "$yn" == "yes" || "$yn" == "YES" ]]
do
    read -p "Please input yes/YES to stop this program" yn
done
echo "Ok! program stopping"

练习 使用 while 来计算 1100 之和

#!/bin/bash

s=0
i=0
while [ "$i" != "100" ]
do
    ((i++))
    ((s=$s+$i))
done
echo "the result is : $s"

练习 使用 while 来计算 1 到你输入的数字之和比如

  • 你输入 10 就计算 1 到 10 之和
  • 你输入 100 就计算 1 到 100 之和
  • 你输入 1000 就计算 1 到 1000 之和
#!/bin/bash

read -p "Please input you number " num

s=0
i=0
while [ "$i" != "$num" ]
do
    ((i++))
    ((s=$s+$i))
done
echo "the result is : $s"

for

循环遍历指定的值
for var in con1 con2 con3 ...
do
    程序段
done

比如程序段

for i in a b c
do
    echo $i
done
循环指定范围

如下代码段

for i in {1..9}
do
    echo $i
done

还比如

for i in $(seq 1 10)
do
    echo $i
done
循环目录中的所有文件
filelist=$ (ls /)
echo $filelist

for filename in $filelist
do
    echo ${filename}_file
done
c 语言风格的 for

语法如下

for (( 初始值; 限制值; 执行步长 ))
do
    程序段
done

使用上面的 for 语句来计算 1100 的累加值先建名为 14.sh 的脚本内容为

#!/bin/bash
s=0
for (( i=1; i<=100; i=i+1 ))
do 
    s=$(($s+$i))
done
echo "the result is : $s"

函数

现在假设有个需求

  • 输入一个目录然后打印出这个目录下的所有的普通文件

我们创建一个名为 list_file_for_dir.sh 的脚本内容如下

#!/bin/bash

function is_file() {
    test -f $1
}

function list_file() {
    file_list=`ls $1`
    for filename in $file_list
    do
        if is_file $1/$filename; then
            echo $1/$filename
        fi
    done
}

这个时候我们可以写一个名为 15.sh 的脚本然后调用上面的函数

#!/bin/bash

## 导入需要调用的函数所在的脚本
source ./list_file_for_dir.sh

dir=$1

list_file $dir

重定向

当我们在 Centos 7 系统上启动一个进程的话默认的话会打开标准输入、标准输出、错误输出三个文件描述符。

比如当我们打开一个终端的时候会启动一个 bash 进程我们可以通过 ps 来查看这个进程的 PID :

[root@master ~]# ps
   PID TTY          TIME CMD
  2063 pts/1    00:00:00 bash
  2080 pts/1    00:00:00 ps

然后我们可以查看目录 /proc/2033/fd 下的文件

[root@master fd]# ls -l /proc/2033/fd
total 0
lrwx------. 1 root root 64 Oct  6 15:19 0 -> /dev/pts/0
lrwx------. 1 root root 64 Oct  6 15:19 1 -> /dev/pts/0
lrwx------. 1 root root 64 Oct  6 15:19 2 -> /dev/pts/0
lrwx------. 1 root root 64 Oct  6 15:20 255 -> /dev/pts/0

/proc 目录下就是存放了每个进程的状态信息

从上可以看出有 4 个链接文件其中

  • 0 表示标准输入
  • 1 表示标准输出
  • 2 表示错误输出

从上还可以看出不管是输入还是输出默认都是终端。

不管是输入还是输出我们都可以进行重定向。

输入重定向

## 使用 wc -l 统计从终端输入的数据的行数
[root@master ~]# wc -l
123
2342
2
[root@master ~]#

ctrl + d 退出 wc -l 界面

我们可以使用输入重定向符号 < 来对输入进行重定向比如我们使用 wc -l 统计一个文件中的数据的行数如下

## 将输入重定向为一个文件
[root@master ~]# wc -l < /etc/passwd
22

还比如我们可以读取终端的数据饭后赋值给一个变量

[root@master ~]# read var 
123
[root@master ~]# echo $var
123

我们也可以将一个文件作为输入然后将文件中第一行内容赋值给一个变量

[root@master ~]# read var2 < /etc/passwd
[root@master ~]# echo $var2
root:x:0:0:root:/root:/bin/bash

输出重定向

默认情况下输出是终端比如

## 将数据字符串 123 输出到终端
[root@master ~]# echo 123
123

如果我们想将输出重定向到一个文件中我们可以使用 > 或者 >> 符号如下

[root@master ~]# echo 123 > a.txt
[root@master ~]# cat a.txt
123
[root@master ~]# echo 456 > a.txt
[root@master ~]# cat a.txt
456
[root@master ~]# echo 456 >> a.txt
[root@master ~]# cat a.txt
456
456
[root@master ~]# echo 456 >> a.txt
[root@master ~]# cat a.txt
456
456
456
  • > 会清除文件中之前的内容
  • >> 不会清除文件会将内容追加到文件的最后

以上是将正确的数据输入到指定的文件我们也可以将一个执行脚本或者命令时候的报错信息输出到指定的文件中比如

[root@master ~]# nocmd
-bash: nocmd: command not found
[root@master ~]# nocmd 2> error.tx
[root@master ~]# cat error.tx 
-bash: nocmd: command not found

可以看出我们使用 2> 来重定向错误输出。

如果不管是正确结果还是错误结果我们都想重定向到一个指定文件中我们可以使用 &> 符号

[root@master ~]# nocmd &> d.txt
[root@master ~]# cat d.txt 
-bash: nocmd: command not found
[root@master ~]# ls &> d.txt
[root@master ~]# cat d.txt 
10.sh
11.sh
12.sh
13.sh
14.sh
15.sh
2.sh
5.sh

nohup 和 输出重定向

我们前面讲过使用 nohup 的方式来启动进程使得进程在后台运行并且将进程的输出会输出到当前目录下 nohup.out 的文件中

我们可以使用 nohup 结合输出重定向 &> 来自定义进程的输出文件如下

## 后台运行 15.sh 脚本并且将结果都输出到 test.out 文件中
nohup /root/15.sh /etc &> test.out &

## 你也可能会遇到这样的写法效果和上面的是一样
## 先将脚本标准输出输出到文件 test.out 中
## 然后 2>&1 表示将错误输出输出到标准输出中
nohup /root/15.sh > test.out 2>&1 &

管道

执行一个命令或者一个脚本都会有一个标准输入和一个标准输出有很多场景下我们想将一个命令的标准输出作为另一个命令的标准输入这个就是我们这篇文章讲到的管道技术。

比如我们执行 ls /etc 的时候发现输出结果太多了我们想分页看这个命令输出的结果那么我们就可以将 ls /etc 这个命令的输出作为命令 less 的输入如下

ls /etc | less

上面的 | 就是管道符。它的作用就是将 ls /etc 的输出作为 less 命令的输入。

再比如我们使用 history 来查看历史命令但是发现执行的历史命令太多了我们需要从历史命令中找到包含关键字 alias 的命令我们可以

history | grep alias

也就是将 history 的输出作为 grep alias 的输入grep alias 就是查找包含 alias 的行。

有的时候我们通过 ps -ef 来查找进程发现进程数太多了也可以使用 grep 来过滤出我们自己想要的进程的信息

ps -ef | grep wc

date 命令

我们可以使用 date 命令查看系统当前的时间

[root@master ~]# date
Sun Oct  6 14:20:26 CST 2019

如果你的时间不对的话可以通过 date 命令来设置

date -s '2019-10-10 08:55:55'

我们也可以通过 date 命令拿到系统当前的年、月、日、时、分、秒

[root@master ~]# date +%Y
2019
[root@master ~]# date +%m
10
[root@master ~]# date +%d
06
[root@master ~]# date +%H
14
[root@master ~]# date +%M
21
[root@master ~]# date +%S
16
[root@master ~]# date +%Y%m%d
20191006

有了这些信息后我们就可以对当前的时间进行格式化比如我们想使用格式 YYYY-mm-dd HH:MM:SS 来显示当前的时间

[root@master ~]# date "+%Y-%m-%d %H:%M:%S"
2019-10-06 14:24:24
[root@master ~]# date +%Y%m%d%H%M%S
20191006142531

我们还可以使用 date 命令获取到前一天的时间

[root@master ~]# date --d="1 days ago" 
Sat Oct  5 14:28:55 CST 2019
[root@master ~]# date --d="1 days ago" "+%Y-%m-%d %H:%M:%S"
2019-10-05 14:29:08

还可以获取明天或者后天的时间

[root@master ~]# date --d="1 days" 
Mon Oct  7 14:29:51 CST 2019
[root@master ~]# date --d="1 days" "+%Y-%m-%d %H:%M:%S"
2019-10-07 14:29:58

一次性计划任务

我们前面都是手动的执行脚本那么在实际的环境中有可能在半夜、或者指定的时间来运行脚本这个我们可以通过 at 命令来实现使用 at 前需要安装 at

yun -y install at

接下来我们使用 at 命令来执行 15.sh 脚本

[root@master ~]# chmod u+x 15.sh
[root@master ~]# date
Sun Oct  6 14:37:56 CST 2019
[root@master ~]# at 14:40
at> /root/15.sh /etc > /tmp/test.txt
at> <EOT>
job 4 at Sun Oct  6 14:40:00 2019

注意最后是使用 ctrl + d 退出 at 界面保存 job

可以通过命令 atq 来查询有多少的 at job

[root@master ~]# atq
2	Sun Oct  6 14:35:00 2019 a root
4	Sun Oct  6 14:40:00 2019 a root

等到 14:40 分钟的时候我们查看结果文件 /tmp/test.txt输出内容如下

[root@master ~]# ls -al /tmp/test.txt 
-rw-r--r--. 1 root root 2996 Oct  6 14:47 /tmp/test.txt

at 适合一次性的计划任务执行完了就不会再执行了。

周期性计划任务

如果需要周期性的执行命令或者脚本的话我们要用到 crontab 这个命令。

比如我们现在实现每分钟将当前的日期追加到文件 /tmp/date.txt 中。

执行 crontab -e 进行任务编辑界面这个编辑界面和 vi 是一样的

crontab -e

## 分钟 小时 日 月 星期 命令
* * * * * /usr/bin/date >> /tmp/date.txt

可以通过 which date 查看 date 命令的全路径

以上就是每分钟会将 date 追加到 /tmp/date.txt 文件中。

可以通过 tail -fn300 /var/log/cron 来查看周期性任务的执行情况。

[root@master ~]# cat /tmp/date.txt 
Sun Oct  6 17:29:02 CST 2019
Sun Oct  6 17:30:01 CST 2019

可以使用 crontab -l 来查看所有的周期性任务。

## 每个星期一的每分钟执行一次
* * * * 1 /usr/bin/date >> /tmp/date.txt

## 每个星期一或者星期五的每分钟执行一次
* * * * 1,5 /usr/bin/date >> /tmp/date.txt

## 每个星期一到星期五的每分钟执行一次
* * * * 1-5 /usr/bin/date >> /tmp/date.txt

## 7 月 7 号并且属于周一到周五每分钟执行一次
* * 7 7 1-5 /usr/bin/date >> /tmp/date.txt

## 每天凌晨 3 点 30 分执行一次
30 3 * * * /usr/bin/date >> /tmp/date.txt

## 每个星期一的凌晨 3 点 30 分执行一次
30 3 * * 1 /usr/bin/date >> /tmp/date.txt

## 每 15 分钟执行一次
15 * * * * /usr/bin/date >> /tmp/date.txt

文本操作

grep

在文件内容查找的时候我们会使用 grep 命令来实现查找比如

  • /root/anaconda-ks.cfg 文件中查找到单词 password 所在的位置
[root@master ~]# grep password anaconda-ks.cfg 
# Root password

[root@master ~]# grep -n password anaconda-ks.cfg 
20:# Root password

-n 选项表示显示行号

如果我们想找到单词 pass 开头后面有 4 个字符的字符串

[root@master ~]# grep -n pass.... anaconda-ks.cfg 
3:auth --enableshadow --passalgo=sha512
20:# Root password

[root@master ~]# grep -n pass....$ anaconda-ks.cfg 
20:# Root password
  • . 表示匹配除换行符外的任意单个字符
  • $ 表示匹配一行的结尾
  • * 表示匹配任意一个或者零个跟在它前面的字符
[root@master ~]# grep -n pas* anaconda-ks.cfg 
3:auth --enableshadow --passalgo=sha512
20:# Root password
28:autopart --type=lvm
30:clearpart --none --initlabel
32:%packages

[root@master ~]# grep -n pass.* anaconda-ks.cfg 
3:auth --enableshadow --passalgo=sha512
20:# Root password
  • [] 表示匹配方括号中字符类中的任意一个
  • ^ 表示匹配开头
[root@master ~]# grep [Nn]etwork anaconda-ks.cfg 
# Network information
network  --bootproto=dhcp --device=ens33 --onboot=off --ipv6=auto --no-activate
network  --hostname=localhost.localdomain

[root@master ~]# grep -i network anaconda-ks.cfg 
# Network information
network  --bootproto=dhcp --device=ens33 --onboot=off --ipv6=auto --no-activate
network  --hostname=localhost.localdomain

[root@master ~]# grep ^# anaconda-ks.cfg 
#version=DEVEL
# System authorization information
# Use CDROM installation media
# Use graphical install
# Run the Setup Agent on first boot
# Keyboard layouts
# System language
# Network information
# Root password
# System services
# System timezone
# System bootloader configuration
# Partition clearing information

现在要求查找 /root/anaconda-ks.cfg 文件中所有的 . 所在的行

[root@master ~]# grep "\." anaconda-ks.cfg 
lang en_US.UTF-8
network  --hostname=localhost.localdomain
  • 因为 . 是特殊符号所以需要使用 \ 来转移而且转义后不让 . 再次成为匹配所有所以需要加上双引号。

cut & sort

cut

cut 这个命令可以将一段信息的某一段切出来处理数据的时候也是以行为单位。

比如当我们输出 $PATH 的时候它的取值是

echo $PATH

## 输出是
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

## 现在我们想拿到上面的第 4 个字段
echo $PATH | cut -d ":" -f 4

## 输出为
/usr/bin

## 现在想拿到第 3 个和第 5 个字段
echo $PATH | cut -d ":" -f 3,5

## 输出为
/usr/sbin:/root/bin

选项参数的解释

  • -d 表示分割字符一般与 -f 一起使用
  • -f 依据 -d 指定的分割字符将一段信息切割成数段用 -f 取出第几段

使用 cut 也可以指定字符区间来切割数据

[root@localhost ~]# export
declare -x HISTCONTROL="ignoredups"
declare -x HISTSIZE="1000"
declare -x HOME="/root"
declare -x HOSTNAME="localhost.localdomain"
declare -x LANG="en_US.UTF-8"
declare -x LESSOPEN="||/usr/bin/lesspipe.sh %s"
declare -x LOGNAME="root"

## 从第 12 个字符开始截取
[root@localhost ~]# export | cut -c 12-
HISTCONTROL="ignoredups"
HISTSIZE="1000"
HOME="/root"
HOSTNAME="localhost.localdomain"
LANG="en_US.UTF-8"
LESSOPEN="||/usr/bin/lesspipe.sh %s"
LOGNAME="root"

## 也可以指定范围进行切割比如 cut -c 12-20
sort

sort 命令可以帮我们排序而且可以依据不同的数据类型进行排序。

[root@localhost ~]# cat /etc/passwd | sort
adm:x:3:4:adm:/var/adm:/sbin/nologin
bigdata:x:1000:1000::/home/bigdata:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin

## 按照第三个字段排序
[root@localhost ~]# cat /etc/passwd | sort -t ":" -k 3
root:x:0:0:root:/root:/bin/bash
bigdata:x:1000:1000::/home/bigdata:/bin/bash
user1:x:1001:1001::/home/user1:/bin/bash
user2:x:1002:1001::/home/user2:/bin/bash
user3:x:1003:1002::/home/user3:/bin/bash

## 反向排序
[root@localhost ~]# cat /etc/passwd | sort -t ":" -k 3 -r
nobody:x:99:99:Nobody:/:/sbin/nologin
polkitd:x:999:997:User for polkitd:/:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin

find

前面使用 grep 是查找文件中的内容如果我们想在指定的文件目录中查找指定的文件名的话需要使用 find 命令如下

## 查找 /etc 目录下的 passwd 文件
[root@master ~]# find /etc -name passwd
/etc/passwd
/etc/pam.d/passwd

这样就可以找出 /etc 这个目录下的所有的名字为 passwd 的文件。

对于需要查找的文件名我们也可以使用通配符

## 查找 /etc 目录下的所有的以 pass 开头的文件
[root@master ~]# find /etc -name pass*
/etc/openldap/certs/password
/etc/passwd
/etc/passwd-
/etc/pam.d/passwd
/etc/pam.d/password-auth-ac
/etc/pam.d/password-auth
/etc/selinux/targeted/active/modules/100/passenger

对于需要查找的文件名我们也可以使用正则来匹配

## 查找 /etc 目录下的所有以 wd 结尾的文件
[root@master ~]# find /etc -regex .*wd$
/etc/passwd
/etc/pam.d/passwd
/etc/security/opasswd

sed

我们前面讲过 vi 和 vim 编辑器这个编辑器主要是针对一个文件进行编辑的而 sed 命令主要是针对文件中的每一行数据进行编辑的包括

  1. 以行为单位的新增/删除功能
  2. 以行为单位的替换/显示功能
以行为单位的新增/删除功能
  1. /etc/passwd 的前十条数据列出并且打印行号同时请将 2~5 行删除
[root@master ~]# nl /etc/passwd | head -10 | sed '2,5d'
     1	root:x:0:0:root:/root:/bin/bash
     6	sync:x:5:0:sync:/sbin:/bin/sync
     7	shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
     8	halt:x:7:0:halt:/sbin:/sbin/halt
     9	mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
    10	operator:x:11:0:operator:/root:/sbin/nologin

## 删除第二行
[root@master ~]# nl /etc/passwd | head -10 | sed '2d'
     1	root:x:0:0:root:/root:/bin/bash
     3	daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4	adm:x:3:4:adm:/var/adm:/sbin/nologin
     5	lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
     6	sync:x:5:0:sync:/sbin:/bin/sync
     7	shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
     8	halt:x:7:0:halt:/sbin:/sbin/halt
     9	mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
    10	operator:x:11:0:operator:/root:/sbin/nologin

## 删除第 3 行到最后一行使用 $ 表示最后一行
[root@master ~]# nl /etc/passwd | head -10 | sed '3,$d'
     1	root:x:0:0:root:/root:/bin/bash
     2	bin:x:1:1:bin:/bin:/sbin/nologin
  1. 承上例在第二行的后面加上一句话how are you
[root@master ~]# nl /etc/passwd | head -5 | sed '2a how are you'
     1	root:x:0:0:root:/root:/bin/bash
     2	bin:x:1:1:bin:/bin:/sbin/nologin
how are you
     3	daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4	adm:x:3:4:adm:/var/adm:/sbin/nologin
     5	lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

## 新增两行
[root@master ~]# nl /etc/passwd | head -5 | sed '2a how are you \
> this '
     1	root:x:0:0:root:/root:/bin/bash
     2	bin:x:1:1:bin:/bin:/sbin/nologin
how are you 
this 
     3	daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4	adm:x:3:4:adm:/var/adm:/sbin/nologin
     5	lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

## 在第二行前面加上一句话
[root@master ~]# nl /etc/passwd | head -5 | sed '2i how are you'
     1	root:x:0:0:root:/root:/bin/bash
how are you
     2	bin:x:1:1:bin:/bin:/sbin/nologin
     3	daemon:x:2:2:daemon:/sbin:/sbin/nologin
     4	adm:x:3:4:adm:/var/adm:/sbin/nologin
     5	lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
以行为单位的替换/显示功能
  1. 将第 2~5 行的内容替换为 “No 2-5 number”
[root@master ~]# nl /etc/passwd | head -10 | sed '2,5c No 2-5 number'
     1	root:x:0:0:root:/root:/bin/bash
No 2-5 number
     6	sync:x:5:0:sync:/sbin:/bin/sync
     7	shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
     8	halt:x:7:0:halt:/sbin:/sbin/halt
     9	mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
    10	operator:x:11:0:operator:/root:/sbin/nologin
  1. 仅列出 /etc/passwd 文件内的第 5-7 行
[root@master ~]# nl /etc/passwd | head -7 | tail -3
     5	lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
     6	sync:x:5:0:sync:/sbin:/bin/sync
     7	shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
[root@master ~]# nl /etc/passwd | sed -n '5,7p'
     5	lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
     6	sync:x:5:0:sync:/sbin:/bin/sync
     7	shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown

我们可以使用 sed 来替换文本语法如下

sed 's/要被替换的字符串/新的字符串/g'
  1. ifconfig 中获取 ip 的数据
## 获取网卡 ens33 的信息
[root@master ~]# ifconfig ens33
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.126.133  netmask 255.255.255.0  broadcast 192.168.126.255
        inet6 fe80::6ddf:bb00:12ed:7ab9  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:60:74:d6  txqueuelen 1000  (Ethernet)
        RX packets 4215  bytes 346199 (338.0 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1877  bytes 242074 (236.4 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

## 匹配到 ip 所在的行
[root@master ~]# ifconfig ens33 | grep 'inet '
        inet 192.168.126.133  netmask 255.255.255.0  broadcast 192.168.126.255
        
## 替换 ip 前面的内容为空字符串
[root@master ~]# ifconfig ens33 | grep 'inet ' | sed 's/^.*inet //g'
192.168.126.133  netmask 255.255.255.0  broadcast 192.168.126.255

## 替换 ip 后面的内容为空字符串
[root@master ~]# ifconfig ens33 | grep 'inet ' | sed 's/^.*inet //g' | sed 's/ net.*$//g'
192.168.126.133 
  1. 利用 sed/root/anaconda-ks.cfg 内每一行开头的 # 替换为 !
sed -i 's/^#/\!/g' anaconda-ks.cfg

sed -i 's/^!/\#/g' anaconda-ks.cfg

awk

上一篇文章中我们介绍使用 sed 来处理每行数据如果要处理每一行中的每个字段的话我们使用 awk 这个命令来完成。

我们现在查看 /etc/passwd 前面 5 行数据

[root@master ~]# head -5 /etc/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
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

可以看出以上每一行中的每一个字段都是使用 : 来分割开的我们现在想显示每一行的第 1 个字段和第 4 个字段我们可以

[root@master ~]# head -5 /etc/passwd | awk -F ":" '{print $1,$4}'
root 0
bin 1
daemon 2
adm 4
lp 7

[root@master ~]# head -5 /etc/passwd | awk -F ":" '{print $0}'
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

可以看出

  • -F 选项用于指定切分的字符
  • $1、$2、$3... 分别表示第一个字段、第二个字段、第三个字段等等
  • $0 表示一整行

我们也可以通过指定 FS 来指定切分的字符如下

[root@master ~]# head -5 /etc/passwd | awk 'BEGIN{FS=":"}{print $1,$4}'
root 0
bin 1
daemon 2
adm 4
lp 7

通过指定 OFS 来指定打印之后的字段的连接符

[root@master ~]# head -5 /etc/passwd | awk 'BEGIN{FS=":";OFS="-"}{print $1,$4}'
root-0
bin-1
daemon-2
adm-4
lp-7

通过指定 RS 来指定记录的分隔符如下

## 指定 : 为行分隔符
[root@master ~]# head -5 /etc/passwd | awk 'BEGIN{RS=":"}{print $0}' | head -10
root
x
0
0
root
/root
/bin/bash
bin
x
1

指定 NR 来指定行号

[root@master ~]# head -5 /etc/passwd | awk '{print NR,$0}'
1 root:x:0:0:root:/root:/bin/bash
2 bin:x:1:1:bin:/bin:/sbin/nologin
3 daemon:x:2:2:daemon:/sbin:/sbin/nologin
4 adm:x:3:4:adm:/var/adm:/sbin/nologin
5 lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin

NF 表示字段的个数

[root@master ~]# head -5 /etc/passwd | awk 'BEGIN{FS=":"}{print NF}'
7
7
7
7
7
[root@master ~]# head -5 /etc/passwd | awk 'BEGIN{FS=":"}{print $NF}'
/bin/bash
/sbin/nologin
/sbin/nologin
/sbin/nologin
/sbin/nologin

awk 也可以使用逻辑计算

[root@localhost ~]# ll | awk 'BEGIN{FN=" "}{print $1,$2,$3,$4}'
total 100  
-rwxr--r--. 1 root root
-rwxr--r--. 1 root root
-rw-r--r--. 1 root root
-rw-r--r--. 1 root root
-rw-r--r--. 1 root root
-rw-r--r--. 1 root root
-rwxr--r--. 1 root root

## 过滤除文件大小大于等于 1259 字节的文件
[root@localhost ~]# ll | awk 'BEGIN{FN=" "} $5 >= 1259 {print $0}'
-rw-------. 1 root root    1259 Mar 19  2018 anaconda-ks.cfg
-rw-------. 1 root root    3010 Oct  9 17:20 nohup.out
-rw-r--r--. 1 root root    1547 Oct  9 17:24 test.out
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: shell