6. 쉘명령과 제어구조

이번장에서는 쉘스크립트를 다루는데 필요한 3가지 부수적인 주제를 설명하게 될것이다.

6.1. 유닉스 명령어들

쉘스크립트는 유닉스 명령어들의 집합이므로, 유닉스 명령어에 대해서 어느정도의 숙지가 필요하다. 이러한 명령어들은 주로 파일과 문자열을 편집하기 위해서 쓰여진다. 이러한 명령어들중 자주 쓰이는 명령어들을 정리했다.

표 1. 자주 사용되는 유닉스 명령어들

echo "some text"some text 를 화면에 출력한다
wc -l file파일의 라인수
cp sourcefile destfilesourcefile 을 destfile 로 복사
mv oldname newname파일이름을 바꾸거나 파일의 이동
rm file파일 지우기
grep 'pattern' file파일에서 pattern의 문자열을 찾기
cub -b colnum file파일에서 문자열을 컬럼단위로 잘라서 보여줌
cat file.txtfile.txt 를 표준출력(stdout) 시킴
file somefilesomefile 의 파일타입 알아내기
read var입력값을 변수명var 에 대입
sort file.txtfile.txt 를 라인단위로 정렬
uniq파일에서 중복되는 문자열을 제거
tee표준출력되는 정보를 파일로 쓰기
basename file디렉토리명을 제외한 파일의 실제이름을 돌려줌
dirname file파일이름을 제외한 디렉토리의 이름을 돌려줌
head file파일의 처음 몇라인을 출력함
tail file파일의 마지막 몇라인을 출력함
sed정규표현에 의한 문자열의 검색및 치환에 사용됨

6.2. pipes(파이프), redirection(재지향)

Pipes(|) 는 하나의 프로그램을 실행시켜서 발생된 표준출력 데이타를 다른 프로그램에 표준입력 시키고자 할때 사용된다. 즉 프로세스간 데이타 통신을 위한 하나의 방법으로 사용된다.
	grep "hello" file.txt | wc -l	
			
위의 스크립트는 file.txt 에서 hello문자열을 포함한 라인을 찾아서(grep), 몇개의 라인이 hello 를 포함하고 있는지의 라인수를 돌려준다.

redirection 은 "재지향" 이라고 불리워진다. 우리나라 말로 표현하자면 "다시 향하게 하다" 이며, 어떤 프로그램의 출력 정보를 다른곳으로 다시 향하게 할때 쓰인다. 여기에서 다른곳이란 주로 파일을 뜻한다. 재지향을 위해서는 ">" 과 ">>" 을 쓴다. ">"을 사용하게 되면 새로운 파일을 만들게 된다. 기존에 같은 이름의 파일이 있었다면, 그 파일은 지워지게 된다. ">>" 을 쓰게 되면 기존에 같은 이름의 파일이 있다면 그 파일의 마지막부분에 덧 붙여지게 된다. 같은 이름의 파일이 없다면 물론 새로운 파일을 만들게 된다.
	[root@localhost /root]# cat address.txt | grep "seoul"  > seoul_add.txt
			
address.txt 에는 주소정보가 담겨 있다. 위의 스크립트는 이중 주소지가 "seoul" 인 정보만을 따로 뽑아서 seoul_add.txt 에 저장하는 일을 수행한다.

6.3. 제어구조

"if"는 참인지 거짓인지 판단할때 사용한다. 참이라면 then 부분을 실행하고 그렇지 않다면 else 부분을 실행한다.
	if .....
	then
		....
	else
		....
	fi
		
if 의 가장 유용한 사용처는 "상태" 를 테스트(test) 하는데 있다. 즉 문자비교, 파일이 존재하는지, 파일이 실행파일인지, 디렉토리인지, 읽을수 있는지 ... 등에 유용하게 사용할수 있으며, 이러한 작업의 제어를 위한 특수한 명령어 들을 제공한다. 이러한 "test" 조건들은 "[ ]" 사이에 쓰면된다. "[" 과 "]" 사이에는 반드시 공백문자가 들어가야 된다는 것을 주의하자.
	[ -f "somefile" ]   : somefile 이 파일인지를 테스트 한다.
	[ -x "/bin/ls" ]    : /bin/ls 가 실행파일인지를 검사한다.
	[ -n "$var" ]       : $var 변수에 어떤 값이 대입되어 있는지를 검사한다.
	[ "$a" = "$b" ]     : $a 와 $b 가 같은지 검사한다.
	["$a" = "$b"]       : "[" 과 "]" 사이에 공백이 오지 않았음으로 잘못된 문장이다.
		
"man test" 를 이용해서 어떠한 test operator 이 있는지 확인 할수 있다.
	#!/bin/sh
	if [ "$SHELL" = "/bin/bash" ]
	then
		echo "your login shell is the bash (bourne again shell)"
	else
		echo "your login shell is not bash but $SHELL"
	fi
		
$SHELL 은 환경변수로써 사용자의 로그인 쉘의 이름을 가지고 있다. 위의 스크립트는 $SHELL 의 값을 테스트 함으로써 사용자가 어떤 쉘을 사용하는지 알아내는 일을 한다.

6.4. 간단하게 표현하기

C 언어를 자주 사용해 본사람은 아래와 같은 문장에 익숙할 것이다.
	[ -f "/etc/shadow" ] && echo "This computer uses shadow passwords"
			
위의 문장에서는 && 을 사용해서 if 문을 간단하게 표현하고 있다. 왼쪽 문장이 참이면 오른쪽을 실행하라는 것으로, /etc/shadow 라는 파일이 존재한다면 쉐도우 패스워드를 사용한다고 유저에게 알려주는 일을한다. || 를 사용하면 그반대의 경우이다. 간단한 사용예를 들어 보겠다.
	#!/bin/sh
	mailfolder=/var/spool/mail/james
	[ -r "$mailfolder" ] || { echo "Can not read $mailfolder; exit 1;}
	echo "$mailfolder has mail from : "
	grep "^From " $mailfolder
			
위의 프로그램은 james 계정사용자의 메일파일을 검사해서 메일 파일을 읽을수 없으면 에러메시지와 함께 종료 하고 그렇지 않으면 grep 을 써서 누구에게로 부터 메일이 왔는지를 계정 사용자에게 알려주는 일을한다.

case 는 if elif else 를 좀더 일반화 시킨 제어구조이다. if 문을 쓰더라도 여러번의 조건에 대해서 검사할수있지만 그럴경우 if elif 가 어지럽게 중첩 되는 결과를 보여줄것이다. 이럴때 case 를 사용하면 좀더 가독성과 유지가 용이한 코드를 만들어 낼수 있다. 즉 if elsif 를 간단하게 표현할수 있다. 이해를 쉽게 하기 위해서 특정파일이 어떠한 포멧의 압축파일인지를 알아내는 스크립트를 만들어 보도록 하자. 파일의 종류를 알기 위해서는 file 이란 명령을 쓰면 된다. 아래의 예제를 smartzip 이란 파일로 저장하도록 하자.
	#!/bin/sh
	ftype=`file "$1"`
	case "$ftype" in
		"$1: gzip compressed"*)
			echo "gzip 압축";;
		"$1: Zip archive"*)
			echo "Zip 압축";;
		*)
			echo "FLE $1 can not be uncompressed with smartzip";;
	easc
			
$1 변수는 프로그램의 첫번째 아규먼트를 저장하고 있는 변수다. ivmdemo.tar.gz 의 압축포맷을 알고 싶다면, smartzip ivmdemo.tar.gz 이라고 명령을 내리면 된다.

저 윗장에서 다룬적이 있는 덧셈 스크립트를 case 를 이용하여 사칙연산을 수행하도록 확장시켜보자. 물론 아래의경우 굳이 스크립트를 만들필요 없이 expr 만을 사용해도 동일한 작업이 가능하지만, 어디까지나 case 의 활용법 에 대한 이해를 위주로함이니 효율성, 가용성 기타등등은 무시하고 넘어가기로 하자.
	#!/bin/sh
	add()
	{
	    result=`expr $1 + $2`
	    echo "$1 + $2 = $result"
	}
	min()
	{
	    result=`expr $1 - $2`
	    echo "$1 - $2 = $result"
	}
	div()
	{
	    result=`expr $1 / $2`
	    echo "$1 / $2 = $result"
	}
	mul()
	{
	    result=`expr $1 \* $2`
	    echo "$1 * $2 = $result"
	}
	#echo "$1, $2"
	case $1 in
	    "-") min $2 $3 ;;
	    "+") add $2 $3 ;;
	    "/") div $2 $3 ;;
	    "*") mul $2 $3 ;;
	esac
			
위의 스크립트는 첫번째 아규먼트로 연산자를 받아들이고 두번째 세번째 아규 먼트로 계산하고자 하는 숫자를 입력한다. "add - 1 3" 이런식으로 사용하면 된다. 주의할 점은 곱셈(*) 연산을 사용할 경우 "\" 등을 사용해서 "add \* 1 3" 형식으로 써야한다는 점이다. 쉘상에서 * 는 와일드카드 확장을 실행하기 때문이다.

이번에는 select 제어문에 대해서 알아보자 select 는 interactive(대화형) 메뉴 프로그램을 짜는데 매우 간단한 방법을 제공해준다. 사용자가 어떤 OS를 가장 선호하는지 메뉴를 보고 그중 하나를 선택하는 프로그램을 만들어 보도록 하자.
	#!/bin/sh
	echo "What is your favourite OS ?"
	select var in "Linux" "Free BSD" "Windows" "Solaris" "Other"
	do
		break
	done
	echo "You have selected $var"
			
위의 스크립트를 실행시키면 아래와 같은 메뉴가 뜨고 사용자의 입력을 요구하는 프롬프트가 대기 하게 될것이다.
	What is your favourite OS ?
	1) Linux
	2) Free BSD
	3) Windows
	4) Solaris
			
원하는 운영체제의 번호(1 - 4) 를 선택하면 선택된 번호의 문자열이 var 변수에 저장된다. 1 을 입력하였다면 var 변수엔 Linux 가 저장 된다.

while 은 조건이 만족하는 동안 루프를 반복한다.
	while ...
	do
		...
	done
			

다음은 while 를 사용해서 1부터 10까지 출력하는 간단한 프로그램이다. #!/bin/sh
	a=0
	while [ $a -lt 10 ]
	do
		a=`expr $a + 1`
		echo $a
	done
		

bashsehll 에서의 for 문은 C의 for 문과는 사용에 있어서 차이가 난다. sehll 에있어서는 in 다음의 값들을 차례대로 변수에 입력하는 일을 한다.
		
	#!/bin/sh
	for var in A B C
	do
		echo "var is $var"
	done
			
for 문을 이용한 좀더 유용한 스크립트를 만들어 보도록 하자. 아래의 스크립트는 배포판 CD에 해당 rpm이 있는지를 확인하고, 있다면 rpm 패키지의 정보를 보여주는 일을 한다. 아래의 내용을 showrpm 으로 저장하도록 하자.
	#!/bin/sh
	for rpmpackage in $*
	do
		if [ -r "$rpmpackage" ]
		then 
			echo "================ $rpmpackage ============="
			rpm -qi -p $rpmpackage
		else
			echo "ERROR: cannot read file $rpmpackage"
		fi
	done
			
위의 스크립트를 보면 $* 이라는 변수가 보일것이다. $*는 모든 아규먼트를 저장하는 변수이다.

6.5. Quoting

	#!/bin/sh
	echo $SHELL
	echo "$SHELL"
	echo '$SHELL'
			
위의 스크립트에서 1번째와 2번째의 경우 자신이 사용하는 쉘을 출력하지만 (아마도 /bin/bash) 3번째의 경우 $SHELL 자체를 출력하는걸 볼수 있을것이다. ' 를 사용하면 쉘이 사용하는 특수문자(keyword)를 일반화 시켜서 사용할수 있다. 또한 백슬러쉬를 사용해서 와일드카드나 변수기호와 같은 특수한 문자를 일반화 시킬수도 있다.
	echo \$SHELL
			
위에서 백슬러쉬를 사용함으로써 $ 의 특별한 의미를 제거시켜 버림으로써 $SHELL 이란 문자열을 출력하도록 만든다. 예를 들어서 $1,000 를 화면에 출력 시키려고 한다고 가정하자 이럴때 아래와 같이 써버리면
	echo $1000	
			
아무런 값도 출력되지 않음을 알수 있다. 왜냐면 쉘은 1000 앞에 $ 가 있음으로 이를 변수명으로 생각하고 이 변수명에 저장된 값을 echo 하려고 할것이기 때문이다. 우리가 원하는 값을 얻을려면 아래와 같이 코드를 수정해야 한다.
	echo \$1000
			

6.6. 함수

여러분이 좀더 복잡한 프로그램을 만들다보면 함수의 필요성을 느끼게 될것이다. 함수를 사용함으로써, 좀더 이해하기 쉽고 단순한 프로그램을 만들수 있으며, 재사용을 용이하도록 만들수 잇다. 함수는 다음과 같이 선언한다.
	functionname()
	{
		body
	}
			
sehll 은 스크립트 언어이고 순차적으로 실행이 되므로 함수를 사용하기 전에 먼저 선언을 해주어야만 한다.
	#!/bin/sh
	help()
	{
		cat << HELP
	xtitle bar -- change the name of an xterm, gnome-teminal or kde konsole
	Usage: xtitlebar [-h] "string_for_titlebar"
	OPTIONS: -h help text
	EXAMPLE: xtitlebar "cvs"
	HELP	
		exit 0
	}
	[ -z "$1" ] && help
	[ "$1" = "-h" ] && help
			
함수를 이용한 간단한 덧셈 스크립트를 만들어 보자. 2개의 인자를 받아들이고 이를 더한후 출력하는 일을 한다.
	#!/bin/sh
	add()
	{
		result=`expr $1 + $2`
		echo "$1 + $2 = $result"
	}
	add $1 $2
			

6.7. 명령행 인자(argument)

각 명령행 인자는 $* 과 $1, $2, $3, ... 등의 변수를 통해서 가져올수 있다. 그러나 이러한 명령행 인자들을 단순히 읽어들이는 것만으로는 -h 와 같은 명령행 옵션에 대한 내용은 다룰수 없다. 왜냐면 shell 에서는 -h 를 옵션이 아닌 인자로 취급하기 때문이다. 이를 처리하기 위해서는 약같의 기술이 필요하다. 보통 C에서는 getopt()와 같은 함수를 이용해서 옵션을 처리한다. 아래는 명령행 인자를 분석하는 쉘프로그램이다. 프로그램의 이름은 cmdparser 로 하자
	#!/bin/sh
	help()
	{
		cat << HELP
		This is a generic command line parser demo.
		Usage Example : cmdparser -l hello -f somefile1 somefile2
		HELP
		exit 0
	}	

	while [ -n "$1" ]
	do
		case $1 in
			-h) help; shift1;;
			-f) opt_f=1;shift 1;;
			-l) opt_l=$2;shift 2;;
			--) shift;break;;
			-*) echo "error : no such option $1. -h for help"; exit 1;;
			*) break;
		esac
	done

	echo "opt_f is $opt_f"
	echo "opt_l is $opt_1"
	echo "first arg is $1"
	echo "2nd arg is $2"
			
shift 란 새로운 쉘명령이 나왔는데, 아규먼트를 하나씩 이동시키는 일을한다. 자세한 내용은 man bash 를 참조하기 바란다. 위의 스크립트를 cmdparser -l hello -f -- -somefile1 somefiel2 로 실행시켜보면 아래와 같은 결과가 나올것이다.
	opt_f is 1
	opt_l is hello
	first arg is -somefile1
	2nd arg is somefile2
			
위의 프로그램이 어떻게 동작하는지 알아보자, 위의 루프는 아규먼트가 검색될때까지 계속해서 순환하도록 되어 있으며 case 를 이용해서 아규먼트와 대응되는 값을 매칭시킨다. 만약에 매칭된 값을 찾았다면, 해당 명령어나 함수를 실행하고 shift 를 이용해서 필요한 만큼 아규먼트를 이동시킨다.