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

===============>>#1 票数:215 已采纳

我编写了相当复杂的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等。我建议你把它们放到更有意义的名字中。 将函数内部的变量声明为local:

function foo {
   local bar="$0"
}

容易出错的情况

在bash中,除非另行声明,否则将unset变量用作空字符串。 这在拼写错误的情况下是非常危险的,因为不会报告严重类型的变量,并且它将被评估为空。 使用

set -o nounset

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

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

您可以将变量声明为readonly:

readonly readonly_var="foo"

模块化

如果使用以下代码,则可以实现“python like”模块化:

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中,你不能做面向对象的编程,除非你构建一个非常复杂的对象分配系统(我想到了。这是可行的,但是疯了)。 实际上,您可以执行“面向单例编程”:每个对象只有一个实例,只有一个。

我所做的是:我将一个对象定义为一个模块(参见模块化条目)。 然后我定义空变量(类似于成员变量),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解决。 用另一种语言。 由于人为因素和遗产,我不得不使用它。

===============>>#2 票数:24

查看Advanced Bash-Scripting Guide ,了解shell脚本的大量智慧 - 不仅仅是Bash。

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

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

===============>>#3 票数:20

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

除了这个一般原则,我收集了一些常见的shell脚本错误

===============>>#4 票数:13

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

===============>>#5 票数:9

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

===============>>#6 票数:9

知道何时使用它。 对于快速和脏粘合命令,它没关系。 如果你需要做的不仅仅是几个非平凡的决定,循环,任何事情,那就去Python,Perl和modularize

shell的最大问题通常是最终结果看起来像一个大泥球,4000行bash并且正在增长......你无法摆脱它,因为现在你的整个项目依赖于它。 当然, 它开始于40行美丽的bash。

===============>>#7 票数:8

使用set -e,这样你就不会在错误后向前移动。 如果你想让它在非linux上运行,试着让它兼容而不依赖于bash。

===============>>#8 票数:7

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

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

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

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

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

===============>>#9 票数:-1

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

“使用perl。你会想知道bash而不是使用它。”

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

是的,这些天我会推荐python over perl。

  ask by user14437 translate from so

未解决问题?本站智能推荐:

3回复

在awk中遇到问题,而在文件中搜索时使用变量作为搜索模式

在这里,我将当前日期加上90天,并将其存储在tocompdate 。 并与文件比较tocompdate ,其中同一行出现在同一行中,而其他事件在该行之前的其他行中。 当搜索模式匹配时,我需要打印一些文本,并打印“匹配行之前的行”,然后在几个单词之后再次匹配行。 例如: 文件
1回复

计算文件中的出现次数。 条件模式匹配

抱歉,标题不清楚。 我有一个文件,每15分钟要读取一次,并在其中找到一个特定的模式(例如超时)。 文件没有固定的更新频率。 预期结果:-1.如果在15分钟内发现3次模式,请运行command1。 2.如果在15分钟内找到5次图案,请运行command2。 从每个检查的最后读
4回复

如何远程使用Shell脚本搜索目录模式

我需要使用scp更新另一台服务器上的某个目录。 它类似于 因此,当目标目录位于其他服务器上时,如何进行搜索? 谢谢
2回复

Shell脚本测试命令

我写下面的shell脚本。 在测试命令中,我们可以像这样比较字符串和模式, 但是,以上脚本始终在终端中显示“ good”,并且还出现以下错误: 谁能告诉我为什么以及如何在shell脚本中比较sturgng和pattern?
1回复

MVP设计模式最佳实践[关闭]

考虑以下实现MVP模式的伪代码: 这是MVP模式的另一种实现: 哪一个更正确实现MVP模式? 为什么?
4回复

Android编码最佳实践/设计模式[关闭]

在最近的一个问题中,我问我被引导到这个网站: http : //developer.android.com/design/index.html 令人惊叹的网站,但它没有回答一个特定问题:在应用程序代码设计中应用的最佳实践/设计模式是什么? 我对MVC / MVP等进行了查找,虽然产生
3回复

Shell脚本在文件中查找和替换

我有一个文件postmaster.log,我需要在其中找到模式并更改其值。我需要找到的模式是 我需要将其值更改为 问题是也有类似的模式 我也只需将sed尝试将MaxValue = 3更改为MaxValue = 0 但这仅在MaxValue = 3表示任何其他值时才
2回复

Shell脚本模式匹配

需要有关shell脚本的帮助。 我在文件中有以下结果 我只需要文件中的主机名和相应的0 / ip值。 最终输出将是
2回复

什么是服务器设计模式/最佳实践的最佳来源? [关闭]

我已经搜索了一本关于服务器设计模式的好书。 我正在寻找四人帮之类的东西。 概念包括: - Threaded vs Process vs基于组合的解决方案 - 如何正确分类请求。 即我只期望来自任何域的有限请求,因此我可能只为每个域分配一定数量的工作人员。 - 工人超
2回复

Emacs shell脚本模式钩子

由于某种原因,我的shell脚本模式挂钩不会被执行。 我的.emacs中的示例: (add-hook 'shell-script-mode-hook (lambda () (rainbow-delimiters-mode 1))) 导致设置变量,但未为打开的脚本文件加载模式。 在