2장. #! 으로 시작하기

차례
2.1. 스크립트 실행하기
2.2. 몸풀기 연습문제(Preliminary Exercises)

쉘 스크립트의 가장 간단한 예는 스크립트 파일에 시스템 명령어들을 단순히 나열해 놓는 것입니다. 이렇게 하면 적어도, 특정한 순서로 명령어를 실행시켜야 할 때 다시 치는 수고를 덜어 줍니다.

예 2-1. cleanup: /var/log 에 있는 로그 파일들을 청소하는 스크립트

# cleanup
# 루트로 실행시키세요.

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "로그를 정리했습니다."

별 다른게 없죠? 단순히 콘솔이나 한텀에서 쉽게 실행 시킬 수 있는 명령어들의 조합입니다. 명령어들을 스크립트 상에서 실행시키는 것은 이들을 다시 치지 않아도 된다는 것 이상도 이하도 아닙니다. 스크립트는 특정한 응용에 대해 쉽게 고치고 입맛에 맞게 수정하고 일반화 시킬 수 있습니다.

예 2-2. cleanup: 위 스크립트의 향상되고 일반화된 버전.

#!/bin/bash
# cleanup, version 2
# 루트로 실행시키세요.

LOG_DIR=/var/log
ROOT_UID=0     # $UID가 0인 유저만이 루트 권한을 갖습니다.
LINES=50       # 기본적으로 저장할 줄 수.
E_XCD=66       # 디렉토리를 바꿀 수 없다?
E_NOTROOT=67   # 루트가 아닐 경우의 종료 에러.


if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "이 스크립트는 루트로 실행시켜야 됩니다."
  exit $E_NOTROOT
fi  

if [ -n "$1" ]
# 명령어줄 인자가 존재하는지 테스트(non-empty).
then
  lines=$1
else  
  lines=$LINES # 명령어줄에서 주어지지 않았다면 디폴트값을 씀.
fi  


#  Stephane Chazelas 가 명령어줄 인자를 확인하는 더 좋은 방법을
#+ 제안해 주었는데 지금 단계에서는 약간 어려운 주젭니다.
#
#    E_WRONGARGS=65  # 숫자가 아닌 인자.(틀린 인자 포맷)
#
#    case "$1" in
#    ""      ) lines=50;;
#    *[!0-9]*) echo "사용법: `basename $0` 정리할파일"; exit $E_WRONGARGS;;
#    *       ) lines=$1;;
#    esac
#
#* 이것을 이해하려면 "루프" 절을 참고하세요.


cd $LOG_DIR

if [ `pwd` != "$LOG_DIR" ]  # 혹은    if [ "$PWD" != "LOG_DIR" ]
                            # /var/log 에 있지 않다?
then
  echo "$LOG_DIR 로 옮겨갈 수 없습니다."
  exit $E_XCD
fi  # 로그파일이 뒤죽박죽되기 전에 올바른 디렉토리에 있는지 두번 확인함.

# 더 좋은 방법은:
# ---
# cd /var/log || {
#   echo "필요한 디렉토리로 옮겨갈 수 없습니다." >&2
#   exit $E_XCD;
# }




tail -$lines messages > mesg.temp # message 로그 파일의 마지막 부분을 저장.
mv mesg.temp messages             # 새 로그 파일이 됨.


# cat /dev/null > messages
#* 위의 방법이 더 안전하니까 필요 없음.

cat /dev/null > wtmp  # > wtemp   라고 해도 같은 결과.
echo "로그가 정리됐습니다."

exit 0
#  스크립트 종료시에 0을 리턴하면
#+ 쉘에게 성공했다고 알려줌.

시스템 로그 전체를 날려 버릴 생각이 없을 테니까 여기서는 message 로그의 마지막 부분을 그대로 남겨 놓습니다. 앞으로는 이렇게 앞서 썼던 스크립트를 가공해서 다시 쓰는 식의 좀 더 효과적인 방법을 계속 보게 될 것입니다.

The #! 은 스크립트의 제일 앞에서 이 파일이 어떤 명령어 해석기의 명령어 집합인지를 시스템에게 알려주는 역할을 합니다. #! 은 두 바이트 [1]"매직 넘버"(magic number)로서, 실행 가능한 쉘 스크립트라는 것을 나타내는 특별한 표시자입니다(man magic을 하면 재미있는 주제의 이야기들을 볼 수 있습니다). #! 바로 뒤에 나오는 것은 경로명으로, 스크립트에 들어있는 명령어들을 해석할 프로그램의 위치를 나타내는데 그 프로그램이 쉘인지, 프로그램 언어인지, 유틸리티인지를 나타냅니다. 이 명령어 해석기가 주석은 무시하면서 스크립트의 첫 번째 줄부터 명령어들을 실행시킵니다. [2]

#!/bin/sh
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f

각각의 줄은 기본 쉘인 /bin/sh이나 기본쉘(리눅스에서는 bash), 혹은 다른 명령어 해석기를 부르고 있습니다. [3] 거의 대부분의 상업용 유닉스 변종에서 기본 본쉘인 #!/bin/sh을 쓰면 비록 Bash 만 가지고 있는 몇몇 기능들을 못 쓰게 되겠지만 리눅스가 아닌 다른 머신에 쉽게 이식(port)할 수 있게 해 줍니다(이렇게 작성된 스크립트는 POSIX [4] sh 표준을 따르게 됩니다.

"#!" 뒤에 나오는 경로는 정확해야 합니다. 만약 이를 틀리게 적는다면 스크립트를 돌렸을 때 거의 대부분 "Command not found"라는 에러 메세지만 보게 될 것입니다.

스크립트에서 내부 쉘 지시자를 안 쓰고 일반적인 시스템 명령들만 쓴다면 #!는 안 써도 괜찮습니다. 위의 2번 예제에서는 #!이 필요한데, lines=50이라는 쉘 전용 생성자를 써서 변수에 값을 대입하고 있기 때문입니다. #!/bin/sh이 리눅스에서 기본 쉘 해석기인 /bin/bash을 부르고 있는 것에 주의하십시오.

중요: 이 튜토리얼은 스크립트를 만들 때 모듈별 접근 방식을 사용하도록 유도합니다. 나중에 유용하게 쓸 수 있어 보이고 "자주 등장"(boilerplate)하는 코드 조각들을 모아 두세요. 이렇게 모아두면 나중에 아주 다양하고 매력적인 루틴들을 만들 수 있을 겁니다. 예를 들어, 다음 스크립트 조각은 스크립트 시작 부분에 두어서 원하는 수 만큼의 매개변수를 받았는지 확인하는데 쓰일 수 있습니다.
if [ $# -ne 원하는_매개변수_갯수 ]
then
  echo "사용법: `basename $0` 어쩌구저쩌구"
  exit $WRONG_ARGS
fi

주석

[1]

몇몇 유닉스 버전(4.2BSD 에 기반한)에서는 매직 넘버로 4 바이트를 받아들이기 때문에 ! 다음에 빈 칸이 필요합니다. #! /bin/sh.

[2]

해당 명령어 해석기(sh이나 bash)는 #!이 있는 줄을 처음 해석하려고 할텐데, 이 줄은 이미 명령어 해석기를 부르는 자신의 역할을 수행했고 #으로 시작하기 때문에 주석으로 올바르게 해석될 것입니다.

[3]

그래서 이렇게 멋진 트릭도 가능해 집니다.

#!/bin/rm
# 자기 자신을 지우는 스크립트.

# 이 스크립트를 실행시키면 이 파일이 지워지는 것 말고는 아무일도 안 생깁니다.

WHATEVER=65

echo "확신하건데, 이 부분은 절대 출력되지 않을 겁니다."

exit $WHATEVER  # 여기서 exit로 빠져 나가지 못하니까 뭐라고 적든 상관없겠죠.

재미있는게 또 있는데, README 파일의 시작 부분에 #!/bin/more 라고 적고 실행 퍼미션을 주면, 자기 스스로 내용을 보여주는 문서 파일이 됩니다.

[4]

Portable Operating System Interface, 유닉스류의 OS들을 위한 표준화 작업