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


167

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


2
방금 어제 밤 BASH 에서 템플릿 패턴에 대한 작은 기사를 썼습니다 . 당신의 생각을보십시오.
quickshiftin

답변:


222

나는 매우 복잡한 쉘 스크립트를 작성했고 나의 첫번째 제안은 "하지 말라"이다. 그 이유는 스크립트를 방해하거나 위험하게 만드는 작은 실수를하기가 상당히 쉽다는 것입니다.

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

기도

스크립트가 길고 짧은 옵션을 허용하도록하십시오. 옵션을 구문 분석하는 명령 인 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로 작업하는 것도 고려하지 마십시오. 함수를 지원하지 않으며 일반적으로 끔찍합니다.

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


7
와우, 나는 bash에서 과잉 살려고 생각했다. 나는 고립 된 기능을 사용하고 서브 쉘을 남용하는 경향이있다 (그래서 속도가 어떤 식 으로든 관련이있을 때 고통 받는다). 온전한 변수는 없습니다. 모두 stdout 또는 파일 출력을 통해 반환됩니다. set -u / set -e (너무 나쁜 set -e는 처음에 빨리 쓸모 없게되며 대부분의 코드는 종종 거기에 있습니다). [local something = "$ 1"으로 함수 인수 사용; shift] (리팩토링시 쉽게 재정렬 할 수 있음). 하나의 3000 라인 bash 스크립트 후 나는이 방식으로 가장 작은 스크립트조차 작성하는 경향이 있습니다 ...
Eugene

모듈화를위한 작은 수정 : 1 당신은 후에 귀환이 필요합니다. 경고가 누락되지 않도록 "$ script_absolute_dir / $ module.shinc" 2 $ SHELL_LIBRARY_PATH에서 찾기 모듈로 돌아 오기 전에 IFS = "$ saved_IFS"를 설정해야합니다.
Duff

"인적 요소"가 최악입니다. 더 좋은 것을 줄 때 기계는 당신과 싸우지 않습니다.
jeremyjjbrown

1
getoptgetopts? getopts이식성이 뛰어나 모든 POSIX 셸에서 작동합니다. 특히 질문은 구체적으로 모범 사례를 작성하는 대신 쉘 모범 사례 이므로 가능한 경우 여러 쉘을 지원하기 위해 POSIX 준수를 지원합니다.
Wimateeka

1
솔직하지만 쉘 스크립팅에 대한 모든 조언을 제공해 주셔서 감사합니다. 쉘. 다른 언어를 사용하십시오. 인적 요소와 유산 때문에 언어를 사용해야했습니다. "
dieHellste

25

Bash 뿐만 아니라 쉘 스크립팅에 대한 많은 지혜를 얻으려면 Advanced Bash-Scripting Guide 를 살펴보십시오 .

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

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


9
약간의 복잡도를 가진 스크립트의 경우 이는 모범 사례가 아닙니다. 코딩은 단순히 무언가를 얻는 것이 아닙니다. 빠르고 쉽게 구축하는 것이 중요하며 신뢰할 수 있고 재사용이 가능하며 읽기 및 유지 관리가 용이합니다 (특히 다른 사람을 위해). 셸 스크립트는 어떤 수준으로도 확장 할 수 없습니다. 보다 강력한 언어는 논리가있는 프로젝트에서 훨씬 간단합니다.
drifter

20

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

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



11

언제 사용해야하는지 아십시오. 빠르고 더러운 접착 명령을 함께 사용하면 괜찮습니다. 사소한 결정, 루프 등을 결정해야하는 경우 Python, Perl 및 모듈화를 수행하십시오 .

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


9

쉬움 : 쉘 스크립트 대신 파이썬을 사용하십시오. 불필요한 것을 복잡하게하지 않고 거의 100 배 가량의 가독성을 얻을 수 있으며, 스크립트의 일부를 거의없이 함수, 객체, 영속 객체 (zodb), 분산 객체 (pyro)로 진화시키는 기능을 보존 할 수 있습니다. 추가 코드.


7
"복잡 할 필요없이"라고 말한 다음 가치를 더할 것으로 생각되는 다양한 복잡성을 나열하면 자신과 모순되는 반면, 대부분의 경우 문제와 구현을 단순화하는 대신 못생긴 괴물로 남용됩니다.
Evgeny

3
이것은 큰 단점을 의미합니다. 파이썬이 존재하지 않는 시스템에서는 스크립트를 이식 할 수 없습니다
astropanic

1
나는 이것이 08 년에 답변되었다는 것을 알고있다 (현재는 12 년 전 이틀이다). 그러나 나중에 몇 년 동안보고있는 사람들에게는 파이썬이나 루비와 같은 언어로 되돌릴 수 없도록주의를 기울일 것입니다. . 추가 이식성이 필요한 경우, JVM을 사용할 수없는 시스템을 찾기가 어려울 때 프로그램을 Java로 작성하는 것을 고려하십시오.
Wil Moore III

@astropanic 오늘날 파이썬이있는 거의 모든 리눅스 포트
Pithikos

@Pithikos, python2 대 python3의 번거 로움과 바이올린. 요즘에는 모든 도구를 쓸 수 있었으며 더 행복해질 수 없었습니다.
astropanic

9

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


7

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

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

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

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

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


-1

또는 Joao가 말한 것과 비슷한 오래된 인용문 :

"펄을 사용하십시오. bash는 알고 싶지만 사용하지는 말아야합니다."

슬프게도 누가 그런 말을했는지 잊어 버렸습니다.

그리고 요즘에는 펄보다 파이썬을 추천합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.