1 脚本基础介绍
在前一章节Bash入门指南,我们已经初步熟悉了Shell的基本使用,可以看到,本质上,Shell的使用就是用一堆命令去完成各类功能。
而有些时候,一个功能可能需要许多个命令、根据各种不同的条件去执行,如果一条一条去手动输入的话,效率实在是太低了。
而本章介绍的脚本就是为了解决这个问题。
脚本(script)就是包含一系列命令的一个文本文件,Shell 读取这个文件,依次执行里面的所有命令,就好像这些命令直接输入到命令行一样。所有能够在命令行完成的任务,都能够用脚本完成。
脚本的好处是可以重复使用,也可以指定在特定场合自动调用,比如系统启动或关闭时自动执行脚本。
在学习脚本之前,最好先了解vim编辑器的基本使用,当然,你也可以直接在windows系统上编写、然后将其复制到wsl中去执行,但这样做肯定更加麻烦。
1.1 最简单的脚本
一个脚本实际上就是一个文本文件,通常由三部分组成:Shebang行、命令、注释。
其中Shebang指的是脚本的第一行,作用是指定用什么Shell来执行本脚本。
命令指的就是我们前面已经使用过的各种命令。
而注释则是用来解释说明的、给我们自己看的,并不会被执行。
比如下面就是一个最简单的脚本:
#!/bin/bash
# 输出hello world字符串
echo 'hello world'
上面第一行是Shebang行,第二行是注释,第三行是命令。
将上面的内容保存到任意一个文本文件中,比如test.sh
,其中.sh
后缀指的是script
这个单词,给我们自己看的,即使你不加也没有任何影响。
在linux系统中,编辑文件一般使用vim命令,如果不会使用的可以参考文章:精通 VIM ,这篇文章写的很好,我这里就不再赘述了。
但将上面的文本复制进去之后,它也仅仅只是个普通的文本,想让其变得可执行,就需要给它添加可执行权限:
chmod +x ./test.sh
然后它就可以被执行了,就像下面这样:
在有了这个最简单脚本雏形的前提下,我们再来一步一步深入学习脚本更多有用的东西。
1.2 Shebang
脚本的第一行通常是指定解释器,即这个脚本必须通过什么解释器执行。这一行以#!
字符开头,这个字符称为 Shebang,所以这一行就叫做 Shebang 行。
#!
后面就是脚本解释器的位置,Bash 脚本的解释器一般是/bin/sh
或/bin/bash
。
#!/bin/sh
# 或者
#!/bin/bash
#!
与脚本解释器之间有没有空格,都是可以的。
如果 Bash 解释器不放在目录/bin
,脚本就无法执行了。为了保险,可以写成下面这样。
#!/usr/bin/env bash
上面命令使用env
命令(这个命令总是在/usr/bin
目录)返回 Bash 可执行文件的位置。
Shebang 行不是必需的,但是建议加上这行。
在老式shell中,如果缺少该行,就需要手动将脚本传给解释器,举例来说,脚本是script.sh
,有 Shebang 行的时候,可以直接调用执行。
./script.sh
上面例子中,script.sh
是脚本文件名,脚本通常使用.sh
后缀名,不过这不是必需的。
如果没有 Shebang 行,有时就只能手动将脚本传给解释器来执行。
/bin/sh ./script.sh
# 或者
bash ./script.sh
但在当下的大部分shell中,实际上已经可以不加该行直接执行了,加上该行更多的作用是指定一下执行该脚本的shell。
1.3 执行权限与路径
前面提到,一个脚本想要能执行,需要一个前提条件,就是脚本需要有执行权限,可以使用下面的命令,赋予脚本执行权限。
# 给所有用户执行权限
chmod +x script.sh
# 给所有用户读权限和执行权限
chmod +rx script.sh
# 或者
chmod 755 script.sh
# 只给脚本拥有者读权限和执行权限
chmod u+rx script.sh
脚本的权限通常设为755
(拥有者有所有权限,其他人有读和执行权限)或者700
(只有拥有者可以执行)。
文件权限有关内容可以参考文章:Linux 文件基本属性。
除了执行权限,脚本调用时,一般需要指定脚本的路径,比如前面调用时使用的就是:.\test.sh
而不是直接test.sh
,前面的.
代表的就是当前目录,\
是路径分隔符,更多介绍可以参考: 文件路径。
1.4 env 命令
前面使用了这个命令,因此这里也提一下。
env
命令总是指向/usr/bin/env
文件,或者说,这个二进制文件总是在目录/usr/bin
。
#!/usr/bin/env NAME
这个语法的意思是,让 Shell 查找$PATH
环境变量里面第一个匹配的NAME
。如果你不知道某个命令的具体路径,或者希望兼容其他用户的机器,这样的写法就很有用。
/usr/bin/env bash
的意思就是,返回bash
可执行文件的位置,前提是bash
的路径是在$PATH
里面。其他脚本文件也可以使用这个命令。比如 Node.js 脚本的 Shebang 行,可以写成下面这样:
#!/usr/bin/env node
env
命令的参数如下。
-i
,--ignore-environment
:不带环境变量启动。-u
,--unset=NAME
:从环境变量中删除一个变量。--help
:显示帮助。--version
:输出版本信息。
下面是一个例子,新建一个不带任何环境变量的 Shell。
env -i /bin/sh
1.5 注释
注释主要是我们自己看的,Shell并不会执行。
在Bash 脚本中,#
表示注释,可以放在行首,也可以放在行尾。
# 本行是注释
echo 'Hello World!'
echo 'Hello World!' # 井号后面的部分也是注释
建议在脚本开头,使用注释说明当前脚本的作用,这样有利于日后的维护。
在vim中、乃至其它编辑器中,一般注释的内容颜色与正常命令之间也都是不一样的:
1.6 脚本参数
调用脚本的时候,我们时常还会有需要往脚本中传入参数的需求,脚本根据不同的参数执行不同的操作。
比如:
.\script.sh word1 word2 word3
script.sh
是一个脚本文件,word1
、word2
和word3
就是传入脚本的三个参数。
脚本文件内部,可以使用特殊变量,引用这些参数。
$0
:脚本文件名,即script.sh
。$1
~$9
:对应脚本的第一个参数到第九个参数。$#
:参数的总数。$@
:全部的参数,参数之间使用空格分隔。$*
:全部的参数,参数之间使用变量$IFS
值的第一个字符分隔,默认为空格,但是可以自定义。
如果脚本的参数多于9个,那么第10个参数可以用${10}
的形式引用,以此类推。
注意,如果命令是command -o foo bar
,那么-o
是$1
,foo
是$2
,bar
是$3
。
下面是一个脚本内部读取命令行参数的例子。
#!/bin/bash
# script.sh
echo "全部参数:" $@
echo "命令行参数数量:" $#
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3
将其保存到script.sh
文件中,添加上可执行权限,然后执行:
用户可以输入任意数量的参数,利用for
循环,可以读取每一个参数,循环的内容后面会单独介绍,这里先使用一下:
#!/bin/bash
for i in "$@"; do
echo $i
done
上面例子中,$@
返回一个全部参数的列表,然后使用for
循环遍历所有参数。
如果多个参数放在双引号里面,视为一个参数。
./script.sh "a b"
上面例子中,Bash 会认为"a b"
是一个参数,$1
会返回a b
。注意,返回时不包括双引号。
而shift
命令可以改变脚本参数,每次执行都会移除脚本当前的第一个参数($1
),使得后面的参数向前一位,即$2
变成$1
、$3
变成$2
、$4
变成$3
,以此类推。
while
循环结合shift
命令,也可以读取每一个参数:
#!/bin/bash
echo "一共输入了 $# 个参数"
while [ "$1" != "" ]; do
echo "剩下 $# 个参数"
echo "参数:$1"
shift
done
上面例子中,shift
命令每次移除当前第一个参数,从而通过while
循环遍历所有参数,效果如下:
shift
命令可以接受一个整数作为参数,指定所要移除的参数个数,默认为1
。
shift 3
上面的命令移除前三个参数,原来的$4
变成$1
。
此外,我们还可以使用getopts
命令在脚本内部,用于解析复杂的脚本命令行参数,通常与while
循环一起使用,取出脚本所有的带有前置连词线(-
)的参数。
getopts optstring name
它带有两个参数,第一个参数optstring
是字符串,给出脚本所有的连词线参数。
比如某个脚本可以有三个配置项参数-l
、-h
、-a
,其中只有-a
可以带有参数值,而-l
和-h
是开关参数,那么getopts
的第一个参数写成lha:
,顺序不重要。
注意,a
后面有一个冒号,表示该参数带有参数值,getopts
规定带有参数值的配置项参数,后面必须带有一个冒号(:
)。
getopts
的第二个参数name
是一个变量名,用来保存当前取到的配置项参数,即l
、h
或a
。
下面是一个例子:
while getopts 'lha:' OPTION; do
case "$OPTION" in
l)
echo "linuxconfig"
;;
h)
echo "h stands for h"
;;
a)
avalue="$OPTARG"
echo "The value provided is $OPTARG"
;;
?)
echo "script usage: $(basename $0) [-l] [-h] [-a somevalue]" >&2
exit 1
;;
esac
done
shift "$(($OPTIND - 1))"
上面例子中,while
循环不断执行getopts 'lha:' OPTION
命令,每次执行就会读取一个连词线参数(以及对应的参数值),然后进入循环体。
变量OPTION
保存的是,当前处理的那一个连词线参数(即l
、h
或a
)。如果用户输入了没有指定的参数(比如-x
),那么OPTION
等于?
。循环体内使用case
判断,处理这四种不同的情况。
如果某个连词线参数带有参数值,比如-a foo
,那么处理a
参数的时候,环境变量$OPTARG
保存的就是参数值。
注意,只要遇到不带连词线的参数,getopts
就会执行失败,从而退出while
循环。比如,getopts
可以解析command -l foo
,但不可以解析command foo -l
。另外,多个连词线参数写在一起的形式,比如command -lh
,getopts
也可以正确处理。
变量$OPTIND
在getopts
开始执行前是1
,然后每次执行就会加1
。等到退出while
循环,就意味着连词线参数全部处理完毕。这时,$OPTIND - 1
就是已经处理的连词线参数个数,使用shift
命令将这些参数移除,保证后面的代码可以用$1
、$2
等处理命令的主参数。
效果如下:
-
和--
开头的参数,会被 Bash 当作配置项解释。但是有时它们不是配置项,而是实体参数的一部分,比如文件名叫做-f
或--file
。
cat -f
cat --file
上面命令的原意是输出文件-f
和--file
的内容,但是会被 Bash 当作配置项解释。
这时就可以使用配置项参数终止符--
,它的作用是告诉 Bash,在它后面的参数开头的-
和--
不是配置项,只能当作实体参数解释。
cat -- -f
cat -- --file
上面命令可以正确展示文件-f
和--file
的内容,因为它们放在--
的后面,开头的-
和--
就不再当作配置项解释了。
如果要确保某个变量不会被当作配置项解释,就要在它前面放上参数终止符--
。
ls -- $myPath
上面示例中,--
强制变量$myPath
只能当作实体参数(即路径名)解释。如果变量不是路径名,就会报错。
myPath="-l"
ls -- $myPath
上面例子中,变量myPath
的值为-l
,不是路径。但是,--
强制$myPath
只能作为路径解释,导致报错“不存在该路径”。
下面是另一个实际的例子,如果想在文件里面搜索--hello
,这时也要使用参数终止符--
。
grep -- "--hello" example.txt
上面命令在example.txt
文件里面,搜索字符串--hello
。这个字符串是--
开头,如果不用参数终止符,grep
命令就会把--hello
当作配置项参数,从而报错。
1.7 执行结果
在执行一个命令以及执行完成一个脚本后,我们常常需要知道执行的结果是成功还是失败。
虽然可以通过使用echo命令输出字符串实现,但这并不标准,因为不同人可以会输出不同的内容代表正确或错误。
因此我们规定,当一个命令执行结束后,必定会有一个返回值。0
表示执行成功,非0
(通常是1
)表示执行失败,环境变量$?
可以读取前一个命令的返回值。
可以看到,当一条命令执行成功时,$?
的值就是0,否则就是非0的。
在我们自己写的脚本中,也可以通过exit命令设置我们的脚本退出状态:
# 退出值为0(成功)
exit 0
# 退出值为1(失败)
exit 1
注意,一旦执行了这条命令,无论它后面写了多少命令,都不会再执行了,脚本直接结束。
利用所有命令与脚本都有返回值的这一点,我们也可以在脚本中对命令执行结果进行判断。
而判断的方式,便是使用后文将要介绍的条件判断,这里就先不提了。
1.8 source执行
像前面直接执行脚本,实际上都是新建的一个子Shell去执行的命令,其执行的一切效果都不会影响当前的Shell。
但这在某些时候就会存在问题,比如我想通过脚本改变当前Shell的环境变量、路径,那么这样直接执行就是无效的。
而source
命令就可以直接在当前Shell执行脚本,脚本所造成的影响会直接改变当前Shell的行为。
方式如下:
source .bashrc
正因为它这个特性,所有它常常被用来执行一些配置文件。
下面是一个例子:
#!/bin/bash
# test.sh
echo $foo
上面脚本输出$foo
变量的值:
上面例子中,由于变量foo
只存在于当前Shell,所以直接执行是无法读取,但是source
由于就是在当前Shell中执行,所以可以读取。
source
命令的另一个用途,是在脚本内部加载外部库。
#!/bin/bash
source ./lib.sh
function_from_lib
上面脚本在内部使用source
命令加载了一个外部库,然后就可以在脚本里面,使用这个外部库定义的函数。
source
有一个简写形式,可以使用一个点(.
)来表示。
. .bashrc