Shell概述

Shell是一个命令行解释器,他接收应用程序/用户命令,然后调用操作系统内核。

image-20220818220842079

Shell还是一个功能相当强大的编程语言,易编写,易调试,灵活性强。

Linux提供的Shell解释器有

1
2
3
4
5
6
7
8
# 查看Linux支持的shell解释器
cat /etc/shells

# 查看的值
/bin/sh
/bin/bash
/usr/bin/sh
/usr/bin/bash

bash和sh关系

1
2
3
4
5
6
7
8
# 进入bin目录
cd /bin/

# 查看bash和sh的关系
ll | grep bash

-rwxr-xr-x. 1 root root 964536 4月 1 2020 bash
lrwxrwxrwx. 1 root root 4 8月 8 16:00 sh -> bash

CentOS默认解析器bash

1
2
3
4
# 查看CentOS7默认解析器
echo $SHELL

/bin/bash

Shell脚本入门

脚本格式

脚本以#!/bin/bash开头(指定解析器)

第一个Shell脚本

创建一个shell脚本,输出helloworld

1
2
3
4
5
6
7
8
9
10
11
# 创建文件夹
mkdir /root/script

# 进入新建文件夹
cd /root/script

# 创建脚本文件
touch helloworld.sh

# 编辑脚本内容
vim helloworld.sh

脚本内容

1
2
#!/bin/bash
echo "helloworld"

脚本执行

  1. 采用bashsh+脚本的相对路径或绝对路径(不需要赋予脚本+x权限)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # sh+脚本相对路径
    sh ./helloworld.sh

    # sh+绝对路径
    sh /root/script/helloworld.sh

    # bash+脚本相对路径
    bash ./helloworld.sh

    # bash+绝对路径
    bash /root/script/helloworld.sh
  2. 采用输入脚本的绝对路径或相对路径执行脚本(必须具有可执行权限+x

    1
    2
    3
    4
    5
    6
    7
    8
    # 赋予脚本可执行权限
    chmod +x helloworld.sh

    # 相对路径
    ./helloworld.sh

    # 绝对路径
    /root/script/helloworld.sh
  3. 在脚本前加上.source

    1
    2
    3
    4
    5
    # . 执行脚本
    . helloworld.sh

    # source执行脚本
    source helloworld.sh
  • 前两种方式都是在当前shell中打开一个子shell来执行脚本内容,当脚本内容结束,则子shell关闭,回到父shell中。
  • 第三种,也就是使用在脚本路径前加“.”或者source的方式,可以使脚本内容在当前shell里执行,而无需打开子shell!这也是为什么我们每次要修改完/etc/profile文件以后,需要source一下的原因。
  • 开子shell与不开子shell的区别就在于,环境变量的继承关系,如在子shell中设置的当前变量,父shell是不可见的。

注意:第一种执行方法,本质是bash解析器帮你执行脚本,所以脚本本身不需要执行权限。第二种执行方法,本质是脚本需要自己执行,所以需要执行权限。

变量

系统预定义变量

常用系统变量

$HOME、$PWD、$SHELL、$USER等

操作

  1. 查看系统变量的值

    1
    2
    3
    4
    echo $HOME
    echo $PWD
    echo $SHELL
    echo $USER
  2. 显示当前Shell中所有变量:set

    1
    set

自定义变量

基本语法

  • 定义变量:变量名=变量值。注意:=号前后不能有空格
  • 撤销变量:unset 变量名
  • 声明静态变量:readonly变量。注意:静态变量不能unset(撤销变量)

自定义变量规则

  • 变量名称可以由字母、数字和下划线组成,但是不能以数字开头,环境变量名建议大写。
  • 等号两侧不能有空格
  • 在bash中,变量默认类型都是字符串类型,无法直接进行数值运算。
  • 变量的值如果有空格,需要使用双引号或单引号括起来。

操作

  1. 定义变量

    1
    2
    3
    4
    5
    # 定义变量(下面实操不需要进入Shell脚本,直接命令行操作即可)
    A=5

    # 输出变量
    echo $A
  2. 给变量重新赋值

    1
    2
    3
    4
    5
    # 给变量重新赋值
    A=8

    # 输出变量
    echo $A
  3. 撤销变量

    1
    2
    3
    4
    5
    # 撤销变量
    unset A

    # 输出变量
    echo $A
  4. 声名静态变量,不能使用unset删除,也不能对其进行修改

    1
    2
    3
    4
    5
    # 设置静态变量
    readonly B=2

    # 输出静态变量B
    echo $B
  5. 在bash中,变量默认类型都是字符串类型,无法直接进行数值运算

    1
    2
    3
    4
    5
    # 设置变量进行算术运算
    C=1+2

    # 输出变量
    echo $C
  6. 变量的值如果有空格,需要使用双引号或单引号括起来

    1
    2
    3
    4
    5
    6
    7
    # 设置变量
    D='Hello World'
    E="Hello World"

    # 输出变量
    echo $D
    echo $E
  7. 将变量提升为全局变量,可供其他Shell程序使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 格式
    export 变量名

    # 设置变量
    F=20

    # 提升变量为全局变量
    export F

    # 设置脚本内容为如下内容
    vim helloworld.sh
    1
    2
    3
    #!/bin/bash
    echo "helloworld"
    echo $F
    1
    2
    # 执行脚本
    ./helloworld.sh

特殊变量

执行脚本将参数传入进去。格式:./脚本 参数列表,空格隔开

  • $n:获取传入的参数,例如第1-9个参数取值方式为$1等,超过第9个参数取值方式为${10}
  • $#:获取传入参数的个数
  • $*:将所有传入的所有参数拼接为一个整体
  • $@:将所有参数放入一个集合中,可以进行遍历,遍历方法看后面笔记
  • $?:最后一次执行的命令的返回状态。如果这个变量的值为0,证明上一个命令正确执行;如果这个变量的值为非0(具体是哪个数,由命令自己来决定),则证明上一个命令执行不正确了

脚本编写

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
echo '==============$n============='
echo "参数一:$1"

echo '==============$#============='
echo "所有参数个数为:$#"

echo '==============$*============='
echo "所有参数:$*"

echo '==============$@============='
echo "所有参数组成的集合为:$@"

脚本执行

1
./parameter.sh 1 2 3

算术运算

执行算术运算。格式$((运算式))或$[运算式]

1
2
3
4
5
6
7
echo $((1+1))

echo $[1+1]

echo $[(1+1)+2]

echo $[((1+1))+2]

条件判断

语法

  • test 条件表达式 (注意:条件表达式中的=的前后都需要有空格)
  • [ 条件表达式 ](注意:条件表达式前后都要有空格)

注意:条件非空即为true,[atguigu]返回true,[ ] 返回false

常用判断条件

两个整数之间比较

注意:如果是字符串之间比较,用等号=判断相等;用!=判断不等

  • -eq:等于 (equal)
  • -ne:不等于(not equal)
  • -lt:小于(less than)
  • -le:小于等于(less equal)
  • -gt:大于(greater than)
  • -ge:大于等于(greater equal)

根据文件权限进行判断

  • -r:有读权限(read)
  • -w:有写权限(write)
  • -x:有执行权限(execute)

按照文件类型进行判断

  • -e:文件存在(existence)
  • -f:文件存在并且是一个常规文件(file)
  • -d:文件存在并且是一个目录(directory)

操作

  • 23是否大于等于22

    1
    2
    3
    4
    5
    # 使用[  ]进行比较
    [ 23 -ge 22 ]

    # 使用$?查看执行状态【0:true。1:false
    echo $?
  • 判断helloworld.sh是否有写权限

    1
    2
    3
    4
    5
    # 查看文件是否有权限
    [ -w helloworld.sh ]

    # 查看结果
    echo $?
  • 查看指定目录指定文件是否存在

    1
    2
    3
    4
    5
    # 查看文件是否有权限
    [ -e /root/script/xiaofei.txt ]

    # 查看结果
    echo $?
  • 多条件判断(&&表示前一条命令执行成功时,才执行后一条命令,|| 表示上一条命令执行失败后才执行下一条命令)

    1
    2
    3
    4
    5
    [ 23 -ge 22 ] && echo OK || echo NotOk
    # 输出:OK

    [ 20 -ge 22 ] && echo OK || echo NotOk
    # 输出:NotOk

流程控制

Shell脚本if的使用:https://blog.csdn.net/tootsy_you/article/details/95597376

if

语法

  • 单分支

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

    或者

    1
    2
    3
    4
    if [ 条件判式 ]
    then
    程序
    fi
  • 多分支

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if [ 条件判断式 ]
    then
    程序
    elif [ 条件判断式 ]
    then
    程序
    else
    程序
    fi

注意事项:

  • [ 条件判断式 ]:中括号个条件判断式之间必须有空格
  • if后面要有空格

案例

编写Shell脚本

1
2
3
4
5
6
7
8
#!/bin/bash
if [ $1 -eq 1 ]
then
echo "测试1"
elif [ $1 -eq 2 ]
then
echo "测试2"
fi

测试

1
2
3
4
./if.sh 1

# 输出
测试1

case

语法

1
2
3
4
5
6
7
8
9
10
11
case: $变量名 in
"值1")
如果变量的值等于值1,则执行程序1
;;
"值2")
如果变量的值等于2,则执行程序2
;;
...省略其他分支...
*)
如果变量的值都不是以上的值,则执行此程序
esac

案例

编写Shell脚本

1
2
3
4
5
6
7
8
9
10
case $1 in
"1")
echo "传入的值:$1"
;;
"2")
echo "传入的值:$1"
;;
*)
echo "传入其他值:$1"
esac

测试

1
2
3
4
./case.sh 1

# 输出
传入的值:1

for

语法一

1
2
3
4
for (( 初始值;循环控制条件;变量变化 )) 
do
程序
done

案例一

编写Shell脚本

1
2
3
4
5
6
7
#!/bin/bash
sum=0
for(( i=1 ; i < 10 ; i++ ))
do
sum=$[ $sum+$i ]
done
echo "sum = $sum"

测试

1
2
3
4
./for1.sh

# 输出
sum = 45

语法二

1
2
3
4
for 变量 in 值1 值2 值3....
do
程序
done

案例二

编写Shell脚本

1
2
3
4
5
#!/bin/bash
for i in cls mly wls
do
echo "值:$i"
done

测试

1
2
3
4
5
6
./for2-1.sh 

# 输出
值:cls
值:mly
值:wls

比较$*和$@区别

$*和$@都表示传递给函数或脚本的所有参数,不被双引号""包含时,都以$1$2…$n的形式输出所有参数。

  • 不被""包含时

    1
    2
    3
    4
    5
    #!/bin/bash
    for i in $*
    do
    echo $i
    done
    1
    2
    3
    4
    5
    #!/bin/bash
    for i in $@
    do
    echo $i
    done
    1
    2
    3
    4
    5
    ./for2.sh 1 2

    # 两个shell脚本都是输出:
    1
    2
  • ""包含时

    1
    2
    3
    4
    5
    #!/bin/bash
    for i in "$*"
    do
    echo $i
    done
    1
    2
    3
    4
    5
    #!/bin/bash
    for i in "$@"
    do
    echo $i
    done
    1
    2
    3
    4
    5
    6
    7
    8
    ./for2.sh 1 2

    # "$*"输出:
    1 2

    # "$@"输出:
    1
    2

while循环

语法

1
2
3
4
while [ 条件判断式 ]
do
程序
done

案例

1
2
3
4
5
6
7
#!/bin/bash
i=10
while [ $i -gt 0 ]
do
echo $i
i=$[ $i-1 ]
done

read读取控制台输入

Shell脚本执行之后,可以将从控制台输入的值,赋值给指定变量

语法

read (选项) (参数)

  1. 选项:

    -p:指定读取值时的提示

    -t:指定读取值时等待的时间(秒),如果-t不加表示一直等待

  2. 参数

    变量:指定读取值的变量名

案例

1
2
3
#!/bin/bash
read -t 3 -p "请输入:" value
echo "输入的值:$value"

函数

系统函数

basename

功能

basename:basename命令会删除所有的前缀包括最后一个(‘/‘)字符,然后将字符串显示出来

basename可以理解为获取路径里面的文件名称

语法

basename [string / pathname] [suffix]

选项:

  • suffix为后缀,如果传入了suffix的值,basename会将pathname或string中的suffix去掉

案例

获取/root/script/for2-1.sh的文件名字

1
2
3
4
5
6
7
# 获取全部文件名字
basename /root/script/for2-1.sh
# 输出 for2-1.sh

# 获取去除后缀的文件名字
basename /root/script/for2-1.sh .sh
# 输出:for2-1

dirname

功能

dirname:获取文件的绝对路径名称

dirname:从给定的包含绝对路径的文件名中去除文件名(非目录部分),然后返回剩下的路径(目录部分)

语法

dirname 文件绝对路径

案例

1
2
3
4
# 获取/root/script/if.sh的路径名称
dirname /root/script/if.sh
# 输出
/root/script

自定义函数

语法

1
2
3
4
5
[ function ] funname[()]
{
Action
[return int;]
}

经验技巧

  • 必须在调用函数之前声明函数,先声明函数,Shell脚本都是逐行执行的,不会像其他语言一样先进行编译
  • 函数返回值,只能通过$?系统变量获得,使用return返回。如果不使用return指定返回值,则程序将会默认把最后一条执行的语句运行的结果最为返回值。return 后面跟数值0-255

案例

计算两个输入参数的和

1
2
3
4
5
6
7
8
9
10
#!/bin/bash
function sum()
{
return $[$1+$2]
}
# 调用函数和传入参数
sum $1 $2

# 使用$?获取上一次命令执行的返回值
echo "函数的返回值:$?"
1
2
3
# 执行脚本
./function.sh 2 2
# 函数的返回值:4

正则表达式

正则表达式使用单个字符串来描述、匹配一系列符合某个语法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。在Linux中,grep,
sed,awk等文本处理工具都支持通过正则表达式进行模式匹配。

常规匹配

特殊字符^

^匹配一行的开头

1
2
# 匹配虽有以a开头的行
cat /etc/passwd | grep ^a

匹配特殊字符$

$匹配一行的结束,例如

1
2
# 获取所有以n结束的行的内容
cat /etc/passwd | grep n$

^$会匹配什么

特殊字符.

.匹配一个任意的字符

1
2
3
4
5
# 匹配r`a-z`t,例如【rat、rbt、rct......rzt】
cat /etc/passwd | grep r.t

# 匹配任意两个字符 例如【raat、rabt......rzzt等】
cat /etc/password | grep r..t

特殊字符*

*不单独使用,他和上一个字符连用,表示匹配上一个字符0次或多次

1
2
# 会匹配rt、root、rooot、roooot、rooooot等宝航的所有行
cat /etc/passwd | grep ro*t

.*会匹配什么

字符区间(中括号):[]

[ ]表示匹配某个方位内的一个字符,例如

[6,8]———匹配6或者8

[0-9]———匹配一个0-9的数字

[0-9]*———匹配任意长度的数字字符串

[a-z]———匹配一个a-z治安的字符

[a-z]*———匹配任意长度的字符字符串

[a-c,e-f]匹配a-c或者e-f之间的任意字符

1
2
# 会匹配rt、rat、rabt、rbact、rabccbaaacbt等所有行
cat /etc/passwd | grep r[a,b,c]*t

特殊字符

\表示转义,并不会单独使用。由于所有特殊字符都有其特特定匹配模式,当想匹配某一特殊字符本身时(例如,想找出所有包含$的行),就会碰到困难,此时就要将转义字符和特殊字符连用来表示特殊字符本身

1
2
# 查找所有包含 a$b的行。使用时需要使用单引号将表达式引起来
cat /etc/passwd | grep 'a\$b'

文本处理工具

cut

cut的工作就是,具体的说就是在文件中负责剪切数据用的。cut命令从文件的每一行剪切字节、字符和字段并将这些字节、字符和字段输出

用法

cut [选项参数] filename

说明:默认分隔符就是制表符

选项参数

选项参数 功能
-f 数字[-] 列号,提取第几列,-截取到指定
-d “分隔符” 指定分隔符进行切割,例如-d " "则使用`进行切割,默认为\t`,
-c 数字 按字符进行切割,后加n表示取第几列,比如-c 1

案例

  • 数据准备

    1
    2
    3
    4
    5
    6
    7
    # vim cut.txt

    dong shen
    guan zhen
    wo wo
    lai lai
    le le
  • 切割第一列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 切割第一列
    cut -d " " -f 1 cut.txt

    # 输出
    dong
    guan
    wo
    lai
    le
  • 切割第二三列

    1
    2
    3
    4
    5
    6
    7
    8
    # 切割第二三列
    cut -d " " -f 2,3 cut.txt
    # 输出
    shen
    zhen
    wo
    lai
    le
  • 获取第一列的值

    1
    2
    3
    4
    5
    6
    7
    8
    # 获取第一列的值
    cut -c 1 cut.txt
    # 输出
    d
    g
    w
    l
    l
  • 在cut.txt中切割出guan

    1
    2
    # 从cut.txt文件中获取guan【先过滤以guan开头的行,在进行切割】
    cat cut.txt | grep guan | cut -d " " -f 1
  • 获取$PATH中第二个 : 后面的值

    1
    2
    3
    4
    5
    6
    7
    8
    # 查看PATH的值
    echo $PATH
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

    # 获取环境变量的值,再根据 : 进行切割 ,然后获取第二个 : 后面的所有值
    echo $PATH | cut -d ":" -f 3
    # 输出
    /usr/sbin:/usr/bin:/root/bin
  • 获取 ifconfig 中网卡的IP地址

    1
    ifconfig ens33  | grep netmask | cut -d " " -f 10

awk

一个强大的文本分析工具,把文件逐行的读入,以空格为默认分隔符将每行切片,切开的部分再进行分析处理

用法

awk [ 选项参数 ] ‘/pattern1/{action1} /pattern2/{action2}…’ filename

pattern:表示awk再数据中查找的内容,就是匹配模式,可以输入正则表达式

action:再找到匹配内容时所执行的一系列命令

只有pattern匹配了才会执行action,如果pattern没有匹配,则对应的action不会执行

选项参数

选项参数 功能
-F 指定输入文件分隔符
-v 赋值一个用户定义变量

案例

  • 数据准备

    1
    2
    3
    4
    5
    # 准备数据
    cp /etc/passwd ./

    # passwd文件含义
    用户名:密码(加密过后的)用户id:组id:注释:用户家目录:shell解释器
  • 搜索passwd文件以root关键字开头的所有行,并输出该行的第七列

    1
    awk -F : '/^root/{print $7}' passwd
  • 搜索passwd文件以root关键字开头的所有行,并输出改行的第一列和第七列,中间以,分开

    1
    awk -F : '/^root/{print $1","$7}' passwd
  • 搜索passwd文件的第一列和第七列,以逗号分割,且在所有行前面添加列名user,shell在最后一行添加”dahaige,/bin/zuishuai”

    1
    2
    3
    awk -F : 'BEGIN{print "user, shell"} {print $1","$7} END{print "dahaige,/bin/zuishuai"}' passwd

    # 注意:BEGIN 在所有数据读取行之前执行;END 在所有数据执行之后执行。
  • 将 passwd文件中的用户id增加数值1并输出

    1
    awk -v i=1 -F : '{print $3+i}' passwd

awk内置变量

变量 说明
FILENAME 文件名
NR 已读的记录数(行号)
NF 浏览记录的域的个数(切割后,列的个数)

案例

  • 统计passwd文件名,每行的行号,每行的列数

    1
    awk -F : '{print "filename:" FILENAME ",linenum:" NR ",col:"NF}' passwd
  • 查询ifconfig命令输出结果中的空行所在行号

    1
    ifconfig | awk '/^$/{print NR}'
  • 切割IP

    1
    ifconfig ens33 | awk '/netmask/ {print $2}'

综合应用案例

归档文件

实际生产应用中,往往需要对重要数据进行归档备份。

需求:实现一个每天对指定目录归档备份的脚本,输入一个目录名称(末尾不带/),将目录下所有文件按天归档保存,并将归档日期附加在归档文件名上,放在/root/archive下。

这里用到了归档命令:tar

后面可以加上-c选项表示归档,加上-z选项表示同时进行压缩,得到的文件后缀名为.tar.gz。

脚本实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#!/bin/bash 

# 首先判断输入参数个数是否为 1

if [ $# -ne 1 ]
then
echo "参数个数错误!应该输入一个参数,作为归档目录名"
exit
fi

# 从参数中获取目录名称
if [ -d $1 ]
then
echo
else
echo
echo "目录不存在!"
echo
exit
fi
DIR_NAME=$(basename $1)
DIR_PATH=$(cd $(dirname $1); pwd)

# 获取当前日期
DATE=$(date +%y%m%d)

# 定义生成的归档文件名称
FILE=archive_${DIR_NAME}_$DATE.tar.gz
DEST=/root/archive/$FILE

# 开始归档目录文件
echo "开始归档..."
echo

tar -czf $DEST $DIR_PATH/$DIR_NAME

if [ $? -eq 0 ]
then
echo
echo "归档成功!"
echo "归档文件为:$DEST"
echo
else
echo "归档出现问题!"
echo
fi

exit

发送消息

Linux自带的mesg和write工具,向其它用户发送消息。

需求:实现一个向某个用户快速发送消息的脚本,输入用户名作为第一个参数,后面直接跟要发送的消息。脚本需要检测用户是否登录在系统中、是否打开消息功能,以及当前发送消息是否为空。

脚本实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/bin/bash l

ogin_user=$(who | grep -i -m 1 $1 | awk '{print $1}')

if [ -z $login_user ]
then
echo "$1 不在线!"
echo "脚本退出.."
exit
fi

is_allowed=$(who -T | grep -i -m 1 $1 | awk '{print $2}')

if [ $is_allowed != "+" ]
then
echo "$1 没有开启消息功能"
echo "脚本退出.."
exit
fi

if [ -z $2 ]
then
echo "没有消息发出"
echo "脚本退出.."
exit
fi

whole_msg=$(echo $* | cut -d " " -f 2- )

user_terminal=$(who | grep -i -m 1 $1 | awk '{print $2}')

echo $whole_msg | write $login_user $user_terminal

if [ $? != 0 ]
then
echo "发送失败!"
else
echo "发送成功!"
fi

exit