반응형

    정보

    • 업무명     : 쉘 스크립트를 사용할 때의 팁

    • 작성자     : 박진만

    • 작성일     : 2020-07-17

    • 설   명      :

    • 수정이력 :

     

     내용

    [특징]

    • 쉘 스크립트를 사용할 때의 유용한 팁 모음

     

    [활용 자료]

    • 없음

     

    [자료 처리 방안 및 활용 분석 기법]

    • 없음

     

    [사용법]

    • 작업 환경 구축

    • 소스 코드 작성 및 실행

    • 실행 결과 확인

     

    [사용 OS]

    • Linux (CentOS v7.0)

    • VMware Workstation Pro v15.5

     

    [사용 언어]

    • Bash Script

     

     소스 코드

    [코딩 스타일]

    • 여기에 쓰여져있는 내용은 어디 까지나 필자의 취향이며 거의 완전히 주관적임,

    • 그러나 경험적으로 최적이라고 판단한 스타일도 있으므로 개인적으로 추천하는 바임,

     

    [스크립트의 헤더를 만들기]

    • 최근에는별로 사용되는  곳이 없을지도 모르지만, @(#) 등과 같은 표현과 스크립트의 사용 방법, 요약 파일의 머리 부분에 주석으로 둘 수 있다.
    #!/bin/bash
    #
    # @(#) hoge.sh ver.1.0.0 2008.04.24
    #
    # Usage:
    #   hoge.sh param1 param2
    #     param1 - 매개변수1
    #     param2 - 매개변수2
    #
    # Description:
    #   hoge.shスクリプトです.
    #
    ###########################################################################
    
    # 실제 처리 부분
    echo "start..."

     

    [들여 쓰기는 공백 두개가 적절]

    • 공백 4 개가 이상적이지만, 쉘 스크립트는 파이프 연산자가 있기 때문에 공백을 2개정도만 사용하기를 권장함.

    {
      touch hoge
      ls hoge
      rm hoge
    }

     

    [ |과 <> 전후의 공간 ]

    • | 앞뒤로 하나의 공백을 비운다.

    hoge | fuga

     

    • >에 공백을 하나 비운다.

    hoge >hoge.txt
    

     

    • >> 뿐만 아니라 앞에 공백을 하나 비운다.

    hoge >>hoge.txt
    

     

    • <, <<도 >마찬가지로 전에 공백을 하나 비운다.

    hoge <hoge.txt
    hoge <<'__EOT__'

     

    • 덧붙여서, |나 >, >>그리고 <, <<앞뒤에 공백이 전혀 없어도 문법 오류는 아니다.

     

    [if와 then은 동일한 행의 세미콜론 뒤에만 공백을 둔다]

    • then을 if 뒤에 사용하면 if 와 fi이 갖추어지므로 가독성이 좋아진다. ( if과 fi대응하는 단어의 관점). 

    • then이 if바로 아래 행에 있는 경우 가독성이 떨어지고 행이 낭비된다.

    if [ "$str" = "hoge" ]; then
      echo "strはhoge."
    fi

     

    [for, while의 do와 done 의 경우]

    • if의 then경우와는 달리, do은 for, while바로 아래 줄에 기술한다.do와 done이 짝을 맞춰 정렬되기 때문에 가독성이 높아진다. ( do과 done대응하는 단어의 관점)

    for i in 1 2 3 4 5
    do
      total=`expr $total + $i`
    done
    
    
    
    while [ $total -lt 10 ]
    do
      total=`expr $total + 1`
    done

     

    [함수 정의 function은 생략한다]

    • 함수는 function Hoge()로 정의하지만,이 경우 function는 생략 가능하다. function을 생략해도 가독성이 떨어졌다고 느낄 수 없었기 때문에 원칙적으로 생략할 수 있다.

    • ※ 쉘 스크립트 초보자 function를 명시 적으로 지정되어있는이 알기 쉬운 것으로, 필자는 초보자를 배려하고 최근에는 function생략하지 않도록 하고 있다.

    • 반대로 초보자가 아니라면 원칙적 생략이 좋다고 생각한다. 어쨌든 명시하거나 생략하거나 어느 한쪽으로 통일한다.

    Hoge()
    {
      echo "함수 Hoge() 이 호출됨"
      return 0
    }

     

    [if문은 생략하지 말자]

    • 명령이 성공할 때만 다음 명령을 실행하는 &&를 사용하는 응용 프로그램은 [$ cnt -eq 10] && exit 0과 같은 if 문과 동등한 프로세스를 쉽게 설명 할 수 있다.

    • 그러나 해당 경로에 처리를 추가하려는 경우 if 문의 생략은 프로세스 분기점을 이해하기 어렵게 만드는 부정적인 영향을 미치며, 간단한 설명 이외의 장점은 거의 없다.

    if [ $cnt -eq 10 ]; then
      # 여기서 추가 처리.
      exit 0
    fi

     

    [비슷한 연속적인 파일로 리디렉션 그룹화하기]

    • 연속 된 명령의 실행 결과 출력이 동일한 파일인 경우 {}에 명령 그룹화하고, 해당 그룹의 출력을 파일로 리디렉션 할 수 있다.

    • 즉 echo "hoge" >>logfile.log과 같이 각 명령마다 리디렉션하지 않도록 한다.

    {
      echo "hoge"
      echo "fuga"
      echo "foo"
      echo "bar"
    } >>logfile.log

     

    [여러 줄의 일괄 주석]

    • 이 Tips은 실제 사용하는 경우 내부적으로는 엉뚱하게 처리되기 때문에 비추천 한다. 그러나 히어 닥의 이해를 위해, 또한 응용력 향상을 위해 읽어 두는 것을 추천한다.

    • 쉘 스크립트에서 여러 줄을 주석 처리하는 경우는 대상이되는 각 행의 선두에 #쓸 필요가있다.C 언어 등의 /* */에 해당하는 여러 행을 한 번에 코멘트하는 기능은 존재하지 않는다.

    • 하지만 null 명령과 히어 닥을 응용하여 여러 행 일괄 주석이 가능하게 된다.

    • : (null 명령)은 어떠한 처리를하지 않고 종료하는 명령 에 주석하고 싶은 부분을이 명령에 히어 닥으로하는 것으로, 해당 부분의 처리를 무효화하고 결과적으로 주석 처리 한 경우와 같은 결과를 얻을 수 있다.

    : <<'#__COMMENT_OUT__'
    echo "여기의 처리는 모두 무효가 된다."
    
    # hoge 파일은 생성되지 않는다.
    touch hoge
    
    # #  명령 치환도하지 않는다.
    dummy=`touch fuga`
    
    #__COMMENT_OUT__

     

    • 히어 닥은 종료 문자 (※)를 인용 혹은 탈출하여 히어 문서를 완전히 문자열로 취급 할 수 있다 .

    • ※ 히어 닥이 종료 부분을 식별하는 캐릭터 라인은  #__COMMENT_OUT__.

    • 따라서 변수 확장이나 역 따옴표에 의한 명령 치환도 발생하지 않는다. 즉 어떠한 처리를하지 않는 단순한 문자열로 널 명령의 표준 입력으로 전달된다.

    • 널 명령은 아무것도 처리하지 않은 명령이므로, 표준 입력 장치가 있어도이를 무시하고 종료한다. 결과적으로 어떠한 처리를하지 않는 것, 주석 한 경우와 같은 결과가 된다.

    • 또한 종료 문자의 시작 부분에 #사용하고 있기 때문에, 주석을 해제하려면, 널 명령 앞에 #를 작성 만 하면 된다 .

    • 이를 통해 히어 닥의 시작이 비활성화되면 아래쪽에있는 종료 문자는 미리 선두에 #이 붙어 있기 때문에 자동으로 코멘트가된다 .

     

    [변수 명명 규칙]

    • 필자는 다른 언어로도 그렇듯이으로 상수는 대문자 , 상수가 아닌 변수는 소문자 를 사용하도록 하고 있다. 변수 이름이 여러 단어로 이루어진 경우 "_"로 결합한 이른바 뱀 케이스를 사용하는 (eg variable_name).

    • 또한 상수는 readonly 선언을 추가하여 읽기 전용으로 할 것을 권장한다 .

    # for 루프의 변수는 비 상수이므로 문자가 됨
    for i in 1 2 3 4 5
    do
      echo $i
    done
    
    # read 명령 처리를 일시 중지
    echo -e "Enter to continue...\c"
    read dummy
    # 상수는 readonly 선언 된 대문자
    readonly MAX_COUNT=10
    readonly FILENAME="hoge.txt"
    • 일회용이 아닌 상수가 아닌 변수 이름은 a및 버튼 b등의 의미없는 이름 temp등 사용 목적이 명확하지 않은 변수 이름은 사용하지 않는다. temp켜져 사용 뻔 아무런 temp인지 모르기 때문에 가독성이 현저히 저하한다. 원래 변수는 아래에서 모든 temp이다.

     

    [파일 및 디렉토리 이름의 상수화]

    • 유지 보수성 향상을 위해 파일 및 디렉토리 이름을 스크립트 상향 변수로 정의하고 그 지정은 변수를 사용하도록하고있는 사람이 대부분이라고 생각하지만, 이 명명 규칙이 개인에 따라 제각각 있기 때문에 (원래 전혀 규칙 성이없는 변수 이름을 사용하는 사람도있다), 가독성의 저하와 그에 따른 버그로 이어지는 경우가 많다.
    • 필자는 파일 이름, 디렉터리 이름을 저장하는 변수 이름은 다음 명명 규칙을 사용하고 있다.

     

    [파일 이름]

    1. 전체 경로 (상대 경로) 스펙 대신 단순히 파일 이름 만 지정하는 경우  HOGE_FILENAME(나 HOGE_FILENM)와 같이 FILENAME 변수 이름에 사용한다.
    2. 반대로 전체 경로 (상대 경로) 지정 파일 이름을 설정하는 경우 에는 NAME이없이 단순하게 HOGE_FILE한다.
    3. readonly로 정의하고 완전히 상수로 취급 .

     

    [디렉토리 이름]

    1. 전체 경로 (상대 경로) 스펙 대신 단순히 디렉토리 이름만을 설정하는 경우  HOGE_DIRNAME(나 HOGE_DIRNM)와 같이 DIRNAME 을 변수 이름에 사용한다.
    2. 반대로 전체 경로 (상대 경로) 지정 디렉토리 이름을 설정하는 경우 에는 NAME이없이 단순하게 HOGE_DIR한다.
    3. 전체 경로 (상대 경로) 지정 디렉터리 이름 또는 단순히 디렉토리 이름만을 설정하는 경우도 끝에 / 부가 하지 않는다.
    4. readonly로 정의하고 완전히 상수로 취급 .
    • 예를 들어 이 명명 규칙을 사용하여 로그 파일과 저장 디렉토리를 변수로 정의하면 다음과 같다.

    # 디렉토리 이름
    readonly LOG_DIRNAME="hoge"
    
    # 전체 경로 지정 디렉토리 이름
    readonly LOG_DIR="/tmp/${LOG_DIRNAME}"
    
    # 파일 이름
    readonly LOG_FILENAME="fuga.log"
    
    # 전체 경로 지정 파일 이름
    readonly LOG_FILE="${LOG_DIR}/${LOG_FILENAME}"

     

    • 간단히 설명하면,

    • NAME (또는 NM)이 가진 경우 이름 만 (경로 제외)

    • NAME (또는 NM)이 붙지 않는 경우는 전체 (경로 포함 대상 등으로 그대로 사용할 수있다) 가된다.

    • 또한 각 변수를 readonly를하고있는 것으로, 처리 도중에 값을 변경하고 예상치 못한 버그가 발생하는 것을 억제 하고있다.

     

    [함수의 동작을 변수로 변경]

    • 함수의 동작을 변경하는 방법은 매개 변수가 일반적이지만 함부로 매개 변수화하면 매개 변수 처리가 복잡하고 가독성이 떨어지거나 버그로 이어진다.

    • 매개 변수화하는 정도도 섬세한 동작 변경은 변수화 함수 외부에서 정의하면 좋다. 예를 들어 다음과 같은 함수를 만들어 본다.

    Message()
    {
      _SEPARATOR=${_SEPARATOR:-"-----"}
    
      echo "$1"
      echo "$_SEPARATOR"
      echo "$2"
    
      return 0
    }
    • 이 함수를 그대로 실행 해 본다.

    $ Message hoge fuga
    hoge
    -----
    fuga
    • 다음 제 1 파라미터 및 제 2 파라미터의 경계 부분의 문자열을 변경 본다. 변경하려면 함수 호출 전에 변수 _SEPARATOR 변경 후 문자열을 설정한다.

    $ _SEPARATOR="*****"
    $ Message hoge fuga
    hoge
    *****
    fuga
    • 이러한 변수를 사용하는 함수의 동작 변경은 매개 변수 처리를 복잡하지 않고 간단하게 처리를 변경할 수있다.

    • ※ 물론 같은 처리를 함수의 출력에 sed 등을 사용 가도 상관 없지만이 방법이 더 간단하고, 변수를 한 번 정의하면 동작을 변경할 수있는 점에서 추천하고 있다.

    • 이번에 사용한 ${_SEPARATOR:-"-----"}은 변수 _SEPARATOR가 사용되지 않거나 혹은 설정 값이 하늘 ( "")이면 기본값 ( -----)를 사용하는 의미이다.

    • 또한 변수 이름 선행 밑줄은 함수 내에서 사용되는 변수임을 의미한다 (필자의 개인적인 코딩).

    • 쉘 스크립트의 변수는 기본적으로 모든 전역 변수가되기 때문에, 함수 내에서 사용되는 변수, 특히 local로 선언되지 않은 변수는 함수 외부 변수와의 충돌을 피하기 위해 변수 이름 앞에 밑줄을 붙이도록하면 좋다.

    •  local선언 한 경우는 특히 그 필요는 없지만, 함수 내에서 사용되는 변수는 변수 이름을 일률적으로 밑줄로 시작하게하는 것이 좋다 .

    • 같은 방법으로 임의의 값을 변경 가능한 값으로 할 수 있지만, 당연히 남용하면 버그로 이어질 때문에 정말 필요한 값만 변경 가능하게한다.

     

    [루프를 쓰지 마라]

    • 물론 전혀 사용하지 말라는 의미가 아니라 낭비 루프를 사용하지 마라 는 뜻이다. 예를 들어 다음과 같은 경우를 생각한다.

    for str in "hoge" "fuga"
    do
      echo "$str" >${str}.txt
    done

     

    • 가끔 이런 식으로 사용 하고있는 사람을 볼 수 있는데, 문제가되는 것은 hoge과 fuga이 더 이상 증가하는 것은 아닌데, 일부러 for 루프를 사용하고 있다는 점 이다.

    • 이런 처리 루프를 사용할 필요가 전혀 없다. 유사한 처리가 100 개, 200 개라면 이야기는 다르지만 2 ~ 5 개 정도면 그대로 적는 것이, 훨씬 가독성이 높다.

    echo "hoge" >hoge.txt
    echo "fuga" >fuga.txt

     

    • 조금 늘어나도 루프는 사용하지 않고 그대로 기술해야한다.

    echo "hoge" >hoge.txt
    echo "fuga" >fuga.txt
    echo "foo"  >foo.txt
    echo "bar"  >bar.txt

     

    [루프 중첩보다 함수]

    •  이 Tips는 비교적 큰 스크립트를 상정하고 있음.

    • 예를 들어 다음과 같은 경우를 생각하면 좋겠다.

    for i in 1 2 3
    do
      # 매우 긴 처리 1
      ・・・
    
      for j in "hoge" "fuga"
      do
        # 매우 긴 처리 2 
        ・・・
      done
    done
    • 루프 중첩는 유지 보수가 낮은 데다 가독성면에서 불리므로 함수로 대체 하는 것을 고려하면 좋다.

    • 루프 중첩 함수로 대체하면 다음과 같이된다.

    SubNestedLoop()
    {
      # 매우 긴 처리
      ・・・
      return 0
    }
    SubNestedLoop 1 "hoge"
    SubNestedLoop 2 "hoge"
    SubNestedLoop 3 "hoge"
    
    SubNestedLoop 1 "fuga"
    SubNestedLoop 2 "fuga"
    SubNestedLoop 3 "fuga"

     

    • 이렇게하면 루프 중첩에 비해 상당히 쉽게 읽을 수있을 것이다.

    • 포인트는 함수의 정의를 호출 부분의 바로 윗쪽으로 할 것이다. 이렇게함으로써 소스를 위에서 아래로 읽어 나가는 것만으로 처리를 이해하는 것이 가능하게된다 (호출 부분 바로 위가 아니라 스크립트 위쪽으로 정의하면 호출되는 곳에서 위로 스크롤하여 읽지 않으면 안된다).

    • 여러 곳에서 호출되는 함수는 스크립트의 위쪽에 정의해야하는데, 특정 장소에서만 호출되지 않은 함수는 바로 위에 정의하면 좋다.

     

    [파이프의 끝은 서브 쉘]

    • bash에서 파이프에 결합 된 그 끝이 서브 쉘에서 실행된다는 사양이 존재하고있다.

    • 다음에 예 (pipe_bug.sh)를 보자

    #!/bin/bash
    
    str="dummy"
    
    cat <<'__EOT__' >temp.$$
    hoge hoge
    fuga fuga
    foo foo
    bar bar
    __EOT__
    
    cat temp.$$ | while read line
    do
      str="$line"
    done
    
    rm -f temp.$$
    
    echo $str
    • 이 스크립트는 변수 str가 미리 문자열 "dummy"을 설정 한 후, cat 명령의 출력을 파이프로 while 루프에 부어 read 명령으로 흐르는 데이터를 한 줄씩 읽어보자. 또한 가져온 값 (행)에서 변수 str의 값을 무시하고있다.

    • 아마 많은 사람들은이 스크립트의 실행 결과는 while 루프에서 변수 str의 마지막에 설정되는 값, 즉 'bar bar "가 출력되는 것을 기대하는 것이다.

    • 하지만 실제 실행 결과는 다음과 같이 된다.

    $ ./pipe_bug.sh
    dummy

     

    • 이것은 전술 한 바와 같이, 파이프로 결합한 앞의 while 루프가 서브 쉘에서 실행되고 있기 때문이다. 서브 쉘에서 실행 됨으로써 변수 str은 while 루프 내부와 외부에서 범위가 다르기 때문이다.

    • 따라서 while 루프에서 설정된 값 (루프 변수 str) while 루프 종료와 함께 폐기되고 마지막 echo 명령의 출력 결과는 현재 쉘에서 변수 str 설정 한 값 "dummy"가된다.

    • while 루프를 빠져 나오면 서브 쉘이 종료하므로 다음과 같이 export해도 원래 변수가 소멸되기 때문에 결과는 변하지 않는다.

    • ※ pipe_bug.sh의 while 부분 만 다시 작성

    cat temp.$$ | while read line
    do
      str="$line"
      export str
    done
    • ※ export를 추가 한 경우의 실행 결과

    $ ./pipe_bug_export.sh
    dummy
    
    • export해서 while 루프의 변경은 반영되지 않을 것을 확인할 수있다.

    • 덧붙여서이 while 루프에서 exit도 무효가된다 (정확하게는 exit 명령을 실행하면 서브 쉘을 종료하기 만하면 현재 쉘이 종료한다는 의미).

    • ※ pipe_bug.sh의 while 부분 만 다시 작성

    cat temp.$$ | while read line
    do
      str="$line"
      exit
    done
    
    • ※ exit를 추가 한 경우의 실행 결과

    $ ./pipe_bug_exit.sh
    dummy
    
    • exit를 실행하면 스크립트는 종료되지 않고 마지막 echo 명령까지 실행되는 것을 확인할 수 있다.

    • 이번 같은 경우 문제를 해결하려면 파이프 대신 리디렉션을 사용하면 좋다 .

    • ※ pipe_bug.sh의 while 부분 만 다시 작성

    while read line
    do
      str="$line"
      exit
    done <temp.$$
    
    • 리디렉션이면 while 루프는 현재 쉘의 상태가되기 때문에, 예상 결과 ( "bar bar"가 출력되는)가된다.

    • 실제 실행 결과는 다음과 같다.

    $ ./pipe_bug_redirect.sh
    bar bar

     

     참고 문헌

    [논문]

    • 없음

    [보고서]

    • 없음

    [URL]

    • 없음

     

     문의사항

    [기상학/프로그래밍 언어]

    • sangho.lee.1990@gmail.com

    [해양학/천문학/빅데이터]

    • saimang0804@gmail.com

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

     

    본 블로그는 파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음

     

    반응형
    • 네이버 블러그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기