쉘 스크립트 (Shell Script) - (Ⅰ)
어떤 것이 쉘 스크립트?
우리는 알게 모르게 많은 쉘 스크립트을 사용한다. 그렇다면 어떤 것이 쉘스크립트인가? 확인을 해보는 방법은 file와 grep명령을 사용해서 찾아보는 것이다. 필자의 시스템에서의 결과는 다음과 같다.
$ (cd /bin ; file * |grep “shell script”)
false : Bourne shell script text
igawk : Bourne shell script text
remadmin : Bourne shell script text
true : Bourne shell script text
$ (cd /usr/bin ; file * |grep “shell script”)
anytopnm : Bourne shell script text
apropos : Bourne shell script text
audiocompose : C shell script text
audiosend : C shell script text
autoconf : Bourne shell script text
autoheader : Bourne shell script text
autoreconf : Bourne shell script text
autoupdate : Bourne shell script text
bash2bug : Bourne shell script text
bashbug : Bourne shell script text
batch : Bourne shell script text
bdftops : Bourne shell script text
bzless : Bourne shell script text
......
whatis : Bourne shell script text
wv-incconfig : Bourne shell script text
wv-libconfig : Bourne shell script text
xbm2ikon : Bourne shell script text
xbmcut48 : Bourne shell script text
xbmsize48 : Bourne shell script text
zcmp : Bourne shell script text
zdiff : Bourne shell script text
zforce : Bourne shell script text
zgrep : Bourne shell script text
zipgrep : Bourne shell script text
zless : Bourne shell script text
zmore : Bourne shell script text
znew : Bourne shell script text
위의 명령으로 확인을 해보면 false, true, apropos, netscape, nohup, pdf2ps, ps2pdf, whatis, zgrep, zless 등 많은 쉘 스크립트가 있음을 알 수 있다.
쉘 스크립트 작성하기
스 크립트의 특징 중의 하나는 그 내용이 텍스트 형식이기 때문에 일반 편집기로 볼 수 있다는 것이다. 위에서 찾는 스크립트 중 아무 것이나 cat, vi, emacs 등으로 보아라. 여러 명령들과 스크립트 문법의 조합으로 되어 있는 것을 확인할 수 있다.
스 크립트의 가장 첫줄은 항상 #!로 시작한다. #! 다음에는 이 스크립트를 실행할 인터프리터와 그 실행 옵션을 지정한다. 쉘 스크립트에서는 #!/bin/sh, #!/bin/csh, #!/bin/bash, #!/bin/ksh, #!/bin/tcsh와 같이 쉘의 절대 경로를 써준다. 전통적인 관습에 의하여 #!만 써줄 경우에는 #!/bin/sh로 인식을 한다.
따라서 본 쉘 스크립트 작성시에는 첫 줄은 #!/bin/sh나 #!로 하면 된다.
이제 앞의 예제를 바탕으로 간단한 ‘findscript’ 쉘 스크립트를 작성하여 보자. 이 스크립트는 /bin, /usr/bin에서 쉘 스크립트를 찾아 보여준다.
$ cat findscript
#!/bin/sh#
# findscript: /bin, /usr/bin에 있는 쉘 스크립트를 찾는다.
#
(cd /bin; file * | grep “shell script”)
(cd /usr/bin; file * | grep “shell script”)
$ chmod +x findscript.sh
$ ./findscript.sh
cat 또는 다른 편집기를 이용하여 위에서 처럼 작성을 하고, 실행 가능하도록 퍼미션을 준다. 그리고서 다른 프로그램 처럼 실행을 하면 된다. 위에서 본 것과 같이 기본적으로 쉘 스크립트는 쉘에서 사용할 수 있는 명령들의 집합이다. 여기에 덧붙여 쉘에서는 쉘 스크립트에서 사용할 수 있는 나름의 문법을 제공한다.
종료 상태 (Exit status)
각 명령들은 실행을 올바로 마치고 정상적인 종료(성공/참)하거나, 실행 오류 또는 인터럽트에 의한 중단과 같은 이유로 비정상적으로 종료(실패/거짓)할 수 있다. 이 성공과 실패는 명령의 종료 상태값으로 알 수 있다. 보통 0은 성공을 나타내며, 0이 아닌 값은 실패를 나타낸다. 쉘에서는 이전에 실행한 명령이 성공을 하였는지 실패를 하였는지를 ? 변수값에 저장을 한다.
$ who | grep root
$ echo $?
1
$ who | grep hermes44
hermes44 : 0 Sep 16 11:34
hermes44 pts/0 Sep 16 11:36
hermes44 pts/1 Sep 16 14:07
$ echo $?
0
$ false; echo $?
1
$ true; echo $?
0
false 명령은 항상 1, 실패의 종료 상태를 반환하고 true 명령은 항상 0, 성공의 종료 상태를 반환한다.
쉘 스크립트에서는 exit를 이용하여 스크립트를 종료하면서 종료 상태를 반환할 수 있다. exit만을 사용하면 쉘 스크립트를 종료하고 가장 마지막에 실행한 명령의 종료 상태를 반환하고, exit <숫자>를 사용하면 종료하고 <숫자> 값을 쉘에 반환한다.
$ cat return_exit
#!/bin/sh
#
#return_exit: <인자>의 값을 종료 상태로
#반환한다.
#
if [ $# -eq 0 ]
then
exit
else
exit $1
fi
$ ./return_exit; echo $?
0
$ ./return_exit 8; echo $?
8
if 구문은 조건식에 따라서 분기를 한다.
형식은 다음과 같다.
if 조건식
then
명령들
[ elif 조건식
then
명령들 ]
[ else
명령들 ]
fi
if 의 조건식이 참이면, 즉 0을 반환하면 then 다음의 명령들을 수행하고, 거짓이면, 즉 0이 아닌 값을 반환하면 (대개 1이다) else 다음의 명령을 수행한다. elif는 if의 조건식이 거짓이 될 경우에 분기되어 다시 새로운 조건식을 검사한다. elif나 else 구문은 생략이 가능하다. fi는 if 구문의 끝은 나타낸다. (fi는 if를 거꾸로 쓴 것이다. :-))
위 의 예제 스크립트에서 if 조건문의 [ $# -eq 0 ]는 스크립트의 인자의 갯수($#)가 0과 같은지(-eq)를 검사하는 것이다. 인자가 없다면 exit를 수행하고 마지막으로 수행한 명령인 조건식의 종료 상태값 0을 반환하다. 인자가 있다면 else 구문에서 첫번째 인자 값을($1)을 종료 상태로 반환한다.
쉘 스크립트가 수행도중에 인터럽트를 받아서 비정상적으로 종료할 수도 있다. 이런 경우에는 임시로 사용한 파일을 삭제하는 것과 같은 정리 작업을 수행할 수 없게 된다. 이를 방지하기 위한 trap 구문이 있다. 형식은 다음과 같다.
trap ‘명령들’ [시그널 번호들]
인터럽트에 해당하는 시그널 번호를 써준다. 이 번호는 kill -l을 하면 알 수 있다.시그널 번호를 0을 쓸 경우는 쉘 스크립트가 정상적인 종료를 할 경우를 나타낸다.
아 래의 쉘 스크립트를 수행하면 loop 라는 글씨가 화면에 계속 출력될 것이다. 여기서 사용된 while은 무한 루프를 돌게 된다. 루프에 대해서는 뒤에서 보겠다. 종료를 하기 위해서 Ctrl+C(SIGINT,2)를 누르더라도 이는 첫번째 trap 구문에 의해서 메세지만을 출력한다.
Ctrl+\(SINQUIT,3)를 누르면 스크립트는 종료를 하게된다.
$ kill -1
1) SIGHUP
2) SIGINT
3) SIGQUIT
4) SIGILL
5) SIGTRAP
6) SIGIOT
7) SIGBUS
8) SIGFPE
9) SIGKILL
10) SIGUSR1
11) SIGSEGV
12) SIGUSR2
13) SIGPIPE
14) SIGALRM
15) SIGTERM
16) SIGCHLD
17) SIGCONT
18) SIGSTOP
19) SIGTSTP
20) SIGTTIN
21) SIGTTOU
22) SIGURG
23) SIGXCPU
24) SIGXFSZ
25) SIGVTALRM
26) SIGPROF
27) SIGWINCH
28) SIGIO
29) SIGPWR
$ cat trap_exit
#!/bin/sh
#
#trap_exit: trap 테스트
#
trap ‘echo basename $0: signal catch’ 1 2 15
trap ‘echo script exit; exit’ 3
while :
do
echo loop
done
$ ./trap_exit
명령행 인자 (Command-line argument)
쉘 스크립트에서는 최대 9개의 명령행 인자를 받을 수 있다. 그 값은 $1~$9에 저장된다. (최신 쉘에서는 그 이상의 인자를 받을 수도 있다. ${10}과 같은 형식이다.) $0에는 실행한 쉘 스크립트의 경로가 저장된다. 또한 위에서 잠깐 보았듯이 $#에는 인자의 개수가 저장이 된다.
스크립트 중에 shift를 사용하였음을 주목하라. shift는 $1 인자를 없애고 각 인자번호를 1씩 줄인다. 즉 $2는 $1, $3은 $2, $4은 $3과 같은 식으로 된다.
#!/bin/sh
#
# prarg: 세 개의 인자를 출력하다.
#
prog= basename $0
if [$# -eq 3 ]
then
echo “Script $prog path: $0”
echo “Arg1: $1”
echo “Arg2: $2”
shift
echo “Arg3: $2”
else
echo “Usage: $ $prog arg1 arg2 arg3”
exit
fi
$ ./prog es 34
Usage: $ prarg arg1 arg2 arg3
$ ./prog 28 ksl 9
Script prarg path: ./prarg
Arg1: 28
Arg2: ksl
Arg3: 9
그래서 shift한 이후에는 세번째 인자가 $2가 되는 것이다. 이때 $# 값도 1준다.
모 든 인자는 “$@”, $*에 저장이 된다. “$@”는 명령행에서 사용한 따옴표가 그래도 적용되지만, $*에서는 그 따옴표가 적용되지 않는다. 따옴표에 유무에 따라서 쉘에서 사용하는 특수문자들 - 공백, 탭, , ‘, “, ` 등 - 해석할 것인지의 여부가 결정이 된다. 이는 쉘을 사용하는 것에 관한 문제이기 때문에 더이상 자세히는 다루지 않겠다.
$$에는 현재 쉘 스크립트의 프로세스 ID가 저장이 된다. 이는 명령행 인자는 아니지만 임시 파일을 만들 때와 같은 경우에 유융하게 사용된다.
루프 (Loop)
쉘 스크립트에서 사용할 수 있는 루프는 세 가지가 있다.
while 조건식
do
명령들
done
while 루프는 조건식이 참인 동안 do~done 사이의 명령을 수행한다.
$ cat arg-while
#!/bin/sh
#
# arg-while: 모든 인자를 출력한다.
#
echo Argument number: $#
while [ $# -gt 0 ]
do
echo $1
shift
done
$ ./arg-whilel 1 2 3 4
Argument number: 4
1
2
3
4
until 조건식
do
명령들
done
until 루프는 조건식이 참이 될 때까지 do~done 사이의 명령을 수행한다.
$ cat arg-until
#!/bin/sh
#
# arg-until: 모든 인자를 출력한다.
#
echo Argument number: $#
until [ $# -eq 0 ]
do
echo $1
shift
done
$ ./arg-until 1 2 3 4
Argument number: 4
1
2
3
4
for 변수 in [문자열 목록]
do
명령들
done
$ cat arg-for
#
!/bin/sh
#
# arg-for: @ 인자앞의 인자를 출력한다.
#
echo Argument number: $#
for arg
do
if [ $arg = @ ]
then
break
fi
echo $arg
done
$ ./arg-for 1 2 @ 3 2 4 6
Argument number: 7
1
2
for 루프는 문자열 목록의 값들이 for 변수에 지정되어 do~done 사이의 명령을 수행한다. in 부분을 생략하면 명령행 인자가 사용된다. break를 사용하면 루프를 빠져나온다.
루프는 아닌지만 C에서의 switch와 비슷한 역할을 하는 case구문이 있다.
case의 형식은 다음과 같다.
case 변수 in
패턴1) 명령들 ;;
패턴2) 명령들 ;;
*) 명령들 ;;
esac
변 수값을 패턴1부터 비교해가면서 해당하는 패턴이 있으면 그에 해당하는 명령들을 수행한다. ;;은 패턴에 해당하는 명령의 끝을 나타내고 esac은 (case를 거꾸로 쓴 것이다.) case 구문의 끝을 나타낸다. 해당되는 패턴이 없다면 * 패턴의 명령을 수행하게 된다. 패턴에는 쉘에서 사용할 수 있는 와일드 카드(*,?), 정규표현식, 문자열, 변수가 올 수 있다.
$ cat check-flag
#!/bin/sh
#
# check_flag: 인자를 검사하여 flag를 켠다.
#
aflag=0
bflag=0
for arg
do
case $arg in
-a) aflag=1 ;;
-b) bflag=1 ;;
*) echo “Usage: `basename $0` [-a] [-b]” 1>&2
exit 1 ;;
esac
done
echo aflag=$aflag bflag=$bflag
$ ./check-flag
aflag=0 bflag=0
$ ./check-flag -a
aflag=1 bflag=0
$ ./check-flag -a -b
aflag=1 bflag=1
조건식
if, while, until 구문에서는 조건식이 사용된다. 조건식은 참과 거짓의 결과를 내는 구문이다. 참일 경우 조건식은 0을 반환하고, 거짓일 경우에는 1을 반환한다. 조건식의 형식은 다음과 같다.
test 식
[ 식 ]
:
식은 파일식, 문자열식, 수식의 세 종류로 크게 나눌 수 있다. :는 항상 참임을 나타낸다. while, until에서 무한 루프를 만들기 위해 사용된다.
파일식은 어떤 파일의 속성을 검사하는 것으로 다음과 같은 종류가 있다.
-b 파일 : 파일이 블럭 장치 파일이면 참
-c 파일 : 파일이 문자 장치 파일이면 참
-d 파일 : 파일이 디렉토리이면 참
-e 파일 : 파일이 존재하면 참
-f 파일 : 파일이 정규 파일이면 참
-L 파일 : 파일이 심볼릭 링크이면 참
-p 파일 : 파일이 네임드(named) 파이프이면 참
-S 파일 : 파일이 소켓이면 참
-r 파일 : 파일이 읽기 가능이면 참
-s 파일 : 파일의 크기가 0보다 크면 참
-w 파일 : 파일이 쓰기 가능이면 참
-x 파일 : 파일이 실행 가능이면 참
파일1 -nt 파일2 : 파일1이 파일2보다 새로운 파일이면 참
파일1 -ot 파일2 : 파일1이 파일2보다 오래된 파일이면 참
파일1 -ef 파일2 : 파일1과 파일2가 같은 파일이면 참
문자열식은 문자열에 대한 비교를 한다.
-z 문자열 : 문자열의 길이가 0이면 참
-n 문자열 : 문자일의 길이가 0이 아니면 참
문자열1 = 문자열2 : 문자열1과 문자열2가 같으면 참
문자열1 != 문자열2 : 문자열1과 문자열2가 다르면 참
수식은 숫자값을 비교한다. 양의 정수, 음의 정수, 0, 변수값이 올 수 있다.
값1 -eq 값2 : 값1 = 값2
값1 -ne 값2 : 값1 != 값2
값1 -lt 값2 : 값1 < 값2
값1 -le 값2 : 값1 <= 값2
값1 -gt 값2 : 값1 > 값2
값1 -ge 값2 : 값1 >= 값2
이외에도 다음과 같이 식 전체에 대한 연산이 가능하다.
! 식 : 식에 대한 부정(not)
식1 -a 식2 : 식1과 식2에 대한 논리곱(and)
식1 -o 식2 : 식1과 식2에 대한 논리합(or)
결론
이 번 기사에서는 쉘 스크립트에 대한 기본적인 사항들을 살펴보았다. 작성, 실행 방법과 기본적인 문법만으로도 간단히 개인적으로 유용하게 사용할 수 있는 스크립트를 작성할 수 있을 것이다. 다음 기사에는 각 세부 사항에 대한 자세한 설명과 기법에 대해서 알아도록 하자.
쉘 스크립트 (Shell Script) - (II)
임종균 : hermes44@secsm.org / 서울대학교 컴퓨터공학과 /리눅스 프로그래머
표준 입출력
이 전 기사에서 표준 출력(stdout)으로 출력을 하기 위해서 echo 명령을 - 외부 프로그램으로 같은 역할을 하는 echo가 있지만 여기서의 echo는 쉘의 내장(built-in)명령이다. - 사용하였다. 그렇다면 표준 입력(stdin)으로 사용자 입력을 받을 수 있는 방법은? 표준 입력을 받아 변수에 저장을 해주는 read 명령이 있다.
$ cat ./stdio
#!/bin/sh
#
# stdio: 표준 입력을 받아 표준 출력으로 표시한다.
#
echo -n “Type the filename: “
read filename
if [ -e $filename ]
then
echo $filename exists.
else
echo $filename doesn\’t exist.
fi
$ ./stdio
Type the filename: /dev/fd0
/dev/fd0 exists.
$ ./stdio
Type the filename: /dev/fd0 ./stdio
./stdio: [: /dev/fd0: binary operator expected
/dev/fd0 ./stdio doesn’t exist.
stdio 예제에서 처럼 read는 표준 입력의 한 줄을 모두 filename이라는 변수에 넣는다. (한 줄은 엔터를 입력함으로서 끝난다.) 하지만 위와 같이 할 경우, 입력값이 여러 개라면 문제가 생길 수 있다. 두 개의 파일명을 공백으로 구분하여 입력할 경우 각각이 따로 저장되는 것이 아니라 하나의 filename 변수에 들어가기 때문에 if 조건식에서 실패를 한다. 이런 경우 가장 처음에 입력된 파일명만을 받아 처리하고 나머지는 무시하려면? read 다음에 변수를 여러 개를 명시하면 된다. 표준 입력은 공백 문자로 - 스페이스와 탭 - 구분이 되고 구분된 각각의 값들은 read 다음에 명시된 변수에 차례로 채워진다. 가장 마지막 변수에는 나머지 남아있는 입력값들이 모두 다 들어간다.
$ cat ./stdio2
#!/bin/sh
#
# stdio2: 표준 입력을 받아 표준 출력으로 표시한다.
#
echo -n “Type the filename: “
read filename1 filename2 dummy
for fn in $filename1 $filename2
do
if [ -e $fn ]
then
echo $fn exists.
else
echo $fn doesn\’t exist.
fi
done
echo Ignore $dummy
$ ./stdio2
Type the filename: /dev/hda1 /etc/bashrc /bin/sh /bin/ls
/dev/hda1 exists.
/etc/bashrc exists.
Ignore /bin/sh /bin/ls
즉, stdio2의 예에서 공백으로 구분된 입력 /dev/hda1은 filename1에, /etc/bashrc는 filenam2에 /bin/sh /bin/ls는 dummy에 들어가게 되는 것이다.
암 호를 입력받을 경우와 같이 잠시 키보드 입력이 화면에 표시되지 - 이렇게 입력이 화면에 출력되는 것을 echo라고 말한다. - 않게 하려면 stty 프로그램을 이용하여 echo를 없애면 된다. 후에 다시 복원을 해야한다. 스크립트가 echo를 다시 복원하지 못하고 비정상적으로 종료할 경우를 대비하여 반드시 trap 구문을 두어야 한다.
$ cat pswd
#!/bin/sh
#
# pswd: 암호를 입력받는다.
#
trap ‘stty echo; exit’ 0 1 2 3 15
echo -n “Enter password: “
stty -echo
read password
stty echo
echo
echo “Your password is $password”
리다이렉션 (Redirection)
우리는 쉘 상에서 <, >, >>를 이용하여 표준 입출력을 리다이렉션(redirection)할 수 있다.
쉘 스크립트 상에서도 그와 같은 일이 가능하다. exec를 이용하는 것이다.
$ cat redirect
#!/bin/sh
#
# redirect: 표준 입력을 파일로 리다이렉션한다.
#
temp=/tmp/delme$$
# $$는 현재 프로세스의 id값을 넘겨준다.
echo “This is line1.
This is line2.
This is line3.” > $temp
exec < $temp
read line; echo $line
read line; echo $line
read line; echo $line
$ ./redirect
This is line1.
This is line2.
This is line3.
redirect 예제에서 exec 명령에 의해서 표준 입력이 /tmp/delme$$ 파일로 리다이렉션되어 read 명령은 그 파일에서 한 줄씩 입력을 받게 된다. 본 쉘의 리다이렉션에서 각 표준 입력, 표준 출력, 표준 에러는 파일 디스크립터 0, 1, 2에 해당한다. 그 외에 3~9까지 디스크립터를 사용할 수 있다.
$ cat fredirect
#!/bin/sh
#
# fredirct: 동적인 리다이렉션 변경
#
outfile=fredirect.out
exec 3<&1
# 표준 출력과 3번 파일 디스크립터를 일치시킨다.
# 표준 출력을 3번 파일 디스크립터에 저장해두는 역할
/bin/rm -f $outfile
while
echo -n “Enter command or CTRL-D to quit: “
read cmd
do
exec >> $outfile
# 표준 출력을 fredirect.out 파일로 리다이렉션
echo $cmd
exec >&3
# 파일로 변환된 것은 다시 표준 출력으로 복원
done
$ ./fredirect
Enter command or CTRL-D to quit: 21
Enter command or CTRL-D to quit: 234
Enter command or CTRL-D to quit: 2
Enter command or CTRL-D to quit: sjf
Enter command or CTRL-D to quit: sd
Enter command or CTRL-D to quit: kd
Enter command or CTRL-D to quit:
$ cat fredirect.out
21
234
2
sjf
sd
kd
while, until, for, if, case 구문에서 가장 마지막 줄에 - done, fi, esac 뒤에 - 리다이렉션을 지정하여 그 구문 내에서의 입출력만을 변경할 수 있다. 그 구문을 빠져나올 때에는 원래대로 복원이 된다.
$ cat bredirect
#!/bin/sh
#
# bredirct: 특정 구문에서만 리다이렉션을 한다.
#
for arg
do
echo $arg
done > bredirect.out 2> /tmp/bredirect.err
# 에러를 표준 에러로 출력하고 임시 에러 파일을 지운다.
if [ -s bredirect.err ]
then
/bin/cat /tmp/bredirect.err 1>&2
fi
/bin/rm -f /tmp/bredirect.err
$ ./bredirect 1 2 3 45
$ cat ./bredirect.out
1
2
3
45
경로명
쉘 스크립트를 이용하여서 파일을 다루다보면 경로명을 조작해야할 필요가 있다. 보통 경로명은 ‘디렉토리명/파일명’ 형식으로 되어 있다. 이렇게 사용자의 입력이나 환경 변수 등을 통해서 얻어진 경로명에서 디렉토리명와 파일명을 분리해주는 basename과 dirname이라는 프로그램이 있다.
basename <경로명>
은 경로명에서 파일명만을 넘겨준다. 이 때 파일이 실제로 존재할 필요는 없다.
basename <경로명><확장자>
은 파일명에서 지정한 확장자 부분을 없앤다.
dirname <경로명>
은 경로명에서 디렉토리 부분만을 넘겨준다.
$ basename /home/httpd/index.html
index.html
$ basename /home/httpd/index.html .html
index
$ dirname /home/httpd/index.html
/home/httpd
수식 계산 : expr
스크립트에서는 기본적인 모든 값들을 문자열로 처리하기 때문에 숫자값을 이용한 수식 계산을 하기 위해서는 expr 프로그램을 이용해야 한다.
expr 인자1 연산자 인자2 [연산자 인자3 ...]
인 자는 숫자값이나 문자열이 될 수 있고, 연산자는 수식 연산자, 관계 연산자, 논리 연산자 세 가지 종류가 있다. 인자와 연산자 사이에는 반드시 공백으로 구분되어야만 한다. expr의 결과값이 0이 아니거나 null이 아니면 종료 상태값은 0이다. 0이나 null일 경우에는 1이고 수식이 유효하지 않을 경우는 2이다.
지원하는 수식 연산자는 +, -, *, /, %가 있고, 연산 순위는 일반적인 순위를 따른다. 연산 순위 변경을 위한 괄호 또한 사용할 수 있다. *, (, )는 쉘에서 특별히 사용되기 때문에 수식에서 사용하려면 \와 함께 쓰여야 한다. 수식 연산자가 올 때의 인자는 정수값이 되어야만 한다. 수식 계산의 결과값이 출력된다.
$ expr 3 + 5 \* 2
13
$ expr \( 3 + 5 \) \* 2
16
$ echo $1
1
$ expr $i + 1
2
지 원하는 관계 연산자는 =, !=, >, >=, <,< = 이 있고, >와 <는 \과 함께 사용되어야 한다. 비교한 결과가 참이면 1을 출력하고, 거짓이면 0을 출력하다. 이 때 인자는 정수, 실수, 문자열이 모두 다 올 수 있다.
$ echo $USER
hermes44
$ expr $USER = hermes44
1
$ expr 3 \> 5
0
$ expr 4.5 \<= 4.5
1
논리 연산자는 3가지가 있다. |,& , :이다. 각 OR, AND, 정규식 검색을 나타내며 |와 &는 와 함께 사용되어야 한다.
·|는 OR이다. 인자1과 인자2가 둘 다 0이 아니면 인자1을 출력하고 그렇지 않으면 0을 출력한다.
·&는 AND이다. 인자 1이 0이 아니면 인자 1을 출력하고, 그렇지 않으면 인자 2를 출력한다.
·: 는 정규식(regular expression) 검색이다. 인자1에는 어떤 문자열이 오고, 인자2에는 정규식 이 온다. 인자1에서 인자2로 주어진 정규식에 해당하는 패턴을 찾는다.인자2를 \(와 \)로 괄호를 씌우면 패턴에 해당하는 인자1의 부분을 출력하고 괄호가 없을 때에는 패턴이 일치하는 회수를 출력한다.(정규식의 형식에 대한 사항은 이 기사의 범위를 넘어가기 때문에 생략한다).
$ echo $1
1
$ expr $i \> 5 \| $i + 1
2
$ expr $i \< 5 \& $i + 1
1
$ export d=`date`; echo $d
Mon Oct 10 00:53:04 KST 1999
$ expr “$d” : ‘.*’
28
$ expr “$d” : ‘\(.*\)’
Mon Oct 18 00:53:04 KST 1999
$ expr “$d” : ‘[a-zA-Z]*’
3
$ expr “$d” : ‘\([a-zA-Z]*\)’
Mon
여러 프로그램을 하나의 스크립트로 많은 스크립트를 작성을 하다 보면 다음과 같은 경우가 발생할 수 있다.
· 같은 동작을 하는 다른 이름의 프로그램들. 예를 들어 문서 편집기인 ex, vi, view는 이름은 다르지만 다 같은 프로그램이다. 하지만 어떤 이름으로 실행하느냐에 따라 동작이 조금씩 다르다. ex는 줄 편집기 모드로 시작하고, vi는 화면 편집기 모드로 시작한다. view는 읽기 전용 모드로 파일을 연다.
·다른 프로그램이지만 공통되는 부분이 많다. 예를 들어 jdk패키지의 들어있는 java, javac, javadoc, jar, jdb, appletview 등은 모두 .javawrapper 스크립트에 링크되어 있다. .javawrapper는 자바 환경을 설정을 하고 해당하는 프로그램을 실행시켜 준다.
이와 같은 경우에 공통되는 부분을 하나로 묶어 주게 된다면 디스크 공간을 절약할 수 있을 뿐만 아니라, 코딩에 드는 노력도 줄여줄 수 있고, 각 공통되는 부분에 대한 일관성을 유지할 수가 있다. 쉘 스크립트에서는 공통되는 부분을 처리하고 실행한 프로그램의 이름을 살펴보아 각각에 해당하는 처리를 한다.
공통 부분에 대한 핵심 스크립트를 만든다. 각각의 프로그램은 핵심 스크립트에 대한 링크로 만든다. 그 스크립트에서는 공통된 부분을 처리하고 프로그램의 이름, $0를 보고서 각 프로그램에 대한 작업을 수행한다.
$ cat sc_core
#!/bin/sh
#
# sc_core: 공통되는 부분을 처리하는 핵심부분
#
echo Setup the Environments and Flags
case “$0” in
*sc1)
echo Excute sc1 ;;
*sc2)
echo Excute sc2 ;;
*)
echo Invalid commnad !
exit 1 ;;
esac
$ ln -s sc_core sc1
$ ln -s sc_core sc2
$ ./sc1
Setup the Environments and Flags
Excute sc1
$ ./sc2
Setup the Environments and Flags
Excute sc2
실행한 파일이름을 찾을 때 sc_core 예제에서와 같이 ‘*파일명’ 패턴을 이용하는 방법도 있고 basename 프로그램을 사용할 수도 있다.
$ cat jc_core
#!/bin/sh
#
# jc_core: basename을 이용한 또 다른 방법
#
jcpath=’/usr/local/bin’
jcprogram=`basename $0`
echo Setup the Environments and Flags
case “$jcprogram” in
jcc)
echo Excute jcc ;;
jdb)
echo Excute jdb ;;
*)
echo “$jcpath/$jcprogram” ;;
esac
$ ln -s jc_core jcc
$ ln -s jc_core jdb
$ ln -s jc_core jzip
$ ./jcc
Setup the Environments and Flags
Excute jcc
$ ./jdb
Setup the Environments and Flags
Excute jdb
$ ./jzip
Setup the Environments and Flags
/usr/local/bin/jzip
결론
스크립트를 작성하는 일이 다 끝이 났다면 이제 마지막으로 남은 것은 스크립트의 이름을 정하는 것이다. 어떤 이름을 짓던지 상관은 없지만 한 가지 주의할 것은 이미 있는 프로그램의 이름을 사용해서는 안 된다는 것이다. 우리가 정한 이름이 이미 존재하는 지는 다음과 같은 방법으로 찾아보면 된다.
man 1 <스크립트 이름>
which <스크립트 이름>
whereis <스크립트 이름>
alias <스크립트 이름>
type <스크립트 이름>
모든 가능성을 검사해 보고 없는 이름으로 정한다.
이 렇게 스크립트의 작성이 끝이 났다. 이제는 반복적인 일을 간단히 처리해주는 나름대로의 스크립트는 작성할 수 있을 것이다. 부족한 부분이 있기는 하지만 그런 부분들은 많은 시도를 통해서 채득할 수 있을 것이라 생각한다. 또한 쉘 스크립트를 작성하기 위해서는 사용하는 쉘에 대한 이해도 필요하다. 다음 기사에서는 작성한 스크립트를 디버깅하는 방법을 알아보자.
·
쉘 스크립트 (Shell Script) - (III)
임 종 균 : hermes44@secsm.org
서울대학교 컴퓨터공학과/리눅스 프로그래머
디버깅
이 전 기사를 통해 우리는 간단한 정도의 쉘 스크립트는 작성할 수 있게 되었다. 이제는 다음 단계로 작성한 쉘 스크립트를 디버깅하는 방법에 대해서 다루도록 하겠다. 프로그램을 한 번에 에러없이 작성할 수 있는 천재 프로그래머가 아닌 이상 디버깅이란 코드의 안정성과 완벽함을 높이는 필수적인 작업이다. 또한 대부분의 경우, 코드를 작성하는 것보다도 더 많은 시간이 걸리는 일이 디버깅이다. 보통 리눅스 개발 환경에서 C로 프로그램을 작성하였다면 디버깅 도구로 gdb를 사용할 것이다. 하지만 gdb이전에 가장 좋은 디버깅 방법은 코드 곳곳에 printf를 추가하여 실행 결과를 보고 문제점을 파악하는 것일 것이다. 쉘 스크립트에도 별다른 디버거가 존재하지 않기 때문에 echo를 사용하여 디버깅하는 것이 일반적이다. 하지만 다음에서는 그 방법 이외에 사용할 수 있는 방법을 알아보도록 하겠다.
쉘 옵션 기능
쉘 자체적으로 디버깅을 위해 사용되는 명령행 옵션이 있다. -v는 쉘이 스크립트를 수행하기 위해서 읽은 부분을 화면에 출력해준다. -x는 읽어들인 부분 중에서 실제 실행을 한 부분을 보여준다. 이 결과는 stderr로 출력이 되면 다음의 세 가지 방법 중 하나로 사용할 수 있다.
·쉘 스크립트의 첫 줄에 다음과 같이 옵션을 지정한다.
#!/bin/sh -xv
·소스를 수정하지 않고 한 번만 실행하려면 다음과 같이 스크립트를 실행한다.
$ sh -xv [스크립트이름]
·스크립트의 전체가 아닌 일부분에서만 이 기능을 사용하려면 사용하려고 하는 부분 앞에 다음을 추가한다.
set -xv
·이 기능을 없애기 위해서 필요한 부분 이후에 다음을 추가한다.
set +xv
첫 번 기사에서 다루었던 예제를 다시 보면서 사용법을 설명하도록 하겠다.
보면 쉽게 알 수 있듯이 prarg 스크립트는 세 인자를 입력받아서 출력하는 매우 간단한 스크립트이다. 두번째 방법을 이용하여서 실행해 보자.
$ cat ./prarg
#!/bin/sh
#
# prarg: 인자를 출력하다.
#
prog=`basename $0`
if [ $# -eq 3 ]
then
echo “Script $prog path: $0”
echo “Arg1: $1”
echo “Arg2: $2”
shift
echo “Arg3: $2”
else
echo “Usage: $ $prog arg1 arg2 arg3”
exit 1
fi
$ ./prarg 1 2 3
Script prarg path: ./prarg
Arg1: 1
Arg2: 2
Arg3: 3
$ sh -x ./prarg 1 2 3
++ basename ./prarg
+ prog=prarg
+ [ 3 -eq 3 ]
+ echo Script prarg path: ./prarg
Script prarg path: ./prarg
+ echo Arg1: 1
Arg1: 1
+ echo Arg2: 2
Arg2: 2
+ shift
+ echo Arg3: 3
Arg3: 3
$ sh -xv ./prarg 1 2 3
#!/bin/sh
#
# prarg: 인자를 출력하다.
#
prog=`basename $0`
basename $0
++ basename ./prarg
+ prog=prarg
if [ $# -eq 3 ]
then
echo “Script $prog path: $0”
echo “Arg1: $1”
echo “Arg2: $2”
shift
echo “Arg3: $2”
else
echo “Usage: $ $prog arg1 arg2 arg3”
exit 1
fi
+ [ 3 -eq 3 ]
+ echo Script prarg path: ./prarg
Script prarg path: ./prarg
+ echo Arg1: 1
Arg1: 1
+ echo Arg2: 2
Arg2: 2
+ shift
+ echo Arg3: 3
Arg3: 3
-x 옵션만으로 실행을 할 경우에는 스크립트에서 실행하는 부분만을 보여준다. + 은 쉘이 실행하는 부분, ++는 다른 외부 프로그램이 (이 외부 프로그램은 쉘이 실행한다.) 실행하는 부분을 의미한다. -v 옵션과 같이 실행하면 스크립트를 읽은 부분을 보여준다. if, for, while, case 등의 블럭들은 한 번에 블럭 전체를 읽어서 수행하기 때문에 위의 예제에서와 같은 결과가 나온다. 출력시에는 스크립트의 실행 결과와 디버깅을 위한 출력이 같은 화면으로 보이는 듯하지만 전자는 stdout으로, 후자는 stderr로 표시되는 것이다. 따라서 이 결과를 파일로 리다이렉션하기 위해서는 다음과 같이 실행해야 한다.
$ sh -xv [스크립트이름]> [파일명] 2>&1
2>&1은 stderr(2)의 결과를 stdout(1)과 같은 곳으로 리다이렉션하는 것이다.
또 한 실행문의 결과를 검사하기 위한 -e 옵션이 있다. -e는 -x에 의해 출력되는 실행문들의 실행 결과가 0이 아닌 값 즉, 실패를 리턴하였을 경우 쉘 스크립트를 종료한다. 하지만 while, until, if문에서 사용하는 조건식이나 &&, ||, !이 포함되있는 실행문에서는 -e가 효과 없다.
사용법은 -x, -v를 사용할 때와 같다.
$ cat ./stdio3
#!/bin/sh
#
# stdio3: 표준 입력을 받아 표준 출력으로 표시한다.
#
filename=`line`
if [ -e $filename ]
then
echo $filename exists.
else
echo $filename doesn\’t exist.
fi
$ ./stdio3
./stdio3: line: command not found
exists.
$ sh -x ./stdio3
++ line
./stdio3: line: command not found
+ filename=
+ [ -e ]
+ echo exists.
exists.
$ sh -ev ./stdio3
++ line
./stdio3: line: command not found
+ filename=
-e 옵션을 주고 실행한 경우 if 이전의 실행문이 실패를 하여 if 가 실행되기 전에 종료함을 알 수 있다.
조건식
조건식을 일반적인 컴파일 언어에서와 비슷하게 사용할 경우 생각지도 않는 에러가 발생하는 경우가 있다.
수식 검사 조건식에서 검사하려는 변수가 존재하지 않거나 값이 설정되지 않았을 경우 에러가 발생한다. 즉, 다음과 같은 조건식에서
if [ “$count” -eq 3 ]
count 변수가 존재하지 않거나 값이 설정되어 있지 않으면 다음과 같이 해석이 되어
if [ “” -eq 3 ]
아래와 같은 에러가 발생한다.
[: integer expression expected before -eq
이를 방지하기 위해서는 위의 수식을 다음과 같이 바꾼다.
if [ “0$count” -eq 3 ]
이 때는 위와 같은 에러 상황일 때 0을 리턴하게 되고 count 변수값이 3일 경우에는 03을 리턴하게 되어 에러없이 조건식이 수행이 된다.
문 자열 검사 조건식에서는 검사하려는 문자열이 조건식에서 사용하는 키워드일 경우 에러가 발생한다. 즉, 그 문자열이 -eq, -ne, -gt, -r, -w, -z, -f등의 것들일 때이다. 이 경우에는 비교하려는 문자열 앞에 적당한 문자를 추가하여 앞에서와 같은 것들이 조건식에서 나오지 않도록 한다.
if [ “$op” = -gt ]
를 다음과 같이 변경을 한다.
if [ “Z$op” = Z-gt ]
앞 의 -e 옵션을 설명할 때 보았던 것과 같이 쉘 스크립트에서 실행문이 실패한다고 해서 스크립트가 중단되지는 않는다. (중단시키려면 -e 옵션을 주어야 한다.) 하지만 중단이 된다는 잘못된 착각을 하여 스크립트를 작성하는 경우가 있다. 다음의 경우에는 그것이 치명적인 결과를 나을 수 있다.
$ cat clean
#!/bin/sh
tempdir=’/tmp /usr/tmp /var/tmp’
for dir in $tempdir
do
cd $dir
rm -rf *
done
echo “Cleanup done !”
‘cd $dir’이 실패할 경우에도 다음 실행문인 ‘rm -rf *’은 실행이 된다. cd가 실패를 한다면 생각지도 않는 디렉토리의 내용을 다 지워버리는 결과를 나을 수 있다. 이런 문제를 방지하기 위해 이와같이 다음에 치명적인 실행문이 올 경우 이전의 실행문이 실패하면 실행하지 않도록 한다. || 조건을 이용 cd 실행문을 바꾼다.
cd $dir || exit
이 경우 || 앞의 cd가 실패할 경우 exit가 실행되어 더 이상 진행되지 않도록 한다. cd가 성공한 경우에는 || 조건식은 항상 참이 되기 때문에 || 우측의 exit는 실행되지 않는다. 위의 예제에서는 exit 대신 continue를 써도 무방하다.
변수값 확인하기
쉘 스크립트에 대화식으로 변수값을 확인할 수 있는 방법이 있다. 그런 기능을 하는 코드를 쉘 스크립트 중간에 삽입하는 방법이다.
while echo -n “Enter a variable name you want to know; \
just ENTER to quit: “
read var
do
case “$var” in
“”) break ;;
*) eval echo \$$var ;;
esac
done
이 트레이스(trace) 코드 이전에 사용된 변수의 값을 확인해 볼 수가 있다.
$ cat trace
#!/bin/sh
#
# trace: 변수값을 보여주는 트레이스 코드를 사용해 본다.
#
dir=`dirname $0`
file=`basename $0`
count=0
for arg in $*
do
count=`expr $count + 1`
# Trace code
while echo -n “Enter a variable name you want to know; just ENTER to quit: “
read var
do
case “$var” in
“”) break ;;
*) eval echo \$$var ;;
esac
done
done
$ ./trace a b cde fg h
Enter a variable name you want to know; just ENTER to quit: dir
.
Enter a variable name you want to know; just ENTER to quit: file
trace
Enter a variable name you want to know; just ENTER to quit: count
1
Enter a variable name you want to know; just ENTER to quit: arg
a
Enter a variable name you want to know; just ENTER to quit: [ENTER]
Enter a variable name you want to know; just ENTER to quit: count
2
Enter a variable name you want to know; just ENTER to quit: arg
b
Enter a variable name you want to know; just ENTER to quit: [ENTER]
Enter a variable name you want to know; just ENTER to quit: count
3
Enter a variable name you want to know; just ENTER to quit: arg
cde
Enter a variable name you want to know; just ENTER to quit: [ENTER]
Enter a variable name you want to know; just ENTER to quit: count
4
Enter a variable name you want to know; just ENTER to quit: arg
fg
Enter a variable name you want to know; just ENTER to quit: [ENTER]
Enter a variable name you want to know; just ENTER to quit: count
5
Enter a variable name you want to know; just ENTER to quit: arg
h
Enter a variable name you want to know; just ENTER to quit: [ENTER]
([ENTER]은 ENTER를 눌렀음을 의미한다.)
결론
이 제까지 쉘 스크립트의 작성과 디버깅에 관한 사항을 알아보았다. 물론 지금까지 다룬 내용이 쉘 스크립트의 전부는 아니다. 자잘하고 미묘한 부분들 하지만 중요한 부분들은 다루지 못했으며, 현재 많이 사용하는 bash나 tcsh의 확장 기능도 남아 있다. 이제 그 부분은 독자들에게 남겨진 몫이며 이 기사를 통해 그 시작을 할 수 있기를 바란다. 일단은 다른 사람들이 만들어 놓은 스크립트를 보고 이해하면서 코드를 수정해보고 필요한 스크립트를 하나씩 만들어 가면서 익히면 될 것이다. 그리고 작성중에 막히는 부분은 매뉴얼 페이지가 큰 도움이 될 것이다. man bash !
참고자료
UNIX POWER TOOLS, 2ed. Jerry Peek, Tim O’Reilly, Mike Loukides. O’Reilly
bash 매뉴얼 페이지
'Linux' 카테고리의 다른 글
awk 명령어 (0) | 2018.01.14 |
---|---|
리눅스 시스템 모니터링 툴 20가지 (0) | 2018.01.14 |
ModSecurity 구버전 2.5, 2.7 다운로드 (0) | 2017.09.27 |
CentOS 6 apache 2.4 modsecurity 2.9 compile install (0) | 2017.09.27 |
리눅스 하드디스크 온도측정 (0) | 2017.09.06 |
댓글