关于bash:shell脚本的设计模式或最佳实践

关于bash:shell脚本的设计模式或最佳实践

Design patterns or best practices for shell scripts

是否有人知道任何有关shell脚本(sh,bash等)的最佳实践或设计模式的资源?


我写了相当复杂的shell脚本,我的第一个建议是"不要"。原因是犯一个小错误很容易阻碍脚本,甚??至使脚本变得危险。

就是说,除了我的个人经历,我没有其他资源可以传递给您。
这是我通常所做的事情,虽然过于冗长,但它已经过时了,但往往很扎实。

调用方式

使您的脚本接受长短选项。请注意,因为有两个命令可以解析选项getopt和getopts。使用getopt可以减少麻烦。

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
CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: --"$@"`

if test $? != 0
then
    echo"unrecognized option"
    exit 1
fi

eval set --"$getopt_results"

while true
do
    case"$1" in
        --config_file)
            CommandLineOptions__config_file="$2";
            shift 2;
            ;;
        --debug_level)
            CommandLineOptions__debug_level="$2";
            shift 2;
            ;;
        --)
            shift
            break
            ;;
        *)
            echo"$0: unparseable option $1"
            EXCEPTION=$Main__ParameterException
            EXCEPTION_MSG="unparseable option $1"
            exit 1
            ;;
    esac
done

if test"x$CommandLineOptions__config_file" =="x"
then
    echo"$0: missing config_file parameter"
    EXCEPTION=$Main__ParameterException
    EXCEPTION_MSG="missing config_file parameter"
    exit 1
fi

另一个重要的一点是,如果程序成功完成,则应始终返回零,如果出现问题,则应始终返回非零。

函数调用

您可以在bash中调用函数,只记得在调用之前定义它们。函数就像脚本,它们只能返回数字值。这意味着您必须发明一种不同的策略来返回字符串值。我的策略是使用一个名为RESULT的变量来存储结果,如果函数完全完成,则返回0。
另外,如果返回的值不为零,则可以引发异常,然后设置两个"异常变量"(例如:EXCEPTION和EXCEPTION_MSG),第一个包含异常类型,第二个包含人类可读的消息。

调用函数时,函数的参数将分配给特殊变量vars $ 0,$ 1等。建议您将它们放入更有意义的名称中。将函数内部的变量声明为局部变量:

1
2
3
function foo {
   local bar="$0"
}

容易出错的情况

在bash中,除非另行声明,否则将未设置的变量用作空字符串。如果输入错误,这将非常危险,因为将不会报告错误键入的变量,并且该变量将被评估为空。采用

1
set -o nounset

以防止这种情况发生。不过请小心,因为如果这样做,则每次评估未定义的变量时,程序都会中止。因此,检查变量是否未定义的唯一方法是:

1
2
3
4
if test"x${foo:-notset}" =="xnotset"
then
    echo"foo not set"
fi

您可以将变量声明为只读:

1
readonly readonly_var="foo"

模块化

如果使用以下代码,则可以实现"类似于python"的模块化:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
set -o nounset
function getScriptAbsoluteDir {
    # @description used to get the script path
    # @param $1 the script $0 parameter
    local script_invoke_path="$1"
    local cwd=`pwd`

    # absolute path ? if so, the first character is a /
    if test"x${script_invoke_path:0:1}" = 'x/'
    then
        RESULT=`dirname"$script_invoke_path"`
    else
        RESULT=`dirname"$cwd/$script_invoke_path"`
    fi
}

script_invoke_path="$0"
script_name=`basename"$0"`
getScriptAbsoluteDir"$script_invoke_path"
script_absolute_dir=$RESULT

function import() {
    # @description importer routine to get external functionality.
    # @description the first location searched is the script directory.
    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
    # @param $1 the .shinc file to import, without .shinc extension
    module=$1

    if test"x$module" =="x"
    then
        echo"$script_name : Unable to import unspecified module. Dying."
        exit 1
    fi

    if test"x${script_absolute_dir:-notset}" =="xnotset"
    then
        echo"$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
        exit 1
    fi

    if test"x$script_absolute_dir" =="x"
    then
        echo"$script_name : empty script path. Dying."
        exit 1
    fi

    if test -e"$script_absolute_dir/$module.shinc"
    then
        # import from script directory
        ."$script_absolute_dir/$module.shinc"
    elif test"x${SHELL_LIBRARY_PATH:-notset}" !="xnotset"
    then
        # import from the shell script library path
        # save the separator and use the ':' instead
        local saved_IFS="$IFS"
        IFS=':'
        for path in $SHELL_LIBRARY_PATH
        do
            if test -e"$path/$module.shinc"
            then
                ."$path/$module.shinc"
                return
            fi
        done
        # restore the standard separator
        IFS="$saved_IFS"
    fi
    echo"$script_name : Unable to find module $module."
    exit 1
}

然后可以使用以下语法导入扩展名为.shinc的文件

导入" AModule / ModuleFile"

将在SHELL_LIBRARY_PATH中进行搜索。始终导入全局名称空间时,请记住为所有函数和变量添加适当的前缀,否则可能会导致名称冲突。我使用双下划线作为python点。

另外,将其作为模块的第一件事

1
2
3
4
5
6
# avoid double inclusion
if test"${BashInclude__imported+defined}" =="defined"
then
    return 0
fi
BashInclude__imported=1

面向对象编程

在bash中,您不能进行面向对象的编程,除非您构建了一个非常复杂的对象分配系统(我认为这是可行的,但是很疯狂)。
但实际上,您可以执行"面向单一编程":每个对象只有一个实例,而只有一个。

我要做的是:我将一个对象定义到一个模块中(请参阅模块化条目)。然后,我定义空的vars(类似于成员变量),一个init函数(构造函数)和成员函数,如本示例代码所示

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# avoid double inclusion
if test"${Table__imported+defined}" =="defined"
then
    return 0
fi
Table__imported=1

readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"

# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"

# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command

p_Table__initialized=0

function Table__init {
    # @description init the module with the database parameters
    # @param $1 the mysql config file
    # @exception Table__NoException, Table__ParameterException

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -ne 0
    then
        EXCEPTION=$Table__AlreadyInitializedException  
        EXCEPTION_MSG="module already initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi


    local config_file="$1"

      # yes, I am aware that I could put default parameters and other niceties, but I am lazy today
      if test"x$config_file" ="x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter config file"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi


    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e"

    # mark the module as initialized
    p_Table__initialized=1

    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0

}

function Table__getName() {
    # @description gets the name of the person
    # @param $1 the row identifier
    # @result the name

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -eq 0
    then
        EXCEPTION=$Table__NotInitializedException
        EXCEPTION_MSG="module not initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi

    id=$1

      if test"x$id" ="x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter identifier"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi

    local name=`$p_Table__mysql_exec"SELECT name FROM table WHERE id = '$id'"`
      if test $? != 0 ; then
        EXCEPTION=$Table__MySqlException
        EXCEPTION_MSG="unable to perform select"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
      fi

    RESULT=$name
    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0
}

诱捕和处理信号

我发现这对于捕获和处理异常很有用。

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
function Main__interruptHandler() {
    # @description signal handler for SIGINT
    echo"SIGINT caught"
    exit
}
function Main__terminationHandler() {
    # @description signal handler for SIGTERM
    echo"SIGTERM caught"
    exit
}
function Main__exitHandler() {
    # @description signal handler for end of the program (clean or unclean).
    # probably redundant call, we already call the cleanup in main.
    exit
}

trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT

function Main__main() {
    # body
}

# catch signals and exit
trap exit INT TERM EXIT

Main__main"$@"

提示和技巧

如果由于某种原因某些操作不起作用,请尝试重新排序代码。顺序很重要,但并不总是直观的。

甚至不考虑使用tcsh。它不支持功能,并且总体上来说太可怕了。

希望它会有所帮助,但请注意。如果您必须使用我在这里写的那种东西,那意味着您的问题太复杂了,无法用Shell解决。使用另一种语言。由于人为因素和传统,我不得不使用它。


看看Advanced Bash-Scripting Guide,了解有关Shell脚本的很多知识-不仅限于Bash。

不要听别人说要看其他更复杂的语言。如果外壳脚本满足您的需求,请使用该脚本。您需要功能,而不是幻想。新语言为您的简历提供了宝贵的新技能,但是如果您有需要完成的工作并且已经了解Shell,这将无济于事。

如上所述,shell脚本没有很多"最佳实践"或"设计模式"。像其他任何编程语言一样,不同的用法具有不同的准则和偏见。


Shell脚本是一种用于操纵文件和进程的语言。
尽管这样做很不错,但它不是通用语言,
因此,始终尝试从现有实用程序中胶合逻辑,而不是
在shell脚本中重新创建新逻辑。

除了一般原则之外,我还收集了一些常见的Shell脚本错误。


今年(2008年)在OSCON上有一个关于此主题的精彩会议:http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf


知道何时使用它。对于快速而肮脏的粘合命令,也可以。如果您需要做出不多的重要决策,循环或其他任何事情,请使用Python,Perl和模块化。

shell的最大问题通常是最终结果看起来像是一个大泥泞球,4000行bash并不断增长……而您无法摆脱它,因为现在您的整个项目都依赖它。当然,它始于40行漂亮的bash。


使用set -e,这样您就不会在出错后继续前进。如果要使其在非Linux上运行,请尝试使其不兼容bash,使其与sh兼容。


简单:
使用python而不是shell脚本。
您可以将可读性提高近100倍,而无需使您不需要的任何事情变得复杂,并且保留了将脚本的各个部分演化为函数,对象,持久性对象(zodb),分布式对象(pyro)的能力,而几乎没有任何复杂性额外的代码。


要找到"最佳做法",请查看Linux发行版(例如Debian)如何编写其初始化脚本(通常在/etc/init.d中找到)

它们中的大多数没有" bash-isms",并且将配置设置,库文件和源格式很好地分开了。

我的个人风格是编写一个定义一些默认变量的master-shellscript,然后尝试加载("源")可能包含新值的配置文件。

我尽量避免使用函数,因为它们会使脚本更加复杂。 (Perl是为此目的而创建的。)

为了确保脚本可移植,不仅可以使用#!/ bin / sh进行测试,还可以使用#!/ bin / ash,#!/ bin / dash等进行测试。您将很快发现Bash特定的代码。


或更旧的报价类似于Joao所说的话:

"使用perl。您将想了解bash但不使用它。"

可悲的是,我忘了是谁说的。

是的,这些天我会推荐在perl上使用python。


推荐阅读

    linux脚本命令修改?

    linux脚本命令修改?,密码,系统,文件,资料,工具,软件,基础,地址,标准,命令,lin

    linux脚本命令教学?

    linux脚本命令教学?,标准,数据,系统,脚本,代码,流程,官网,底部,命令,变量,lin

    linux命令注释脚本?

    linux命令注释脚本?,代码,工具,名称,工作,脚本,发行,服务,环境,数据,基础,lin

    脚本linux上运行命令?

    脚本linux上运行命令?,工具,代码,时间,密码,系统,环境,名字,位置,第三,下来,t

    linux命令错误的是?

    linux命令错误的是?,系统,信息,异常,密码,工具,地址,网络,实时,状态,数据,Lin

    linux修改脚本的命令?

    linux修改脚本的命令?,系统,密码,服务,工作,工具,环境,信息,百度,代码,脚本,

    linux运行命令的脚本?

    linux运行命令的脚本?,系统,服务,工具,脚本,意外,技术,分析,文件,方法,命令,s

    linux脚本命令单引号?

    linux脚本命令单引号?,系统,工作,美元,地址,命令,信息,情况,标准,管理,引号,l

    执行linux脚本命令行?

    执行linux脚本命令行?,工具,位置,地方,环境,数据,状态,暂停,增长,系统,基础,

    linux暂停脚本命令?

    linux暂停脚本命令?,暂停,系统,状态,工具,命令,进程,材料,电脑,公开,名称,Lin

    linux运行脚本的命令?

    linux运行脚本的命令?,系统,工具,代码,服务,脚本,状态,密码,环境,位置,暂停,l

    linux显示错误命令?

    linux显示错误命令?,信息,系统,电脑,状态,时间,环境,命令,搜狐,密码,异常,虚

    linux退出错误命令的?

    linux退出错误命令的?,系统,电脑,环境,命令,位置,管理,工具,设备,终端,进程,L

    linux没有该命令错误?

    linux没有该命令错误?,系统,第一,环境,命令,分析,软件,异常,文件,目录,空格,

    linux脚本多个命令?

    linux脚本多个命令?,系统,工作,标准,服务,命令,软件,管理,连续,电脑,设备,lin

    linux脚本注释命令?

    linux脚本注释命令?,代码,系统,工作,名称,地方,脚本,命令,信息,状态,简介,lin

    脚本运行linux命令?

    脚本运行linux命令?,系统,环境,工具,工作,位置,底部,代码,发行,官网,终端,lin

    linux命令错误代码?

    linux命令错误代码?,系统,密码,电脑,网络,手机,网址,软件,代码,设备,老板,Lin

    linux命令下写脚本?

    linux命令下写脚本?,服务,系统,环境,代码,名称,命令,脚本,发行,在线,情况,三