· KLDP.org · KLDP.net · KLDP Wiki · KLDP BBS ·
Yong Ki/Abs Guide


1. 들어가면서

쉘(shell)이라는 건 일종의 명령 해석기입니다. OS 커널과 사용자 사이의 중간자적 역할 이상으로 프로그래밍 언어로서의 기능 또한 강력하죠. 스크립트(script)라고하는 쉘 프로그램은 시스템 콜, 유틸리티, 이진파일 등을 서로 붙이고 연결해서 손쉽게 응용프로그램을 개발하도록 도와주는 툴이라고 보시면 됩니다. 사실상 모든 UNIX 명령어, 유틸리티, 툴 등이 스크립트의 재료가 될 수 있습니다만 이것으로도 충분치 않다면 쉘 내부의 조건문, 반복문 등 여러 구문들을 쓰신다면 스크립트가 좀더 강력하고 유연해지겠네요. 쉘 스크립트는 시스템 관리(admin) 작업이나 덩치 큰 프로그래밍 언어까지는 필요하지 않은 루틴 반복적인 작업에 특히 능력을 발휘합니다.

1.1. 왜 쉘 프로그래밍인가?

숙련된 시스템 관리자에게 있어서 쉘 스크립팅 지식은 필수입니다. 스크립트 작성할 일이 없다고 생각하는 관리자에게도 예외없죠. 아시다시피 리눅스 시스템이 부팅할 때 /etc/rc.d 에 있는 스크립트들을 실행해서 시스템 설정을 복원하고 서비스들을 세팅합니다. 시스템의 동태를 분석하고 수정하려면 이런 시동(startup) 스크립트들을 세부적으로 이해해야겠죠.

쉘 스크립트를 작성하는 것은 그리 어렵지 않습니다. 스크립트는 한입거리의 부분들로 짜집기 되어 있고 쉘 고유의 연산자나 옵션들[1]도 많지 않기 때문이죠. 문법도 단순하고 직관적이며 명령행 상에서 유틸리티들을 실행하고 연결하는 방식과 비슷합니다. 익혀야 할 규칙(rule)도 그리 많지 않아요.

복잡한 응용프로그램의 프로토타입(prototype)을 작성하는 데 "quick and dirty"한 방법으로 쉘 스크립트가 이용되기도 합니다. 프로젝트 개발의 첫 단계로서 부분적으로 기능을 구현하는 데 쉘스크립트를 이용하여 테스트하면서 C, C++, Java 나 Perl 등으로 최종 코딩되기 전에 루틴의 구조적 혹은 논리적 결함이 발견되죠.

쉘 스크립팅은 복잡한 프로젝트는 더 단순한 요소들로 나눠서 작업하고 다시 결합한다는 고전적인 UNIX 철학을 잘 이행하는 방법입니다. 많은 사람들이 차세대의 올인원(all-in-one) 고급 언어를 사용하는 것보다는 이러한 방식이 더 좋거나 매력적이라고 생각하죠. 모든 사람에게 모든 일이 가능하도록 기획된 그런 언어를 사용하려면 자신의 사고방식과 절차를 그 도구에 맞게 고쳐야 하는 부담은 감수해야 합니다.

다음의 작업을 할 생각이라면 쉘 스크립트는 적당하지 않습니다.
  • 자원(resource)집약적인 작업 : 특히 속도가 생명인 작업 (Sorting, Hashing 등)
  • 중요한 수치 연산 : 특히 부동 소수점, 임의의 정밀도, 복소수 계산 등 (C++ 이나 포트란을 쓰세요)
  • 플랫폼간 이식성이 요구되는 작업 (C를 쓰세요)
  • 구조화된 프로그래밍이 필요한 작업 (함수형, 변수형 등을 확인해야 하는 작업)
  • 회사의 미래가 걸려있는 중대한 응용프로그램
  • 보안이 중요시되는 상황 : 시스템의 무결성을 보장해야 하거나 해킹이나 크래킹으로부터 보호해야 하는 상황
  • 규모가 큰 파일 연산이 요구되는 작업 (Bash는 연속적인 파일 접근에 제한적입니다)
  • 다차원 배열이 필요한 작업
  • 연결리스트나 트리 같은 자료구조가 필요한 작업
  • 그래픽이나 GUI를 생성해야 하는 작업
  • 하드웨어에 직접 접근해야 하는 작업
  • 포트(port)나 소켓I/O를 필요로 하는 작업
  • 구식 코드로 라이브러리나 인터페이스를 사용해야 하는 작업
  • 소유권이 있거나 소스가 공개되어서는 안되는 프로그램
위의 작업을 해야겠으면 좀더 강력한 스크립트 언어(Perl, Tcl, Python, Ruby 등)나 컴파일 언어(C, C++, Java 등)를 쓰는게 좋습니다. 물론 이 때에도 부분 기능을 구현하는 데 쉘 스크립트로 프로토타입을 작성해보는 건 쓸모가 있겠죠.

우리가 앞으로 함께 배워볼 쉘은 Bash입니다. Bash는 "Bourne-Again shell"의 머릿글자이고 Stephen Bourne의 Bourne Shell에서 유래했죠. Bash는 사실상 거의 모든 UNIX(-like)에서 쉘스크립팅의 표준이 되었습니다. 앞으로 얘기할 대부분의 원리들은 Korn Shell[2]이나 C Shell과 같은 다른 쉘에서의 스크립팅에도 적용해 볼 수 있어요. (C Shell 프로그래밍은 추천하지 않습니다. 1993년 10월에 Tom Christiansen 이 C Shell의 타고난 결함에 대해 유즈넷에 포스트했었네요.)

이어질 내용은 쉘 스크립팅 튜토리얼(tutorial)입니다. 쉘의 다양한 특징들을 설명하기 위해 많은 예제들을 곁들였는데 몇몇은 그대로 가져다 써도 유용한 것들이죠. 예제 스크립트의 소스파일(scriptname.sh 또는 scriptname.bash)[3]을 실행하려면 먼저 chmod u+rx scriptname 과 같이 실행퍼미션을 주어야 한다는 것 잊지 마세요.

1.2. Sha-Bang(#!)으로 시작하자

가장 간단한 스크립트를 보면, 스크립트라는 것은 시스템 명령어들을 파일 안에 나열한 것에 지나지 않구나 하는 생각이 들겁니다. 그렇다고 해도 실행할 때마다 동일한 명령어들을 다시 타이핑하는 수고는 덜어주는 거죠.

예제 2-1. cleanup : /var/log 안에 있는 로그파일 청소하는 스크립트
# Cleanup
# Run as root, of course.

cd /var/log
cat /dev/null > messages
cat /dev/null > wtmp
echo "Logs cleaned up."
보시는 것처럼 그리 특별한 건 없습니다. 콘솔(console)이나 xterm 의 명령행에서 한줄 한줄 실행할 수 있는 명령어들을 묶어놓은 것 뿐이니까요. 그래도 이 작업을 할 때마다 다시 타이핑하지 않아도 되는 이점이 있으니 이 스크립트는 우리에게 유용한 툴이고, 필요에 따라 쉽게 수정하고 새로운 기능을 추가할 수 있습니다.

예제 2-2. cleanup : 개선된 clean-up 스크립트
#!/bin/bash
# Proper header for a Bash script.

# Cleanup, version 2

# Run as root, of course.
# Insert code here to print error message and exit if not root.

LOG_DIR=/var/log
# Variables are better than hard-coded values.
cd $LOG_DIR

cat /dev/null > messages
cat /dev/null > wtmp

echo "Logs cleaned up."

exit # The right and proper method of "exiting" from a script.
이제야 진짜 스크립트처럼 보이기 시작하네요. 좀더 가봅시다...

예제 2-3. cleanup : 위 스크립트를 강화하고 일반화한 버전
#!/bin/bash
# Cleanup, version 3

#  Warning:
#  -------
#  This script uses quite a number of features that will be explained
#+ later on.
#  By the time you've finished the first half of the book,
#+ there should be nothing mysterious about it.

LOG_DIR=/var/log
ROOT_UID=0     # Only users with $UID 0 have root privileges.
LINES=50       # Default number of lines saved.
E_XCD=66       # Can't change directory?
E_NOTROOT=67   # Non-root exit error.

# Run as root, of course.
if [ "$UID" -ne "$ROOT_UID" ]
then
  echo "Must be root to run this script."
  exit $E_NOTROOT
fi  

if [ -n "$1" ]
# Test if command line argument present (non-empty).
then
  lines=$1
else  
  lines=$LINES # Default, if not specified on command line.
fi  

#  Stephane Chazelas suggests the following,
#+ as a better way of checking command line arguments,
#+ but this is still a bit advanced for this stage of the tutorial.
#
#    E_WRONGARGS=65  # Non-numerical argument (bad arg format)
#
#    case "$1" in
#    ""      ) lines=50;;
#    *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;;
#    *       ) lines=$1;;
#    esac
#
#* Skip ahead to "Loops" chapter to decipher all this.

cd $LOG_DIR

if [ `pwd` != "$LOG_DIR" ]  # or   if [ "$PWD" != "$LOG_DIR" ]
                            # Not in /var/log?
then
  echo "Can't change to $LOG_DIR."
  exit $E_XCD
fi  # Doublecheck if in right directory, before messing with log file.

# far more efficient is:
#
# cd /var/log || {
#   echo "Cannot change to necessary directory." >&2
#   exit $E_XCD;
# }

tail -$lines messages > mesg.temp # Saves last section of message log file.
mv mesg.temp messages             # Becomes new log directory.

# cat /dev/null > messages
#* No longer needed, as the above method is safer.

cat /dev/null > wtmp  #  ': > wtmp' and '> wtmp'  have the same effect.
echo "Logs cleaned up."

exit 0
#  A zero return value from the script upon exit
#+ indicates success to the shell.
이 버전은 시스템 로그를 몽땅 지우지 않고 로그의 마지막 부분을 조금 남겨둡니다. 이후에 스크립트 효율을 높인다든지 다른 필요가 생긴다든지 할 때마다 여러분이 스크립트를 다듬어 가세요.

스크립트 맨 처음에 등장하는 sha-bang(#!)은 아래의 스크립트는 지정한 명령 해석기를 통해서 수행되어야 한다는 걸 시스템에 알려주는 역할을 합니다. #! 는 사실 2바이트[4] 매직넘버(magic number, 자세한 내용은 man magic을 쳐보세요)입니다. sha-bang 다음에는 곧바로 경로명이 와야 하는데, 스크립트에 쓰인 명령들을 해석할 프로그램 경로이고 쉘, 프로그래밍 언어 또는 유틸리티 등이 올 수 있어요. sha-bang라인의 다음 줄부터는 호출된 명령 해석기가 순차적으로 처리를 하고, 이때 주석(comments)은 무시합니다.[5]
#!/bin/bash
#!/usr/bin/perl
#!/usr/bin/tcl
#!/bin/sed -f
#!/usr/awk -f
위의 sha-bang라인들은 각기 다른 명령 해석기를 호출하니까 필요한 대로 하나만 선택하세요. 대부분의 상업적인 UNIX류의 운영체제에서 기본(default)으로 채택하고 있는 #!/bin/sh을 사용한다면 스크립트를 비리눅스 머신에 이식할 수 있게 됩니다. 물론 Bash 고유의 특징들은 못 쓰겠지만 스크립트는 POSIX[6] sh표준을 따르게 되는 거죠.

sha-bang라인의 경로명은 유효한 것이어야 합니다. 그렇지 않으면 에러 메세지(보통 "Command not found")만 딸랑 내보내거든요.

쉘 내부의 명령들을 사용하지 않고 일반적인 시스템 명령들만 쭉 나열한 스크립트라면(위의 첫번째 예제처럼) #!라인을 생략할 수 있습니다. 위의 두번째 예제에서는 쉘 고유의 구문인 변수 할당문(lines=50)을 사용했기 때문에 반드시 #!라인을 써야 하죠. 또 하나 명심할 것은 #!/bin/sh은 기본(default) 쉘을 실행합니다. 리눅스 머신에서는 /bin/bash가 기본 쉘이에요.

(!) 스크립트를 작성하실 때 모듈방식의 접근을 권장합니다. 반복적으로 사용되는 토막 코드(snippets)는 나중에 재사용할 수 있도록 모아두세요. 이렇게 하다보면 마침내는 멋진 라이브러리가 완성되겠죠. 예를 들어, 아래의 토막 코드는 스크립트가 실행될 때 넘겨받은 파라메터(parameter)의 수가 올바른지 검사하는 겁니다.
E_WRONG_ARGS=65
script_parameters="-a -h -m -z"
#                  -a = all, -h = help, etc.

if [ $# -ne $Number_of_expected_args ]
then
  echo "Usage: `basename $0` $script_parameters"
  # `basename $0` is the script's filename.
  exit $E_WRONG_ARGS
fi
처음에는 예제 2-1처럼 한가지 특정 작업만 수행하는 스크립트를 작성하겠지만, 나중에는 비슷하거나 조금 다른 작업에도 이용할 수 있도록 스크립트를 일반화하게 될 겁니다. 그럴려면 먼저, 반복된 코드를 함수로 묶는 것처럼, 문자 상수를 변수로 바꾸세요. (예제 2-1에서 2-2로 변화된 점을 살펴보세요.)

1.2.1. 스크립트 실행하기

스크립트를 작성했으면 sh scriptname[7]bash scriptname을 쳐서 실행해 보세요. (Not recommended is using sh chmod로 스크립트를 직접 실행가능하게 만드는 것이 더 편하겠죠.

chmod 555 scriptname (모든 사용자에게 읽기/실행 퍼미션을 부여합니다.)[8]
또는
chmod +rx scriptname (위와 같습니다.)
이나
chmod u+rx scriptname (파일 소유자에게만 읽기/실행 퍼미션을 부여합니다.)


이렇게 하면 ./scriptname[9]를 쳐서 실행할 수 있어요. 물론, 스크립트가 sha-bang라인이 있으면 지정된 명령 해석기를 호출해서 실행합니다.

스크립트의 테스트와 디버깅이 끝나고 완성도가 높아진 후 root권한으로 스크립트 파일을 /usr/local/bin으로 옮기면, 명령행에서 scriptname를 치는 것만으로도 스크립트를 실행할 수 있게 되고 다른 사용자들도 스크립트를 이용할 수 있습니다.

1.2.2. 연습 문제

  1. 시스템 관리자들은 일상적인 작업들을 스크립트로 만들어서 자동화하는 경우가 많습니다. 스크립트를 썼을 때 유용한 작업들의 예를 몇가지 들어보세요.

  2. 현재 시간과 날짜, 로그인한 사용자 목록, 시스템 가동시간(uptime)을 출력하고 로그파일을 남기는 스크립트를 작성해 보세요.

2. 기본기

2.1. 특수 문자

스크립트나 기타 곳곳에서 자주 보이는 특수문자들

#
주석(comments). # 에서부터 줄 끝까지는 주석처리 합니다. # 앞에 공백이 와도 되죠.
# This line is a comment.
echo "A comment will follow." # Comment here.
#                            ^ Note whitespace before #
            # A tab precedes this comment.
<!> 주석라인 안에 명령문을 쓰시면 마찬가지로 주식처리 되니까 다음 명령문은 새로운 줄에 쓰세요. #로 한번 시작한 주석라인을 그 줄에서 종료시킬 방법은 없습니다.


(./) 물론, echo문에서 이스케이프 처리한 #은 주석으로 보지 않습니다. 파라메터 치환식이나 숫자 상수 표현식, 패턴 매칭 연산에서 쓰이는 #도 마찬가지죠.
echo "The # here does not begin a comment."
echo 'The # here does not begin a comment.'
echo The \# here does not begin a comment.
echo The # here begins a comment.

echo ${PATH#*:}       # Parameter substitution, not a comment.
echo $(( 2#101011 ))  # Base conversion, not a comment.

# Thanks, S.C.
#를 이스케이프 처리하는 데 " ' \ 와 같은 문자가 사용됩니다.

;
명령 구분자 [semicolon]. 한줄에 둘 이상의 명령문이 가능하게 하죠.
echo hello; echo there

if [ -x "$filename" ]; then    # Note that "if" and "then" need separation.
                               # Why?
  echo "File $filename exists."; cp $filename $filename.bak
else
  echo "File $filename not found."; touch $filename
fi; echo "File test complete."


;;
case 구문에서 선택문 종결자 [double semicolon].
case "$variable" in
abc)  echo "\$variable = abc" ;;
xyz)  echo "\$variable = xyz" ;;
esac


.
"dot" command [period]. Equivalent to source (see Example 11-20). This is a bash builtin.

.
"dot", 파일명에서. 파일명이 "."으로 시작하면 숨김파일이 됩니다.
bash$ touch .hidden-file
bash$ ls -l	      
total 10
 -rw-r--r--    1 bozo      4034 Jul 18 22:04 data1.addressbook
 -rw-r--r--    1 bozo      4602 May 25 13:58 data1.addressbook.bak
 -rw-r--r--    1 bozo       877 Dec 17  2000 employment.addressbook


bash$ ls -al	      
total 14
 drwxrwxr-x    2 bozo  bozo      1024 Aug 29 20:54 ./
 drwx------   52 bozo  bozo      3072 Aug 29 20:51 ../
 -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.addressbook
 -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.addressbook.bak
 -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.addressbook
 -rw-rw-r--    1 bozo  bozo         0 Aug 29 20:54 .hidden-file
디렉토리명을 대신해 쓸 수도 있는데, "."은 현재 디렉토리를, ".."은 상위 디렉토리를 의미합니다.
bash$ pwd
/home/bozo/projects

bash$ cd .
bash$ pwd
/home/bozo/projects

bash$ cd ..
bash$ pwd
/home/bozo/
"."은 파일을 이동시키는 명령에서 목적지(디렉토리)를 대신해서 자주 쓰입니다.
bash$ cp /home/bozo/current_work/junk/* .


.
"dot", 정규표현식에서. 정규표현식에서 "."은 문자 하나에 매치됩니다.

"
partial quoting [double quote]. "STRING" preserves (from interpretation) most of the special characters within STRING. See also Chapter 5.

'
full quoting [single quote]. 'STRING' preserves all special characters within STRING. This is a stronger form of quoting than using ". See also Chapter 5.

,
콤마(comma) 연산자. 콤마 연산자는 일련의 수치 연산을 연결합니다. 연결된 연산식 모두를 평가하지만 마지막 것만 반환해요.
let "t2 = ((a = 9, 15 / 3))"  # Set "a = 9" and "t2 = 15 / 3"


\
이스케이프 문자 [backslash]. A quoting mechanism for single characters.

\X "escapes" the character X. This has the effect of "quoting" X, equivalent to 'X'. The \ may be used to quote " and ', so they are expressed literally.

See Chapter 5 for an in-depth explanation of escaped characters

/
파일명 경로 구분자 [forward slash]. /home/bozo/projects/Makefile처럼 파일명에서 구분자 역할을 하는데, 산술에서 나눗셈 연산자로도 쓰이죠.

`
command 치환 backquote, backtick. `로 감싼 command는 command의 출력물을 변수에 대입할 때 이용합니다.

:
null command [colon]. "NOP" (no op, a do-nothing operation)와 같습니다. 쉘 내장의 true와 동의어로 보시면 됩니다. ":" 은 그 자체로 쓰일 수 있고 exit값은 "true" (0)입니다.
:
echo $?   # 0
무한 루프:
while :
do
   operation-1
   operation-2
   ...
   operation-n
done

# Same as:
#    while true
#    do
#      ...
#    done
Placeholder in if/then test:
if condition
then :   # Do nothing and branch ahead
else
   take-some-action
fi
Provide a placeholder where a binary operation is expected, see Example 8-2 and default parameters.
: ${username=`whoami`}
# ${username=`whoami`}   Gives an error without the leading :
#                        unless "username" is a command or builtin...



----
  • [1] 11장. 내부 명령어와 내장함수 에서 설명합니다.
  • [2] Many of the features of ksh88, and even a few from the updated ksh93 have been merged into Bash.
  • [3] 사용자가 작성한 스크립트는 확장자를 .sh 로 하는 것이 관례입니다. /etc/rc.d 에 있는 것과 같은 시스템 스크립트는 이 명명법을 따르지 않아요.
  • [4] 4.2BSD 기반의 몇몇 UNIX에서는 4바이트 매직넘버를 쓰기도 합니다. #! /sh/bash 처럼 ! 다음에 빈칸을 넣어주어야 하죠.
  • [5] sh나 bash와 같은 명령 해석기가 스크립트 처음에 등장하는 #!라인에서부터 해석을 시작합니다만 이 줄은 #로 시작했기 때문에 주석으로 보고 무시합니다. 물론 #!라인은 명령 해석기 호출이라는 본래의 목적을 달성한 이후이기 때문에 상관은 없는거죠. 만약에 스크립트 중간에 또 #!라인이 나타난다고 해도 bash는 이를 주석으로 처리합니다.
  • [6] Portable Operating System Interface. UNIX-like OS들을 표준화하기 위한 시도로, POSIX 스펙은 Open Group 사이트에 잘 나와 있습니다.
  • [7] 주의: "sh scriptname"으로 Bash 스크립트를 실행하면 Bash 고유의 기능은 꺼집니다. 따라서 실행이 안될 수도 있어요.
  • [8] 쉘이 스크립트를 읽어야 하기 때문에 읽기 권한도 있어야 합니다.
  • [9] 그냥 scriptname만 쳐서 실행하지 않는 이유가 뭘까요? scriptname이 ($PWD)에 있는데도 안되는 이유가 뭘까요? 그것은 보안상의 이유로 $PATH에 현재 디렉토리를 포함시켜 놓지 않았기 때문입니다. 그래서 ./scriptname 과 같이 디렉토리 지정까지 해주어야 하는 거죠.



sponsored by andamiro
sponsored by cdnetworks
sponsored by HP

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2005-06-29 16:26:24
Processing time 0.0573 sec