跟老男孩学Linux运维:Shell编程实战
上QQ阅读APP看书,第一时间看更新

2.6 Shell脚本的建立和执行

2.6.1 Shell脚本的建立

在Linux系统中,Shell脚本(bash Shell程序)通常是在编辑器vi/vim中编写的,由UNIX/Linux命令、bash Shell命令、程序结构控制语句和注释等内容组成。这里推荐用Linux自带的功能更强大的vim编辑器来编写,可以事先做一个别名alias vi='vim',并使其永久生效,这样以后习惯输入vi的读者也就可以直接调用vim编辑器了,设置方法如下:

        [root@oldboy ~]# echo "alias vi='vim'" >>/etc/profile
        [root@oldboy ~]# tail -1 /etc/profile
        alias vi='vim'
        [root@oldboy ~]# source /etc/profile

1.脚本开头(第一行)

一个规范的Shell脚本在第一行会指出由哪个程序(解释器)来执行脚本中的内容,这一行内容在Linux bash的编程一般为:

        #! /bin/bash
        或
        #! /bin/sh #<==255个字符以内。

其中,开头的“#! ”字符又称为幻数(其实叫什么都无所谓,知道它的作用就好),在执行bash脚本的时候,内核会根据“#! ”后的解释器来确定该用哪个程序解释这个脚本中的内容。

注意,这一行必须位于每个脚本顶端的第一行,如果不是第一行则为脚本注释行,例如下面的例子。

        [oldboy@oldboy ~]$ cat test.sh
        #! /bin/bash
        echo "oldboy start"
        #! /bin/bash                          #<==写到这里就是注释了。
        #! /bin/sh                            #<==写到这里就是注释了。
        echo "oldboy end"

2. bash与sh 的区别

早期的bash与sh稍有不同,它还包含了csh和ksh的特色,但大多数脚本都可以不加修改地在sh上运行,比如:

        [root@oldboy ~]# ll /bin/sh
        lrwxrwxrwx. 1 root root 4 3月  19 20:54 /bin/sh -> bash
        [root@oldboy ~]# ll /bin/bash
        -rwxr-xr-x 1 root root 940416 10月 16 21:56 /bin/bash

提示:sh为bash的软链接,大多数情况下,脚本的开头使用“#! /bin/bash”和“#! /bin/sh”是没有区别的,但更规范的写法是在脚本的开头使用“#! /bin/bash”。

下面的Shell脚本是系统自带的软件启动脚本的开头部分。

        [root@oldboy ~]# head -1 /etc/init.d/sshd
        #! /bin/bash
        [root@oldboy ~]# head -1 /etc/init.d/ntpd
        #! /bin/bash
        [root@oldboy ~]# head -1 /etc/init.d/crond
        #! /bin/sh

提示:如果使用/bin/sh执行脚本出现异常,那么可以再使用/bin/bash试一试,但是一般不会发生此类情况。

一般情况下,在安装Linux系统时会自动安装好bash软件,查看系统的bash版本的命令如下。

        [root@oldboy ~]# cat /etc/redhat-release
        CentOS release 6.8 (Final)        #<==这里显示的是作者写作的Linux的环境版本。
        [root@oldboy ~]# bash --version
        GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)
                                          #<==这里显示的是bash的版本。
        Copyright (C) 2009 Free Software Foundation, Inc.
                                          #<==下面几行是自由软件提示的相关信息。
        License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
        This is free software; you are free to change and redistribute it.
        There is NO WARRANTY, to the extent permitted by law.

如果读者使用的是较老版本的Shell,那么建议将其升级到最新版本的Shell,特别是企业使用,因为近两年老版本的bash被暴露出存在较严重的安全漏洞。

例如:bash软件曾经爆出了严重漏洞(破壳漏洞),凭借此漏洞,攻击者可能会接管计算机的整个操作系统,得以访问各种系统内的机密信息,并对系统进行更改等。任何人的计算机系统,如果使用了bash软件,都需要立即打上补丁。检测系统是否存在漏洞的方法为:

        [root@oldboy ~]# env x='() { :; }; echo be careful' bash -c "echo this is a test"
        this is a test

如果返回如下两行,则表示需要尽快升级bash了,不过,仅仅是用于学习和测试就无所谓了。

        be careful
        this is a test

升级方法为:

        [root@oldboy ~]# yum -y update bash
        [root@oldboy ~]# rpm -qa bash
        bash-4.1.2-40.el6.x86_64

提示:如果没有输出be careful,则不需要升级。

下面是Linux中常用脚本开头的写法,不同语言的脚本在开头一般都要加上如下标识内容:

        1 #! /bin/sh
        2 #! /bin/bash
        3 #! /usr/bin/awk
        4 #! /bin/sed
        5 #! /usr/bin/tcl
        6 #! /usr/bin/expect      #<==expect解决交互式的语言开头解释器。
        7 #! /usr/bin/perl        #<==perl语言解释器。
        8 #! /usr/bin/env python  #<==python语言解释器。

CentOS和Red Hat Linux下默认的Shell均为bash。因此,在写Shell脚本的时候,脚本的开头即使不加“#! /bin/bash”,它也会交给bash解释。如果写脚本不希望使用系统默认的Shell解释,那么就必须要指定解释器了,否则脚本文件执行后的结果可能就不是你所要的。建议读者养成好的编程习惯,不管采用什么脚本,最好都加上相应的开头解释器语言标识,遵守Shell编程规范。

如果在脚本开头的第一行不指定解释器,那么就要用对应的解释器来执行脚本,这样才能确保脚本正确执行。例如:

如果是Shell脚本,就用bash test.sh执行test.sh。

如果是Python脚本,就用python test.py执行test.py。

如果是expect脚本,就用expect test.exp执行test.exp。

提示:其他的脚本程序大都是类似的执行方法。

3.脚本注释

在Shell脚本中,跟在#后面的内容表示注释,用来对脚本进行注释说明,注释部分不会被当作程序来执行,仅仅是给开发者和使用者看的,系统解释器是看不到的,更不会执行。注释可自成一行,也可以跟在脚本命令的后面与命令在同一行。开发脚本时,如果没有注释,那么团队里的其他人就会很难理解脚本对应内容的用途,而且若时间长了,自己也会忘记。因此,我们要尽量养成为所开发的Shell脚本书写关键注释的习惯,书写注释不光是为了方便别人,更是为了方便自己,避免影响团队的协作效率,以及给后来接手的人带来维护困难。特别提示一下,注释尽量不要用中文,在脚本中最好也不要有中文。

2.6.2 Shell脚本的执行

当Shell脚本运行时,它会先查找系统环境变量ENV,该变量指定了环境文件(加载顺序通常是/etc/profile、~/.bash_profile、~/.bashrc、/etc/bashrc等),在加载了上述环境变量文件后,Shell就开始执行Shell脚本中的内容(更多Shell加载环境变量的知识请见第3章)。

Shell脚本是从上至下、从左至右依次执行每一行的命令及语句的,即执行完了一个命令后再执行下一个,如果在Shell脚本中遇到子脚本(即脚本嵌套)时,就会先执行子脚本的内容,完成后再返回父脚本继续执行父脚本内后续的命令及语句。

通常情况下,在执行Shell脚本时,会向系统内核请求启动一个新的进程,以便在该进程中执行脚本的命令及子Shell脚本,基本流程如图2-3所示。

图2-3 Shell脚本的基本执行流程

特殊技巧:设置Linux的crond任务时,最好能在定时任务脚本中重新定义系统环境变量,否则,一些系统环境变量将不会被加载,这个问题需要注意!

Shell脚本的执行通常可以采用以下几种方式。

1)bash script-name或sh script-name:这是当脚本文件本身没有可执行权限(即文件权限属性x位为-号)时常使用的方法,或者脚本文件开头没有指定解释器时需要使用的方法。这也是老男孩推荐使用的方法

2)path/script-name或./script-name:指在当前路径下执行脚本(脚本需要有执行权限),需要将脚本文件的权限先改为可执行(即文件权限属性加x位),具体方法为chmod +x script-name。然后通过脚本绝对路径或相对路径就可以直接执行脚本了。

在企业生产环境中,不少运维人员在写完Shell脚本之后,由于忘记为该脚本设置执行权限,然后就直接应用了,结果导致脚本没有按照自己的意愿手动或定时执行,对于这一点,避免出现该问题的方法就是用第1种方法替代第2种。

3)source script-name或.script-name:这种方法通常是使用source或“.”(点号)读入或加载指定的Shell脚本文件(如san.sh),然后,依次执行指定的Shell脚本文件san.sh中的所有语句。这些语句将在当前父Shell脚本father.sh进程中运行(其他几种模式都会启动新的进程执行子脚本)。因此,使用source或“.”可以将san.sh自身脚本中的变量值或函数等的返回值传递到当前父Shell脚本father.sh中使用。这是它和其他几种方法最大的区别,也是值得读者特别注意的地方。

source或“.”命令的功能是:在当前Shell中执行source或“.”加载并执行的相关脚本文件中的命令及语句,而不是产生一个子Shell来执行文件中的命令。注意“.”和后面的脚本名之间要有空格。

如果读者学过PHP开发就会明白,source或“.”相当于include的功能。HTTP服务软件Apache、Nginx等配置文件里都支持这样的用法。

4)sh<script-name或cat scripts-name|sh:同样适用于bash,不过这种用法不是很常见,但有时也可以有出奇制胜的效果,例如:不用循环语句来实现精简开机自启动服务的案例,就是通过将所有字符串拼接为命令的形式,然后经由管道交给bash操作的案例(见《跟老男孩学习Linux运维:Web集群实战》第3章)。

范例2-4:创建模拟脚本test.sh,并输入如下内容。

          [oldboy@oldboy ~]$ cat >test.sh #<==编辑test.sh脚本文件。
          echo 'I am oldboy'

输入“echo 'I am oldboy'”内容后按回车键,然后再按Ctrl+d组合键结束编辑。此操作为特殊编辑方法,这里是作为cat用法的扩展知识(通过使用来记忆是个好习惯)。

现在使用第1种方法实践,命令如下:

        [oldboy@oldboy ~]$ cat test.sh
        echo 'I am oldboy'
        [oldboy@oldboy ~]$ sh test.sh    #<==使用第1种方式的sh命令执行test.sh脚本文件。
        I am oldboy
        [oldboy@oldboy ~]$ bash test.sh  #<==使用第1种方式的bash命令执行test.sh脚本文件。
        I am oldboy

这里使用第1种方法的bash和sh,均可以执行脚本并得到预期的结果。

使用第2种方法实践,命令如下:

        [oldboy@oldboy ~]$ ls -l test.sh
        -rw-rw-r-- 1 oldboy oldboy 19 Apr 30 02:46 test.sh
        [oldboy@oldboy ~]$ ./test.sh     #<==使用第2种方式“./”在当前目录下执行test.sh
    脚本文件,细心的读者可以发现,这个地方无法自动补全,这是因为没有权限所导致的。
        -bash: ./test.sh: Permission denied     #<==提示:强制执行会提示权限拒绝,此处是
                                                        因为没有执行权限。

虽然没有权限的test.sh脚本不能直接被执行,但是可以用source或“.”(点号)来执行,如下:

        [oldboy@oldboy ~]$ . test.sh              #<==请注意开头的“.”后面有空格。
        I am oldboy
        [oldboy@oldboy ~]$ source test.sh
        I am oldboy

提示:“.”或source命令的功能相同,都是读入脚本并执行脚本。

给test.sh添加可执行权限,命令如下:

        [oldboy@oldboy ~]$ chmod u+x test.sh
        [oldboy@oldboy ~]$ ./test.sh
        I am oldboy

可以看到,给test.sh加完可执行权限后就能执行了。前面也提到了,这种方法在使用前每次都需要给定执行权限,但容易被忘记,且多了一些步骤,增加了复杂性。

使用第3种方法实践时,会将source或“.”执行的脚本中的变量值传递到当前的Shell中,如下:

        [oldboy@oldboy ~]$ echo 'userdir=`pwd`' >testsource.sh #<==第一行的内容通常用
                                                                        echo处理更方便
        [oldboy@oldboy ~]$ cat testsource.sh
        userdir=`pwd`  #<==定义了一个命令变量,内容是打印当前路径。注意,打印命令用反引号
        [oldboy@oldboy ~]$ sh testsource.sh     #<==采用sh命令执行脚本
        [oldboy@oldboy ~]$ echo $userdir
                            #<==此处为空,并没有出现当前路径/home/oldboy的输出,这是为什么

根据上面的例子可以发现,通过sh或bash命令执行过的脚本,若在脚本结束之后,在当前Shell窗口中查看userdir变量的值,会发现值是空的。现在以同样的步骤改用source或“.”执行,然后再看看userdir变量的值:

        [oldboy@oldboy ~]$ source testsource.sh  #<==采用source执行同一脚本
        [oldboy@oldboy ~]$ echo $userdir
        /home/oldboy #<==此处输出了当前路径/home/oldboy,这又是为什么呢

来了解一下系统NFS服务的脚本是如何使用“.”的:

        # Source function library.
        . /etc/init.d/functions                      #<==通过"."加载系统函数库functions

说明:操作系统及服务自带的脚本是我们学习的标杆和参考(虽然有时感觉这些脚本也不是十分规范)。

结论:通过source或“.”加载执行过的脚本,由于是在当前Shell中执行脚本,因此在脚本结束之后,脚本中的变量(包括函数)值在当前Shell中依然存在,而sh和bash执行脚本都会启动新的子Shell执行,执行完后退回到父Shell。因此,变量(包括函数)值等无法保留。在进行Shell脚本开发时,如果脚本中有引用或执行其他脚本的内容或配置文件的需求时,最好用“.”或source先加载该脚本或配置文件,处理完之后,再将它们加载到脚本的下面,就可以调用source加载的脚本及配置文件中的变量及函数等内容了。

以下采用第4种方法来实践:

        [root@oldboy ~]# ls -l oldboy.sh
        -rw-r--r--. 1 root root 28 Nov 18 15:52 oldboy.sh
        [root@oldboy ~]# cat oldboy.sh
        echo "I am oldboy teacher."
        [root@oldboy ~]# sh<oldboy.sh            #<==尽量不要使用这种方法。
        I am oldboy teacher.
        [root@oldboy ~]# cat oldboy.sh|bash       #<==这种方法在命令行拼接字符串命令后,需
                                                        要执行时就会用到
        I am oldboy teacher.

提示:代码中提到的两种执行方法相当于sh scripts-name,效率很高,但是初学者用得少。

范例2-5:已知如下命令及返回结果,请问echo $user的返回的结果为()。

        [oldboy@test ~]$ cat test.sh
        user=`whoami`
        [oldboy@test ~]$ sh test.sh
        [oldboy@test ~]$ echo $user

参考的选择项如下:

a)当前用户

b)oldboy

c)空(无内容输出)

这是某互联网公司Linux运维职位的笔试题。在这里c)是正确答案,原因前面已经讲过了,即使用sh执行脚本会导致当前Shell无法获得变量值。

通过上述面试题可得出如下的结论:

□儿子Shell脚本会直接继承父亲Shell脚本的变量、函数(就好像是儿子随父亲姓,基因也会继承父亲的)等,反之则不可以。

□如果希望反过来继承(就好像是让父亲随儿子姓,让父亲的基因也继承儿子的),就要用source或“.”在父亲Shell脚本中事先加载儿子Shell脚本。

2.6.3 Shell脚本开发的基本规范及习惯

Shell脚本的开发规范及习惯非常重要,虽然这些规范不是必须要遵守的,但有了好的规范和习惯,可以大大提升开发效率,并能在后期降低对脚本的维护成本。当多人协作开发时,大家有一个互相遵守的规范就显得更重要了。即使只是一个人开发,最好也采取一套固定的规范,这样脚本将会更易读、更易于后期维护,最重要的是要让自己养成一个一出手就很专业和规范的习惯。下面来看看有哪些规范,这些规范在第14章也会提及,以便于大家进一步巩固。

1)Shell脚本的第一行是指定脚本解释器,通常为:

        #! /bin/bash
        或
        #! /bin/sh

2)Shell脚本的开头会加版本、版权等信息:

        # Date:    16:29 2012-3-30
        # Author: Created by oldboy
        # Blog:http://oldboy.blog.51cto.com
        # Description:This scripts function is.....
        # Version:1.1

说明:以上两点在Linux系统场景中不是必需的,只属于优秀规范和习惯,第16章有自动加载此内容的方法,读者可以做进一步了解。

可修改“~/.vimrc”配置文件配置vim编辑文件时自动加上以上信息的功能。

3)在Shell脚本中尽量不用中文(不限于注释)。

尽量用英文注释,防止本机或切换系统环境后中文乱码的困扰。如果非要加中文,请根据自身的客户端对系统进行字符集调整,如:export LANG="zh_CN.UTF-8",并在脚本中,重新定义字符集设置,和系统保持一致。

4)Shell脚本的命名应以.sh为扩展名。

例如:script-name.sh

5)Shell脚本应存放在固定的路径下。

例如:/server/scripts

以下则是Shell脚本代码书写的良好习惯。

1)成对的符号应尽量一次性写出来,然后退格在符号里增加内容,以防止遗漏。这些成对的符号包括:

        {}、[]、''、``、""

说明:这部分也可以配置.vimrc实现自动添加,但是老男孩不推荐这样做,因为养成良好的习惯很重要。

2)中括号([])两端至少要有1个空格,因此,键入中括号时即可留出空格[ ],然后再退格键入中间的内容,并确保两端都至少有一个空格,即先键入一对中括号,然后退1格,输入两个空格,再退1格,双中括号([[]])的写法也是如此。

3)对于流程控制语句,应一次性将格式写完,再添加内容。

比如,一次性完成if语句的格式,应为:

        if 条件内容
          then
            内容
        fi

一次性完成for循环语句的格式,应为:

        for
        do
            内容
        done

提示:while和until, case等语句也是一样。

4)通过缩进让代码更易读,比如:

        if条件内容
          then
            内容
        fi

5)对于常规变量的字符串定义变量值应加双引号,并且等号前后不能有空格,需要强引用的(指所见即所得的字符引用),则用单引号(' '),如果是命令的引用,则用反引号(` `)。例如:

        OLDBOY_FILE="test.txt"

6)脚本中的单引号、双引号及反引号必须为英文状态下的符号,其实所有的Linux字符及符号都应该是英文状态下的符号,这点需要特别注意。

说明:好的习惯可以让我们避免很多不必要的麻烦,提升工作效率。

有关Shell开发规范及习惯的更多内容,感兴趣的读者可参考本书的第14章。很多开发习惯也可以通过配置vim的功能来实现,例如实现自动缩进、自动补全成对符号、自动加入起始解释器及版权信息等,这部分的知识可参考本书第16章。对于一些开发规范和习惯,在新手入门学习期间,我们不建议将其搞得太傻瓜化、智能化,这会让我们产生惰性,所以有关Shell的开发规范及习惯的知识放在第14章来讲解,有关搭建高效的Shell开发环境的知识放在第16章讲解!