shell脚本的设计模式或最佳实践[关闭]

Posted

技术标签:

【中文标题】shell脚本的设计模式或最佳实践[关闭]【英文标题】:Design patterns or best practices for shell scripts [closed] 【发布时间】:2010-09-09 20:39:03 【问题描述】:

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

【问题讨论】:

昨晚我刚刚在template pattern in BASH 上写了一篇小文章。看看你的想法。 【参考方案1】:

我编写了相当复杂的 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

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

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

导入“AModule/ModuleFile”

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

【讨论】:

哇,我还以为我在 bash 中太过分了……我倾向于使用孤立的函数并滥用子外壳(因此,当速度与任何方式相关时,我都会受苦)。从来没有全局变量,既不进也不出(以保持理智)。所有通过标准输出或文件输出返回。 set -u/set -e (太糟糕了 set -e 一开始就变得没用,而且我的大部分代码经常在那里)。使用 [local something="$1"; 获取的函数参数shift](允许在重构时轻松重新排序)。在一个 3000 行的 bash 脚本之后,我倾向于以这种方式编写最小的脚本...... 对模块化的小修正: 1 你需要在 . "$script_absolute_dir/$module.shinc" 以避免丢失警告。 2 您必须在返回 $SHELL_LIBRARY_PATH 中查找模块之前设置 IFS="$saved_IFS" “人为因素”是最糟糕的因素。当你给它们更好的东西时,机器不会与你作对。 为什么是 getoptgetoptsgetopts 更便携,可以在任何 POSIX shell 中工作。特别是因为问题是 shell 最佳实践 而不是专门的 bash 最佳实践,我会支持 POSIX 合规性以尽可能支持多个 shell。 感谢您提供有关 shell 脚本的所有建议,尽管您是诚实的:“希望它有所帮助,但请注意。如果您必须使用我在这里写的那种东西,这意味着您的问题太复杂,shell无法解决。使用另一种语言。由于人为因素和遗留问题,我不得不使用它。”【参考方案2】:

查看Advanced Bash-Scripting Guide,了解有关 shell 脚本的大量知识——不仅仅是 Bash。

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

如前所述,shell 脚本没有很多“最佳实践”或“设计模式”。不同的用途有不同的指导方针和偏见——就像任何其他编程语言一样。

【讨论】:

请注意,对于稍微复杂的脚本,这不是最佳实践。编码不仅仅是让某些东西工作。它是关于快速、轻松地构建它,并且它是可靠的、可重用的、易于阅读和维护的(尤其是对其他人而言)。 Shell 脚本不能很好地扩展到任何级别。对于任何逻辑的项目来说,更健壮的语言都会简单得多。【参考方案3】:

shell 脚本是一种设计用于操作文件和进程的语言。 虽然它很好,但它不是通用语言, 所以总是尝试从现有实用程序中粘合逻辑,而不是 在 shell 脚本中重新创建新逻辑。

除了一般原则之外,我还收集了一些common shell script mistakes。

【讨论】:

【参考方案4】:

知道何时使用它。 为了快速而肮脏的将命令粘合在一起,这是可以的。如果您需要做出一些重要的决定、循环等任何事情,请选择 Python、Perl 和 模块化

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

【讨论】:

【参考方案5】:

今年(2008 年)在 OSCON 上就这个主题举办了一场精彩的会议:http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf

【讨论】:

【参考方案6】:

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

【讨论】:

【参考方案7】:

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

【讨论】:

你自相矛盾地说“不必复杂化”,然后列出你认为增加价值的各种复杂性,而在大多数情况下被滥用成丑陋的怪物,而不是用来简化问题和实施。跨度> 这意味着一个很大的缺点,您的脚本将无法在没有 python 的系统上移植 我意识到这是在 08 年回答的(现在比 12 年早了两天);但是,对于那些年后看的人,我会提醒任何人不要拒绝像 Python 或 Ruby 这样的语言,因为它更有可能是可用的,如果没有,它是一个命令(或几次点击)就可以安装.如果您需要进一步的可移植性,请考虑用 Java 编写程序,因为您将很难找到没有可用 JVM 的机器。 @astropanic 现在几乎所有的 Linux 端口都使用 Python @Pithikos,当然,并摆弄 python2 与 python3 的麻烦。现在我所有的工具都是用 go 写的,再开心不过了。【参考方案8】:

要找到一些“最佳实践”,请查看 Linux 发行版(例如 Debian)如何编写他们的 init 脚本(通常在 /etc/init.d 中找到)

它们中的大多数都没有“bash-isms”,并且很好地分离了配置设置、库文件和源格式。

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

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

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

【讨论】:

以上是关于shell脚本的设计模式或最佳实践[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

寻找示例数据库设计的好地方 - 最佳实践[关闭]

REST API 设计 - 最佳实践:链接现有子资源 [关闭]

最佳用户角色权限数据库设计实践? [关闭]

最佳用户角色权限数据库设计实践? [关闭]

使用 PostgreSQL 进行一对多设计的 Sequelize.js 最佳实践 [关闭]

编写Shell脚本的最佳实践,规范一