it-swarm-ko.tech

셸 스크립트의 디자인 패턴 또는 모범 사례

누구든지 Shell 스크립트 (sh, bash 등)의 모범 사례 또는 디자인 패턴에 대해 설명하는 리소스를 알고 있습니까?

161
user14437

저는 매우 복잡한 Shell 스크립트를 작성했으며 첫 번째 제안은 "하지 마십시오"입니다. 그 이유는 스크립트를 방해하거나 위험하게 만드는 작은 실수를하기가 상당히 쉽다는 것입니다.

즉, 나는 당신에게 전달할 다른 자원이 없지만 내 개인적인 경험이 있습니다. 여기에 내가 일반적으로하는 일은 과잉이지만 very 장황하지만 견고한 경향이 있습니다.

호출

스크립트가 길고 짧은 옵션을 허용하도록하십시오. 옵션을 구문 분석하는 명령 인 getopt 및 getopts가 있으므로주의하십시오. 문제가 적을수록 getopt를 사용하십시오.

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "[email protected]"`

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에서 검색됩니다. 항상 전역 네임 스페이스로 가져올 때 모든 함수와 변수 앞에 올바른 접두사를 붙여야합니다. 그렇지 않으면 이름이 충돌 할 수 있습니다. 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 "[email protected]"

힌트와 팁

어떤 이유로 인해 작동하지 않으면 코드를 다시 정렬하십시오. 순서는 중요하며 항상 직관적 인 것은 아닙니다.

tcsh로 작업하는 것도 고려하지 마십시오. 함수를 지원하지 않으며 일반적으로 끔찍합니다.

도움이되기를 바랍니다. 여기에 쓴 것들을 사용해야한다면 문제가 너무 복잡해서 셸로 해결할 수 없다는 의미입니다. 다른 언어를 사용하십시오. 나는 인적 요소와 유산으로 인해 그것을 사용해야했습니다.

215
Stefano Borini

쉘 스크립팅에 대한 많은 지혜를 얻으려면 고급 Bash 스크립팅 안내서 를 살펴보십시오. Bash뿐만 아니라.

다른 복잡한 언어를 보라고 말하는 사람들의 말을 듣지 마십시오. 셸 스크립팅이 필요에 맞는 경우 사용하십시오. 당신은 기발함이 아닌 기능성을 원합니다. 새로운 언어는 이력서에 귀중한 새로운 기술을 제공하지만,해야 할 일이 있고 이미 셸을 알고 있다면 도움이되지 않습니다.

언급했듯이 셸 스크립팅에 대한 "모범 사례"또는 "디자인 패턴"은 많지 않습니다. 용도에 따라 다른 프로그래밍 언어와 마찬가지로 지침과 바이어스가 다릅니다.

24
jtimberman

쉘 스크립트는 파일과 프로세스를 조작하도록 설계된 언어입니다. 그것이 좋은 점이지만 범용 언어는 아니기 때문에 항상 셸 스크립트에서 새 논리를 다시 작성하지 말고 기존 유틸리티에서 논리를 붙이십시오.

그 일반적인 원칙 외에는 일반적인 쉘 스크립트 실수 를 수집했습니다.

20
pixelbeat

올해 OSCON (2008)에서이 주제에 관한 훌륭한 세션이있었습니다 : http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf

13
Fhoxh

쉬움 : 쉘 스크립트 대신 python 사용) 필요없는 것을 복잡하게하지 않고 스크립트의 일부로 진화 할 수있는 능력을 보존하지 않고도 100 배 가까이의 가독성을 얻을 수 있습니다. 함수, 객체, 영속 객체 (zodb), 분산 객체 (pyro)는 거의 별도의 코드없이 제공됩니다.

9
Joao S O Bueno

사용시기를 알 수 있습니다. 빠르고 더러운 접착 명령을 함께 사용하는 것은 좋습니다. 사소한 결정, 루프 등을 결정해야하는 경우 Python, Perl 및 modularize로 이동하십시오.

쉘의 가장 큰 문제는 종종 최종 결과물이 큰 진흙 덩어리, 4,000 줄의 배쉬 및 성장처럼 보인다는 것입니다. 이제 전체 프로젝트가 그것에 의존하기 때문에 제거 할 수 없습니다. 물론 40 줄에서 시작 아름다운 배쉬.

9
Paweł Hajdan

set -e를 사용하면 오류 후에 쟁기질을하지 않아도됩니다. 리눅스가 아닌 곳에서 실행하려면 bash에 의존하지 않고 호환되도록하십시오.

8
user10392

"모범 사례"를 찾으려면 Linux 배포판 (예 : 데비안)이 init 스크립트를 작성하는 방법 (보통 /etc/init.d에 있음)을 살펴보십시오.

대부분은 "bash-isms"가 없으며 구성 설정, 라이브러리 파일 및 소스 형식이 잘 구분되어 있습니다.

내 개인 스타일은 일부 기본 변수를 정의하는 마스터 셸 스크립트를 작성한 다음 새 값을 포함 할 수있는 구성 파일을로드 ( "소스")하려고합니다.

스크립트를 더 복잡하게 만드는 경향이 있기 때문에 함수를 피하려고합니다. (펄은 그 목적을 위해 만들어졌습니다.)

스크립트가 이식 가능하도록하려면 #!/bin/sh뿐만 아니라 #!/bin/ash, #!/bin/dash 등을 사용하여 테스트하십시오. Bash 특정 코드는 곧 발견 될 것입니다.

7
Willem