design-patterns - Shell 脚本的设计模式或最佳实践

标签 design-patterns bash shell

<分区>

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

最佳答案

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

就是说,除了我的个人经验,我没有其他资源可以传递给您。 这是我通常做的事情,这有点矫枉过正,但往往很扎实,尽管非常冗长。

调用

让你的脚本接受多头和空头选项。要小心,因为有两个命令可以解析选项,getopt 和 getopts。使用 getopt 会减少麻烦。

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),第一个包含异常类型,第二个包含人类可读的消息。

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

function foo {
   local bar="$0"
}

容易出错的情况

在 bash 中,除非您另外声明,否则未设置的变量将用作空字符串。这在打字错误的情况下非常危险,因为输入错误的变量将不会被报告,并且会被评估为空。使用

set -o nounset

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

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

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

readonly readonly_var="foo"

模块化

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

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

导入“模块/模块文件”

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

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

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

面向对象编程

在 bash 中,你不能进行面向对象的编程,除非你构建一个相当复杂的对象分配系统(我考虑过。它是可行的,但很疯狂)。 然而实际上,您可以进行“面向单例的编程”:每个对象都有一个实例,而且只有一个。

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

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

捕获和处理信号

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

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 是解决不了的。使用另一种语言。由于人为因素和遗留问题,我不得不使用它。

关于design-patterns - Shell 脚本的设计模式或最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31495435/

相关文章:

java设计模式消除代码重复

swift - 调用 "strategy design pattern"中的方法时缺少参数标签 -SWIFT

linux - ls 命令在 bash 中不起作用。每当我打开 bash 时都会显示错误消息

eclipse - 如何在 Eclipse 中运行系统 shell/终端?

design-patterns - 如何使用变量名称而不覆盖模块名称?

.net - 为什么 Rectangle.Size 在每次调用时都会创建新实例?

linux - 在特定目录中查找文件

bash - ffmpeg 不适用于包含空格的文件名

bash - 如何在 trap 命令后解除陷阱

java - 如何通过java获取命令 "top -c -b -n 1"的完整结果,Runtime.getRuntime().exec(command)