shell 脚本的设计模式或最佳实践 [英] Design patterns or best practices for shell scripts

查看:28
本文介绍了shell 脚本的设计模式或最佳实践的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

有谁知道关于 shell 脚本(sh、bash 等)的最佳实践或设计模式的任何资源?

Does anyone know of any resources that talk about best practices or design patterns for shell scripts (sh, bash etc.)?

推荐答案

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

I wrote quite complex shell scripts and my first suggestion is "don't". The reason is that is fairly easy to make a small mistake that hinders your script, or even make it dangerous.

也就是说,我没有其他资源可以通过您,但我的个人经验.这是我通常做的,这有点矫枉过正,但往往是可靠的,虽然非常冗长.

That said, I don't have other resources to pass you but my personal experience. Here is what I normally do, which is overkill, but tends to be solid, although very verbose.

调用

让你的脚本接受多头和空头选项.请注意,因为有两个命令可以解析选项,getopt 和 getopts.使用 getopt 可以减少麻烦.

make your script accept long and short options. be careful because there are two commands to parse options, getopt and getopts. Use getopt as you face less trouble.

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

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

Another important point is that a program should always return zero if completes successfully, non-zero if something went wrong.

函数调用

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

You can call functions in bash, just remember to define them before the call. Functions are like scripts, they can only return numeric values. This means that you have to invent a different strategy to return string values. My strategy is to use a variable called RESULT to store the result, and returning 0 if the function completed cleanly. Also, you can raise exceptions if you are returning a value different from zero, and then set two "exception variables" (mine: EXCEPTION and EXCEPTION_MSG), the first containing the exception type and the second a human readable message.

当你调用一个函数时,函数的参数被分配给特殊的变量 $0、$1 等.我建议你把它们放在更有意义的名字中.将函数内部的变量声明为局部变量:

When you call a function, the parameters of the function are assigned to the special vars $0, $1 etc. I suggest you to put them into more meaningful names. declare the variables inside the function as local:

function foo {
   local bar="$0"
}

容易出错的情况

在 bash 中,除非另外声明,否则未设置的变量将用作空字符串.这在拼写错误的情况下是非常危险的,因为错误类型的变量不会被报告,它会被评估为空.使用

In bash, unless you declare otherwise, an unset variable is used as an empty string. This is very dangerous in case of typo, as the badly typed variable will not be reported, and it will be evaluated as empty. use

set -o nounset

防止这种情况发生.不过要小心,因为如果你这样做,程序将在你每次评估一个未定义的变量时中止.因此,检查变量是否未定义的唯一方法如下:

to prevent this to happen. Be careful though, because if you do this, the program will abort every time you evaluate an undefined variable. For this reason, the only way to check if a variable is not defined is the following:

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

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

You can declare variables as readonly:

readonly readonly_var="foo"

模块化

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

You can achieve "python like" modularization if you use the following code:

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 的文件

you can then import files with the extension .shinc with the following syntax

导入AModule/ModuleFile"

import "AModule/ModuleFile"

将在 SHELL_LIBRARY_PATH 中搜索.由于您总是在全局命名空间中导入,请记住使用适当的前缀为所有函数和变量添加前缀,否则您将面临名称冲突的风险.我使用双下划线作为 python 点.

Which will be searched in SHELL_LIBRARY_PATH. As you always import in the global namespace, remember to prefix all your functions and variables with a proper prefix, otherwise you risk name clashes. I use double underscore as the python dot.

另外,把它作为你模块中的第一件事

Also, put this as first thing in your module

# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
    return 0
fi
BashInclude__imported=1

面向对象编程

在 bash 中,您不能进行面向对象的编程,除非您构建了一个相当复杂的对象分配系统(我想过.这是可行的,但很疯狂).然而,在实践中,您可以进行面向单例编程":每个对象都有一个实例,并且只有一个.

In bash, you cannot do object oriented programming, unless you build a quite complex system of allocation of objects (I thought about that. it's feasible, but insane). In practice, you can however do "Singleton oriented programming": you have one instance of each object, and only one.

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

What I do is: i define an object into a module (see the modularization entry). Then I define empty vars (analogous to member variables) an init function (constructor) and member functions, like in this example code

# 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
}

捕获和处理信号

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

I found this useful to catch and handle exceptions.

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 "$@"

提示和技巧

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

If something does not work for some reason, try to reorder the code. Order is important and not always intuitive.

甚至不要考虑使用 tcsh.它不支持功能,而且总体上很糟糕.

do not even consider working with tcsh. it does not support functions, and it's horrible in general.

希望它有所帮助,但请注意.如果非得用我这里写的那种东西,说明你的问题太复杂,用shell解决不了.使用另一种语言.由于人为因素和遗留问题,我不得不使用它.

Hope it helps, although please note. If you have to use the kind of things I wrote here, it means that your problem is too complex to be solved with shell. use another language. I had to use it due to human factors and legacy.

这篇关于shell 脚本的设计模式或最佳实践的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆