본문 바로가기

프로그래밍(TA, AA)/스크립트

[쉘스크립트] 고급 Bash 스크립팅 가이드 Part1

고급 Bash 스크립팅 가이드는 kldp.org 공개 문서 내용을 포스팅합니다.

 

 

Part1. 소개

쉘은 명령어 해석기(command interpreter)로서, 단지 커널과 사용자 중간에 놓여 있는것 이상으로 꽤 강력한 프로그래밍 언어입니다. 보통 스크립트(script)라고 부르는 쉘 프로그램은 시스템 콜, 여러 프로그래밍 도구들, 유틸리티, 실행파일 등을 "묶어서" 어떤 어플리케이션을 쉽게 만들어 줍니다. 사실 모든 종류의 유닉스 명령어, 유틸리티, 도구들이 쉘에서 쓰일 수 있습니다. 만약에 이런 것들로 부족하다면 테스트문이나 루프문등의 쉘 내부 명령어를 써서 추가적인 강력함과 유연함을 얻을 수 있습니다. 쉘 스크립트는 완전히 구조적인 프로그래밍 언어의 편리한 부가 기능들(bells and whistles)이 필요없는 작업들, 특별히 시스템 곤리자의 시스템 관련 작업이나 반복적인 일들에 아주 잘 맞습니다.

 

1장. 왜 쉘 프로그래밍을 해야 하죠?

쉘 스크립트가 어떻게 동작하는지를 이해하는 것은 실력있는 시스템 관리자가 되고 싶어하는 이들에게는 필수적입니다. 비록 그들이 실제로 스크립트를 작성하지 않는다고 해도 말이죠. 여러분의 리눅스 머신이 부팅될 때를 생각해 봅시다. 부팅이 되면 시스템 설정 정보들을 읽어 들이고 서비스를 구동하기 위해서 /etc/rc.d에 있는 쉘 스크립트를 돌립니다. 이 스크립트들을 자세히 이해하는 것은 시스템의 동작을 분석하기 위해서 매우 중요하기도 하지만 나중에 고칠 필요가 있을지도 모르는 일입니다.

 

쉘 스크립트를 만드는 것은 배우기가 어렵지 않습니다. 왜냐하면, 몇 개의 쉘용 연산자와 옵션들만으로 아주 작게 만들 수 있기 때문입니다. 쉘 문법은 간단하고 명확합니다. 명령어줄 상에서 명령어를 실행시키거나 유틸리티들을 연결해서 실행시키는 것과 거의 비슷하지만 단지 몇 개의 "규칙"만 배우면 됩니다. 거의 대부분의 스크립트가 한 번에 잘 동작하지만 덩치가 큰 스크립트라도 디버깅하기는 쉽습니다.

 

쉘 스크립트는 아주 복잡한 어플리케이션을 작성하기 전에 "빠르고 간단한" 프로토타입으로 쓰일 수 있습니다. 스크립트가 원래 하려고 하던 기능보다 제한된 기능만 제공하고 속도가 느리더라도 이는 프로젝트 개발의 첫 단계에 있어 아주 유용합니다. 이렇게 하면 실제로 C, C++, 자바, 펄 등으로 마지막 코딩에 들어가기에 앞서 전체 동작 상태를 점검해 볼 수 있기 때문에 전체 구조상의 중요한 결함을 발견할 수도 있습니다.

 

쉘 스크립팅은 복잡한 일들을 작은 단위로 나누어 처리하거나 여러 요소들과 유틸리티를 묶어 처리하는 고전적인 유닉스 철학을 따릅니다. 많은 사람들은, 펄처럼 모든 이에게 모든 것을 제공하면서 모든 기능을 갖고 있는 신세대 언어를 써서 문제를 푸는데 쓸 시간을 그 도구를 익히는데 쓰게 하는 이런 방법보다는 유닉스식을 더 낫다고 생각하고, 적어도 미적으로는 유닉스식이 더 유쾌한 해결법이라고 생각합니다.

 

쉘 스크립트를 쓰면 안될 때

- 리소스에 민감한 작업들, 특히 속도가 중요한 요소일 때(정렬, 해쉬 등등)

- 강력한 산술 연산 작업들, 특히 임의의 정밀도 연산이나 복소수를 써야할때(C++이나 포트란을 써라)

- 플랫폼간 이식성이 필요할 때(C를 써라)

- 구조적 프로그래밍이 필요한 복잡한 어플리케이션(변수의 타입체크나 함수 프로토타입등이 필요할때)

- 업무에 아주 중요하거나 회사의 미래가 걸렸다는 확신이 드는 어플리케이션

- 보안상 중요해서, 여러분 시스템의 무결성을 보장하기 위해 외부의 침입이나 크래킹, 파괴 등을 막아야 할 필요가 있을때

- 서로 의존적인 관계에 있는 여러 컴포넌트로 이루어진 프로젝트

- 과도한 파일 연산이 필요할때(Bash는 제한적인 직렬적 파일 접근을 하고, 특히나 불편하고 불충분한 줄단위 접근만 가능)

- 다차원 배열이 필요할 때

- 링크드 리스트나 트리같은 데이터 구조가 필요할 때

- 그래픽이나 GUI를 만들고 변경하는 등의 일이 필요할 때

- 시스템 하드웨어에 직접 접근해야 할 때

- 포트나 소켓 I/O가 필요할 때

- 예전에 쓰던 코드를 사용하는 라이브러리나 인터페이스를 써야할 필요가 있을때

- 독점적이고 소스 공개를 안하는 어프리케이션을 짜야할때(쉘 스크립트는 필연적으로 오픈 소스)

 

위에서 얘기한 것중 하나라도 맞는 상황이라면 펄이나 Tcl, 파이썬 같은 같은 다른 스크립팅 언어를 쓰거나 C, C++, 자바 같은 고수준 언어를 고려해 보는게 낫습니다. 어쨌든, 어플리케이션의 프로토타입으로 쉘 스크립트를 쓰는 것은 유용한 개발 단계가 될 것입니다.

 

우리는 Bash를 사용할 것인데 Bash란 "Bourne-Again Shell"의 앞 글자를 딴 것입니다. 이제는 고전이 된 Stephen Bourne의 Bourne Shell에 대한 말장난 같은 겁니다. Bash는 이제 모든 종류의 유닉스에서 쉘 스크립트에 관한 실질적인 표준입니다. 이 문서에서 다루고 있는 거의 대부분의 원리들은 Bash가 몇몇 특징을 이어 받은 Korn 쉘이나, C 쉘과 그 변형들에도 동일하게 적용됩니다.

 

다음부터는 쉘 스크립팅에 대한 튜토리얼입니다. 쉘의 특징들을 설명하기 위해서 최대한 예제들을 통해 접근했습니다. 예제들은 가능한한 모두 테스트해보았고, 몇몇은 실제로 쓸 만합니다. 독자 여러분은 이 문서의 소스 아카이브에서 실제 예제를 사용할 수가 있습니다. 실행권한을 주고(chmod u +rx scriptname), 실행을 시킨 다음 어떤 일들이 일어나는지 살펴보십시오.

 

2장. #!으로 시작하기

쉘 스크립트의 가장 간단한 예는 스크립트 파일에 시스템 명령어들을 단순히 나열해 놓는 것입니다. 이렇게 하면 적어도, 특정한 순서로 명령어를 실행시켜야 할 때 다시 치는 수고를 덜어 줍니다.

 

2-1. cleanup: /var/log 에 있는 로그 파일들을 청소하는 스크립트

# cleanup
# 루트로 실행시키세요.

 

cd /var/log

cat /dev/null > messages

cat /dev/null > wtmp

echo "로그를 정리했습니다."

 

별 다른게 없죠? 단순히 콘솔이나 한텀에서 쉽게 실행 시킬 수 있는 명령어들의 조합입니다. 명령어들을 스크립트 상에서 실행시키는 것은 이들을 다시 치지 않아도 된다는 것 이상도 이하도 아닙니다. 스크립트는 특정한 응용에 대해 쉽게 고치고 입맛에 맞게 수정하고 일반화 시킬 수 있습니다.

 

2-2. cleanup: 위 스크립트에 비해 향상되고 일반화된 버전

#!/bin/bash

# cleanup, verseion 2

# 루트로 실행시키세요.

 

LOG_DIR=/var/log

ROOT_UID=0         # $UID가 0인 유저만이 루트 권한을 갖습니다.

LINES=50              # 기본적으로 저장할 줄 수.

E_XCD=66             # 디렉토리를 바꿀 수 없다?

E_NOTROOT=67     # 루트가 아닐 경우의 종료 에러.

 

if [ "$UID" -ne "$ROOT_UID" ]

then

echo "이 스크립트는 루트로 실행시켜야 됩니다."

exit $E_NOTROOT

fi

 

if [ -n "$1" ]

# 명령어줄 인자가 존재하는지 테스트(non-empty)

then

lines=$1

else

lines=$LINES # 명령어줄에서 주어지지 않았다면 디폴트값을 씀

fi

 

# Stephane Chazelas 가 명령어줄 인자를 확인하는 더 좋은 방법을 제안해주었는데.

# 지금 단계에서는 약간 어려운 주제입니다.

#

#    E_WRONGARGS=65    # 숫자가 아닌 인자.(틀린 인자 포맷)

#

#    case "$1" in

#    ""        ) lines = 50;;

#    *[!0-9]* ) echo "사용법 : `basename $0` 정리할파일"; exit $E_WRONGARGS;;

#    *         ) lines=$1;;

#    esac

#

# * 이것을 이해하려면 "루프" 절을 참고하세요

 

cd $LOG_DIR

 

if [ `pwd` != "$LOG_DIR" ]  # 혹은 if [ "$PWD" != "LOG_DIR" ]

 # /var/log 에 있지 않다?

then

echo "$LOG_DIR로 옮겨갈 수 없습니다."

exit $E_XCD

fi  # 로그파일이 뒤죽박죽되기 전에 올바른 디렉토리에 있는지 두번 확인함.

 

# 더 좋은 방법은:

# --

# cd /var/log || {

#    echo "필요한 디렉토리로 옮겨갈 수 없습니다." >&2

#    exit $E_XCD;

# }

 

tail -$lines messages > mesg.temp     # message 로그 파일의 마지막 부분을 저장.

mv mesg.temp messages                 # 새 로그 파일이 됨.

 

# cat /dev/null > messages

# 위의 방법이 더 안전하니까 필요 없음.

 

cat /dev/null > wtmp # > wtemp 라고 해도 같은 결과.

echo "로그가 정리됐습니다."

 

exit 0

# 스크립트 종료시에 0을 리턴하면

# 쉘에게 성공했다고 알려줌.

 

시스템 로그 전체를 날려 버릴 생각이 없을 테니까 여기서는 message 로그의 마지막 부분을 그대로 남겨 놓습니다. 앞으로는 이렇게 앞서 썼던 스크립트를 가공해서 다시 쓰는 식의 좀 더 효과적인 방법을 계속 보게 될 것입니다.

 

The #!은 스크립트의 제일 앞에서 이 파일이 어떤 명령어 해석기의 명령어 집합인지를 시스템에게 알려주는 역할을 합니다. #!은 두 바이트의 "매직 넘버"(magic number)로서, 실행 가능한 쉘 스크립트라는 것을 나타내는 특별한 표시자입니다(man magic을 하면 재미있는 주제의 이야기들을 볼 수 있습니다). #! 바로 뒤에 나오는 것은 경로명으로, 스크립트에 들어있는 명령어 들을 해석할 프로그램의 위치를 나타내는데 그 프로그램이 쉘인지, 프로그램 언어인지, 유틸리티인지를 나타냅니다. 이 명령어 해석기가 주석은 무시하면서 스크립트의 첫 번째 줄부터 명령어들을 실행시킵니다.

 

#!/usr/bin/python

#!/usr/bin/env python

#!/usr/bin/perl

#!/usr/bin/ruby

#!/bin/sh

#!/bin/bash

#!/usr/bin/perl

#!/usr/bin/tcl

#!/bin/sed -f

#!/usr/awk -f

 

각각의 줄은 기본 쉘인 /bin/sh이나 기본쉘(리눅스에서는 bash), 혹은 다른 명령어 해석기를 부르고 있습니다. 거의 대부분의 상업용 유닉스 변종에서 기본 본쉘인 #!/bin/sh을 쓰면 비록 Bash만 가지고 있는 몇몇 기능들을 못 쓰게 되겠지만 리눅스가 아닌 다른 머신에 쉽게 이식(port)할 수 있게 해줍니다.

 

"#!" 뒤에 나오는 경로는 정확해야 합니다. 만약 이를 틀리게 적는다면 스크립트를 돌렸을때 거의 대부분 "Command not found"라는 에러 메시지만 보게 될 것입니다.

 

유닉스 스크립트 첫 라인에 오는 #!는 Shebang이라 부르고 해당 라인을 shebang line이라 부릅니다. shebang line은 이후 부터 스크립트가 어떻게 해석될지를 지정한다. 또한 절대경로를 사용하는 shebang의 제약을 우회하기 위해 #!/usr/bin/env 명령어를 사용할 수도 있습니다.

 

스크립트에서 내부 쉘 지시자를 안쓰고 일반적인 시스템 명령들만 쓴다면 #!는 안 써도 괜찮습니다. 위의 2번 예제에서는 #!이 필요한데, lines=50이라는 셀 전용 생성자를 써서 변수에 값을 대입하고 있기 때문입니다. #!/bin/sh이 리눅스에서 기본 쉘 해석기인 /bin/bash을 부르고 있는 것에 주의하십시오.

 

이 튜토리얼은 스크립트를 만들때 모듈별 접근 방식을 사용하도록 유도합니다. 나중에 유용하게 쓸 수 있어 보이고 "자주 등장"(boilerplate)하는 코드 조각들을 모아 두세요. 이렇게 모아두면 나중에 아주 다양하고 매력적인 루틴들을 만들 수 있을 겁니다. 예를 들어, 다음 스크립트 조각은 스크립트 시작 부분에 두거서 원하는 수 만큼의 매개변수를 받았는지 확인하는데 쓰일 수 있습니다.

if [ $# -ne 원하는_매개변수_갯수 ]

then

echo "사용법: `basename $0` 어쩌구저쩌구"

exit $WRONG_ARGS

fi

재미있는 트릭 예제

#!/bin/rm

# 자기 자신을 지우는 스크립트

 

# 이 스크립트를 실행시키면 이 파일이 지워지는 것 말고는 아무일도 안 생깁니다.

 

WHATEVER=65

 

echo "확신하건데, 이 부분은 절대 출력되지 않을 겁니다."

 

exit $WHATEVER # 여기서 exit로 빠져 나가지 못하니까 뭐라고 적든 상관없겠죠.

 

재미있는게 또 있는데, README 파일의 시작 부분에 #!/bin/more 라고 적고 실행 퍼미션을 주면, 자기 스스로 내용을 보여주는 문서 파일이 됩니다.

 

2.1. 스크립트 실행하기

스크립트를 다 만들었고 실행시키려고 한다면 sh scriptname이나, bash scriptname이라고 치면 됩니다. (sh <scriptname은 스크립트가 표준입력(stdin)에서 읽는 것을 사실상 막기 때문에 별로 권장할 만한 방법은 아닙니다.) 더 편한 방법은 chmod를 써서 스크립트 자체를 실행할 수 있게 만드는 것입니다.

 

이렇게 하거나:

chmod 555 scriptname (아무나 읽고/실행 할 수 있게)

아니면 :

chmod +rw scriptname (아무나 읽고/실행 할 수 있게)

chmod u+rx scriptname (스크립트 소유자만 읽고/실행할 수 있게)

 

이렇게 스크립트를 실행할 수 있게 해 놓았다면, ./scriptname 이라고 쳐서 실험해 볼수 있습니다. 그 스크립트가 "#!"으로 시작한다면 해당하는 명령어 해석기를 불러서 스크립트를 실행 시키게 됩니다.

 

끝으로, 테스트와 디버깅이 끝난 다음에 여러분과 사용자들이 그 스크립트를 쓸 수 있게 하려면 /usr/local/bin 디렉토리로 옮기면 됩니다. 이렇게 해 놓으면 명령줄에서 간단히 scriptname[ENTER]을 치는 것만으로 실행 시킬 수 있습니다.

 

주의사항: Bash 스크립트를 sh scriptname이라고 실행 시키게 되면 Bash 전용의 확장된 기능이 꺼져서 실행이 안 될 수도 있습니다.

 

쉘이 스크립트를 실행시키려면 스크립트를 읽어야 하기 때문에 실행 퍼미션뿐만 아니라 읽기 퍼미션도 있어야 됩니다.

 

왜 간단히 scriptname 이라고 실행 시키지 않을까요? 여러분이 현재 있는 디렉토리에 scriptname이 있는데도, 왜 실행되지 않을까요? 왜냐하면, 보안상의 이유로 현재 디렉토리를 나타내는 "."은 사용자의 $PATH에 들어 있지 않기 때문입니다. 따라서 현재 디렉토리에 있는 스크립트를 실행 시키려면 ./scriptname이라고 강제로 실행 경로를 알려줘야 합니다.

 

2.2. 연습문제

1. 시스템 관리자들은 일반적인 작업들을 자동으로 하기 위해서 가끔씩 스크립트를 만들어 씁니다. 이런 스크립트로 하기에 좋은 상황들을 몇 개 나열해 보세요.

2. 시간과 날짜, 현재 로그인해 있는 모든 사용자들, 시스템 업타임을 보여주는 스크립트를 만들어보세요. 그 다음에는 로그 파일에 그 정보들을 저장하도록 해보세요.