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


167

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


2
昨晚我刚刚在BASH中写了一篇有关模板模式的小文章。看看你的想法。
quickshiftin

Answers:


222

我写了相当复杂的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),第一个包含异常类型,第二个包含人类可读的消息。

调用函数时,函数的参数将分配给特殊变量vars $ 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中,您不能进行面向对象的编程,除非您构建了一个非常复杂的对象分配系统(我认为这是可行的,但是很疯狂)。但是实际上,您可以执行“面向单一编程”:每个对象只有一个实例,只有一个。

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

# 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解决。使用另一种语言。由于人为因素和传统,我不得不使用它。


7
哇,我以为我要对bash进行过度杀伤...我倾向于使用隔离的功能和滥用subshel​​l(因此,当速度以任何方式相关时,我会遭受痛苦)。没有全局变量,也没有内部或外部(以保持理智)。所有通过标准输出或文件输出返回。set -u / set -e(太糟糕了,set -e会在第一个情况下立即变得无用,而且我的大部分代码都在其中)。[[local something =“ $ 1”; 移位](允许在重构时轻松重新排序)。经过一千行bash脚本后,我倾向于以这种方式编写甚至最小的脚本……
Eugene

模块化的小修正:1。“ $ script_absolute_dir / $ module.shinc”以避免丢失警告。2您必须先设置IFS =“ $ saved_IFS”,然后再在$ SHELL_LIBRARY_PATH中查找模块返回
Duff,

“人为因素”是最糟糕的因素。当您给机器带来更好的东西时,机器不会与您抗争。
jeremyjjbrown 2014年

1
为什么getoptvs getoptsgetopts具有更高的可移植性,可以在任何POSIX shell中使用。特别是由于问题是shell最佳实践而不是bash最佳实践,因此我将支持POSIX遵从性,以在可能时支持多个shell。
Wimateeka

1
即使您说实话,也感谢您提供所有有关Shell脚本的建议:“希望它会有所帮助,但请注意。如果您必须使用我在这里写的那种内容,则意味着您的问题太复杂了,无法解决shell。请使用另一种语言。由于人为因素和传统,我不得不使用它。”
dieHellste

25

看看《高级Bash脚本指南》了解有关Shell脚本的很多知识-不仅限于Bash。

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

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


9
请注意,对于稍微复杂的脚本,这不是最佳实践。编码不只是使某些事情起作用。它是关于快速,轻松地构建它,并使其可靠,可重用以及易于阅读和维护(尤其是对于其他人)而言。Shell脚本无法很好地扩展到任何级别。对于具有任何逻辑的项目,更健壮的语言要简单得多。
漂流者

20

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

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



11

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

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


9

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


7
您说“无需复杂化”,然后列出您认为可以增加价值的各种复杂性,而与自己矛盾,而在大多数情况下,它们被滥用为丑陋的怪物,而不是用来简化问题和实现。
Evgeny

3
这意味着一个很大的缺点,您的脚本将无法在不存在python的系统上移植
astropanic 2011年

1
我知道这是在'08(现在是'12的前两天)开始回答的;但是,对于那些希望在此后的几年中使用的人,我会提醒任何人不要放弃使用Python或Ruby之类的语言,因为它很有可能可用,如果没有的话,这是一个远离安装的命令(或几次单击) 。如果您需要进一步的可移植性,请考虑使用Java编写程序,因为您将很难找到没有JVM的计算机。
维尔摩尔三世

@astropanic如今几乎所有使用Python的Linux端口
Pithikos

@Pithikos,当然,并且在弄麻烦python2 vs python3的麻烦。如今,我用go编写了所有工具,而且再也不会高兴了。
astropanic

9

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


7

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

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

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

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

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


-1

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

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

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

是的,这些天我会建议在perl上使用python。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.