Linux 之旅 10:Shell 脚本
(图片来自shell/bash脚本编程)
Linux 上的 Shell 脚本可以看做是类似于Windows上的批处理程序(.bat)一样的东西,其本质就是将一组 shell 命令整合在一起,并添加上一些编程语言普遍使用的控制流程、函数之类的结构,实现自动化和批处理的效果。
事实上,从之前 Linux 之旅 8:初识 BASH 中我们学到的内容就可以发现,Bash 本身就相当有被开发成一门完善的编程语言的潜质,因为其本身就具有很多编程语言的基础特性,比如变量,比如多行执行,比如逻辑运算符等。
而我们要做的就是用vim
之类的文本编辑器新建一个文本文件,然后写下一行行的 Bash 命令,然后再加上控制流程或者函数之类的结构组织一下,就可以搞出类似程序源代码之类的东东,这就是所谓的 Shell Script,或者说 Shell 脚本。
Shell Script 概述
程序 or 脚本
以前,刚开始接触Javascript
之类的脚本语言的时候对脚本这个词很困惑,不理解这到底是个什么意思,后来明白了,所谓脚本,可以看做是一种“轻量级代码”,就像常见的Javascript
或PHP
脚本,长的数百行,短的两三行,它们都以实现一个简单且单纯的任务为目标,一般仅会使用单个文件加上一些必要的第三方库引用就可以完成所需的任务,所以我们可以将此类的js
或PHP
代码称之为脚本。
但并不是脚本语言创建的程序一定是脚本,比如使用Js
创建的完整前端框架,或者PHP
创建的包含数百个源码文件的Web应用,都很难再被看作是“轻量级代码”了,这种规模的程序我们一般称之为应用或产品。
当然,无论是简单的脚本还是复杂的应用,只要是用代码敲出来的,我们都可以被叫做程序。
Shell Script
知道什么是脚本(Script)之后我们再来看什么是Shell Script
。
很简单不是?既然是Shell Script
,那自然是用Shell
编写的脚本,准确的说应当是用Shell
命令编写的脚本。我们前边已经说过了,Shell
是Linux内核之上的一种“壳程序”,我们通过使用各种Shell
命令与内核交互,最简单的当然是通过命令行逐行或者多行执行命令,但那依然只能实现一些简单的功能,如果是某些复杂的相互影响的任务,通过那种方式执行就有点强人所难了,如果这种任务还是需要长时间重复执行的,那就更是一种折磨,这时候比较优雅,或者说懒人福音的方式就是编写一个Shell
命令组成的脚本,然后一键执行。
了解什么是Shell脚本之后,很自然的,我们就能想到Shell分为很多种,而且不同的Shell的使用方式也不同,自然的,因为使用Shell种类的不同,Shell脚本也会有所不同。
就像前边说的,因为Bash是目前最流行的Shell,所以我们学习的Shell脚本也是用Bash命令编写的Shell脚本。
Hellow World
几乎所有的编程语言学习的第一课都是Hellow World
,我们的第一个Shell脚本也从这里开始。
使用vim
在任意一个地方新建一个hellow.sh
:
#!/bin/bash
# a first shell script
echo 'Hellow Wolrd!'
exit 0
然后执行:
[icexmoon@xyz bin]$ sh hellow.sh
Hellow Wolrd!
- 需要注意的是编写代码的时候所有标点符号都只能使用英文标点,也就是说在中文输入情况下也要使用半角标点,否则会出现初学者难以发现的错误,比如某个中文感叹号之类的。有种便捷的做法是在输入法中设置中文下使用英文标点,就会杜绝此类错误,但这样会对需要编写中文文章的人带来不便,可以设置两个中文输入法,一个专门用来编程,一个用来编写中文文章。\
Hellow Wolrd
已经变成了程序员文化的一部分,有个梗是就是关于这个的——“程序员的墓志铭应当写什么?答:Goodby World”。
其中第一行#!/bin/bash
的作用是表明这个shell
脚本使用的是Bash
命令编写,所以应当用/bin/bash
执行。
我尝试了删除这一行注释后直接通过
hellow.sh
执行,结果依然可以正常执行,大概是因为默认Shell
是Bash
的缘故,如果是csh
估计结果就不同了,总之还是遵守这种约定比较好。
最后一行以exit 0
结尾,这是为了在程序正常退出时候给Bash
提供一个返回值,这样其它命令就可以通过$?
变量来获取到这个脚本的返回值了。
良好的编码习惯
所有编程教学都会将编码习惯放在首位,在所有教学内容之前,毕竟好的习惯需要从开始就培养,而坏的习惯可能会伴随终生。
但也不要对编程习惯痴迷或者奉为信条,因为编程习惯都是为了某些目的而存在的,如果你有更好的方案解决此类问题,或者这种目的影响到了你的工作,那么抛弃对应的编码习惯完全是可行的。
一般来说,无论使用何种编程语言,都需要在每份源代码前加上这些内容作为注释:
- 源代码的内容概述或目的
- 创建日期和修改日期
- 作者及联系方式
- 版本号
- 修订历史记录
但要让职业的程序员在每份源码上都手动编写这些内容显然是不现实的且是对人力的一种浪费,所以实际中基本上只有“源码的内容或目的”需要程序员编写,其它的都由IDE等工具自动生成,而版本号和修订历史记录这类信息其实属于版本管理,目前都会用专门的软件版本管理应用代劳,比如svn
或git
,所以你完全可以使用Github
或Gitee
创建个人仓库来存放和管理Shell脚本,这回省去很多事。
脚本的执行方式
Shell
脚本的执行方式有3种:
-
直接执行脚本文件:
也就是通过绝对路径或相对路径直接执行,或者脚本文件位于
PATH
包含的目录中,比如:[icexmoon@xyz bin]$ ~/bin/hellow.sh Hellow Wolrd!
需要注意的是这种情况下脚本文件本身必须要有执行权限,也就是说当前用户要能有权限执行该脚本文件。
-
通过
/bin/bash
执行:这种方式最常见,只要用户对脚本文件有读权限就可以执行脚本:
[icexmoon@xyz bin]$ sh hellow.sh Hellow Wolrd!
因为
sh
位于PATH
目录,且本身是/bin/bash
的链接,所以当然也可以执行。 -
通过
source
执行:前边我们说过,使用
source
或.
可以执行shell
的配置文件,事实上shell
的环境配置文件本身就是shell
脚本,所以普通的shell
脚本也同样可以这么执行,通过这种方式执行和前两种的区别在于,前两种都是新建一个Bash
进程,然后执行,执行完毕后会退出该进程,而后者则是在当前的主Shell
进程上执行。区别在于子进程的命名空间是独立的,除了继承父
Bash
进程的全局变量以外,局部变量是互不影响的,也就是说命名空间是干净的,不会包含其它Bash的局部变量。而后者是非子进程方式执行,也就意味着其命名空间就是主Bash
的命名空间,造成的影响也同样会影响到主Bash
进程。这点可以使用一个简单的脚本证实:
[icexmoon@xyz bin]$ cat env_test.sh #!/bin/bash # test the namesapce in father and child bash subscript_var='test'
测试:
[icexmoon@xyz ~]$ cd ~/bin [icexmoon@xyz bin]$ vim env_test.sh [icexmoon@xyz bin]$ unset subscript_var [icexmoon@xyz bin]$ source env_test.sh [icexmoon@xyz bin]$ echo $subscript_var test [icexmoon@xyz bin]$ unset subscript_var [icexmoon@xyz bin]$ sh env_test.sh [icexmoon@xyz bin]$ echo $subscript_var
可以看到,通过
source
加载后可以访问到脚本创建的局部变量,但通过sh
执行后是访问不到的。需要注意的是,通过
source
加载的脚本千万不要在结尾写exit 0
这样的语句,因为会直接让主Bash
进程退出。
简单的脚本练习
在学习流程控制语句和函数之前,先通过几个简单的脚本案例感受一下Shell脚本的编写。
简单的交互式程序
最简单的交互式程序是从键盘读取数据,然后根据数据进行相应的处理后输出,下面看一个例子。
在这个例子中将会让用户通过键盘分别输入first_name
和last_name
,然后拼接处全名后输出:
#!/bin/bash
#测试用的shell脚本
read -p 'first name:' first_name
read -p 'last name:' last_name
echo "full name is ${first_name} ${last_name}"
exit 0
以时间作为文件名创建文件
此类行为在Shell脚本的编写中很常见,因为我们往往需要用脚本来定时备份数据,自然的,最简单且直观的方式就是用时间来命名备份,比如filename_2021-08-07.backup
这样,下面用一个简单的脚本练习来说明。
在这个脚本的作用是,从键盘读入一个文件名,然后根据今天、昨天和前天的日期作为文件名来创建三个空文件:
#!/bin/bash
# create 3 files by date
# such as filename_2021-08-16
# filename_2021-08-15
# filename_2021-08-14
#read filename
read -p "Pleas enter a filename:" filename
#get today's date
date_today=$(date "+%Y-%m-%d")
date_1_before=$(date -d "1 day ago" "+%Y-%m-%d")
date_2_before=$(date -d "2 day ago" "+%Y-%m-%d")
filename1="${filename}_${date_today}"
filename2="${filename}_${date_1_before}"
filename3="${filename}_${date_2_before}"
touch $filename1
touch $filename2
touch $filename3
exit 0
计算器
制作一个简单计算器也算是编程的常见题目了,不过目前没有学习控制流程,所以只能创建一个简单的只能做乘法的计算器。
这个案例中将会读入用户输入的两个数,然后做乘法后进行输出:
#!/bin/bash
# a multiply test
# input two numbers and return multiply result
# get two numbers from user input
declare -i number1
declare -i number2
read -p "Please input number1:" number1
read -p "Please input number2:" number2
declare -i result_num
# multiply and return result
result_num=$number1*$number2
echo "${number1}*${number2}=${result_num}"
exit 0
这里对所有数值类型的变量都使用declare -i
进行了类型声明,所以自然是没有问题的。但Bash中虽然变量是有类型的,但实际上变量类型约束相当宽泛,并不像C++那样严格,所以我们也可以这样写:
#!/bin/bash
# a multiply test
# input two numbers and return multiply result
# get two numbers from user input
read -p "Please input number1:" number1
read -p "Please input number2:" number2
# multiply and return result
result_num=$(($number1*$number2))
echo "${number1}*${number2}=${result_num}"
exit 0
$(())
的作用是对其内部包裹的字符串进行数学运算,所以虽然脚本中用到的三个变量都是默认的字符串类型,但依然能得出正确的结果。
Bash的语法的确有点奇怪,说它是弱类型语言吧,它有类型。说它是强类型语言吧,数字类型和数字类型整合到一起居然变成了字符串类型,并不存在在运算时根据类型的不同执行不同的操作这种强类型语言常见的特性。
运算符
变量替换
在Bash
中,使用$
标记的名称会尝试使用变量的值进行替换:
#!/bin/bash
# test value
name='icexmoon'
echo 'My name is '$name
echo "My name is ${name}"
echo 'My name is ${name}'
exit 0
也就是说Bash
在执行的时候会先使用变量name
的值替换掉$name
,然后将'My name is icexmoon
这样的字符串传递给echo
命令。
当然单引号中是例外,不会执行变量替换。
命令替换
可以使用$(commnad)
的方式捕获命令的输出:
#!/bin/bash
# test command
users=$(cut -d ':' -f 1 /etc/passwd)
echo "$users"
也就是说,示例中cut -d ':' -f 1 /etc/passwd
命令原本执行后会向屏幕输出账号信息,但通过$()
,可以将这部分信息捕获,不再输出到屏幕,而是直接作为字符串赋值给了变量users
。
除了$()
,也可以使用``(ESC下边的那个键),是一样的效果。
数值计算
使用双层小括号可以执行数学计算:
#!/bin/bash
# test cal
num1=1+2+3
num2=$((1+2+3))
echo $num1
echo $num2
结果:
[icexmoon@xyz bin]$ sh cal_test.sh
1+2+3
6
如果要赋值给变量,必须使用
$(())
。
此外还可以进行变量自增或自减等类似于C
语言的操作:
#!/bin/bash
# test cal
num=10
((num--))
echo $num
((num+=3))
echo $num
exit 0
注释
在Shell
脚本中,使用#
可以标记单行注释,对于多行注释,可以:
#!/bin/bash
<< 'BLOCK'
This is a multi lines comments
This is a multi lines comments
This is a multi lines comments
BLOCK
echo 'Hello world'
exit 0
事实上上边的BLOCK
是一个文档的起始和结尾标识,中间可以包含一个多行的长字符串,用这种方式构建的长字符串一般称之为“文档串”(document string)。
文档串的标识是可以自己定义的:
#!/bin/bash
<< 'DOCUMENT'
This is a multi lines comments
This is a multi lines comments
This is a multi lines comments
DOCUMENT
echo 'Hello world'
exit 0
通过这种方式实现多行注释时,需要注意:
- 事实上这种风格的注释在C语言中也有使用,实际使用中最好用大写字母来命名文档标识。
- 对于文档的起始标识
<< ’DOCUMENT‘
,可以不使用单引号,但用文档串实现注释时候使用单引号是一个好习惯,原因见后文中文档串的详细说明。
此外,还可以这样:
#!/bin/bash
: '
This is a multi lines comments
This is a multi lines comments
This is a multi lines comments
'
echo