원문: http://www.linuxnewbie.org/nhf/intel/programming/introbashscript.html
쉘 스크립트?? 이건 머하는거얌 ㅋㅋ
잊지 않기위해.... 하아~~ 이런거 너무 어려워 ㅠㅠ
리눅스에서 쓸 수 있는 모든 쉘들처럼, BASH(Bourne Again Shell)은 뛰어난 명령 라인 쉘이면서, 그 자체로도 하나의 스크립팅 언어이다. 당신은 쉘 스크립팅을 이용해서 쉘이 가진 능력을 충분히 활용할 수 있으며, 쉘 스크립팅이 아니었으면 수많은 명령을 필요로 했을 많은 일들을 자동적으로 처리할 수도 있다. 당신의 리눅스 박스에 놓여 있는 많은 프로그램들은 쉘 스크립트들이다. 만일 쉘 스크립트가 어떻게 작동하는지 배우고 싶거나 당신이 가지고 있는 쉘 스크립트를 수정하고 싶다면, bash 문법을 이해하는 것은 필수적이다. 게다가, bash 언어를 이해하면 정확히 당신이 원하는 방식으로 일을 하는 당신 자신의 프로그램을 작성할 수 있다.
프로그래밍 또는 스크립팅?
프로그래밍을 처음 하는 사람들은 대개 프로그래밍 언어와 스크립팅 언어 사이의 차이를 혼동한다. 프로그래밍 언어는 일반적으로 스크립팅 언어에 비해 보다 강력하고 보다 빠르다. 프로그래밍 언어의 예로는 C, C++, Java가 있다. 프로그래밍 언어는 대개 소스 코드(최종 프로그램이 어떻게 실행될 것인가에 대한 지시문을 담고 있는 텍스트 파일)에서 시작해서 컴파일 과정을 통해 실행 가능 파일로 만들어 진다(built). 이렇게 해서 만들어진 실행 가능 파일은 다른 운영 체제로 쉽게 이식되어지지 않는다. 예를 들어, 당신이 리눅스에서 C 프로그램을 작성했다면, Windows 98시스템에서는 그 프로그램을 실행할 수 없을 것이다. 프로그램을 실행하기 위해서는, Windows 98 시스템 하에서 소스 코드를 다시 컴파일해야만 한다. 스크립팅 언어 역시 소스 코드에서 시작을 하지만, 실행 가능 파일로 만들기 위한 컴파일 과정이 없다. 대신, 번역기(interpreter)가 소스 파일에서 지시문을 읽고 각 지시문을 실행시킨다. 불행히도, 번역기가 각 지시문을 하나 하나 읽어야만 하기 때문에, 일반적으로 번역기를 통해 실행되는 프로그램은 컴파일된 프로그램보다 느리다. 스크립팅 언어의 가장 큰 장점은 소스 파일을 어떤 운영 체제에나 쉽게 이식할 수 있으며 바로 그 자리에서 번역기를 통해 실행할 수 있다는 것이다. 이런 점은 작은 프로그램에서는 장점으로 여겨질 수 있지만, 큰 규모의 어플리케이션을 작성할 것을 계획하고 있다면 프로그래밍 언어를 사용하는 편이 알맞다. 스크립팅 언어의 예로는 Perl, Lisp, Tcl이 있다.
무엇을 알아야 하는가?
당신 자신의 쉘 스크립트를 작성하기 위해서는 매일 사용하는 기본적인 리눅스 명령어를 알아야 한다. 예를 들면, 어떻게 파일을 복사, 이동하고 새로운 파일을 만드는지 하는 것들을 알아야만 한다. 반드시 알아두어야 할 또 한 가지는 텍스트 편집기를 쓰는 방법이다. 리눅스의 대표적인 세 가지 텍스트 편집기는 vi, emacs, pico이다. 만일 vi나 emacs를 사용하는데 익숙하지 않다면, pico나 사용하기 쉬운 다른 텍스트 편집기를 이용하라.
주의!!!
루트 사용자인 상태에서는 절대로 스크립팅을 연습하지 않도록 하라! 어떤 일이 일어날 지 모른다. 만일 당신이 코딩을 하는 도중 우연히 실수를 해서 시스템을 망쳤다 해도 나는 책임질 수 없다. 루트 권한이 없는 일반 사용자 계정을 사용하라. 당신은 스크립팅 연습을 하기 위한 새로운 사용자를 만들기를 원할 지도 모른다. 이 경우, 일어날 수 있는 가장 나쁜 일은 새로 만든 사용자 디렉토리가 없어지는 것에 불과하다.
첫번째 BASH 프로그램
우리의 첫번째 프로그램은 고전적인 "Hello World" 프로그램이 될 것이다. 만일 당신이 이전에 프로그램을 해본 경험이 있다면, 지금까지 수많은 "Hello World" 프로그램을 봐왔을 것이다. 하지만, 이것은 전통이고, 누가 그 전통을 바꾸겠는가? 이 "Hello World" 프로그램은 단순히 "Hello World"란 문구를 화면에 프린트하는 것이다. 그럼 우선 텍스트 편집기를 시작해서, 그 안에 다음의 내용을 입력하라:
#!/bin/bash
echo "Hello World"
첫번째 라인은 리눅스에게 이 스크립트를 실행하기 위해서 bash 번역기를 사용하라고 알려준다. 이 경우에는, bash는 /bin 디렉토리에 있다. 만일 bash가 시스템의 다른 디렉토리에 있다면, 그 위치를 첫번째 라인에 써주어라. 스크립트 안에 있는 지시문을 실행할 때 어떤 인터프리터를 사용할 것인지 리눅스에게 알려주는 것이므로 번역기를 정확하게 명시하는 것은 매우 중요하다. 다음엔 스크립트를 hello.sh란 이름으로 저장하자. 모두 다 되었으면, 스크립트를 실행 가능하도록 한다:
xconsole$ chmod 700 ./hello.sh
파일의 퍼미션을 어떻게 바꾸는지 모른다면 chmod에 대한 매뉴얼 페이지를 참조하라. 일단 위의 명령을 실행하면 단지 파일 이름을 타이핑하는 것만으로 당신의 프로그램을 실행할 수 있다:
xconsole$ ./hello.sh
Hello World
됐다! 당신의 첫번째 프로그램이다! 그 자체로는 지루하고 쓸모없지만, 모든 사람이 이런 식으로 프로그래밍을 시작한다. 첫번째 프로그램을 실행하기까지의 과정을 되새겨 보자. 코드를 작성하고, 파일을 저장하고, chmod 명령으로 실행 가능하도록 만들었다.
명령들, 명령들, 명령들
당신의 첫번째 프로그램이 한 일은 정확히 무엇인가? 그 프로그램은 화면에 "Hello World"란 문구를 프린트했다. 하지만 어떻게 그렇게 한 것일까? 프로그램은 명령을 사용했다. 프로그램 상에서 당신이 썼던 딱 한 줄의 코드는 echo "Hello World"였다. 그렇다면, 어떤 것이 명령인가? 바로 echo이다. echo 프로그램은 하나의 인자를 취해서 그 인자를 화면에 프린트한다.
인자(argument)는 프로그램 이름을 입력한 다음에 따라온다. 첫번째 예제에서 당신이echo에 넘겨 준 인자는 "Hello World"이다. 당신이 ls /home/root란 명령을 입력할 때, ls에 대한 인자는 /home/root이다. 그렇다면 이 모든 것이 뜻하는 것은 무엇인가? 만일 당신이 인자 하나를 취해서 화면에 프린트하는 프로그램을 가지고 있다면, 그 프로그램 대신에 echo를 사용할 수 있다는 것이다. 우리가 foo라고 하는 프로그램을 가지고 있다고 가정하자. 이 프로그램은 문자열 하나를 인자로 가져서 화면에 출력한다. 우리는 위의 예제를 다음과 같이 다시 쓸 수 있다:
#!/bin/bash
foo "Hello World"
이 내용을 저장하고 chmod 로 실행 가능하도록 하여 실행하자:
xconsole$ ./hello
Hello World
결과는 정확하게 똑같다. 프로그램 이름을 제외하고 어느 한 곳이라도 다른 곳이 있는가? 전혀 없다. 실제로 무엇인가 작성한 것이 있는가? 당신이 echo 프로그램을 작성했다면 모르지만 그렇지 않다면 전혀 작성한 것이 없다. 당신이 한 것이라고는 이미 주어진 인자와 함께 echo 프로그램을 당신의 쉘 프로그램에 사용한 것 뿐이다. 실제로 echo 명령 대신 쓸 수 있는 명령은 printf 가 있다. 특히 C 프로그래밍에 능한 사람의 경우에는printf의 여러 기능을 이용해서 보다 다양한 결과를 낼 수 있다. 사실, 쉘 프로그램을 만들지 않고도 위의 예제와 꼭 같은 결과를 얻을 수 있다:
xconsole$ echo "Hello World"
Hello World
bash 쉘 스크립팅은 다양한 제어를 가능하게 하며 또한 배우기 쉽다. 방금 보았듯이, 쉘 프로그램에 리눅스 명령을 함께 쓸 수 있다. 쉘 프로그램은 특정한 일을 하기 위해 특별히 조립된 여러 프로그램의 집합체이다.
보다 유용한 프로그램
이제 우리는 모든 파일을 하나의 디렉토리로 옮기고, 그 디렉토리를 내용물 모두와 함께 지운 뒤, 다시 그 디렉토리를 만드는 프로그램을 만들 것이다. 이 작업은 다음의 명령들을 통해 이루어질 수 있다:
xconsole$ mkdir trash
xconsole$ mv * trash
xconsole$ rm -rf trash
xconsole$ mkdir trash
위의 명령을 쉘 상에서 대화식으로 입력하는 대신, 쉘 프로그램을 작성하자:
#!/bin/bash
mkdir trash
mv * trash
rm -rf trash
mkdir trash
echo "Deleted all files!"
위의 내용을 clean.sh으로 저장하자. 이제 당신이 해야 할 일은 clean.sh를 실행하는 것뿐이다. clean.sh가 모든 파일을 trash디렉토리로 옮기고, trash 디렉토리를 삭제하고는 다시 만든 후, 모든 파일이 성공적으로 삭제되었다는 메시지까지 출력한다. 만일 당신이 명령을 입력하고 기다렸다가 다시 입력해야 하는 일을 해야 한다면, 쉘 프로그램으로 자동화하는 방법을 고려하라.
주석
주석을 달면 코드 읽기가 보다 쉬워진다. 주석을 단다고 해서 프로그램의 출력에 영향을 주지 않는다. 코드를 읽는 사람을 위해 주석은 특별히 만들어졌다. bash에서 첫번째 라인(#!/bin/bash)을 제외한 모든 주석은 해쉬 심벌("#")로 시작한다. 첫번째 라인은 주석이 아니다. 첫번째 라인 다음에 오는 "#"으로 시작하는 모든 라인은 주석이다. 다음의 코드를 보자:
#!/bin/bash
# 이 프로그램은 1부터 10까지 카운트한다:
for i in 1 2 3 4 5 6 7 8 9 10; do
echo $i
done
만일 당신이 bash 스크립팅을 모른다고 해도, 주석 때문에 위의 프로그램이 무엇을 하는지 바로 알 수 있다. 주석을 적절하게 이용하는 것은 좋은 습관이다. 만일 훗날 당신의 프로그램을 유지 보수할 필요가 있다면, 주석을 달아 놓는 것이 일을 얼마나 편하게 만드는지 알게 될 것이다.
변수
변수는 기본적으로 값들을 담고 있는 "상자"이다. 당신은 많은 이유로 인해 변수들을 만들고 싶어할 것이다. 사용자 입력, 인자들, 또는 수치 값을 보관하기 위해 변수들이 필요하다. 다음 짧은 코드를 예로 들자:
#!/bin/bash
x=12
echo "The value of variable x is $x"
여기서 한 일은, x의 값을 12로 정한 것이다. echo "The value of variable x is $x" 이라는 라인은 x의 현재 값을 프린트한다. 변수를 정의할 때, 할당 연산자 "=" 사이에는 공백 문자가 있으면 안 된다. 여기 할당 연산자를 사용하는 문법이 있다:
variable_name=this_value
변수의 값은 변수 이름 앞에 달러 심벌"$"을 붙임으로써 얻을 수 있다. 위의 예제에서 보듯이, x의 값을 echo $x 를 사용해서 얻을 수 있다.
변수의 타입에는 지역 변수와 환경 변수, 이렇게 두 가지가 있다. 환경 변수들은 시스템에 의해 정해지고 주로 env 명령을 사용해서 볼 수 있다. 환경 변수는 특별한 값을 담고 있다. 예를 들어, 만일 다음과 같이 입력하면:
xconsole$ echo $SHELL
/bin/bash
현재 사용 중인 쉘의 이름을 얻을 수 있다. 환경 변수들은 /etc/profile과 ~/.bash_profile 안에 정의되어 있다. 환경 변수이든지, 지역 변수이든지 현재 변수 값을 체크할 때 echo 명령이 유용하게 쓰인다. 여전히 왜 변수가 필요한지 이해하지 못하겠다면, 여기 좋은 예제가 있다:
#!/bin/bash
echo "The value of x is 12."
echo "I have 12 pencils."
echo "He told me that the value of x is 12."
echo "I am 12 years old."
echo "How come the value of x is 12?"
이제 당신은 x의 값을 12 대신 8로 하기로 결정했다고 하자. 어떻게 하겠는가? "x is 12"라고 되어 있는 모든 라인을 바꿔야만 한다. 잠깐 기다려 보라…숫자 12를 가진 다른 라인들도 있다. 이 라인들도 바꿔야 하는가? 아니다. 그것들은 x와 관련이 없으니 그냥 둔다. 좀 복잡한가? 이제, 여기 변수를 사용한 것만 제외하면 똑 같은 예제가 있다:
#!/bin/bash
x=12 # 변수 x에 값 12를 대입한다
echo "The value of x is $x."
echo "I have 12 pencils."
echo "He told me that the value of x is $x."
echo "I am 12 years old." echo "How come the value of x is $x?"
여기서, 우리는 $x가 변수 x의 현재 값, 12를 프린트하는 것을 볼 수 있다. 그래서 만일 x의 값을 8로 바꾼다면, 당신이 해야하는 일은 x=12라고 되어 있는 라인을 x=8로 바꾸는 일뿐이다. 그러면 프로그램은 자동적으로 $x가 있는 라인을 12 대신 8로 바꾸어 보여 줄 것이다. 다른 라인은 바뀌지 않는다. 뒤에서 보겠지만, 변수는 다른 용도로도 중요하게 사용된다.
제어 구조
제어 구조는 당신의 프로그램을 보다 간결하게 하며, 프로그램이 결정을 내릴 수 있게 한다. 뿐만 아니라, 더욱 중요한 것은 에러를 체크하도록 할 수 있다는 것이다. 지금까지 우리가 본 예제는 모두 첫번째 라인에서부터 마지막 라인까지 프로그램의 모든 명령을 실행하는 프로그램이었다. 예를 들면 다음과 같은 방식이다:
#!/bin/bash
cp /etc/foo .
echo "Done."
이것은 bar.sh란 작은 프로그램인데, /etc/foo란 파일을 현재의 디렉토리로 복사하고는 화면에 "Done"이라고 프린트한다. 이 프로그램은 한 가지 조건 하에서 작동한다. /etc/foo라고 하는 파일이 반드시 존재해야만 한다. 그렇지 않다면 다음과 같은 화면을 보게 될 것이다:
xconsole$ ./bar.sh
cp: /etc/foo: No such file or directory
Done.
당신이 보듯이, 문제가 있다. 당신의 프로그램을 실행하는 모든 사람이 그들의 시스템에 /etc/foo라는 파일을 가지고 있지는 않다. 그래서 /etc/foo라는 파일이 있는지 체크해서 만일 그 파일이 있으면 복사하도록 하고, 그렇지 않다면 끝나도록 당신의 프로그램을 수정하는 편이 좋을 것이다. 의사 코드(pseudo code)로 이 내용을 나타내면 다음과 같다:
만일 /etc/foo가 존재하면,
/etc/foo를 현재 디렉토리로 복사한다
화면에 "Done."을 출력한다.
그렇지 않으면,
화면에 "This file does not exist."을 촐력한다
exit
이것을 bash에서 할 수 있을까? 물론! bash의 제어 구조에는 if, while, until, for, case가 있다. 각 구조는 시작을 나타내는 starting tag와 끝을 나타내는 ending tag로 쌍을 이루고 있다. 예를 들면, if 구조는 if로 시작해서 fi로 끝난다. 제어 구조는 당신의 시스템에서 발견되어지는 프로그램이 아니다. 그것들은 bash의 내재된 특성이다. 이러한 제어 구조를 이용해서, 단지 시스템의 프로그램만을 이용해서 쉘 프로그램을 만드는 것에 그치지 않고 당신 자신만의 고유한 코드를 작성할 수 있다.
if ... else ... elif ... fi
가장 널리 쓰이는 구조 중 하나가 if 구조이다. 이것을 이용해서 "만일 이 조건이 존재한다면 이것을 해라, 그렇지 않다면, 다른 것을 해라"라는 방식으로 프로그램이 결정을 내리도록 할 수 있다. 효율적으로 if 구조를 쓰기 위해서, 우리는 반드시 test 명령을 사용해야 한다. test는 파일의 존재 여부, 퍼미션 또는 유사점과 차이점을 체크한다. 여기 다시 작성한 bar.sh이 있다:
#!/bin/bash
if test -f /etc/foo
then
# 파일이 존재하면, 복사하고 메시지를 출력한다.
cp /etc/foo .
echo "Done."
else
# 파일이 존재하지 않으면, 메시지를 출력하고 프로그램을 종료한다.
echo "This file does not exist."
exit
fi
then과 else 다음 라인들을 들여 쓴 것에 주목하라. 들여쓰기는 선택 사항이지만, 그렇게 함으로써 어떤 조건 하에 어떤 라인이 실행될 것인지 눈에 잘 들어와서 코드를 훨씬 쉽게 읽을 수 있게 된다. 이제 프로그램을 실행하자. 만일 당신의 시스템에 /etc/foo 파일이 있다면, 프로그램은 파일을 복사한다. 그렇지 않다면, 에러 메시지를 출력할 것이다. test는 /etc/foo 파일이 존재하는지 체크한다. –f 옵션은 인자로 오는 것이 정규 파일인지 체크한다. 다음은 test의 옵션 리스트를 보여 주고 있다:
-d 파일이 디렉토리인지 체크
-e 파일이 존재하는지 체크
-f 파일이 일반적인 파일인지 체크
-g 파일이 SGID 퍼미션을 가졌는지 체크
-r 파일이 읽기 가능인지 체크
-s 파일의 크기가 0이 아닌지 체크
-u 파일이 SUID 퍼미션을 가졌는지 체크
-w 파일이 쓰기 가능인지 체크
-x 파일이 실행 가능인지 체크
else는 첫번째 조건이 만족되지 않아서 프로그램이 다른 일을 하도록 하고 싶을 때 사용된다. if문 안에 또 다른 if를 사용하고 싶을 때 사용할 수 있는 elif도 있다. 기본적으로 elif는 "else if"를 나타낸다. 첫번째 조건이 만족되지 않았고, 다른 조건을 테스트하고 싶을 때 elif를 사용한다.
만일 다음과 같은if와 test 구조 형식이 사용하기에 불편하다고 느껴진다면:
if test -f /etc/foo
then
다음과 같이 쓸 수 있다:
if [ -f /etc/foo ]; then
대괄호가 test 역할을 한다. 당신이 C 프로그래밍에 경험이 있다면, 이 문법이 보다 편안하게 느껴질 것이다. 양쪽 대괄호 사이에 공백 문자가 있어야 한다는 사실을 염두에 둬라. 세미콜론 ";"은 명령의 끝이라고 쉘에게 알려준다. 세미콜론 뒤에 오는 모든 것은 분리된 라인에 있는 것처럼 실행된다. 세미콜론을 사용함으로써 보다 읽기 쉬워진다. 물론 세미콜론을 사용하는 것은 선택 사항이다. 만일 괜찮다면, then을 다음 라인에 두자.
test에서 변수를 사용할 때, 따옴표로 변수를 둘러 싸서 사용하는 것은 좋은 생각이다. 다음의 예를 보자:
if [ "$name" -eq 5 ]; then
while ... do ... done
while 구조는 루프 구조이다. 기본적으로 while 구조가 하는 일을 말로 표현하면 다음과 같다. "이 조건이 참인 동안에는, 이것을 실행한다. 단, 조건이 더 이상 참이 아닐 때까지만". 이제 예제 하나를 보자:
#!/bin/bash
while true; do
echo "Press CTRL-C to quit."
done
true는 실제 하나의 프로그램이다. 이 프로그램이 하는 일은 중단없이 루프가 계속 돌도록 하는 것이다. while 구조에서 true를 사용할 때는 쉘 프로그램이 true를 부른 다음 실행해야 하므로 아무래도 속도면에서 느릴 수 밖에 없다. 하지만 true의 대신 ":" 명령을 사용할 수 있다.
#!/bin/bash
while :; do
echo "Press CTRL-C to quit."
done
이것은 정확하게 같은 결과를 보여주지만, ":" 명령이 bash 안에 내포된 특성이기 때문에 훨씬 빠르다. 위의 예제가 이전 예제와 다른 점은 읽기가 조금 어려워진 대신 속도가 빨라졌다는 것이다. true와 ":" 명령 중 더 편하게 느껴지는 것을 사용하라. 다음에 제시되는 변수를 사용하는 예제는 아마도 보다 더 유용할 것이다:
#!/bin/bash
x=0; # x의 값을 0으로 초기화
while [ "$x" -le 10 ]; do
echo "Current value of x: $x"
# x의 값을 증가시킨다:
x=$(expr $x + 1)
sleep 1
done
위의 예제에서 보듯이, 여기서는 test(대괄호 안의 폼)를 이용해서 변수 x의 조건을 체크하고 있다. 옵션 –le는 만일 x가 값 10과 같거나 작은지 체크한다. 위의 코드를 말로 하면, " x가 10 보다 작거나 같은 동안, x의 현재 값을 프린트하고는 x의 현재 값에 1을 더한다"이다. sleep 1 은 단지 프로그램이 잠깐 쉬도록 한다. 이 문장은 없어도 상관없다. 이 예제에서 하는 일은 상등(equality) 테스트이다. 만일 변수가 어떤 값과 같은지 체크해서 같다면 그에 해당하는 일을 하는 것이다. 여기서 상등 테스트들의 리스트를 보도록 하자:
숫자들 사이의 상등 체크:
x -eq y x가 y와 같은지 체크
x -ne y x가 y와 같지 않은지 체크
x -gt y x가 y 보다 큰지 체크
x -lt y x가 y 보다 작은지 체크
문자열 사이의 상등 체크:
x = y 문자열 x가 문자열 y와 같은지 체크
x != y 문자열 x가 문자열 y와 다른지 체크
-n x 문자열 x가 널 문자가 아니면 true로 간주함
-z x 문자열 x가 널 문자이면 true로 간주함.
위에서 우리가 작성한 루프 스크립트는 다음의 한 라인을 제외하면 이해하기에 어렵지 않다:
x=$(expr $x + 1)
위의 명령은 x의 값을 1만큼씩 증가시킨다고 알려주고 있다. 하지만 $(...)가 뜻하는 것은 무엇인가? 변수인가? 아니다. 사실, 이것은 당신이 명령 expr $x + 1 를 먼저 실행하고, 그 결과를 다시 x 값으로 하기 원한다고 쉘에게 알려주는 방법이다. 어떤 명령이든 $(...)으로 둘러 싸인 명령은 먼저 실행된다:
#!/bin/bash
me=$(whoami)
echo "I am $me."
이 예제를 실행해보면 내가 말하는 바를 이해하게 될 것이다. 위의 예제는 다음과 같이 쓰여질 수도 있는데 결과는 같다:
#!/bin/bash
echo "I am $(whoami)."
어느 쪽이 보다 읽기 쉬운지는 당신이 결정하라. 명령을 실행하거나 명령의 결과를 변수에 대입하는 다른 방법이 또 있다. 이것은 뒤에서 설명하도록 하겠다. 지금은 $(...)을 사용하자.
until ... do ... done
until 구조는 while 구조와 매우 유사하다. 단 하나의 차이점은 조건이 반대라는 점이다. while 구조는 조건이 참인 동안은 계속해서 반복되지만 until 구조는 조건이 참이 될 때까지 반복된다. 그래서 기본적으로 until 구조는 "이 조건이 참이 될 때까지, 이것을 해라"라는 구조이다. 다음 예제 하나를 보도록 하자:
#!/bin/bash
x=0
until [ "$x" -ge 10 ]; do
echo "Current value of x: $x"
x=$(expr $x + 1)
sleep 1
done
아마도 이 짧은 코드는 어디서 본 듯할 것이다. 한번 실행해서 결과가 어떻게 나오는지 보라. 기본적으로, until은 x가 10 보다 크거나 같게 될 때까지 계속해서 반복할 것이다. x의 값이 10에 이르게 되면, 루프는 정지할 것이다. 따라서, 마지막에 프린트 되는 x의 값은 9가 될 것이다.
for ... in ... do ... done
for 구조는 일정 범위의 변수 안에서 루프를 돌 때 사용된다. 예를 들자면, 매 초마다 10개의 점을 프린트하는 작은 프로그램을 작성할 때 for를 사용할 수 있다:
#!/bin/bash
echo -n "Checking system for errors"
for dots in 1 2 3 4 5 6 7 8 9 10; do
echo -n "."
echo "System clean."
done
모르는 사람도 있을 수 있으므로, -n 옵션에 대해 설명하자. -n 옵션은 echo 사용 시 자동적으로 개행 문자가 더해지지 않도록 하기 위해 사용된다. 한번은 –n 옵션을 가지고, 또 한번은 없이 실행해보면 내가 말하는 것이 무엇인지 알 수 있을 것이다. 변수 dots은 1부터 10까지의 값을 갖고 루프를 돌면서 각 값마다 점 하나씩을 프린트한다. 변수가 값을 갖고 루프를 돈다는 것이 무엇을 뜻하는지 보기 위해 다음의 예제를 해보자:
#!/bin/bash
for x in paper pencil pen; do
echo "The value of variable x is: $x"
sleep 1
done
프로그램을 실행할 때, 맨 처음엔 x가 paper란 값을 갖고, 다음엔 pencil, 그 다음엔 pen이란 값을 갖는 것을 볼 수 있다. 더 이상의 값이 없을 때, 루프는 끝난다.
여기 좀 더 유용한 예제가 있다. 다음 프로그램은 현재 디렉토리에 있는 모든 파일에 .html 확장자를 붙인다:
#!/bin/bash
for file in *; do
echo "Adding .html extension to $file..."
mv $file $file.html
sleep 1
done
*는 와일드 카드 문자이다. *가 뜻하는 것은 "현재 디렉토리의 모든 것"으로 이 예제에서는 현재 디렉토리의 모든 파일을 뜻한다. 이 프로그램을 실행하면 현재 디렉토리의 모든 파일 뒤에 .html 확장자가 붙게 된다. 변수 file이 모든 값을 가지면서 루프를 돈다는 것을 상기하라. 이 경우에는 현재 디렉토리의 파일들을 값으로 갖는다. mv는 변수 file의 값을 .html 확장자를 가진 이름으로 수정하는데 사용된다.
case ... in ... done
case 구조는 if 구조와 매우 유사하다. 기본적으로 체크 해야할 조건이 많고 if 문을 계속해서 쓰고 싶지 않을 때 case 구조는 그 위력을 발휘한다. 다음의 짧은 코드를 보자:
#!/bin/bash
x=5 # x 값을 5로 초기화
# 이제 x의 값을 체크한다:
case $x in
0) echo "Value of x is 0."
;;
5) echo "Value of x is 5."
;;
9) echo "Value of x is 9."
;;
*) echo "Unrecognized value."
esac
case 구조는 x의 값에 대해서 세 가지의 가능성을 체크할 것이다. 이 예제에서는 처음에 x의 값이 0인지 체크 한 후, 그 값이 5인지 체크하고 다음에는 9인지 체크한다. 마지막으로 앞의 모든 경우에 해당하지 않는다면, "Unrecognized value."라는 메시지를 출력한다. "*"가 "모든 것"을 뜻한다고 했던 것을 기억하라. 이 예제에서는 "앞서 명시되었던 값이 아닌 모든 값"을 뜻한다. 만일 x의 값이 0, 5, 9가 아닌 다른 값이라면 그 값은 *의 범주에 들어가게 된다. case를 사용할 때, 각 조건은 반드시 두 개의 세미콜론으로 끝나야 한다. if를 쓸 수 있는데도 왜 case를 사용하는가? 여기 if를 사용해서 작성한 위의 예제와 똑 같은 프로그램이 있다. 어느 쪽이 작성하기 쉬우며 읽기 쉬운지 비교해 보라:
#!/bin/bash
x=5 # x의 값을 5로 초기화
if [ "$x" -eq 0 ]; then
echo "Value of x is 0."
elif [ "$x" -eq 5 ]; then
echo "Value of x is 5."
elif [ "$x" -eq 9 ]; then
echo "Value of x is 9."
else
echo "Unrecognized value."
fi
인용 부호
쉘 스크립팅에서 인용 부호는 중요한 부분을 차지한다. 인용 부호에는 세 가지 종류가 있다. 따옴표: ", 작은 따옴표(어포스트로피): ', 역 따옴표(억음 악센트라고도 함): `의 세 가지이다. 이들 각각이 의미하는 것이 다른가? 그렇다.
따옴표는 주로 공백 문자를 포함한 문자열을 담을 때 이용된다. 예를 들면, "This string contains whitespace."을 보자. 따옴표로 둘러 싸인 문자열은 하나의 인자로서 취급된다. 다음의 예제를 보자:
xconsole$ mkdir hello world
xconsole$ ls -F
hello/ world/
이 예제에서 우리는 두 개의 디렉토리를 만들었다. mkdir은 hello와 world를 두 개의 인자로 받아들여서 두 개의 디렉토리를 만든 것이다. 이제, 다음과 같이 했을 때의 결과는 어떤지 살펴 보도록 하자:
xconsole$ mkdir "hello world"
xconsole$ ls -F
hello/ hello world/ world/
이번에는 이름이 두 단어로 이루어진 디렉토리를 만들었다. 따옴표가 두 단어를 하나의 인자로 만든 것이다. 따옴표가 없다면, mkdir은 hello를 첫번째 인자로, world를 두번째 인자로 생각할 것이다.
작은 따옴표는 주로 변수를 다룰 때에 사용된다. 만일 변수가 따옴표로 둘러싸여 있으면 그 값의 수치가 구해질 것이다. 만일 작은 따옴표로 둘러싸여 있으면 그 값의 수치는 계산되어지지 않는다. 이것이 뜻하는 바를 명확히 하기 위해 다음의 예제를 실행해보자:
#!/bin/bash
x=5 # initialize x to 5
# use double quotes
echo "Using double quotes, the value of x is: $x"
# use forward quotes
echo 'Using forward quotes, the value of x is: $x'
무엇이 차이 나는지 보았는가? 만일 문자열을 변수로 사용할 계획이 아니라면 따옴표로 둘러싸서 사용할 수 있다. 하지만 어떻게 할 지 정하지 못했다면 작은 따옴표로도 따옴표처럼 문자열에 공백 문자를 포함하도록 사용할 수 있다:
xconsole$ mkdir 'hello world'
xconsole$ ls -F
hello world/
역 따옴표는 따옴표나 작은 따옴표와는 완전히 다른 용도로 쓰인다. 공백 문자를 포함하는 데에는 쓰이지 않는다. 이 글의 앞쪽에서 다음 라인을 사용했던 것을 기억해 보라:
x=$(expr $x + 1)
이미 당신이 알듯이, 명령 expr $x + 1의 결과가 변수 x에 들어가게 된다. 역 따옴표를 사용한 다음의 명령도 꼭 같은 결과를 낸다:
x=`expr $x + 1`
어느 것을 사용해야 하는가 하는 문제는 순전히 당신에게 달려 있다. 당신이 좋은 쪽을 선택하라. 역 따옴표가 $(...) 보다 자주 사용되는 것을 볼 수 있을 것이다. 하지만, $(...)가 보다 읽기 쉽다. 다음과 같은 경우엔 특히 더 그렇다:
$!/bin/bash
echo "I am `whoami`"
BASH에서의 수식 연산
bash 은 수식 표현을 수행할 수 있도록 해준다. 이미 보았듯이, 수식 연산은 expr 명령을 통해서 수행된다. 그러나, expr은 true 명령과 같이 느린 것으로 생각된다. 이들 명령이 느린 이유는 이들 명령을 수행하기 위해, 쉘은 그것들을 시작해야 한다. 쉘 자체의 내포된 특성을 이용하는 것이 훨씬 더 빠르다. 그래서 true 대신에 ":"을 사용하는 것을 앞서서 보았다. expr을 사용하는 대신, 계산하고자 하는 수식을 $((...)) 안에 쓰면 된다. 이것은 $(...)과는 다르다. 괄호의 숫자가 다르지 않은가. 다음 예제를 실행해 보자:
#!/bin/bash
x=8 # x의 값을 8로 초기화
y=4 # y의 값을 4로 초기화
# 이제 x와 y의 합을 z에 대입한다:
z=$(($x + $y))
echo "The sum of $x + $y is $z"
어떤 것을 선택하든지, 순전히 당신에게 달려 있다. 만일 당신이 $((...))을 사용하는 것보다 expr을 사용하는 것이 편하다면, 그것을 사용하면 된다.
bash에서는 덧셈, 뺄셈, 곱셈, 나눗셈, 모듈러스 계산이 가능하다. 각 계산이 그에 해당하는 연산자를 가지고 있다:
계산 연산자
덧셈 +
뺄셈 -
곱셈 *
나눗셈 /
모듈러스 %
처음의 네 개 연산자는 모든 사람들에게 익숙할 것이다. 마지막에 있는 모듈러스는 두 개의 값으로 나눗셈을 할 때 나머지 값을 말하는 것이다. 다음은 bash에서의 수식 연산에 대한 예제이다:
#!/bin/bash
x=5 # x의 값을 5로 초기화
y=3 # y의 값을 3로 초기화
add=$(($x + $y)) # x와 y 값을 더해서 변수add에 대입
sub=$(($x - $y)) # x의 값에서 y의 값을 빼서 변수 sub에 대입
mul=$(($x * $y)) # x와 y 값을 곱해서 변수 mul에 대입
div=$(($x / $y)) # x의 값을 y의 값으로 나누어 변수div에 대입
mod=$(($x % $y)) # x / y의 나머지 값을 구해 변수 mod에 대입
# 답을 출력:
echo "Sum: $add"
echo "Difference: $sub"
echo "Product: $mul"
echo "Quotient: $div"
echo "Remainder: $mod"
위의 예제 코드는 expr을 사용해서 다시 쓸 수도 있다. 예를 들면, add=$(($x + $y)) 대신에 add=$(expr $x + $y)이나, add=`expr $x + $y`를 사용할 수 있다.
사용자 입력 읽기
이제부터 재미있는 부분이 시작된다. 당신의 프로그램이 사용자와 대화하도록 만들수 있다. 사용자로부터 입력을 받아들이는 명령은 read이다. read는 변수를 사용하는 bash에 내포된 명령이다. 다음의 예를 보자:
#!/bin/bash
# 사용자 이름을 받아들이고 인사를 출력한다
echo -n "Enter your name: "
read user_name
echo "Hello $user_name!"
이 예제에서 변수는 user_name이다. 물론 변수 이름은 당신이 좋아하는 다른 것으로 불러도 된다. read 는 사용자가 무엇인가 입력하고는 엔터키를 누르기를 기다린다. 만일 아무 것도 입력되지 않고 엔터키가 눌러지면, read 는 코드의 다음 라인을 실행한다. 한번 해보라. 여기 사용자가 무엇인가 입력했는지 확인하기 위해 체크하는 것만 제외하고는 위의 예제와 꼭 같은 코드가 있다:
#!/bin/bash
# 사용자 이름을 받아들이고 인사를 출력한다
echo -n "Enter your name: "
read user_name
# 사용자가 아무 것도 입력하지 않으면:
if [ -z "$user_name" ]; then
echo "You did not tell me your name!"
exit
fi
echo "Hello $user_name!"
여기에서, 만일 사용자가 아무 것도 입력하지 않고 엔터키를 누른다면, 우리의 프로그램은 불평을 하고는 끝나 버릴 것이다. 사용자 입력을 받아들이는 것은 사용자가 특정한 것을 입력하도록 하는 대화식 프로그램에 유용하게 쓰인다. 예를 들면, 간단한 데이터베이스를 만들고 사용자에게 데이터베이스에 들어 갈 내용을 입력하도록 할 수 있을 것이다.
함수
함수를 이용하면 스크립팅은 더욱 쉬워지고 코드는 유지 보수하기가 쉬워진다. 기본적으로 함수는 프로그램을 작은 조각으로 나눈다. 함수는 당신이 정의한 일을 수행하고 만일 당신이 원한다면 어떤 값을 리턴할 수도 있다. 함수에 관한 내용을 계속해서 설명하기 전에, 함수를 이용한 쉘 프로그램의 예를 하나 보기로 하자:
#!/bin/bash
# 함수 hello() 는 단지 메시지를 프린트하기만 한다
hello()
{
echo "You are in function hello()"
}
echo "Calling function hello()..."
# 함수 hello() 를 부른다:
hello
echo "You are now out of function hello()"
위의 예제를 실행해 보아라. 함수 hello() 는 여기서는 메시지를 프린트하는 단 하나의 목적을 가지고 있다. 물론 함수는 보다 복잡한 작업을 하도록 만들 수 있다. 위의 예에서, 우리는 함수 hello()를 다음과 같이 이름을 이용해서 불렀다:
hello
이 라인이 실행되었을 때, bash는 hello()가 있는 라인을 찾는다. 그 라인을 맨 처음에서 찾자마자, hello() 함수의 내용을 실행한다.
위에서 봤듯이, 함수는 항상 그것의 이름으로 불리워진다. 함수를 작성할 때는 위에서 했듯이 function_name()으로 시작하거나, 함수라는 것을 명시하고 싶다면 function function_name()으로 시작할 수 있다. 함수 hello()를 다음과 같이 작성할 수도 있다:
function hello()
{
echo "You are in function hello()"
}
함수는 항상 빈 괄호"()"를 가지고 시작해서는 그 뒤에 시작과 끝을 나타내는 중괄호"{...}"가 온다. 이 중괄호는 함수의 시작과 끝을 알려준다. 중괄호로 둘러싸인 모든 코드는 함수가 불려지면 실행될 것이고 오직 그 함수에만 속한다. 함수는 항상 불려지기 이전에 정의되어야 한다. 다음의 예제는 위의 예제 프로그램의 순서를 바꿔서 함수가 정의되기 전에 부르도록 하였다
#!/bin/bash
echo "Calling function hello()..."
# 함수hello() 를 부른다:
hello echo "You are now out of function hello()"
# function hello() just prints a message
hello()
{
echo "You are in function hello()"
}
위의 예제를 실행한 결과는 다음과 같다:
xconsole$ ./hello.sh
Calling function hello()...
./hello.sh: hello: command not found
You are now out of function hello()
당신이 보듯이, 에러가 발생했다. 그러므로, 항상 코드를 작성할 때는 함수를 처음에 정의하라. 적어도 함수가 불려지기 전에 정의하라. 여기 함수를 이용하는 또 다른 예가 있다:
#!/bin/bash
# admin.sh – 관리 도구
# 함수 new_user() 는 새로운 사용자 계정을 만든다
new_user()
{
echo "Preparing to add a new user..."
sleep 2
adduser # run the adduser program
}
echo "1. Add user"
echo "2. Exit"
echo "Enter your choice: "
read choice
case $choice in
1) adduser # 함수 adduser()를 부른다
;;
*) exit
;;
esac
이 예제가 제대로 실행되기 위해서는, 당신은 루트 사용자이어야 한다. adduser 가 오직 루트만 실행할 수 있는 프로그램이기 때문이다. 짤막한 이 예제로 함수가 얼마나 유용하게 쓰일 수 있는지 당신이 알 수 있으면 좋겠다.
시그널 이용하기
당신의 프로그램에서 시그널을 붙잡아 이용하기 위해서 내포된 명령인 trap을 쓸 수 있다. 이것은 프로그램이 실행되고 있는 도중에 갑자기 아무런 메시지도 없이 끝나 버리는 일없이 우아하게 종료시킬 수 있는 좋은 방법이다. 예를 들어 보자. 만일 당신이 프로그램을 실행하고 있는 중이라면, CTRL-C를 누르는 것은 프로그램에게 인터럽트(interrupt) 시그널을 보낸다. 인터럽트 시그널은 프로그램을 강제로 종료 시킨다. trap 명령을 사용하면 이 시그널을 잡아내서 프로그램을 계속할 것인지 아니면 사용자에게 프로그램을 종료한다는 메시지를 보내도록 하든지 하는 선택을 할 수 있다. trap은 다음의 문법으로 사용된다:
trap action signal
action 은 시그널을 잡아 냈을 때 당신이 수행하고 싶은 일이고 signal은 잡아 내고자 하는 시그널이다. 시그널의 리스트는 trap –l 명령으로 볼 수 있다. 당신의 쉘 프로그램에서 시그널을 이용할 때, 시그널의 처음 세 글자, 보통은 SIG를 생략한다. 예를 들면, 인터럽트 시그널은 SIGINT이지만 다음에 보게 될 예제에서는 INT만을 이용한다. 시그널 이름에 덧붙여진 시그널 번호를 이용해도 된다. 예를 들어, SIGINT의 수치 값은 2이다. 다음의 프로그램을 실행해 보자:
#!/bin/bash
# trap 명령을 이용하기
# CTRL-C 를 붙잡아고 함수 sorry()를 실행:
trap sorry INT
# 함수 sorry()는 메시지를 출력한다
sorry()
{
echo "I'm sorry Dave. I can't do that."
sleep 3
}
# 10부터 1까지 카운트:
for i in 10 9 8 7 6 5 4 3 2 1; do
$i seconds until system failure."
sleep 1
done
echo "System failure."
이제, 프로그램이 실행되어 카운트 다운을 하고 있는 동안에, CTRL-C을 누르자. 그러면 프로그램에 인터럽트 시그널이 보내질 것이다. 하지만, 그 시그널은 trap 명령에 의해 붙잡히고, trap 명령은 sorry() 함수를 실행할 것이다. action 자리에 "''" 을 둠으로써 trap이 시그널을 무시하도록 할 수 있다. 또한 "-"를 써서trap을 리셋할 수도 있다. 그 예를 보자:
# 만일 SIGINT 시그널이 붙잡히면 함수sorry()를 실행한다:
trap sorry INT
# trap을 리셋한다:
trap - INT
# SIGINT가 붙잡혀도 아무 것도 하지 않도록 한다:
trap '' INT
trap을 리셋하면 프로그램을 인터럽트하고 강제로 종료하는 원래의 작업이 시행된다. trap이 아무 것도 하지 않도록 하면 들어온 시그널을 무시하고 프로그램은 계속 실행된다.
AND 와 OR
앞에서 제어 구조의 사용과 그 유용성에 대해 살펴 보았다. 거기에 더해질 수 있는 두 가지 내용이 더 있다. 바로 AND "&&"와 OR "||" 구문이다. AND 구문은 다음과 같다:
조건_1 && 조건_2
AND 구문은 처음에 가장 왼쪽에 있는 조건을 체크한다. 만일 참이라면, 두 번째 조건을 체크한다. 두 번째 조건도 참이라면, 나머지 코드가 실행된다. 만일 조건_1이 거짓이라면 조건_2는 실행되지 않는다. 말로 풀어 쓰면:
만일 조건_1이 참이면, 그리고 만일 조건_2가 참이면, 그렇다면...
AND 구문을 사용하는 예제를 보도록 하자:
#!/bin/bash
x=5
y=10
if [ "$x" -eq 5 ] && [ "$y" -eq 10 ]; then
echo "Both conditions are true."
else
echo "The conditions are not true."
fi
여기서, x와 y, 두 변수 모두 우리가 체크하는 값들을 가지고 있으므로 조건들이 참이 된다. 만일 값을 x=5를 x=12로 바꾸고 다시 프로그램을 실행시키면, 이제는 조건이 거짓이 된다.
OR 구문도 비슷한 방식으로 사용된다. 단 한 가지 차이점은 가장 왼쪽의 조건이 거짓인지 체크한 다음에 그 다음 조건을 체크한다는 사실이다:
조건_1 || 조건_2
의사 코드로 이것을 번역해 보면 다음과 같다:
만일 조건_1이 참이라면, 또는 조건_2가 참이라면, 그렇다면...
그렇기 때문에, 테스트되는 조건 중 어느 것 하나라도 참이면 뒤에 오는 코드는 모두 실행된다:
#!/bin/bash
x=3
y=2
if [ "$x" -eq 5 ] || [ "$y" -eq 2 ]; then
echo "One of the conditions is true."
else
echo "None of the conditions are true."
fi
위의 예제에서, 두 개의 조건 중 하나만이 참이라는 것을 알 수 있을 것이다. 하지만, y의 값을 다른 것으로 바꾸고 다시 실행시키면 두 조건 모두 참이 아니라는 것을 알 수 있을 것이다.
AND와 OR 구문 대신에 if 구조를 쓸 수도 있다. 하지만 그렇게 하면 중첩된(nesting) if 문장들을 쓰게 된다. 중첩되었다는 것(nesting)은 if 구조 안에 또 다른 if 구조가 있는 것을 말한다. 물론 다른 제어 구조를 중첩해서 쓰는 것도 가능하다. 다음은 앞선 AND 코드와 같은 내용을 중첩된 if 구조를 이용해서 다시 작성한 것이다:
#!/bin/bash
x=5
y=10
if [ "$x" -eq 5 ]; then
if [ "$y" -eq 10 ]; then
echo "Both conditions are true."
else
echo "The conditions are not true."
fi
fi
이 코드는 AND 구문을 사용하는 것과 같은 목적으로 작성된 것이지만 AND 구문보다 읽기가 어렵고 작성하는데 보다 많은 시간이 걸린다. AND와 OR 구문을 이용하는 것이 더 현명하다.
인자(argument) 사용하기
리눅스 프로그램의 대부분이 비대화식이라는 것을 알고 있을 것이다. 프로그램들은 인자를 입력하도록 요구하고, 만일 인자를 입력하지 않으면 "usage" 메시지를 출력한다. more 명령을 예로 들자. 만일 more 명령 다음에 파일 이름을 입력하지 않으면, 바로 "usage" 메시지를 내보낸다. 당신의 쉘 프로그램이 인자들에 대해 일하도록 하는 것은 가능하다. 그렇게 하기 위해서, "$#" 변수를 알아야만 한다. 이 변수는 프로그램에 넘겨지는 인자들의 총 개수를 나타낸다. 예를 들어, 다음의 프로그램을 실행시킨다면:
xconsole$ foo argument
$#는 1이라는 값을 가질 것이다. 프로그램 foo에 넘겨지는 인자가 하나뿐이기 때문이다. 만일 인자가 두 개라면, $#는 2라는 값을 가질 것이다. 뿐만 아니라, 명령 라인의 각 단어들, 즉, 프로그램 이름(이 경우엔, foo)과 인자들은 쉘 프로그램 안에서 변수로 사용될 수 있다. foo는 $0이 되고 argument는 $1이 된다. 변수는 최고 9개까지 가능한데, 프로그램 이름인 $0과 그 뒤의 인자 하나 하나에 대응되는 $1부터 $9까지를 변수로 가질 수 있다. 다음의 예제를 보도록 하자:
#!/bin/bash
# 첫번째 인자를 출력한다
# 처음에 인자를 갖는지 체크한다:
if [ "$#" -ne 1 ]; then
echo "usage: $0 "
fi
echo "The argument is $1"
이 프로그램을 실행하기 위해서는 오직 한 개의 인자를 필요로 한다. 만일 하나보다 작거나 많은 인자를 입력하면 프로그램은 usage 메시지를 출력할 것이다. 그렇지 않고 한 개의 인자를 프로그램에 건네면, 이 쉘 프로그램은 당신이 건넨 인자를 출력할 것이다. $0가 프로그램의 이름이므로 "usage" 메시지에 사용된다. 마지막 라인에서 $1을 사용하고 있다. $1에 프로그램에 넘겨지는 인자의 값이 들어 있다는 것을 기억하라.
출력 재지정과 파이프
일반적으로, 당신이 명령을 실행할 때, 그 출력은 화면에 나타난다. 예를 들면:
xconsole$ echo "Hello World"
Hello World
출력 재지정(redirection)은 출력 방향을 다른 곳(대부분은 파일)으로 바꿀 수 있게 해 준다. ">" 연산자는 출력의 방향을 재지정할 때 사용된다. 화살표를 출력 내용이 가야할 곳으로 생각하면 이해하기 쉽다. 여기 출력을 파일로 지정한 예제가 있다:
xconsole$ echo "Hello World"> foo.file
xconsole$ cat foo.file
Hello World
이 예제에서, echo "Hello World"의 출력은 foo.file란 이름의 파일로 재지정되었다. 출력 내용이 파일의 내용으로 된 것을 볼 수 있다. ">" 연산자의 한 가지 문제점은 파일의 내용을 겹쳐 쓴다는 것이다. 만일 파일 내용을 겹쳐 쓰지 않고 뒤에 덧붙이고 싶다면 어떻게 하면 될까? 파일 뒤에 덧붙이기 위해서는 반드시 ">>" 연산자를 써야 한다. 이 연산자는 파일 내용을 겹쳐 쓰지 않고 뒤에 덧붙인다는 점을 제외하면 출력 재지정 연산자와 꼭 같다.
마지막으로, 파이프에 대해 이야기 하자. 파이프는 프로그램으로부터 나오는 출력 결과를 가져와서, 다른 프로그램의 입력으로 사용할 수 있게 한다. 파이프를 이용하기 위해서는 파이프 연산자: "|"를 쓰면 된다. "|"는 영문자 "L"의 소문자가 아니다. 파이프 연산자는 SHIFT-\를 이용해서 얻을 수 있다. 이제 파이프에 관한 예제를 보자:
xconsole$ cat /etc/passwd | grep xconsole
xconsole:x:1002:100:X_console,,,:/home/xconsole:/bin/bash
이 예제에서 우리는 /etc/passwd 파일 전체를 읽은 뒤, 파이프를 이용해서 그 출력을 입력 내용 중 xconsole이란 문자열을 찾는grep 명령의 입력으로 사용하여 그 문자열을 포함한 라인을 화면에 출력했다. 마지막 출력을 파일에 저장하기 위해 출력 재지정을 함께 쓸 수도 있다:
xconsole$ cat /etc/passwd | grep xconsole > foo.file
xconsole$ cat foo.file
xconsole:x:1002:100:X_console,,,:/home/xconsole:/bin/bash
제대로 작동하고 있다. /etc/passwd 파일을 읽은 뒤, 전체 출력 내용을 파이프를 통해서 문자열xconsole 을 찾는 grep 명령의 입력으로 사용하고는 최종 출력 결과를 리다이렉션해서 foo.file 에 저장했다. 쉘 프로그램을 작성할 때 출력 재지정과 파이프가 유용한 도구로 쓰인다는 것을 알게 될 것이다.
임시 파일
때때로 임시 파일을 만들어야 할 필요가 있을 때가 있다. 이 파일은 임시적으로 어떤 데이터를 갖고 있거나, 또는 단지 어떤 프로그램과 함께 작동할 수도 있다. 일단 프로그램의 목적이 달성되면, 대부분의 임시 파일은 삭제된다. 파일을 만들 때는 파일의 이름을 주어야만 한다. 파일을 만들 때의 문제는 새로 만드는 파일 이름과 같은 이름의 파일이 같은 디렉토리 안에 있으면 안된다는 것이다. 만일 그렇지 않게 되면, 중요한 데이터를 겹쳐 쓸 수도 있다. 하나뿐인 이름의 임시 파일을 만들기 위해, "$$" 기호를 파일 이름의 첫머리나 끝에 붙일 필요가 있다. 예를 들어, hello라는 이름을 가진 임시 파일을 만들고 싶다고 하자. 당신의 프로그램을 실행시키는 사용자 또한 hello라는 파일을 가질 수 있다고 하면 당신의 프로그램에서 사용하는 임시 파일과 충돌이 일어날 수 있다. hello라는 파일 대신에 hello.$$ 또는 $$hello라는 이름의 파일을 만들어서 단 하나뿐인 파일을 만들 수 있다. 다음을 실행해 보자:
xconsole$ touch hello
xconsole$ ls
hello
xconsole$ touch hello.$$
xconsole$ ls
hello hello.689
당신의 임시 파일이 있는 것을 볼 수 있다.
리턴 값
대부분의 프로그램은 프로그램을 어떻게 나가느냐에 따라 특정한 값을 리턴한다. 예를 들면, grep의 매뉴얼 페이지를 보면 찾고자 하는 문자열을 발견하면 grep은 0을 리턴하고 그렇지 않으면 1을 리턴한다고 나와 있다. 왜 프로그램의 리턴 값에 대해 신경을 써야 하는 것인가? 여러 가지 이유에서이다. 당신이 특정 사용자가 시스템에 있는지 체크하고 싶다고 하자. 이것을 체크해 볼 방법 중의 하나는 /etc/passwd 파일 안에 사용자의 이름이 있는지 grep 명령을 통해 찾아 보는 것이다. 찾고자 하는 사용자 이름이 foobar라고 하자:
xconsole$ grep "foobar" /etc/passwd
xconsole$
아무 것도 출력되지 않았다. 이것이 뜻하는 것은 grep 명령이 찾고자 하는 문자열과 일치하는 것을 찾지 못했다는 것이다. 만일 일치하는 문자열을 찾지 못했다고 메시지를 출력하면 훨씬 더 도움이 될 것이다. 이 때가 바로 프로그램의 리턴 값을 얻을 필요가 있는 때이다. 프로그램의 리턴 값은 특별한 변수가 가지고 있다. 그 변수는 $?이다. 다음의 짧은 코드를 보도록 하자:
#!/bin/bash
# 사용자 foobar 를 찾고 모든 출력을 파이프를 통해 /dev/null로 보낸다:
grep "foobar" > /dev/null 2>&1
# 리턴 값을 받아서 그에 해당하는 코드를 실행한다:
if [ "$?" -eq 0 ]; then
echo "Match found."
exit
else
echo "No match found."
fi
이제 프로그램을 실행하면, grep의 리턴 값을 붙잡을 것이다. 만일 그 값이 0과 같다면, 일치하는 문자열이 발견된 것이어서 그에 해당하는 메시지가 출력된다. 그렇지 않으면, 일치하는 문자열이 없다고 메시지를 출력한다. 이것은 프로그램의 리턴 값을 받아 이용하는 가장 기초적인 예제이다. 계속해서 연습하다 보면, 당신이 원하는 것을 하기 위해서 프로그램의 리턴 값이 필요한 때가 많을 것이다.
그렇다면 프로그램이 종료되는 상황에 따라 당신의 쉘 스크립트가 특정한 값을 리턴하게 하고 싶다면 어떻게 해야 하는가? exit 명령은 리턴할 값을 하나의 인자로서 가진다. 일반적으로 숫자 0은 성공적인 종료, 에러가 하나도 없이 프로그램이 끝났음을 가리킨다. 일반적으로 0보다 크거나 작은 숫자는 에러가 생겼음을 뜻한다. 리턴 값을 정하는 문제는 프로그래머가 결정할 문제이다. 다음의 프로그램을 보자:
#!/bin/bash
if [ -f "/etc/passwd" ]; then
echo "Password file exists."
exit 0
else
echo "No such file."
exit 1
fi
exit에 리턴 값을 명시하였으므로, 이 스크립트를 이용하는 다른 쉘 스크립트에서는 이 스크립트의 리턴 값을 붙잡아 사용할 수 있다.
수치 값인 인자 하나를 갖는 return 명령을 이용해서, 함수들도 값을 리턴할 수 있다. 함수에 적용된다는 점을 제외하면 exit가 사용되는 방식과 똑 같은 방법으로 return을 이용할 수 있다. 다음 예를 보자:
check_passwd()
{
# passwd 파일이 존재하는지 체크:
if [ -f "/etc/passwd" ]; then
echo "Password file exists."
# 존재하면 0을 리턴한다:
return 0
else
# 존재하지 않는다면 1을 리턴한다:
echo "No such file."
return 1
fi
}
# 함수 check_passwd()로부터 리턴 값을 얻는다:
foo=check_passwd
# 값을 체크한다:
if [ "$foo" -eq 0 ]; then
echo "File exists."
exit 0
else
echo "No such file."
exit 1
fi
코드를 자세히 보라. 이해하기 어렵지 않다. 함수 check_passwd()의 리턴 값을 가지는 foo라는 이름의 변수를 가지고 시작한다. 함수 check_passwd()에서 우리는 /etc/passwd 파일이 존재하는지 체크한다. 만일 존재한다면, 0을 리턴하고, 그렇지 않으면 1을 리턴한다. 이제 함수를 빠져 나오고 리턴된 값이 0이면 변수 foo의 값은 0이다. 만일 1이 리턴되면, 변수 foo의 값은 1이다. 이 예제에서 그 다음에 행해진 일은 변수 foo의 값을 체크해서 그에 해당하는 메시지를 프린트하고 0(성공한 경우) 또는 1(실패한 경우)의 리턴 값을 가지고 종료한다.
결론
이것으로 bash 스크립팅에 관한 소개를 마치고자 한다. 하지만 당신의 스크립팅 공부는 아직 끝나지 않았다. 더 알아야 할 것이 많다. 내가 말한 대로, 이 글은 bash 스크립팅에 관한 소개의 글이지만 당신이 쉘 프로그램을 수정하고 당신 자신의 쉘 프로그램을 작성하기 위한 디딤돌이 되기에는 충분하다. Learning the bash shell, 2nd Edition by O'Reilly & Associates, Inc을 구입하기를 강력히 추천하는 바이다. bash 스크립팅은 매일 매일의 관리자 작업을 할 때 사용하기에 매우 훌륭하다. 하지만 당신이 좀 더 큰 프로젝트를 계획하고 있다면, C나 Perl과 같은 보다 강력한 언어를 사용하기 원할 것이다. 행운을 빈다.