 |
|
난이도 : 중급 Adam Cormany, National Data Center Manager, Scientific Games Corporation
옮긴이 : 박재호 이해영 dwkorea@kr.ibm.com
원문 게재일 : 2008 년 9 월 30 일 번역 게재일 : 2009 년 1 월 06 일 유닉스(UNIX®) 사용자가 애용하는 "요상한" 문자를 살펴봅니다. 유닉스에서 파이프라인, 재지정, 연산자 사용 방법도 익힙니다.
이제는 IBM® AIX®에 어느 정도 익숙해졌을 것이다. 지금쯤이면 디렉터리 구조를 탐색하는 명령, 파일을 만들고 수정하는 명령, 현재 실행 중인 프로세스 목록을 얻는 명령, 심지어 사용자와 시스템을 관리하는 명령 등 기본적인 명령에 익숙하리라 믿는다. 그 정도로도 멋지다. 하지만 옆에 앉은 유닉스(UNIX®) 관리자가 두들기는 요상한 기호도 이해하고픈 욕심이 생기지 않는가? 이 기사에서는 |, >, >>, <, <<, [[, ]] 등의 기호가 유닉스와 리눅스(Linux®)에서 무슨 의미인지 그리고 &&, ||, <, <=, != 같은 연산을 최대한 활용하는 방법을 살펴본다.
파이프라인
유닉스에 익숙한 사용자에게 파이프라인 또는 파이프(pipe)는 매일 사용하는 필수불가결한 기능이다. 파이프는 원래 Malcolm Mcllroy가 개발한 개념으로, 명령에서 나온 표준 출력(stdout)을 다음 명령이 받아들이는 표준 입력(stdin)으로 재지정한다. 한 번 실행에 파이프 하나만 사용하라는 법도 없다. 명령 여러 개를 파이프로 연결해 직전 명령에서 나온 stdout을 다음 명령이 받아들이는 stdin으로 계속해서 전달하는 경우도 아주 흔하다.
예를 들어, 시스템에 문제가 생기거나 일상적인 점검을 수행할 때 시스템 관리자는 흔히 시스템에 돌아가는 프로세스를 가장 먼저 확인한다. Listing 1을 참고한다.
Listing 1. 시스템에 돌아가는 프로세스 확인하기
# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 Jul 27 - 0:05 /etc/init
root 53442 151674 0 Jul 27 - 0:00 /usr/sbin/syslogd
root 57426 1 0 Jul 27 - 0:00 /usr/lib/errdemon
root 61510 1 0 Jul 27 - 23:55 /usr/sbin/syncd 60
root 65634 1 0 Jul 27 - 0:00 /usr/ccs/bin/shlap64
root 82002 110652 0 Jul 27 - 0:24 /usr/lpp/X11/bin/X -x abx
-x dbe -x GLX -D /usr/lib/X11//rgb -T -force :0 -auth /var/dt/A:0-SfIdMa
root 86102 1 0 Jul 27 - 0:00 /usr/lib/methods/ssa_daemon -l ssa0
root 106538 151674 0 Jul 27 - 0:01 sendmail: accepting connections
root 110652 1 0 Jul 27 - 0:00 /usr/dt/bin/dtlogin -daemon
root 114754 118854 0 Jul 27 - 20:22 dtgreet
root 118854 110652 0 Jul 27 - 0:00 dtlogin <:0> -daemon
root 131088 1 0 Jul 27 - 0:07 /usr/atria/etc/lockmgr
-a /var/adm/atria/almd -q 1024 -u 256 -f 256
root 147584 1 0 Jul 27 - 0:01 /usr/sbin/cron
root 155816 151674 0 Jul 27 - 0:04 /usr/sbin/portmap
root 163968 151674 0 Jul 27 - 0:00 /usr/sbin/qdaemon
root 168018 151674 0 Jul 27 - 0:00 /usr/sbin/inetd
root 172116 151674 0 Jul 27 - 0:03 /usr/sbin/xntpd
root 180314 151674 0 Jul 27 - 0:19 /usr/sbin/snmpmibd
root 184414 151674 0 Jul 27 - 0:21 /usr/sbin/aixmibd
root 188512 151674 0 Jul 27 - 0:20 /usr/sbin/hostmibd
root 192608 151674 0 Jul 27 - 7:46 /usr/sbin/muxatmd
root 196718 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.mountd
root 200818 151674 0 Jul 27 - 0:00 /usr/sbin/biod 6
root 213108 151674 0 Jul 27 - 0:00 /usr/sbin/nfsd 3891
root 221304 245894 0 Jul 27 - 0:05 /bin/nsrexecd
daemon 225402 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.statd
root 229498 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.lockd
root 241794 151674 0 Jul 27 - 0:51 /usr/lib/netsvc/yp/ypbind
root 245894 1 0 Jul 27 - 0:00 /bin/nsrexecd
root 253960 1 0 Jul 27 - 0:00 ./mflm_manager
root 274568 151674 0 Jul 27 - 0:00 /usr/sbin/sshd -D
root 282766 1 0 Jul 27 lft0 0:00 /usr/sbin/getty /dev/console
root 290958 1 0 Jul 27 - 0:00 /usr/lpp/diagnostics/bin/diagd
root 315646 151674 0 Jul 27 - 0:00 /usr/sbin/lpd
root 319664 1 0 Jul 27 - 0:00 /usr/atria/etc/albd_server
root 340144 168018 0 12:34:56 - 0:00 rpc.ttdbserver 100083 1
root 376846 168018 0 Jul 30 - 0:00 rlogind
cormany 409708 569522 0 19:29:27 pts/1 0:00 -ksh
root 569522 168018 0 19:29:26 - 0:00 rlogind
cormany 733188 409708 3 19:30:34 pts/1 0:00 ps -ef
root 749668 168018 0 Jul 30 - 0:00 rlogind
|
현재 실행 중인 프로세스 목록이 Listing 1처럼 간단한 시스템도 있다. 하지만 대다수 서비스 중인 시스템은 Listing 1보다 훨씬 많은 프로세스를 실행한다. 즉, ps 결과가 아주 길어진다는 뜻이다. 이 때 파이프를 사용하여 ps -ef의 표준 출력을 grep의 표준 입력으로 재지정하면 원하는 정보를 추출하기가 쉬워진다. Listing 2는 Listing 1에서 나타나는 전체 프로세스 목록을 grep으로 보내 "rpc"와 "ksh"가 든 행만 출력한 결과다.
Listing 2. 프로세스 목록을 grep으로 재지정하기
# ps -ef | grep -E "rpc|ksh"
root 196718 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.mountd
daemon 225402 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.statd
root 229498 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.lockd
root 340144 168018 0 12:34:56 - 0:00 rpc.ttdbserver 100083 1
cormany 409708 569522 0 19:29:27 pts/1 0:00 -ksh
cormany 733202 409708 0 19:52:20 pts/1 0:00 grep -E rpc|ksh
|
파이프라인 여러 개를 이어 stdout과 stdin을 여러 차례 재지정하기 시작하면 명령이 아주 복잡해진다. 아래 코드는 앞서 ps와 grep 예제에다 파이프라인 두 개를 추가한다. 첫 번째 파이프라인은 Listing 2 결과를 grep으로 보내 "grep"과 "ttdbserver"가 든 행을 제거한다. 두 번째 파이프라인은 두 번째 grep이 출력한 결과를 awk로 보내 PID가 200,000보다 큰 프로세스를 출력한다.
# ps -ef | grep -E "rpc|ksh" | grep -vE "grep|rpc.ttdbserver" |
awk -v _MAX_PID=200000 '{if ($2 > _MAX_PID) {print "PID for
process",$8,"is greater than", _MAX_PID}}'
PID for process /usr/sbin/rpc.statd is greater than 200000
PID for process /usr/sbin/rpc.lockd is greater than 200000
PID for process -ksh is greater than 200000
|
그림 1은 위 코드가 돌아가는 방식이다. 그림에서 보듯이, 파이프라인은 특정 명령의 stdout을 다음 명령의 stdin으로 재지정한다.
그림 1. 파이프라인 예제
>, >>, <, <<로 자료 재지정하기
명령행 인터페이스(command-line interface, CLI)에서 명령을 실행할 때 또 하나 알아둘 중요한 사항이 디바이스에 자료를 쓰는 방법과 디바이스에서 자료를 읽어들이는 방법이다. 명령이 출력하는 결과를 디바이스에 쓰려면 명령 뒤에 > 또는 >> 기호를 추가한 후 목표 디바이스 이름이나 파일 이름을 지정한다. 파일이 존재하지 않으며 디렉터리에 쓰기 권한이 있다면 >와 >>는 새 파일을 생성한 후 결과를 쓴다. 이 때 새 파일 권한은 사용자의 umask 설정을 따른다. 하지만 파일이 존재한다면 >는 파일을 열고 기존 내용을 완전히 덮어쓴다. 반면, >>는 기존 내용 뒤에 결과를 첨부한다. 명령이 내놓는 결과가 왼쪽에서 오른쪽으로 흐른다고 생각하면 이해하기 쉽다. (즉, <명령> -> <결과> -> <파일>)
다음 예제는 앞서 "파이프라인" 절에서 살펴본 ps -ef 명령이다. 이번에는 명령 결과를 ps_out이라는 파일로 재지정한다.
# ps -ef | grep -E "rpc|ksh" > ps_out
|
다음 코드 역시 앞서 살펴본 명령이다. 이번에도 명령 결과를 ps_out으로 보낸다. 대신 >>를 사용하므로 결과는 현재 파일(ps_out) 내용 뒤에 추가된다.
# ps -ef | grep -E "rpc|ksh" | grep -vE "grep|rpc.ttdbserver" |
awk -v _MAX_PID=200000 '{if ($2 > _MAX_PID) {print "PID for
process",$8,"is greater than", _MAX_PID}}' >> ps_out
|
Listing 3은 두 명령을 실행한 후 ps_out 내용이다.
Listing 3. 재지정을 두 번 한 결과
# cat ps_out
root 196718 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.mountd
daemon 225402 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.statd
root 229498 151674 0 11:00:27 - 0:00 /usr/sbin/rpc.lockd
root 340144 168018 0 12:34:56 - 0:00 rpc.ttdbserver 100083 1
cormany 409708 569522 0 19:29:27 pts/1 0:00 -ksh
cormany 733202 409708 0 19:52:20 pts/1 0:00 grep -E rpc|ksh
PID for process /usr/sbin/rpc.statd is greater than 200000
PID for process /usr/sbin/rpc.lockd is greater than 200000
PID for process -ksh is greater than 200000
|
>은 명령이 stdout으로 출력하는 결과만 재지정한다. 그런데 stdout 말고 stderr도 있다. 유닉스에서 stdout은 1로 표현하고, stderr은 2로 표현한다. 1과 2로 표현해도 출력을 재지정하는 방법은 별반 달라지지 않는다. > 앞에 원하는 숫자를 넣어주면 된다. 즉, 출력을 어디로 내보낼지 셸에 알리는 과정에서 stdout을 재지정하려면 1>을 사용하고, stderr을 재지정하려면 2>를 사용한다.
Listing 4는 fileA.tar.bz2와 fileC.tar.bz2 파일을 열거한다. 그런데 첫 번째 명령 ls에서 보듯이 fileC.tar.bz2라는 파일이 존재하지 않는다. 다행스럽게도 정상 출력과 오류 출력을 각각 ls.out과 ls.err로 분리했다.
Listing 4. fileA.tar.bz2와 fileC.tar.bz2 열거하기
# ls
fileA.tar.bz2 fileAA.tar.bz2 fileB.tar.bz2 fileBB.tar.bz2
# ls fileA.tar.bz2 fileC.tar.bz2 1> ls.out 2> ls.err
# cat ls.out
fileA.tar.bz2
# cat ls.err
ls: 0653-341 The file fileC.tar.bz2 does not exist.
|
AIX에서 >와 >>로 stdout과 stderr을 재지정하는 방법도 똑같다. 예를 들어, Listing 5에서 보듯이, 앞서 생성한 ls.out과 ls.err을 계속 사용해도 문제가 없다.
Listing 5. 출력 파일 계속 사용하기
# ls fileB.tar.bz2 fileD.tar.bz2 1>> ls.out 2>> ls.err
# cat ls.out
fileA.tar.bz2
fileB.tar.bz2
# cat ls.err
ls: 0653-341 The file fileC.tar.bz2 does not exist.
ls: 0653-341 The file fileD.tar.bz2 does not exist.
|
때로는 stdout과 stderr을 동일한 파일이나 동일한 디바이스에 쓸 필요가 생긴다. 방법은 두 가지다. 하나는 1>과 2>를 같은 파일로 보내는 방법이다.
# ls fileA.tar.bz2 fileC.tar.bz2 1> ls.out 2> ls.out
# cat ls.out
fileA.tar.bz2
ls: 0653-341 The file fileC.tar.bz2 does not exist.
|
다른 하나는 동일한 작업을 하는 좀 더 간단하고 빠른 방법으로, 숙련된 유닉스 사용자가 흔히 사용하는 방법이다.
# ls fileA.tar.bz2 fileC.tar.bz2 > ls.out 2>&1
# cat ls.out
fileA.tar.bz2
ls: 0653-341 The file fileC.tar.bz2 does not exist.
|
위 명령을 찬찬히 분석해보자. 첫째, ls fileA.tar.bz2 fileC.tar.bz2를 실행한다. 다음으로, > ls.out이 stdout을 ls.out으로 보낸다. 다음으로, 2>&1이 stderr을 stdout으로 재지정한다. 그런데 stdout을 ls.out으로 재지정했으므로 결국 stderr 역시 ls.out으로 재지정하는 셈이다.
파일뿐만 아니라 다른 디바이스로 출력을 재지정해도 무방하다. 프린터, 플로피 디스크, 터미널 유형(TTY) 등 다양한 디바이스로 재지정이 가능하다. 예를 들어, 현재 로그인한 사용자 중 특정한 사용자에게 메시지를 전송하려면 who 명령으로 사용자를 찾은 후 TTY로 메시지를 재지정한다. 단, TTY로 메시지를 전송할 권한이 충분하다는 가정 아래서다. 구체적인 명령은 Listing 6을 참고한다.
Listing 6. TTY로 메시지 재지정하기
# for _TTY in 'who | grep "cormany" | awk '{print $2}''
> do
> _TTY="/dev/${_TTY}"
> echo "Sending message to cormany on ${_TTY}"
> echo "Test Message to cormany@${_TTY}" > ${_TTY}
> done
Sending message to cormany on /dev/pts/13
Test Message to cormany@/dev/pts/13
Sending message to cormany on /dev/pts/14
|
Stdout이 아니라 Stdin
>와 >>는 대체로 쉽게 이해한다. 반면, <와 <<는 어려워하는 사람들이 꽤 있다. >와 >>는 출력이 왼쪽 명령에서 오른쪽 목적지 파일로 흘러간다고 상상하면 이해하기 쉽다. <와 <<도 같은 원리다. <는 이미 존재하는 자료를 명령에 표준 입력으로 전달한다. 이미 존재하는 자료를 오른쪽에서 왼쪽으로 명령에다 stdin으로 밀어넣어 준다고 상상하면 이해하기 쉬우리라(<명령> <- <자료>).
예를 들어, 다른 사용자에게 ASCII 파일을 전자편지로 보내려고 한다. 파이프라인을 사용하여 cat의 stdout을 mail의 stdin으로 재지정하는 방법이 있다. 즉, cat mail_file.out | mail -s "Here's your E-mail!" acormany@yahoo.com이라고 실행한다. 아니면 파일 내용을 mail의 stdin으로 재지정하는 방법도 있다. 즉, 다음과 같이 명령을 수행한다.
# mail -s "Here's your E-mail!" acormany@yahoo.com < mail_file.out
|
<<는 here-document라고도 부르는데, 명령을 실행하면서 즉석에서 입력 파일을 생성한다. 따라서 파일을 따로 생성하는 번거로움이 없으며, 명령을 실행하는 속력도 빨라진다. << 이후에 입력하는 문자열은 명령에 stdin으로 보내진다. 다시 말해, 사용자가 종결 문자열을 입력할 때까지 모든 입력 문자열을 표준 입력으로 간주한다. 사용하는 방법은 간단하다. 명령 다음에 <<와 종결 문자열을 입력한다. 그런 다음, stdin으로 보내려는 내용을 입력한다. 마지막으로, 행을 바꿔 새 행에 종결 문자열을 입력한다. here-document는 공백, 행 바꿈 문자 등을 그대로 보존한다.
예를 들어, 아래와 같이 echo 명령을 다섯 번 실행하는 대신
# echo "Line 1"
Line 1
# echo "Line 2"
Line 2
# echo "Line 3"
Line 3
# echo "Line 4"
Line 4
# echo "Line 5"
Line 5
|
다음과 같이 여러 명령을 대체해 echo 명령을 한 번만 실행해도 된다.
# cat << EOF
> Line 1
> Line 2
> Line 3
> Line 4
> Line 5
> EOF
Line 1
Line 2
Line 3
Line 4
Line 5
|
셸 스크립트에서 가독성을 높이고자 탭을 사용한다면 << 직후에 하이픈(-)을 추가한 후 종결 문자열을 입력한다. - 옵션은 행 시작에 있는 탭을 무시한다.
# cat <<- ATC
> Line 1
> Line 2
> Line 3
> Line 4
> Line 5
> ATC
Line 1
Line 2
Line 3
Line 4
Line 5
|
Listing 7은 지금까지 설명한 명령을 모두 사용하는 스크립트다.
Listing 7. 지금까지 살펴본 명령을 모두 사용하는 스크립트
# cat redirect_example
#!/usr/bin/ksh
cat <<- ATC | sed "s/^/Redirect Example => /g" >> atc.out
This is an example of how to redirect
stdout to a file as well as pipe stdout into stdin
of another command (i.e. sed), all done inside
a here-document.
Cool eh?
ATC
|
다음은 redirect_example 스크립트를 수행한 결과다.
# ./redirect_example
# cat atc.out
Redirect Example => This is an example of how to redirect
Redirect Example => stdout to a file as well as pipe stdout into stdin
Redirect Example => of another command (i.e. sed), all done inside
Redirect Example => a here-document.
Redirect Example =>
Redirect Example => Cool eh?
|
서브셸
때로는 여러 명령을 묶어 실행할 필요가 생긴다. 예를 들어, 다른 디렉터리에서 특정한 작업을 수행해야 한다면 일반적으로 Listing 8과 같이 명령을 실행할 것이다.
Listing 8. 여러 명령을 묶어 동시에 실행하기
# pwd
/home/cormany
# cd testdir
# tar -cf ls_output.tar ls.out?
# pwd
/home/cormany/testdir
|
Listing 8처럼 명령을 실행해도 문제는 없다. 하지만 위 단계를 거치고 나면 현재 디렉터리가 바뀐다. 대신, 명령을 묶어 서브셸로 전달하면 서브셸이 명령 전체를 한 단위로 실행한다. Listing 9는 서브셸을 사용하여 Listing 8과 동일한 작업을 수행하는 모습이다.
Listing 9. 서브셸을 사용해 여러 명령을 묶어 동시에 실행하기
# pwd
/home/cormany
# (cd testdir ; tar -cf ls_output.tar ls.out?)
# pwd
/home/cormany
|
test 명령, [ ], [[ ]]
현대 언어로 프로그램을 짜거나 셸 스크립트를 작성할 때 프로그램을 정확히 구현하려면 표현식이나 값을 계산하는 기능이 필수다. 그래서 유닉스는 test 명령을 제공한다. 매뉴얼 페이지에서 설명하듯이, test 명령은 표현식을 계산한 후 결과가 참이면 0 종료값(exit value)을 반환한다. 테스트 정의와 사용 가능한 조건 등 자세한 내용은 test 매뉴얼 페이지를 참조한다.
test 명령을 사용하려면 명령에 적절한 옵션과 파일 이름을 지정해준다. test 명령이 표현식을 평가하고 나면 프롬프트가 뜨는데, 여기서 반환 값을 확인한다. 예제는 Listing 10을 참고한다.
Listing 10. 반환 값 확인
# ls -l
-rwxr-xr-x 1 cormany atc 786 Feb 22 16:11 check_file
-rw-r--r-- 1 cormany atc 0 Aug 04 20:57 emptyfile
# test -f emptyfile
# echo $?
0
# test -f badfilename
# echo $?
1
|
매뉴얼 페이지에서도 설명하지만, test 명령은 표현식이 참이면 0을 반환하고 아니면 0이 아닌 값(즉 1)을 반환한다. Listing 10에서는 emptyfile이 존재하므로 0을 반환하고 badfilename이 없으므로 1을 반환한다.
test 명령을 사용하는 또 다른 방법은 표현식을 사각괄호([ ])로 감싸는 방법이다. test 명령을 사용하든 사각괄호를 사용하든 결과는 동일하다.
# [ -f emptyfile ]
# echo $?
0
# [ -f badfilename ]
# echo $?
1
|
단일 사각괄호([ ])와 이중 사각괄호([[ ]])는 순전히 개인적인 성향이다. 처음에 명령 사용법과 스크립트 작성법을 배운 방식에 따라 달라진다. 하지만 둘 사이에 다소 차이가 있다는 사실을 명심한다. 테스트 연산자로서 [ ]와 [[ ]]는 같은 의미지만, 논리 연산자로서 [ ]와 [[ ]]는 다른 의미다.
연산자
AIX에서 사용하는 기본 셸은 ksh인데, 다른 유닉스와 리눅스에서는 다른 셸도 사용한다. 어떤 셸을 사용하든, 셸이 제공하는 테스트 연산자, 논리 연산자, 치환 연산자 등을 알아두어야 한다.
테스트 연산자
셸 스크립트를 작성하면서 오류를 확인하거나 파일 상태를 점검하려면 테스트 연산자가 필수적이다. 다음은 ksh를 포함하여 표준 유닉스 셸이 제공하는 테스트 연산자 목록이다. 많이 사용하는 연산자만 열거했다.
-d <file>: <file>이 디렉터리다.
-e <flle>: <file>이 존재한다.
-f <file>: <file>이 일반(regular) 파일이다.
-n <string>: <string>이 NULL이 아니다.
-r <file>: 사용자가 <file>에 읽기 권한이 있다.
-s <file>: <file> 크기가 0보다 크다.
-w <file>: 사용자가 <file>에 쓰기 권한이 있다.
-x <file>: 사용자가 <file>에 실행 권한이 있다.
-z <string>: <string>이 NULL이다.
-L <file>: <file>이 심볼릭(symbolic) 링크다.
유닉스는 디렉터리, 디바이스, 심볼릭 링크, 기타 객체를 모두 파일로 취급한다는 사실에 주목한다. 따라서 위 테스트 연산자는 모든 파일 유형에 적용 가능하다.
모두가 나름대로 자신만의 방식으로 셸 스크립트를 작성한다. 테스트 명령으로 [ ]를 사용하든 [[ ]]를 사용하든 위 테스트 연산자는 똑같은 결과를 내놓는다. 이 기사에서는 [[ ]]를 사용한다. Listing 11은 위에서 설명한 테스트 연산자 몇 개를 사용한 예다.
Listing 11. 테스트 연산자 사용하기
#!/usr/bin/ksh
while true
do
echo "\nEnter file to check: \c"
read _FNAME
if [[ ! -e "${_FNAME}" ]]
then
echo "Unable to find file '${_FNAME}'"
continue
fi
if [[ -f "${_FNAME}" ]]
then
echo "${_FNAME} is a file."
elif [[ -d "${_FNAME}" ]]
then
echo "${_FNAME} is a directory."
elif [[ -L "${_FNAME}" ]]
then
echo "${_FNAME} is a symbolic link."
else
echo "Unable to determine file type for '${_FNAME}'"
fi
[[ -r "${_FNAME}" ]] && echo "User ${USER} can read '${_FNAME}'"
[[ -w "${_FNAME}" ]] && echo "User ${USER} can write to '${_FNAME}'"
[[ -x "${_FNAME}" ]] && echo "User ${USER} can execute '${_FNAME}'"
if [[ -s "${_FNAME}" ]]
then
echo "${_FNAME} is NOT empty."
else
echo "${_FNAME} is empty."
fi
done
|
Listing 11은 파일 몇 개를 확인한다. Listing 12는 Listing 11을 실행한 결과다.
Listing 12. 테스트 연산을 실행한 결과
# ls -l
-rwxr-xr-x 1 cormany atc 786 Feb 22 16:11 check_file
-rw-r--r-- 1 cormany atc 0 Aug 04 20:57 emptyfile
# ./check_file
Enter file to check: badfilename
Unable to find file 'badfilename'
Enter file to check: check_file
check_file is a file.
User cormany can read 'check_file'
User cormany can write to 'check_file'
User cormany can execute 'check_file'
check_file is NOT empty.
Enter file to check: emptyfile
emptyfile is a file.
User cormany can read 'emptyfile'
User cormany can write to 'emptyfile'
emptyfile is empty.
|
테스트 연산자 사용법과 목록 전체를 보려면 man test를 실행한다.
논리 연산자
유닉스에서 또 하나 중요한 연산자가 논리 연산자다. 대다수 현대적인 프로그래밍 언어와 마찬가지로, 여러 표현식이나 여러 값으로 조건을 따지려면 AND와 OR가 필수적이다.
내가 쓴 다른 기사(참고자료 참조)를 읽어본 독자라면 내가 코드 여러 행보다 논리 연산자를 선호한다는 사실을 눈치챘으리라. 논리 연산자를 사용하면 코드가 깔끔해지고 관리도 쉬워진다. 보통 스크립트를 짤 때 나는 가장 먼저 exit_msg()라는 함수를 구현한다. 함수 정의는 다음과 같다.
exit_msg() {
[[ $# -gt 1 ]] && echo "${0##*/} (${1}) ??${2}"
exit ${1:-0}
}
|
Listing 13은 exit_msg() 함수와 논리 연산자를 쓰지 않은 코드다. 코드가 길고 장황하다.
Listing 13. exit_msg 함수와 깔끔한 논리 연산자를 쓰지 않은 코드
#!/usr/bin/ksh
if [[ -n ${_NUM1} ]]
then
unset _NUM1
fi
if [[ -n ${_NUM2} ]]
then
unset _NUM2
fi
while [[ -z ${_NUM1} ]] || [[ -z ${_NUM2} ]]
do
echo "Enter 2 sets of numbers: \c"
read _NUM1 _NUM2
done
echo "Enter file to log results to: \c"
read _FNAME
if [[ ! -e "${_FNAME}" ]]
then
echo "File '${_FNAME}' doesn't exist. A new log will be created."
fi
touch "${_FNAME}"
if [[ ! -w "${_FNAME}" ]]
then
echo "Unable to write to file '${_FNAME}'"
exit 1
fi
expr ${_NUM1} \/ 1 > /dev/null 2>&1
if [[ $? -ne 0 ]]
then
echo "Number '${_NUM1}' is not numeric."
exit 2
fi
expr ${_NUM2} \/ 1 > /dev/null 2>&1
if [[ $? -ne 0 ]]
then
echo "Number '${_NUM2}' is not numeric."
exit 2
fi
echo "${_NUM1},${_NUM2}" >> "${_FNAME}"
|
반면, exit_msg()라는 함수와 논리 연산자 몇 개만 사용하면 스크립트는 훨씬 깔끔하고 명확해진다. Listing 14는 exit_msg() 함수와 논리 연산자를 사용한 코드다.
Listing 14. 함수와 논리 연산자로 깔끔해진 코드
#!/usr/bin/ksh
exit_msg() {
[[ $# -gt 1 ]] && echo "${0##*/} (${1}) - ${2}"
exit ${1:-0}
}
[[ -n ${_NUM1} ]] && unset _NUM1
[[ -n ${_NUM2} ]] && unset _NUM2
while [[ -z ${_NUM1} ]] || [[ -z ${_NUM2} ]]
do
echo "Enter 2 sets of numbers: \c"
read _NUM1 _NUM2
done
echo "Enter file to log results to: \c"
read _FNAME
[[ ! -e "${_FNAME}" ]] && echo
"File '${_FNAME}' doesn't exist. A new log will be created."
touch "${_FNAME}"
[[ ! -w "${_FNAME}" ]] && exit_msg 1
"Unable to write to file '${_FNAME}'"
expr ${_NUM1} \/ 1 > /dev/null 2>&1
[[ $? -ne 0 ]] && exit_msg 2 "Number '${_NUM1}' is not numeric."
expr ${_NUM2} \/ 1 > /dev/null 2>&1
[[ $? -ne 0 ]] && exit_msg 2 "Number '${_NUM2}' is not numeric."
echo "${_NUM1},${_NUM2}" >> "${_FNAME}"
|
위 예제는 AND(&&)와 OR(||) 연산자를 주로 사용했다. [ ]와 [[ ]] 절에서 보았듯이, AND(-a)와 OR(-o)를 사용해도 무방하다. test 명령이나 단일 사각괄호([ ])를 사용한다면 -a와 -o로 표현식을 평가하라고 권한다. 하지만 이중 사각괄호([[ ]])를 사용한다면 &&와 ||를 사용하는 편이 낫다.
# [[ "Paul" != "Xander" && 2 -gt 0 ]]
# echo $?
0
# [ "Paul" != "Xander" -a 2 -gt 0 ]
# echo $?
0
|
비교 테스트 연산자
또 다른 테스트 연산자로 비교 테스트 연산자가 있다. 앞서 소개한 테스트 연산자와 마찬가지로, 비교 테스트 연산자 역시 오류를 확인하거나 값을 점검할 때 유용하다. 앞서 소개한 테스트 연산자는 주로 파일을 테스트하거나 변수가 정의되었는지 확인할 때 사용하는 반면, 비교 테스트 연산자는 주로 문자열이나 숫자 값에 사용한다. 날짜, 파일 크기, 문자열 비교 등에 적합하다.
테스트 비교 연산자 목록은 다음과 같다.
<fileA> -nt <fileB>: fileA가 fileB보다 나중 파일이다.
<fileA> -ot <fileB>: fileA가 fileB보다 오래된 파일이다.
<fileA> -ef <fileB>: fileA와 fileB가 같은 파일을 가리킨다.
<string> = <pattern>: string이 pattern과 같다.
<string> != <pattern>: string이 pattern과 다르다.
<stringA> < <stringB>: 사전에서 stringA가 stringB보다 먼저 나온다.
<stringA> > <stringB>: 사전에서 stringA가 stringB보다 나중에 나온다.
<exprA> -eq <exprB>: expressionA가 expressionB와 같다.
<exprA> -ne <exprB>: expressionA가 expressionB와 같지 않다.
<exprA> -lt <exprB>: expressionA가 expressionB보다 작다.
<exprA> -gt <exprB>: expressionA가 expressionB보다 크다.
<exprA> -le <exprB>: expressionA가 expressionB보다 작거나 같다.
<exprA> -ge <exprB>: expressionA가 expressionB보다 크거나 같다.
비교 테스트 연산자를 사용하는 형식은 다른 테스트 연산자와 똑같다. test, [ ], [[ ]] 중 어느 명령을 사용해도 무방하다. Listing 15, Listing 16, Listing 17은 각각 숫자, 문자열, 파일을 비교하는 예제다.
Listing 15. 숫자 비교
# ls -l *.file
-rw-r--r-- 1 cormany atc 21 Feb 22 2006 Pauls.file
-rw-r--r-- 1 cormany atc 22 Aug 04 20:57 Xanders.file
# [[ "Pauls.file" -ot "Xanders.file" ]]
# echo $?
0
|
Listing 16. 문자열 비교
# _PSIZE=`ls -l Pauls.file | awk '{print $5}'`
# _XSIZE=`ls -l Xanders.file | awk '{print $5}'`
# [[ ${_PSIZE} -lt ${_XSIZE} ]]
# echo $?
0
|
Listing 17. 파일 비교
# [[ "cat" = "dog" ]]
# echo $?
1
|
치환 연산자
스크립트가 길어지거나 한동안 스크립트에 손대지 않았다면 깜빡하고 변수를 정의하지 않거나 변수 값을 할당하지 않고 넘어가기 십상이다. 어떤 경우에는 스크립트 사용자에게 현재 설정된 값을 알려주거나 기본값을 설정해주면 좋다. 치환 연산자는 이러한 문제를 간단하게 해결한다.
${var-value}: <var>이 존재하면 <var> 값을 반환한다. <var>이 존재하지 않으면 <value>를 반환한다.
${var=value}: <var>이 존재하면 <var> 값을 반환한다. <var>이 존재하지 않으면 <var> 값을 <value>로 설정한 후 <value>를 반환한다.
${var+value}: <var>이 존재하면 <var> 값을 반환한다. <var>이 존재하지 않으면 NULL을 반환한다.
${var?value}: <var>이 존재하면 <var> 값을 반환한다. <var>이 존재하지 않으면 명령이나 스크립트를 종료하고 <value>를 오류 메시지로 출력한다. <value>가 없으면 "Parameter null or not set"이라는 기본 오류 메시지를 출력한다.
${var:-value}: <var>이 존재하고 NULL이 아니면 <var> 값을 반환한다. <var>이 존재하지 않거나 NULL이면 <value>를 반환한다.
${var:=value}: <var>이 존재하고 NULL이 아니면 <var> 값을 반환한다. <var>이 존재하지 않거나 NULL이면 <var> 값을 <value>로 설정한 후 <value>를 반환한다.
${var:+value}: <var>이 존재하고 NULL이 아니면 <value> 값을 반환한다. <var>이 존재하지 않거나 NULL이면 NULL을 반환한다.
${var:?value}: <var>이 존재하고 NULL이 아니면 <var> 값을 반환한다. <var>이 존재하지 않거나 NULL이면 명령이나 스크립트를 종료하고 <value>를 오류 메시지로 출력한다. <value>가 없으면 "Parameter null or not set"이라는 기본 오류 메시지를 출력한다.
전반 연산자 네 개와 후반 연산자 네 개가 살짝 다르다는 사실에 주목한다. 후반 연산자 네 개는 변수 이름과 치환 연산자 사이에 콜론(:)이 들어가서 변수가 NULL인지도 확인한다. 치환 연산자로 변수에 값을 할당할 때 또 하나 주지할 사항이라면, 변수에 값을 할당하는 규칙이 일반 명령행이나 스크립트에서와 똑같다는 사실이다. 즉, 치환 연산자를 사용할 때도 시스템이 예약한 변수는 (예를 들어, $1, $2, $3 등은) 새 값으로 덮어쓰지 못한다.
Listing 18은 치환 연산자를 사용하는 스크립트다. 스크립트 마지막 행에서 보듯이, 치환 연산자 여러 개를 조합해도 괜찮다.
Listing 18. 치환 연산자 사용하기
# cat subops_examples
#!/usr/bin/ksh
_ARG1="${1}"
echo "Test 1A: The 1st argument is ${_ARG1-'ATC'}"
echo "Test 1B: The 1st argument is ${_ARG1:-'ATC'}"
_ARG2="${2}"
echo "Test 2A: The 2nd argument is ${_ARG2-'AMDC'}"
echo "Test 2B: The 2nd argument is ${_ARG2:-'AMDC'}"
_ARG3="${3}"
echo "Test 3A: The 3rd argument is ${_ARG3='PAC'}"
echo "Test 3B: The 3rd argument is ${_ARG3:='PAC'}"
_ARG4="${4}"
echo "Test 4A: ${4:+'The 4th argument was supplied'}"
echo "Test 5: If the 4th argument was provided, the value would be
${4:?'The 4th argument was not supplied.'}. Otherwise, we will not
see this message and get an error instead."
_ARG8="${8}"
echo "${_ARG8:=${7:-${6:-${5:-No Arguments were supplied after the 4th}}}}"
|
Listing 19는 인수 없이 스크립트를 실행한 결과다.
Listing 19. 인수 없이 스크립트를 실행한 결과
# ./subops_examples
Test 1A: The 1st argument is
Test 1B: The 1st argument is ATC
Test 2A: The 2nd argument is
Test 2B: The 2nd argument is AMDC
Test 3A: The 3rd argument is
Test 3B: The 3rd argument is PAC
Test 4A:
./subops_examples[18]: 4: The 4th argument was not supplied.
|
Listing 20은 인수를 세 개만 지정하여 스크립트를 실행한 결과다.
Listing 20. 인수 세 개로 스크립트를 실행한 결과
# ./subops_examples arg1 arg2 arg3
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A:
./subops_examples[18]: 4: The 4th argument was not supplied.
|
Listing 21은 인수를 네 개만 지정하여 스크립트를 실행한 결과다.
Listing 21. 인수 네 개로 스크립트를 실행한 결과
# ./subops_examples arg1 arg2 arg3 arg4
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A: The 4th argument was supplied
Test 5: If the 4th argument was provided, the value would be
arg4. Otherwise, we will not see this message and get an
error instead.
No Arguments were supplied after the 4th
|
Listing 22는 인수 다섯 개를 모두 지정하여 스크립트를 실행한 결과다.
Listing 22. 인수 다섯 개로 스크립트를 실행한 결과
# ./subops_examples arg1 arg2 arg3 arg4 arg5
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A: The 4th argument was supplied
Test 5: If the 4th argument was provided, the value would be
arg4. Otherwise, we will not see this message and get an
error instead.
arg5
|
Listing 23은 인수 일곱 개를 지정하여 스크립트를 실행한 결과다. 인수 일곱 개를 지정하였으므로 인수 5와 인수 6이 무시된다.
Listing 23. 인수 일곱 개로 스크립트를 실행한 결과
# ./subops_examples arg1 arg2 arg3 arg4 arg5 arg6 arg7
Test 1A: The 1st argument is arg1
Test 1B: The 1st argument is arg1
Test 2A: The 2nd argument is arg2
Test 2B: The 2nd argument is arg2
Test 3A: The 3rd argument is arg3
Test 3B: The 3rd argument is arg3
Test 4A: The 4th argument was supplied
Test 5: If the 4th argument was provided, the value would be
arg4. Otherwise, we will not see this message and get an
error instead.
arg7
|
결론
지금까지 유닉스에서 사용하는 "요상한" 문자를 살펴보았다. 구체적으로는 stdin이나 stdout을 재지정하는 법, 파이프를 사용하는 법, 연산자를 사용하는 법 등을 익혔다. 지금까지 살펴본 내용을 잘 활용하면 오류 처리가 탄탄하고 논리가 깔끔한 스크립트를 작성할 수 있으리라. 행운을 빈다!
참고자료 교육
제품 및 기술 얻기
토론
필자소개  | |  | Adam Cormany는 현재 National Data Center의 관리자로 일하고 있다. Cormany는 유닉스 시스템 엔지니어, 유닉스 관리자, Scientific Games Corporation에서 운영 관리자를 맡아왔다. Cormany는 10년 넘게 솔라리스, 레드햇 리눅스 관리자는 물론이고 AIX도 광범위하게 다뤘다. Cormany는 pSeries® AIX 시스템 관리 분야에서 IBM eServer® 공인 전문가다. 관리 업무 이외에, Cormany는 배시, csh, ksh을 사용한 셸 스크립트와 C, PHP, 펄 프로그래밍에도 광범위한 지식을 습득했다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|