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

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

问题描述

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

解决方案

我写了相当复杂的shell脚本,我的第一个建议是不要。原因是这样做是一个很小的错误,妨碍你的脚本,甚至使它变得危险。



那就是说,我没有其他的资源可以通过你,但我的个人经验。
这是我通常做的,这是过分的,但往往是坚实的,虽然非常详细。



调用



使你的脚本接受长和短的选项。要小心,因为有两个命令来解析选项,getopt和getopts。

  CommandLineOptions__config_file =
CommandLineOptions__debug_level =

getopt_results =`getopt -s bash -oc:d :: --long config_file:debug_level :: - $ @`

如果测试$? != 0
then
echounrecognized option
exit 1
fi

eval set - $ getopt_results

$ true



--config_file
CommandLineOptions__config_file =$ 2;
shift 2;
;;
--debug_level)
CommandLineOptions__debug_level =$ 2;
shift 2;
;;
- )
shift
break
;;
*)
echo$ 0:不可稀释选项$ 1
EXCEPTION = $ Main__ParameterException
EXCEPTION_MSG =不可稀释选项$ 1
exit 1
;;
esac
done

如果测试x $ CommandLineOptions__config_file==x
然后
echo$ 0:缺少config_file参数
EXCEPTION = $ Main__ParameterException
EXCEPTION_MSG =缺少config_file参数
退出1
fi


$另一个重要的一点是如果一个程序成功完成,程序总是返回0,否则出现错误。



函数调用



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



当您调用函数时,函数的参数将分配给特殊的vars $ 0,$ 1等。我建议您将它们放入更有意义的名称。将函数内的变量声明为local:

  function foo {
local bar =$ 0

容易出错的情况



在bash中,除非另有声明,否则将使用未设置的变量作为空字符串。这在打字错误的情况下是非常危险的,因为不良报告的类型不正确的变量将被评估为空。使用

  set -o nounset 

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

 如果测试x $ {foo :-notset}==xnotset
then
echofoo not set
fi

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

  readonly readonly_var =foo

模块化



如果您使用以下代码,实现python like模块化:

  set -o nounset 
function getScriptAbsoluteDir {
#@description用于获取脚本路径
#@param $ 1脚本$ 0参数
本地script_invoke_path =$ 1
local cwd =`pwd`

#绝对路径?如果是这样,第一个字符是/
如果测试x $ {script_invoke_path:0:1}='x /'
然后
RESULT =`dirname$ script_invoke_path`
else
RESULT =`dirname$ cwd / $ script_invoke_path`
fi
}

script_invoke_path =$ 0
script_name = $












$ b $ @
#@description搜索的第一个位置是脚本目录。
#@description如果没有找到,搜索$ SHELL_LIBRARY_PATH环境变量
#@param $ 1中包含的路径中的模块导入的.shinc文件,不带.shinc扩展名
module = $ 1

如果测试x $ module==x
然后
echo$ script_name:无法导入未指定的模块,死亡。
exit 1
fi

如果测试x $ {script_absolute_dir:-notset}==xnotset
然后
echo$ script_name:未定义的脚本绝对目录。您是否删除了getScriptAbsoluteDir?死亡。
exit 1
fi

如果测试x $ script_absolute_dir==x
然后
echo$ script_name:空脚本路径死亡。
exit 1
fi

如果test -e$ script_absolute_dir / $ module.shinc
然后
#从脚本目录导入
。 $ script_absolute_dir / $ module.shinc
elif testx $ {SHELL_LIBRARY_PATH:-notset}!=xnotset
然后
#从shell脚本库路径导入
#保存分隔符并使用':'代替
local saved_IFS =$ IFS
IFS ='
$ SHELL_LIBRARY_PATH
$ $ $ $ $ $ $ $ $ $ $如果test -e$ path / $ module.shinc
then
。 $ path / $ module.shinc
return
fi
done
#恢复标准分隔符
IFS =$ saved_IFS
fi
echo$ script_name:无法找到模块$ module。
退出1
}

然后您可以导入扩展名为.shinc的文件使用以下语法



导入AModule / ModuleFile



将在SHELL_LIBRARY_PATH中搜索哪些。当您始终在全局命名空间中导入时,请记住使用正确的前缀对所有的函数和变量进行前缀,否则会导致名称冲突。我使用双下划线作为python点。



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

 #避免双重包含
如果测试$ {BashInclude__imported + defined}==defined
then
return 0
fi
BashInclude__imported = 1

面向对象编程



在bash中,你不能进行面向对象的编程,除非你建立一个相当复杂的对象分配系统(我想到这一点,这是可行的,但是是疯狂的)。
实际上,你可以做Singleton导向编程:你有一个实例的每个对象,只有一个。



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

  #避免双重包含
如果测试$ {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

#一个模块枚举常量的例子,用于mysql表,在这种情况下
只读表_GENDER_MALE =GENDER_MALE
readonly Table__GENDER_FEMALE =GENDER_FEMALE

#private:前缀为p_(bash变量不能以_开头)
p_Table__mysql_exec =#将包含执行的mysql命令

p_Table__initializ ed = 0

函数Table__init {
#@description启动具有数据库参数的模块
#@param $ 1 mysql配置文件
#@exception Table__NoException,Table__ParameterException

EXCEPTION =
EXCEPTION_MSG =
EXCEPTION_FUNC =
RESULT =

如果测试$ p_Table__初始化 - 0
然后
EXCEPTION = $ Table__AlreadyInitializedException
EXCEPTION_MSG =模块已初始化
EXCEPTION_FUNC =$ FUNCNAME
返回1
fi


本地config_file =$ 1

#是的,我知道我可以放置默认参数和其他优点,但我今天懒惰
如果测试 x $ config_file=x;然后
EXCEPTION = $ Table__ParameterException
EXCEPTION_MSG =缺少参数配置文件
EXCEPTION_FUNC =$ FUNCNAME
返回1
fi


p_Table__mysql_exec =mysql --defaults-file = $ config_file --silent --skip-column-names -e

#将模块标记为初始化
p_Table__initialized = 1

EXCEPTION = $ Table__NoException
EXCEPTION_MSG =
EXCEPTION_FUNC =
返回0

}

函数Table__getName(){
#@description获取人的名称
#@param $ 1行标识符
#@result名称

EXCEPTION =
EXCEPTION_MSG =
EXCEPTION_FUNC =
RESULT =

如果测试$ p_Table__initialized -eq 0
然后
EXCEPTION = $ Table__NotInitializedException
EXCEPTION_MSG =未初始化模块
EXCEPTION_FUNC =$ FUNCNAME
返回1
fi

id = $ 1

如果测试x $ id=x;然后
EXCEPTION = $ Table__ParameterException
EXCEPTION_MSG =缺少参数标识符
EXCEPTION_FUNC =$ FUNCNAME
返回1
fi

本地name =`$ p_Table__mysql_execSELECT name FROM table WHERE id ='$ id'`
如果test $? != 0;然后
EXCEPTION = $ Table__MySqlException
EXCEPTION_MSG =无法执行选择
EXCEPTION_FUNC =$ FUNCNAME
返回1
fi

RESULT = $ name
EXCEPTION = $ Table__NoException
EXCEPTION_MSG =
EXCEPTION_FUNC =
返回0
}

陷阱和处理信号



我发现这有用并处理例外。

  function Main__interruptHandler(){
#@description signal handler for SIGINT
echo SIGINT捕获
退出
}
函数Main__terminationHandler(){
#@description信号处理程序为SIGTERM
echoSIGTERM抓住
退出
}
函数Main__exitHandler(){
#@description程序结束信号处理程序(干净或不洁净)。
#可能是冗余调用,我们已经在main中调用了清理。
退出
}

陷阱Main__interruptHandler INT
陷阱Main__terminationHandler TERM
陷阱Main__exitHandler EXIT

函数Main__main(){
#body
}

#捕获信号并退出
陷阱退出INT TERM EXIT

主要主题$ @

提示和提示



由于某种原因不起作用,尝试重新排序代码。订单很重要,并不总是直观的。



甚至不考虑使用tcsh。它不支持功能,一般都是可怕的。



希望有帮助,虽然请注意。如果你必须使用我在这里写的那些东西,这意味着你的问题太复杂,无法用shell解决。使用另一种语言。我不得不使用它由于人为因素和遗产。


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

解决方案

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.

Invocation

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.

Function calls

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.

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

Error prone situations

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"

Modularization

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
} 

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

import "AModule/ModuleFile"

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

Object oriented programming

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.

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
}

Trapping and handling signals

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

Hints and tips

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

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

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天全站免登陆