Skip to main content

14. Hints and Tips

참고: 더 많은 힌트와 팁은 https://www.shellscript.sh/tips 에 자주 게시됩니다. 더 흥미롭고 최신의 힌트가 있는지 확인해 보세요. CGI 스크립팅과 같이 다소 학술적인 내용도 있습니다.

유닉스에는 텍스트를 조작하는 유틸리티가 가득하며, 이 튜토리얼의 이 섹션에서는 그 중 몇 가지 강력한 유틸리티에 대해 설명합니다. 여기서 중요한 점은 유닉스에서는 거의 모든 것이 텍스트라는 점입니다. 여러분이 생각할 수 있는 거의 모든 것이 텍스트 파일이나 명령줄 인터페이스(CLI)로 제어됩니다. 셸 스크립트를 사용하여 자동화할 수 없는 유일한 것은 GUI 전용 유틸리티나 기능입니다. 그리고 유닉스에서는 그 수가 그리 많지 않습니다!

*닉스를 사용하면 "모든 것이 파일이다"라는 말을 들어보셨을 것입니다. - 사실입니다.

여기에는 몇 가지 하위 섹션이 있습니다... 다음은 일반적인 조언, 힌트 및 팁입니다.

CGI Scripting

CGI 프로그래밍을 할 때 주의해야 할 몇 가지 추가 변수와 그 과정에서 얻은 몇 가지 팁이 있습니다. 셸은 CGI 프로그래밍에 적합한 언어가 아닌 것처럼 보일 수 있지만, 작성 속도가 빠르고 디버깅이 간단합니다. 따라서 CGI 스크립트를 위한 이상적인 프로토타이핑 언어이며, 단순하거나 거의 사용하지 않는 CGI 스크립트를 영구적으로 사용하기에도 좋습니다. fortune.cgi를 호출하는 cookie.cgi

Exit Codes

종료 코드는 0에서 255 사이의 숫자로, 모든 Unix 명령이 상위 프로세스로 제어권을 반환할 때 반환되는 숫자입니다. 다른 숫자를 사용할 수도 있지만, 이러한 숫자는 256 모듈로 처리되므로 종료 -10은 종료 246에 해당하고 종료 257은 종료 1에 해당합니다.

셸 스크립트 내에서 이러한 변수를 사용하여 실행된 명령의 성공 또는 실패에 따라 실행 흐름을 변경할 수 있습니다. 이에 대해서는 10장, "변수 - 2부"에서 간략하게 소개했습니다. 여기서는 종료 코드의 해석에 대해 좀 더 자세히 살펴보겠습니다.

성공(Success)은 일반적으로 종료 0으로 표시되며, 실패(Failure)는 일반적으로 0이 아닌 종료 코드로 표시됩니다. 이 값은 실패의 다양한 이유를 나타낼 수 있습니다. 예를 들어 GNU grep은 성공하면 0을, 일치하는 항목이 없으면 1을, 기타 오류(구문 오류, 존재하지 않는 입력 파일 등)가 있으면 2를 반환합니다.

오류 상태를 확인하는 세 가지 방법을 살펴보고 각 방법의 장단점에 대해 논의해 보겠습니다.

첫째, 간단한 접근 방식입니다:

#!/bin/sh
# First attempt at checking return codes 
USERNAME=`grep "^${1}:" /etc/passwd|cut -d":" -f1` 
if [ "$?" -ne "0" ]; then
  echo "Sorry, cannot find user ${1} in /etc/passwd"
  exit 1 
fi
NAME=`grep "^${1}:" /etc/passwd|cut -d":" -f5` 
HOMEDIR=`grep "^${1}:" /etc/passwd|cut -d":" -f6`

echo "USERNAME: $USERNAME"
echo "NAME: $NAME"
echo "HOMEDIR: $HOMEDIR"


이 스크립트는 /etc/passwd에 유효한 사용자 아이디를 입력하면 정상적으로 작동합니다. 그러나 잘못된 코드를 입력하면 처음에 예상했던 대로 작동하지 않고 계속 실행되어 표시만 됩니다:

USERNAME:
NAME:
HOMEDIR:

왜 그럴까요? 앞서 언급했듯이 $? 변수는 마지막으로 실행된 명령의 반환 코드로 설정됩니다. 이 경우, 그것은 cut입니다. 제가 테스트하고 문서를 읽으면서 알 수 있는 한, cut은 보고하는 것처럼 느껴질 정도로 아무런 문제가 없었습니다. 빈 문자열이 입력되면 빈 문자열인 입력의 첫 번째 필드를 반환하는 작업을 수행했습니다. 이제 어떻게 해야 할까요? 여기에 오류가 있으면 grep은 잘라내기가 아니라 오류를 보고합니다. 따라서 cut이 아닌 grep의 반환 코드를 테스트해야 합니다.

#!/bin/sh
# Second attempt at checking return codes 
grep "^${1}:" /etc/passwd > /dev/null 2>&1 
if [ "$?" -ne "0" ]; then
  echo "Sorry, cannot find user ${1} in /etc/passwd"
  exit 1 
fi
USERNAME=`grep "^${1}:" /etc/passwd|cut -d":" -f1` 
NAME=`grep "^${1}:" /etc/passwd|cut -d":" -f5` 
HOMEDIR=`grep "^${1}:" /etc/passwd|cut -d":" -f6`

echo "USERNAME: $USERNAME"
echo "NAME: $NAME"
echo "HOMEDIR: $HOMEDIR"

이렇게 하면 코드가 약간 길어지기는 하지만 문제가 해결됩니다. 이것이 교과서에 나와 있는 기본적인 방법이지만 셸 스크립트에서 오류를 검사하는 방법에 대해 알아야 할 전부는 아닙니다. 이 방법은 특정 명령 시퀀스에 가장 적합하지 않을 수도 있고 유지 관리가 불가능할 수도 있습니다. 아래에서는 두 가지 대체 접근 방식을 살펴보겠습니다.

두 번째 접근 방식으로, 코드를 4줄짜리 테스트로 가득 채우는 대신 테스트를 별도의 함수에 넣음으로써 이 문제를 어느 정도 해결할 수 있습니다:

#!/bin/sh
# A Tidier approach

check_errs()
{
  # Function. Parameter 1 is the return code
  # Para. 2 is text to display on failure.
  if [ "${1}" -ne "0" ]; then
    echo "ERROR # ${1} : ${2}"
    # as a bonus, make our script exit with the right error code. exit ${1}
  fi 
}

### main script starts here ###

grep "^${1}:" /etc/passwd > /dev/null 2>&1
check_errs $? "User ${1} not found in /etc/passwd" 
USERNAME=`grep "^${1}:" /etc/passwd|cut -d":" -f1` 
check_errs $? "Cut returned an error"
echo "USERNAME: $USERNAME"
check_errs $? "echo returned an error - very strange!"

이를 통해 3개의 개별 테스트를 작성할 필요 없이 사용자 정의 오류 메시지를 사용하여 오류를 3번 테스트할 수 있습니다. 테스트 루틴을 한 번만 작성하면 원하는 만큼 호출할 수 있으므로 프로그래머가 거의 비용을 들이지 않고도 더욱 지능적인 스크립트를 만들 수 있습니다. Perl 프로그래머라면 이 기능이 Perl의 die 명령과 유사하다는 것을 알 수 있습니다.

세 번째 접근 방식으로는 더 간단하고 조잡한 방법을 살펴보겠습니다. 저는 리눅스 커널을 구축할 때 이 방법을 사용하는 편인데, 잘 진행되면 그냥 넘어가면 되지만 문제가 발생하면 운영자가 지능적인 작업(즉, 스크립트로는 할 수 없는 작업!)을 해야 하는 간단한 자동화 방식입니다:

#!/bin/sh
cd /usr/src/linux && \
     make dep && make bzImage && make modules && \
     make modules_install && \
     cp arch/i386/boot/bzImage /boot/my-new-kernel && \ cp System.map /boot && \
     echo "Your new kernel awaits, m'lord."

이 스크립트는 리눅스 커널 빌드와 관련된 다양한 작업(시간이 꽤 걸릴 수 있음)을 실행하고 && 연산자를 사용하여 성공 여부를 확인합니다. 이 작업을 수행하려면 if가 필요합니다:

#!/bin/sh
cd /usr/src/linux
if [ "$?" -eq "0" ]; then
  make dep
    if [ "$?" -eq "0" ]; then
      make bzImage
      if [ "$?" -eq "0" ]; then
        make modules
        if [ "$?" -eq "0" ]; then
          make modules_install
          if [ "$?" -eq "0" ]; then
            cp arch/i386/boot/bzImage /boot/my-new-kernel
            if [ "$?" -eq "0" ]; then
              cp System.map /boot/
              if [ "$?" -eq "0" ]; then
                echo "Your new kernel awaits, m'lord."
              fi
            fi 
          fi
        fi 
      fi
    fi 
  fi
fi


... 개인적으로 따라가기가 꽤 어렵다고 생각합니다.

연산자 && 및 ||는 셸에서 AND 및 OR 테스트에 해당하는 연산자입니다. 위와 같이 함께 사용할 수도 있고, 또는:

#!/bin/sh
cp /foo /bar && echo Success || echo Failed

이 코드는 다음과 같이 echo합니다.

Success

또는

Failed

이것은 cp 명령이 성공하느냐 실패하느냐에 따라 다릅니다. 다음 구조를 자세히 보세요: 

command && command-to-execute-on-success \
  || command-to-execute-on-failure

각 부분에는 하나의 명령만 포함될 수 있습니다. 이 방법은 간단한 성공/실패 시나리오에는 편리하지만, 에코 명령 자체의 상태를 확인하려는 경우 어떤 &&와 ||가 어떤 명령에 적용되는지 금방 헷갈리기 쉽습니다. 또한 유지 관리도 매우 어렵습니다. 따라서 이 구조는 간단한 명령 시퀀싱에만 사용하는 것이 좋습니다.

이전 버전에서는 cp 명령의 성공 또는 실패 여부에 따라 서브셸을 사용하여 여러 명령을 실행할 수 있다고 제안한 적이 있습니다:

cp /foo /bar && \
  ( echo Success ; echo Success part II; ) || \
  ( echo Failed ; echo Failed part II )

그러나 실제로 Marcel은 이것이 제대로 작동하지 않는다는 것을 발견했습니다. 서브셸의 구문은 다음과 같습니다:

( command1 ; command2; command3 )

서브셸의 반환 코드는 최종 명령(이 예에서는 command3)의 반환 코드입니다. 이 반환 코드는 전체 명령에 영향을 미칩니다. 따라서 이 스크립트의 출력은 다음과 같습니다:

cp /foo /bar && \
  ( echo Success ; echo Success part II; /bin/false ) ||\ 
  ( echo Failed ; echo Failed part II )

cp가 성공했기 때문에 성공 부분을 실행하고, /bin/false가 실패를 반환하기 때문에 실패 부분도 실행한다는 것입니다:

Success
Success part II
Failed
Failed part II

따라서 다른 조건의 상태에 따라 여러 명령을 실행해야 하는 경우 표준 if, then, else 구문을 사용하는 것이 더 좋고 훨씬 명확합니다.

 

Simple Expect Replacement

다음은 expect를 간단하게 대체할 수 있는 방법입니다. 많은 분들이 이 작업을 수행하는 방법에 대해 문의해 주셨고, 제가 Sun Microsystems의 Explorer 유틸리티에 사용되는 힌트 및 팁에서 보여드린 예제에서 영감을 받아 기대의 매우 간단한 버전을 소개해 드립니다.