2009년 10월 18일 일요일

쉘 스크립팅

#!/bin/sh
스크립트의 첫줄은 #!로 시작하고 다음에 공백없이 바로 인터프리터 이름이 따라온다.

스크립트는 커맨드처럼 실행할 수 있는 데, 위의 스크립트를 rotatelog라는 이름으로 저장한 뒤 $ chmod +x rotatelog 를 실행해서 실행가능하게 만들면 $ ./rotatelog 처럼 실행할 수 있다.

Variables

sh 는 변수를 사용할 수 있다. sh 변수값을 설정하기 위해서는 VAR=value 형식을 사용한다.
변수값을 사용할 때는 $VAR 또는 ${VAR} 형식을 사용한다. 후자는 변수명 뒤에 바로 다른 텍스트가 따라올 때 유용하다.

#!/bin/sh
COLOR=yellow
echo This looks $COLORish
echo This seems ${COLOR}ish
를 실행하면
This looks
This seems yellowish

sh 는 문자열타입의 변수만을 사용할 수 있지만 많은 경우 충분하다.

Local vs. environment variables

sh 변수는 로컬변수 또는 환경변수 일 수 있다. 둘다 같은 방식으로 동작하는 데 차이점은 스크립트에서 다른 프로그램을 실행할 때 발생한다. 환경변수는 서브프로세스로 전달되지만 로컬변수는 전달되지 않는다.

기본적으로, 변수는 로컬변수이다. 로컬변수를 환경변수로 바꾸기 위해서는 export VAR 를 사용한다.

#!/bin/sh
NETSCAPE_HOME=/usr/imports/libdata
CLASSPATH=$NETSCAPE_HOME/classes
export CLASSPATH $NETSCAPE_HOME/bin/netscape.bin

여기서, NETSCAPE_HOME 는 로컬변수 CLASSPATH 는 환경변수다. CLASSPATH 는 netscape.bin 프로그램에 전달된다. 쉘의 심볼테이블에서 변수를 제거하는 방법은 unset VAR 인데 존재하지 않는 변수로 만들어 버리므로 주의바란다.

정의되지 않은 변수를 사용하면 공백문자열로 대체된다.

#!/bin/sh
echo aaa $FOO bbb
echo xxx${FOO}yyy
위를 실행하면 아래와 같게 된다
aaa bbb xxxyyy

Special variables

sh 가 특별하게 다루는 변수들이 있다. 스크립트 실행시에 설정되는 변수도 있고 커맨드를 번역하는 방식에 영향을 주는 변수도 있다.

커맨드 라인 변수
가장 유용한 변수는 커맨드 라인 인수를 지칭하는 변수들이다. $1 는 (실행되는 스트립트 이름 다음에 나오는) 첫번째 인수를 가리키고, $2 는 두번째를 ..., 아홉번째 인수 $9 까지 지정할 수 있다.
9개 이상의 인수가 있을 때는 shift 커맨드를 통해서 한칸씩 이동시킬 수 있으며 이때 첫번째 인수부터 버려진다. 즉 $2 는 $1 이 되고, $8 는 $7 이 되는 식이다. $0 (zero) 는 스크립트 이름을 항상 갖는다.
가끔 모든 커맨드 라인 인수를 나열할 필요가 있는 데, 이를 위해 sh 는 $* (star) 와 $@ (at) 를 제공한다. 이 변수들은 모든 커맨드 라인 인수 $1 $2 $3…를 포함한 문자열로 확장된다. 둘간의 차이점은 이중인용부호(" ")안에 있을 때 차이가 나는 데, "$*" 는 "$1 $2 $3 ..." 으로 , "$@" 는 "$1" "$2" "$3" ... 으로 바뀌게 된다. 마지막으로 $# 는 커맨드 라인 인자 갯수를나타낸다.

기타 변수

  • $? 마지막 커맨드 실행의 종료 상태를 나타낸다. 정상 종료할 경우 0 (zero) 가 되어야 한다.
  • $- sh 가 불렸을 때의 모든 옵션 리스트를 갖는 다.
  • $$ 현재 프로세스의 PID를 갖는다.
  • $! 백그라인드에서 실행된 마지막 커맨드의 PID를 갖는다.
  • $IFS Input Field Separator의 줄임말로 sh 문자열을 단어로 분리하기 위한 분리자를 나타낸다

Quasi-variable constructs


  • ${VAR} 구조 중 의 하나일 뿐이다. 다른 것들도 살펴보자.
  • ${VAR:-expression} 만약 VAR이 설정되어 있고 null이 아니면 원래값을, 아니면 expression값으로 됨
  • ${VAR:=expression} 만약 VAR이 설정되어 있고 null이 아니면 원래값을, 아니면 VAR를 expression으로 설정하고 그 값으로 됨
  • ${VAR:?[expression]} 만약 VAR이 설정되어 있고 null이 아니면 원래값을, 아니면 expression을 stderr에 출력하고 non-zero 종료코드로 종료
  • ${VAR:+expression} 만약 VAR이 설정되어 있고 null이 아니면 공백문자열로, 아니면 expression값으로 됨
  • ${#VAR} VAR의 문자열 길이로 됨


콜론이 있는 경우에는 VAR이 설정되어 있고 null이 아닌지 테스트하고, 콜론이 없으면 설정되어 있는 지만 테스트하게 된다.

Pattern-matching

sh 는 제한된 형태의 패턴 매칭을 제공한다.

  • * 0 또는 그 이상의 문자에 매칭
  • ? 오직 하나의 문자에 매칭
  • [range] range 내의 하나의 문자에 매칭 예를 들어 [ak3] 는 a, k, or 3에 매칭; [a-z] 는 a through z에 매칭; [a-mz] 는 a through m, or z에 매칭. 대시를 포함하고 싶으면 맨 첫자리에 놓아야 한다. 예를 들어 [-p] 는 a dash or p에 매칭.


sh 는 패턴에 매칭되는 모든 파일 리스트로 대체한다. dot으로 시작하는 파일과 매칭하기 위해서는 dot을 표현해야 한다. (예를 들어,.*, /tmp/.*).
주의: MS-DOS에서는 패턴 *.* 은 모든 파일에 매칭되고 sh 에서는 dot 포함하는 파일에 매칭된다.

Quoting

Backslash
역슬래시는 특별한 의미를 갖는 문자에서 그 의미를 제거하기 위해서 사용하며, 특별한 의미를 갖지 않는 문자에 사용하면 아무 효과가 없다. 역슬래시 자신의 특별한 의미를 제거하기 위해서도 역슬래시를 사용한다. 예, \\.

Single quotes
단일인용부호는 말그대로 인용하기 위해서 사용된다.
역슬래시도 단일인용부호에서는 특별한 의미를 잃는다. 단일인용부호안에 단일인용부호를 넣는 방법은 없다.

Double quotes
이중인용부호는 공백과 대부분의 특별한 문자 의미를 유지한다. 다만, 변수나 역인용부호 표현식은 확장되어 그 값으로 치환된다.

Back quotes
역인용부호 표현식 `cmd` 은 커맨드로 인식되며 커맨드가 stdout에 출력하는 것으로 치환된다.
예를 들어
echo You are `whoami`
실행하면 아래와 같이 나타난다.
You are irfan

Built-in commands


  • { commands ; } 또는 ( commands ) 커맨드를 서브쉘에서 실행한다. 즉 하나의 커맨드가 있었던 것처럼 실행한다. { commands; }는 실제로 서브쉘을 만들지 않으므로 효율적일 수도 있다. 즉 그 안에서 변수값을 설정되면 나머지 스크립트에 나타난다.
  • : (colon) 별 의미는 없다. ${VAR:=default} 등에 쓰인다.
  • . filename 지정된 파일을 스크립트 내로 읽어 들여 스크립트와 합친다. C 프로그램의 #include와 유사하다.
  • bg [job] 또는 fg [job] bg 는 지정된 잡(지정된게 없으면 현재의 잡)을 백그라운드로 수행하고 . fg 는 지정된 잡(지정된게 없으면 현재의 잡)을 포그라운드로 재개 한다. 잡은 %number 형식으로 지정되며. jobs 커맨드로 잡을 리스트할 수 있다.
  • cd [dir] 현재 작업 디렉토리를 이동한다. 지정된 디렉토리가 없으면 홈 디렉토리로 이동한다.
  • pwd 현재 작업 디렉토리를 화면에 출력한다.
  • echo args args를 화면에 출력한다.
  • eval args args를 sh 표현식값으로 실행한다. 변수의 문자열을 바로 바꿔서 실행하고자 할때 편리하다.
  • exec command 지정된 command를 실행하고 현재의 shell로 바꿔치기 한다. 즉 exec이 성공하면 exec 커맨드 이후는 실행되지 않게된다.
  • exit [n] 현재의 쉘을 종료 코드 n 으로 종료시킨다. 종료 기본값은 0 이다.
  • kill [-sig] %job 지정된 잡에 시그널 sig 을 보낸다. sig 는 숫자나 심볼을 지원한다. kill -l 은 시스널 리스트를 나열한다. 기본 sig 는 SIGTERM (15) 이다.
  • read name… 표준 입력으로 부터 한 라인을 읽고 지정된 변수 name에 할당한다. 만약 여러개의 변수 name1, name2, name3 등등 이 지어되면 라인의 첫단어는 name1에, 두번째는 name2에, 그리고 모든 나머지 단어들은 마지막 변수에 할당된다.
  • set [+/-flag] [arg] arg가 지정되어 있지 않으면 모든 변수의 값들을 화면에 출력한다. set -x 는 x 옵션을 설정하고 set +x 설정 해제한다. set args… 는 커맨드 라인 인수들을 args에 지정한다.
  • test expression 논리식 expression 을 계산하고, 참이면 종료 코드 0으로, 거짓이면 0이 아닌 종료 코드로 리턴한다.
  • trap [command sig]… 시그널 sig 가 쉘에 보내지면 command 를 실행한다. 스크립트 실행에 인터럽트가 생길때 임시 저장 파일을 제거하는 등의 깨끗한 마무리 작업을 하고 스크립트를 종료할때 유용하다.
  • ulimit 시스템 리소스 사용에 대한 제한값을 출력하거나 설정할 때 사용한다.
  • umask [nnn] umask 를 팔진수 nnn 으로 설정한다. 인수가 없으면 현재의 umask 를 출력한다. 파일 생성시에 아주 유용하다. 그리고 파일에 대한 읽기 또는 쓰기 접근을 제한할 때 쓰이기도 한다.
  • wait [n] 백그라운드 프로세스 PID n이 종료할 때까지 기다린다. 인수가 없으면 모든 백그라운드 프로세스가 종료하기를 기다린다.


주의 : 위의 내장 커맨드는 구현체마다 다를 수 있다.

Flow control

sh는 다양한 제어흐름을 지원한다.


if

if condition ; then commands
[elif condition ; then commands]…
[else commands]
fi


하나의 if-블럭, 그리고 하나 이상의 옵션 elif-블럭, 그리고 하나의 옵션 else-블럭, 그리고 끝맺음은 fi.
if-조건이 참이면 if-블럭을 수행하고 그렇지 않으면 다른 블럭을 수행한다.
#!/bin/sh
myname=`whoami`
if [ $myname = root ]; then
echo "Welcome to FooSoft 3.0"
else
echo "You must be root to run this script"
exit 1
fi
주의 : [ 는 /bin/[ 커맨드이다. test 커맨드의 다른 이름이다.
조건이 커맨드일 수 있다. 커맨드의 종료코드가 0이면 참, 그렇지 않으면 거짓이 된다.
#!/bin/sh
user=arnie
if grep $user /etc/passwd; then
echo "$user has an account"
else
echo "$user doesn't have an account"
fi

while

while condition; do
commands
done


조건이 참인 동안 commands을 수행한다. 마찬가지로 커맨드를 조건으로 사용할 수 있다. 커맨드 종료코드가 0이면 참, 그렇지않으면 거짓이된다.
while loop은 두개의 특별 커맨드를 갖는 다: break 과 continue
break 는 while loop를 즉시 종료하고 done 다음 문장으로 건너뛴다.
continue 는 나머지 본체를 건너뛰고 맨 위 condition 평가부분부터 수행한다.

for

for var in list; do
commands
done


list 는 0개 이상의 단어들이고 var 에 한번에 하나씩 대입되고 commands 를 수행한다. For example:
#!/bin/sh
for i in foo bar baz "do be do"; do
echo "$i"
done
실행결과
foo bar baz do be do
for loop도 while loop 처럼 break 와 continue 커맨드를 사용할 수 있다.

Case

case expression in
pattern)
commands
;;
...
esac


expression는 문자열이다. 대개 변수이거나 역인용부호 커맨드이다.
pattern은 가장 먼저 매칭되는 부분만 실행된다. 매칭되는 패턴이 없는 경우를 위해서 *) 를 마지막 패턴으로 사용할 수도 있다.

IO

커맨드의 입출력은 다른 커맨드 또는 파일로 연관지을 수 있다. 기본적으로 모든 프로세스는 세개의 파일 ID를 갖는 다: 표준 입력 (0), 표준 출력 (1), 표준 에러 (2). 기본적으로 세개의 ID는 사용자 터미널과 연결되어 있다.



  • < filename 표준 입력을 파일 filename으로 연결한다. 손으로 입력하는 대신에 파일로 부터 읽을 수 있다.
  • > filename 표준 출력을 파일 filename으로 연결한다. 커맨드의 결과를 파일로 저장할 수 있다. 파일이 없으면 파일을 만들고 파일이 있으면 모두 지우고 결과를 저장한다.
  • >> filename 표준 출력을 파일 filename으로 연결한다. >과 달리, 커맨드의 결과를 파일 기존 내용 뒤에 붙인다.
  • <<word 표준 입력으로 부터 지정한 word가 나타날때까지 읽어들인다. 주의 : << 과 word 사이에 공백이 있으면 안된다.

    cat > foo.c <<EOT
    #include <stdio.h>
    main() {
    printf("Hello, world!\n");
    } EOT

    멀티라인 메세지 출력시의 활용 예
    line=13 cat <<EOT An error occurred on line $line. See page 98 of the manual for details. EOT
  • <&digit 파일 digit를 표준 입력으로 사용한다.
  • >&digit 파일 digit를 표준 출력으로 사용한다.
  • <&- 표준 입력을 닫는다
  • >&- 표준출력을 닫는다.
  • command1 | command2 파이프라인을 만들어서 command1 의 표준출력이 command2의 표준입력이 되도록한다.
  • command1 && command2 command1 를 실행하고 종료코드가 0이면(참일 경우) command2 를 실행한다.
  • command1 || command2 command1 를 실행하고 종료코드가 0이 아니면(거짓일 경우) command2 를 실행한다. 
  • command 2>&1 > filename  만약 digit이 연결구조에 앞서서 나타나면 파일 ID를 digit과 연계시킨다.  예를 들어, 표준 에러(2)를 표준 출력(1)을 통해서 filename에 결과를 저장한다. 이는 에러 메세지를 함께 출력할 때 편리한다:
    echo "Danger! Danger Will Robinson!" 1>&2


Functions

커맨드 그룹을 스크립트에서 여러번 사용해야 할 경우에는 function를 정의해서 사용하면 편리하다.
name () {
commands
}
함수를 호출할 때는 커맨드를 호출할 때와 같다, 함수에 전달된 인자는 함수 내에서 $1, $2... $9을 통해 사용된다.
name args...
다른 커맨드 처럼 I/O 연결, 역인용부호 등등을 사용할 수 있다. 함수 실행시 subshell을 만들지 않기 때문에, 함수내에서 변수를 설정하면 함수밖에서도 그 변수값을 사용할 수 있다. 함수는 return n 을 통해 종료코드를 지정할 수 있다. exit n 을 사용할 경우 전체 스크립트가 종료된다.

Useful utilities

sh의 일부분은 아니지만 스크립트에서 많이 사용되는 커맨드들이 있다

  • basename pathname 경로명의 마지막 부분을 출력한다.
  • basename /foo/bar/baz 는 baz를 출력한다.
  • dirname pathname 경로명의 마지막 부분을 제외한 부분을 출력한다.
  • dirname /foo/bar/baz 는 /foo/bar를 출력한다.
  • /bin/[ 는 /bin/test의 다른 이름이다. 논리식이 참이면 종료코드 0을, 논리식이 거짓이면 종료코드 1을 제공한다. test로 [ 을 사용할 경우, ] 가 마지막 인자가 된다. test에서 많이 사용되는 식은 아래와 같다.
  • -e filename filename이 존재하면 참값이 된다.
  • -d filename filename이 존재하고 directory면 참값이 된다.
  • -f filename filename이 존재하고 일반 파일이면 참값이 된다.
  • -h filename filename이 존재하고 심볼릭링크이면 참값이 된다.
  • -r filename filename이 존재하고 읽기 가능하면 참값이 된다.
  • -w filename filename이 존재하고 쓰기 가능하면 참값이 된다.
  • -n string string의 길이가 0이 아니면 참값이 된다.
  • -z string string의 길이가 0이면 참값이 된다.
  • string string이 공백문자열이 아니면 참값이 된다.
  • s1 = s2 문자열 s1 과 s2 같으면 참값이 된다.
  • s1 != s2 문자열 s1 과 s2 같지 않으면 참값이 된다.
  • n1 -eq n2 숫자 n1 과 n2 같으면 참값이 된다.
  • n1 -ne n2 숫자 n1 과 n2 같지 않으면 참값이 된다.
  • n1 -gt n2 숫자 n1 가 n2 보다 크면 참값이 된다.
  • n1 -ge n2 숫자 n1 가 n2 보다 크거나 같으면 참값이 된다.
  • n1 -lt n2 숫자 n1 가 n2 보다 작으면 참값이 된다.
  • n1 -le n2 숫자 n1 가 n2 보다 작거나 같으면 참값이 된다.
  • ! expression expression이 거짓이면 참값이 된다.
  • expr1 -a expr2 expr1 과 expr2 이 모두 참이면 참값이 된다..
  • expr1 -o expr2 expr1 또는 expr2 이 참이면, 참값이 된다.
  • ( expression ) expression이 참이면 참값이 된다.


echo

echo 는 인자를 표준 출력에 출력한다.
개행문자(\n)를 넣지 않으려면 BSD-식인 경우 echo -n "string"를 사용하고 SystemV-식인 경우 echo "string\c" 를 사용한다.

awk

awk는 sh 스크립트에서 주로 입력 라인을 필드별로 분리하고 이를 출력하기 위해 사용한다. 예를 들어 /etc/passwd 파일을 읽고 사용자의 name과 uid를 출력하기 위해서
awk -F : ‘{print $1, $3 }’ /etc/passwd
여기서 -F : 옵션은 콜론으로 레코드를 분리하라는 의미이다. 기본값으로는 공백을 필드 분리자로 사용한다.

sed

sed (stream editor) 는 sh 스크립트에서 주로 문자열을 치환하기 위해 사용한다. 예를 들어 표준입력으로 부터 읽어 들인 후 모든 "foo"를 "bar"로 바꾼후 표준출력에 결과를 쓰기 위해서
sed -e ’s/foo/bar/g’
마지막의 g 는 라인에 있는 모든 "foo"를 "bar"로 바꾸게 한다. 이게 없으면 첫번째 "foo"만 "bar"로 바꾼다.

tee

tee [-a] filename 는 표준 입력을 표준 출력으로 복사하고 복사본을 filename으로 저장한다. filename에 내용에 추가할려면 -a 옵션을 사용한다.

Debugging

디버깅에 쓸 수 있는 sh 실행 옵션을 소개하면 아래와 같다.
-n 옵션은 스크립트를 읽지만 수행하지는 않는 다. 구문을 체크할 때 유용하다.
-x 옵션은 command 수행 전에 모든 command를 표준에러에 출력한다.
많은 출력이 만들어 질 수 있으므로, 특정부분 실행전에 설정하고 그 부분 실행 후 설정해제할 때는 set 내장 커맨드를 사용한다.
set -x
# XXX -- What’s wrong with this code?
grep $user /etc/passwd 1>&2 > /dev/null
set +x