shell脚本编程

380 阅读11分钟

shell脚本编程

什么是 Shell

shell 是英语“壳,外壳”的意思。像是嵌入在 Linux 这样的操作系统中的一个“微型编程语言”。不需要安装,不需要编译。可以帮我们完成很多自动化任务,例如:保存数据,监测系统的负载等等。

创建脚本文件

vim test.sh
#!/bin/bash  # #! 开头的一行会设置解释器运行环境

#!/bin/sh:使用 sh,即 Bourne shell 或其它兼容 shell 执行脚本 #!/bin/csh:使用 csh,即 C shell 执行 #!/usr/bin/perl -w:使用带警告的 Perl 执行 #!/usr/bin/python -O:使用具有代码优化的 Python 执行 #!/usr/bin/php:使用 PHP 的命令行解释器执行

#! 必须连接在一起

#! 一句必须在文件的最开始,第一行

这一行( #!/bin/bash )其实并不是必不可少的,但它可以保证此脚本会被我们指定的 Shell 执行。

#!/bin/bash

# 列出目录的文件 Shell 脚本的注释以 #(井号)开头
ls

esc :wq 保存,退出

运行 Shell 脚本

./test.sh # 直接运行正常会报 -bash: ./test.sh: 权限不够

ls -l # 查看test.sh的权限,-rw-r--r-- 没有 x 执行权限

chmod +x test.sh # 为所有用户添加test.sh运行权限

Linux权限

在 Linux 系统里,每个文件和目录都有一列权限属性。这一列访问权限指明了谁有读的权利,谁有修改的权利,谁有运行的权利。

ls -l 显示的每个文件或目录访问权限

image-20220710233802226

文件信息的第一列可以看到不少 d、r、w、l、x 等字母,这些字母为文件访问权限符

d:英语 directory 的缩写,表示“目录”。就是说这是一个目录; l:英语 link 的缩写,表示“链接”。就是说这是一个链接; r:英语 read 的缩写,表示“读”。就是说可以读这个文件; w:英语 write 的缩写,表示“写”。就是说可以写这个文件,也就是可以修改; x:英语 execute 的缩写,表示“执行,运行”。就是说可以运行这个文件。

为什么我们看到这一排有好多个重复出现的 r、w 和 x ?

那是因为访问权限是按照用户来划分的:

image-20220710235505031

如果相应位置有字母,表示有相应权限;如果相应位置是一个短横 - ,则表示没有相应权限。

除开第一个表示文件或目录属性的符号(此处是 d,表示目录;如果是 l,则是链接;还有其它字母,我们暂时不深究;如果是短横 - ,那么是普通文件。),其它的 9 个符号被划分为三组,从左到右分别表示:

  • 第一组 rwx 表示文件的所有者对于此文件的访问权限;
  • 第二组 rwx 表示文件所属群组的其他用户对于此文件的访问权限;
  • 第三组 rwx 表示除前两组之外的其他用户对于此文件的访问权限。

以tesh.sh为例

image-20220710235350463

-rw-r--r--

从左到右这些符号都表示什么

  • - 第一个短横表示这是一个普通文件。如果此处是 d,那么表示目录;如果是 l,那么表示链接等等;
  • rw- 表明文件的所有者(此处是 root)对文件有读、写的权限,但是没有运行的权限。也很好理解,因为这是一个普通文件,默认没有可执行的属性。记住:如果有 w 权限(写的权限),那么表明也有删除此文件的权限;
  • r-- 表明文件所在的群组(此处是 root)的其他用户(除了 root 之外)只可以读此文件,但不能写也不能执行
  • r-- 表示其他用户(除去 root 这个群组的用户)只可以读此文件,但不能写也不能执行。

chmod 命令:修改文件的访问权限

Linux 系统为每种权限(r、w 和 x)分配了对应的数字:

权限数字
r4
w2
x1

所以,如果我们要合并这些权限,需要做简单的加法:将对应的数字相加。

假如我们要分配读、写权限,那么我们就要用 4+2,就等于 6。数字 6 表示具有读和写权限。

以下是可能的组合形式:

权限数字计算
00 + 0 + 0
r–44 + 0 + 0
-w-20 + 2 + 0
–x10 + 0 + 1
rw-64 + 2 + 0
-wx30 + 2 + 1
r-x54 + 0 + 1
rwx74 + 2 + 1

所以,对于访问权限的三组(所有者的权限、群组用户的权限、其他用户的权限),我们只要分别做加法就可以了,然后把三个和连起来。

例如,640 分别表示:

  • 文件的所有者有读和写的权限;
  • 文件所在群组的其他用户具有读的权限;
  • 除此之外的其他用户没有任何权限。

因此,我们可以给的最宽泛的权限就是 777:所有者,群组用户,其他用户都有读、写和运行的权限。这样,所有人就都可以对此文件“为所欲为”。

用字母来分配权限:chmod 的相对用法

除了用数字,我们也可以用另一种方式来分配文件的访问权限:用字母。

原理是类似的,但是有时用字母的方式更加精巧,因为不需要一次性把三组权限都写出来。

我们需要知道不同的字母代表什么:

  • u:user 的缩写,是英语“用户”的意思。表示所有者;
  • g:group 的缩写,是英语“群组”的意思。表示群组用户;
  • o:other 的缩写,是英语“其他”的意思。表示其他用户;
  • a:all 的缩写,是英语“所有”的意思。表示所有用户。

和这些字母配合的还有几个符号:

  • +:加号,表示添加权限;
  • -:减号,表示去除权限;
  • =:等号,表示分配权限。

接下来,我们举例说明如何使用:

#文件 file.txt 的所有者增加读和运行的权限。
chmod u+rx file.txt

#文件 file.txt 的群组其他用户增加读的权限。
chmod g+r file.txt 

#文件 file.txt 的其他用户移除读的权限。
chmod o-r file.txt 

#文件 file.txt 的群组其他用户增加读的权限,其他用户移除读的权限。
chmod g+r o-r file.txt 

#文件 file.txt 的群组其他用户和其他用户均移除读的权限。
chmod go-r file.txt 

#文件 file.txt 的所有用户增加运行的权限。
chmod +x file.txt 

#文件 file.txt 的所有者分配读,写和执行的权限;
#群组其他用户分配读的权限,不能写或执行;
#其他用户没有任何权限。
chmod u=rwx,g=r,o=- file.txt 

回到shell脚本运行,添加执行权限之后脚本就可以正常运行了

image-20220711001209301

以调试模式运行

bash -x test.sh # 调试模式,脚本内部执行的命令也会显示出来 + 后面的
-e  # 使shell立即退出,某些东西会返回一个错误(这通常在shell脚本中用作故障保护机制),Jenkins的脚本默认执行模式就是-xe模式,会打印所有命令执行过程,一旦脚本执行出现错误就会退出执行
# 脚本内部可以使用 set -x  显示 shell 执行过程
# 如果想隐藏某一行的内容:
set -x  # 开启调试模式,后面脚本命令执行过程都会输出显示出来
xxxx  # xxx命令会显示出来
set +x  # 关闭调试模式了
yyyy  # yyyy命令执行过程不会输出显示出来,命令执行结果如果有输出会输出显示出来
set -x  # 再次开启调试模式

# 如果以bash -x 执行脚本,添加set -x,该命令会输出显示,运行模式不会改变,反之亦然

image-20220711001444464

image-20220711104701881

image-20220711104638897

Linux PATH

先看看有什么

 echo $PATH  # 输出显示$PATH变量

image-20220711110622994

PATH 是 Linux 的一个系统变量。这个变量包含了系统里所有可以被直接执行的程序的路径。 这也是date,pwd,ls,cat,cp 等命令为什么可以直接在任意目录执行(不需要在前面加上 ./ 这样的路径)的原因。

那我们自己写的脚本程序能不能也这样运行,答案是可以的,有两种方式

  1. 将test.sh复制到/usr/local/sbin、/usr/local/bin、/usr/sbin:/usr/bin、/root/bin 其中的任意文件夹中,不过不推荐,最好不要乱拷贝文件到系统路径里,就可以在任意目录 test.sh 运行了
  2. 将我们自己的路径添加到PATH中
export PATH=$PATH:/root/liwei/bin  # export会覆盖,所以加上$PATH:的目的就是保留所有的path变量,操作系统通过:切割找到匹配路径
echo $PATH  # 检查自己添加的路径是否存在
test.sh  # 在任意目录就可以执行该脚本了

image-20220711113054354

像我们装一些安卓sdk、mysql等需要配置路径都是一样的原理

定义变量

创建脚本

vim var.sh

编写脚本,定义变量

#!/bin/bash

# 定义变量
message='hello,world'

# 输出变量
echo $message
echo 'message is $message'
echo "message is $message"

msg=`pwd`
echo "所在目录:$msg"

执行

chmod +x var.sh  # 添加执行权限
bash -x var.sh  # 调试模式执行脚本,方便观察

image-20220711115821680

注意4次echo输出,注意第二次$message是直接显示的,第4个输出msg变量是pwd执行后的输出

shell脚本中引号来界定包含空格的字符串,引号有三种类型:单引号''、双引号""、反引号``。引号类型不同,Bash 的处理方式也会不同

  • 单引号:如果变量被包含在单引号里面,那么变量不会被解析,美元符号( $ )保持原样输出。单引号忽略被它括起来的所有特殊字符
  • 双引号:双引号忽略大多数特殊字符,但不包括:美元符号( $ )、反引号( ` )、反斜杠( \ ),这 3 种特殊字符将不被忽略。 不忽略美元符号意味着 Shell 在双引号内部可进行变量名替换
  • 反引号:反引号要求 Shell 执行被它括起来的内容。如上面脚本内容 msg=`pwd`,pwd 命令被执行了,执行的结果(显示当前所在目录)被赋值给 msg 变量

如果变量的值中想要加入一个单引号( ' ),需要在前面加上反斜杠( \ )进行转义

read : 请求输入

read 命令读取到的文本会立即被储存在一个变量里,read后接一个变量名,这样用户输入的文本就会被储存在这个变量中

vim read.sh

#!/bin/bash

read name

echo "my name is $name"

chmod +x read.sh
./read.sh

image-20220711134550892

read还可以一直读取多个参数

read 命令一个单词一个单词(单词是用空格分开的)地读取你输入的参数,并且把每个参数赋值给对应变量。

# 脚本内容
read lastName firstName

echo "my fullName is $lastName-$firstName"

image-20220711134848582

如果输入了比预期更多的参数,比如三个,四个,甚至更多,那么最后一个变量就会把多出来的参数全部

-p :显示提示信息

修改脚本内容

#!/bin/bash

read -p 'please input your name:' name

echo "my name is $name"

read -p '输入姓与名,中间空格隔开:' lastName firstName

echo "my fullName is $lastName-$firstName

image-20220711140504001

-n :限制字符数目

-n 参数,可以限制用户输入的字符串的最大长度(字符数)

#!/bin/bash

read -p 'please input your name(5 char max):' -n 5 name

echo "my name is $name"

read -p '输入姓与名,中间空格隔开:' lastName firstName

echo "my fullName is $lastName-$firstName"

-t :限制输入时间

-t 参数,可以限定用户的输入时间(以秒为单位),超过这个时间,就不读取输入了

-s :隐藏输入内容

-s 参数,可以隐藏输入内容。一般用不到,但如果想要用户输入的是一个密码,那 -s 参数还是有用的

数学运算

在 Bash 中,所有的变量都是字符串!

Bash 本身不会操纵数字,因此它也不会做运算。不过可以用命令来达到目的。需要用到 let 命令。let 命令可以用于赋值

例如

let "a = 5"
let "b = 2"
let "c = a + b"
echo $c

可用的运算符

运算符号
加法+
减法-
乘法*
除法/
幂(乘方)**
余(整数除法的余数)%
let "a = 5 * 3"  # $a = 15
let "a = 4 ** 2" # $a = 16 (4 的平方)
let "a = 8 / 2"  # $a = 4
let "a = 10 / 3" # $a = 3
let "a = 10 % 3" # $a = 1

环境变量

env

env为全局变量,可以在任意脚本程序使用

image-20220711144901171

其中几个比较重要的几个环境变量

  • SHELL:指明目前你使用的是哪种 Shell。我目前用的是 Bash(因为 SHELL=/bin/bash)。

  • PATH:是一系列路径的集合。只要有可执行程序位于任意一个存在于 PATH 中的路径,那我们就可以直接输入可执行程序的名字来执行,而不需要加上所在路径前缀或进入到可执行程序所在目录去执行。

  • HOME:家目录所在的路径。

  • PWD:目前所在的目录。

这些环境变量和平时使用变量一样

#!/bin/bash

echo "default Shell is $SHELL"

如果需要自己定义环境变量。可以用 export 命令来完成

export X_ENV=beta
echo "X_ENV is $X_ENV"

export命令直接在终端中也是可以直接使用的,如果是修改,注意覆盖的问题,比如PATH

参数变量

Shell 脚本也可以接收参数的

./test.sh 参数1 参数2 参数3 ...

这些个 参数1,参数2,参数3 … 被称为“参数变量”。

如何接收这些参数到我们的脚本中

  • $# :包含参数的数目。
  • $0 :包含被运行的脚本的名称 (示例中就是 test.sh )。
  • $1:包含第一个参数。
  • $2:包含第二个参数。 …
  • $8 :包含第八个参数。 … 以此类推。
特殊符号含义
$#传给脚本的参数个数
$0shell脚本文件的名字
$1传递给该shell脚本的第一个参数
$2传递给该shell脚本的第二个参数
$@传给脚本的所有参数的列表
$*以一个单字符串显示所有向脚本传递的参数,与位置变量不同,参数可超过9个
$$脚本运行的当前进程ID号,Shell本身的PID
$?显示最后命令的退出状态,0表示没有错误,其他表示有错误
$!Shell最后运行的后台Process的PID
$-使用Set命令设定的Flag一览
$()相当于``,括号中放的是命令
${}括号中放的是变量。例如echo P A T H 取 P A T H 变 量 的 值 并 打 印 , 也 可 以 不 加 括 号 比 如 {PATH}取PATH变量的值并打印,也可以不加括号比如PATH取PATH变量的值并打印,也可以不加括号比如PATH
$1~n添加到Shell的各参数值。1是第1参数、1是第1参数、2是第2参数…。以此类推

条件

if : 最简单的条件

if [ 条件测试 ]
then 
    做这个
fi

fi 是 if 的反转写法,表示“if 语句结束”。then 是英语“那么”的意思。

“做这个”只有在“条件测试”为真时,才会被执行。

注意:方括号 [] 中的 条件测试 两边必须要空一格。不能写成 [test],而要写成 [ test ]。 Shell 就是这么规定的

当然了,if 语句的基本写法还有一种,那就是把 then 写在 if [ 条件测试 ] 后面,如下:

if [ 条件测试 ]; then
    做这个
fi

用这种写法时,在 if 条件判断和 then 之间要加一个分号。

例子

#!/bin/bash
read -p '输入用户名:' name

if [ $name = "liwei" ]
then
   echo "Hello $name !"
fi

image-20220711151659004

在 Shell 语言中,“等于”是用一个等号( = )来表示的,这和大多数编程语言不同。C 语言中“等于”是用两个等号( == )来表示的。但 Shell 中用两个等号来表示“等于”的判断也是可以的。

else : 否则

if [ 条件测试 ]
then
    做这个
else
    做那个
fi

例子

#!/bin/bash
read -p '输入用户名:' name

if [ $name = "liwei" ]
then
   echo "Hello $name !"
else
echo "$name 你被拦截了"
fi

image-20220711152103268

elif : 否则,如果

if [ 条件测试 1 ]
then
    做事情 1
elif [ 条件测试 2 ]
then
    做事情 2
elif [ 条件测试 3 ]
then
    做事情 3
else
    做其他事情
fi

例子

#!/bin/bash
read -p '输入用户名:' name

if [ $name = "liwei" ]
then
   echo "Hello $name !"
elif [ $name = "xiaoming" ]
then
echo "Hello $name"
elif [ $name = "zhangsan" ]
then
echo "Hello $name"
else
echo "$name who are you 你被拦截了"
fi

image-20220711153317544

条件测试

Bash 中可以做三种测试:

  • 测试字符串
  • 测试数字
  • 测试文件
  1. 测试字符串
条件意义
$string1 = $string2两个字符串是否相等。Shell 大小写敏感,因此 A 和 a 是不一样的。
$string1 != $string2两个字符串是否不同。
-z $string字符串 string 是否为空。z 是 zero 的首字母,是英语“零”的意思。
-n $string字符串 string 是否不为空。n 是英语 not 的首字母,是英语“不”的意思。
  1. 数字测试
条件意义
$num1 -eq $num2两个数字是否相等。和判断字符串所用的符号( = )不一样。eq 是 equal 的缩写,是英语“等于”的意思。
$num1 -ne $num2两个数字是否不同。ne 是 not equal 的缩写,是英语“不等于”的意思。
$num1 -lt $num2数字 num1 是否小于 num2。lt 是 lower than 的缩写,是英语“小于”的意思。
$num1 -le $num2数字 num1 是否小于或等于 num2。le 是 lower or equal 的缩写,是英语“小于或等于”的意思。
$num1 -gt $num2数字 num1 是否大于 num2。gt 是 greater than 的缩写,是英语“大于”的意思。
$num1 -ge $num2数字 num1 是否大于或等于 num2。ge 是 greater or equal 的缩写,是英语“大于或等于”的意思。
  1. 条件测试
条件意义
-e $file文件是否存在。e 是 exist 的首字母,表示“存在”。
-d $file文件是否是一个目录。因为 Linux 中一切都是文件,目录也是文件的一种。d 是 directory 的首字母,表示“目录”。
-f $file文件是否是一个文件。f 是 file 的首字母,表示“文件”。
-L $file文件是否是一个符号链接文件。L 是 link 的首字母,表示“链接”。
-r $file文件是否可读。r 是 readable 的首字母,表示“可读的”。
-w $file文件是否可写。w 是 writable 的首字母,表示“可写的”。
-x $file文件是否可执行。x 是 executable 的首字母,表示“可执行的”。
$file1 -nt $file2文件 file1 是否比 file2 更新。nt 是 newer than 的缩写,表示“更新的”。
$file1 -ot $file2 文件 file1 是否比 file2 更旧。ot 是 older than 的缩写,表示“更旧的”。
  1. 一次测试多个条件
符号意义
&&两个 &。表示“逻辑与”。此符号两端的条件必须全为真,整个条件测试才为真;只要有一个不为真,整个条件测试为假。
II两个竖线。表示“逻辑或”。此符号两端的条件只要有一个为真,整个条件测试就为真;只有两个都为假,整个条件测试才为假。
  1. 反转测试(非测试)

可以用“否定”来反转测试条件,用到感叹号( !

#!/bin/bash

read -p 'Enter a file path : ' file

if [ ! -e $file ]
then
    echo "$file does not exist"
else
    echo "$file exists"
fi

case : 测试多个条件

跟正常编程语言switch case语句类似

例子:

这里用的脚本参数输入

#!/bin/bash

case $1 in
    "liwei")
        echo "Hello liwei !"
        ;;
    "xiaoming")
        echo "Hello xiaoming !"
        ;;
    "zhangsan")
        echo "Hello zhangsan !"
        ;;
    "lisi")
        echo "Hello lisi !"
        ;;
    *)
        echo "Sorry, I do not know you."
        ;;
esac

分析一下上面的脚本内容,因为有很多新的内容:

  • case $1 in$1 表示我们要测试的变量是输入的第一个参数。in 是英语“在…之中”的意思。
  • "liwei") :测试其中一个 case,也就是 $1 是否等于 "liwei"。当然,这里也可以用星号来做通配符来匹配多个字符,例如 "li*") 可以匹配所有以 li 开头的字符串。
  • ;; :类似于主流编程语言中的 break;,表示结束 case 的读取,程序跳转到 esac 后面执行。
  • *) :相当于 if 条件语句的 else,表示“否则”,就是“假如不等于上面任何一种情况”。
  • esac :是 case 的反写,表示 case 语句的结束。

image-20220711154757785

至于使用if还是case语句,合理选择,跟正常编程语言一致

shell 循环

  1. while 循环

    while [ 条件测试 ]
    do
        做某些事
    done
    

    while 循环

  2. until 循环

    while 这个关键字相反的有一个 until 关键字,until 在英语中是“到…为止,直到…时”的意思。
    
    它也可以实现循环,只不过逻辑和 while 循环正好相反。
    
    

    image-20220711155410073

  3. for 循环

    for 变量 in '值1' '值2' '值3' ... '值n'
    do
        做某些事
    done
    

    image-20220711155432228

shell 函数

函数可以把一块代码包裹起来,使之成为一个整体,完成某些任务。方便复用,使程序就会变得有条理。

定义函数

#!/bin/bash

print_something () {
    echo "Hello, I am a function"
}

print_something
print_something

Shell 中的函数的圆括号里不能放置参数

函数的完整定义必须置于函数的调用之前。

传递参数

Shell 函数中,给它传递参数的方式很像给 Shell 脚本传递命令行参数。把参数直接置于函数名字后面,然后就像我们之前 Shell 脚本的参数那样:$1$2$3等等

#!/bin/bash

print_something () {
    echo Hello $1
}

print_something liwei
print_something xiaoming
print_something zhangsan
print_something lisi

返回值

变量作用范围