programing tip

쉘 스크립트에 대한 디자인 패턴 또는 모범 사례

itbloger 2020. 6. 2. 08:22
반응형

쉘 스크립트에 대한 디자인 패턴 또는 모범 사례


쉘 스크립트 (sh, bash 등)의 모범 사례 또는 디자인 패턴에 대해 이야기하는 리소스를 아는 사람이 있습니까?


나는 매우 복잡한 쉘 스크립트를 작성했고 나의 첫번째 제안은 "하지 말라"이다. 그 이유는 스크립트를 방해하거나 위험하게 만드는 작은 실수를하기가 상당히 쉽다는 것입니다.즉, 나는 당신에게 전달할 다른 자원이 없지만 내 개인적인 경험이 있습니다. 여기에 내가 일반적으로하는 일은 과잉이지만

매우

장황 하지만 견고 합니다.

기도

스크립트가 길고 짧은 옵션을 허용하도록하십시오. 옵션을 구문 분석하는 명령 인 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

또 다른 중요한 점은 프로그램이 성공적으로 완료되면 항상 0을, 무언가 잘못되면 0이 아닌 값을 반환해야한다는 것입니다.

함수 호출

bash에서 함수를 호출 할 수 있습니다. 호출 전에 함수를 정의해야합니다. 함수는 스크립트와 비슷하며 숫자 값만 반환 할 수 있습니다. 이것은 문자열 값을 반환하기 위해 다른 전략을 개발해야 함을 의미합니다. 내 전략은 RESULT라는 변수를 사용하여 결과를 저장하고 함수가 완전히 완료되면 0을 반환하는 것입니다. 또한 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 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에서 검색됩니다. 항상 전역 네임 스페이스로 가져올 때 모든 함수와 변수 앞에 올바른 접두사를 붙여야합니다. 그렇지 않으면 이름이 충돌 할 수 있습니다. 파이썬 밑줄로 이중 밑줄을 사용합니다.또한 이것을 모듈에서 첫 번째로 넣으십시오.

# 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로 작업하는 것도 고려하지 마십시오. 함수를 지원하지 않으며 일반적으로 끔찍합니다.도움이되기를 바랍니다. 내가 여기에 쓴 것들을 사용해야한다면, 문제가 너무 복잡해서 쉘로 해결할 수 없다는 것을 의미합니다. 다른 언어를 사용하십시오. 나는 인적 요소와 유산으로 인해 그것을 사용해야했습니다.


Take a look at the Advanced Bash-Scripting Guide for a lot of wisdom on shell scripting - not just Bash, either.

Don't listen to people telling you to look at other, arguably more complex languages. If shell scripting meets your needs, use that. You want functionality, not fanciness. New languages provide valuable new skills for your resume, but that doesn't help if you have work that needs to be done and you already know shell.

As stated, there aren't a lot of "best practices" or "design patterns" for shell scripting. Different uses have different guidelines and bias - like any other programming language.


shell script is a language designed to manipulate files and processes. While it's great for that, it's not a general purpose language, so always try to glue logic from existing utilities rather than recreating new logic in shell script.

Other than that general principle I've collected some common shell script mistakes.


There was a great session at OSCON this year (2008) on just this topic: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf


Know when to use it. For quick and dirty gluing commands together it's okay. If you need to make any more than few non-trivial decisions, loops, anything, go for Python, Perl, and modularize.

The biggest problem with shell is often that end result just looks like a big ball of mud, 4000 lines of bash and growing... and you can't get rid of it because now your whole project depends on it. Of course, it started at 40 lines of beautiful bash.


Easy: use python instead of shell scripts. You get a near 100 fold increase in readablility, without having to complicate anything you don't need, and preserving the ability to evolve parts of your script into functions, objects, persistent objects (zodb), distributed objects (pyro) nearly without any extra code.


use set -e so you don't plow forward after errors. Try making it sh compatible without relying on bash if you want it to run on not-linux.


To find some "best practices", look how Linux distro's (e.g. Debian) write their init-scripts (usually found in /etc/init.d)

Most of them are without "bash-isms" and have a good separation of configuration settings, library-files and source formatting.

My personal style is to write a master-shellscript which defines some default variables, and then tries to load ("source") a configuration file which may contain new values.

I try to avoid functions since they tend to make the script more complicated. (Perl was created for that purpose.)

To make sure the script is portable, test not only with #!/bin/sh, but also use #!/bin/ash, #!/bin/dash, etc. You'll spot the Bash specific code soon enough.


Or the older quote similar to what Joao said:

"Use perl. You will want to know bash but not use it."

Sadly I forgot who said that.

And yes these days I would recommend python over perl.

참고URL : https://stackoverflow.com/questions/78497/design-patterns-or-best-practices-for-shell-scripts

반응형