2012-09-13

FreeBSD]Bash 스크립팅 가이드1


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

쉘 스크립트가 어떻게 동작하는지를 이해하는 것은 실력있는 시스템 관리자가 되고 싶어 하는 이들에게는 필수적입니다. 비록 그들이 실제로 스크립트를 작성하지 않는다고 해도 말이죠. 여러분의 리눅스 머신이 부팅될 때를 생각해 봅시다. 부팅이 되면 시스템 설정 정보들을 읽어 들이고 서비스를 구동하기 위해서 /etc/rc.d에 있는 쉘 스크립트를 돌립니다. 이 스크립트들을 자세히 이해하는 것은 시스템의 동작을 분석하기 위해서 매우 중요하기도 하지만 나중에 고칠 필요가 있을지도 모르는 일입니다.
쉘 스크립트를 만드는 것은 배우기가 어렵지 않습니다. 왜냐하면, 몇 개의 쉘용 연산자와 옵션들 [1] 만으로 아주 작게 만들 수 있기 때문입니다. 쉘 문법은 간단하고 명확합니다. 명령어줄 상에서 명령어를 실행시키거나 유틸리티들을 연결해서 실행시키는 것과 거의 비슷하지만 단지 몇 개의 "규칙"만 배우면 됩니다. 거의 대부분의 스크립트가 한 번에 잘 동작하지만 덩치가 큰 스크립트라도 디버깅하기는 쉽습니다.
쉘 스크립트는 아주 복잡한 어플리케이션을 작성하기 전에 "빠르고 간단한" 프로토타입으로 쓰일 수 있습니다. 스크립트가 원래 하려고 하던 기능보다 제한된 기능만 제공하고 속도가 느리더라도 이는 프로젝트 개발의 첫 단계에 있어 아주 유용합니다. 이렇게 하면 실제로 C, C++, 자바, 펄등으로 마지막 코딩에 들어가기에 앞서 전체 동작 상태를 점검해 볼 수 있기 때문에 전체 구조상의 중요한 결함을 발견할 수도 있습니다.
쉘 스크립팅은 복잡한 일들을 작은 단위로 나누어 처리하거나 여러 요소들과 유틸리티를 묶어 처리하는 고전적인 유닉스 철학을 따릅니다. 많은 사람들은, 펄처럼 모든 이에게 모든 것을 제공하면서 모든 기능을 갖고 있는 신세대 언어를 써서 문제를 푸는데 쓸 시간을 그 도구를 익히는데 쓰게 하는 이런 방법보다는 유닉스식을 더 낫다고 생각하고, 적어도 미적으로는 유닉스식이 더 유쾌한 해결법이라고 생각합니다.
쉘 스크립트를 쓰면 안 될 때
  • 리소스에 민감한 작업들, 특히 속도가 중요한 요소일 때(정렬, 해쉬 등등)
  • 강력한 산술 연산 작업들, 특히 임의의 정밀도 연산(arbitrary precision)이나 복소수를 써야 할 때(C++이나 포트란을 쓰세요)
  • 플랫폼간 이식성이 필요할 때(C를 쓰세요)
  • 구조적 프로그래밍이 필요한 복잡한 어플리케이션(변수의 타입체크나 함수 프로토타입등이 필요할 때)
  • 업무에 아주 중요하거나 회사의 미래가 걸렸다는 확신이 드는 어플리케이션
  • 보안상 중요해서, 여러분 시스템의 무결성을 보장하기 위해 외부의 침입이나 크래킹, 파괴등을 막아야 할 필요가 있을 때
  • 서로 의존적인 관계에 있는 여러 콤포넌트로 이루어진 프로젝트
  • 과도한 파일 연산이 필요할 때(Bash는 제한적인 직렬적 파일 접근을 하고 , 특히나 불편하고 불충분한 줄단위 접근만 가능)
  • 다차원 배열이 필요할 때
  • 링크드 리스트나 트리같은 데이타 구조가 필요할 때
  • 그래픽이나 GUI를 만들고 변경하는 등의 일이 필요할 때
  • 시스템 하드웨어에 직접 접근해야 할 때
  • 포트나 소켓 I/O가 필요할 때
  • 예전에 쓰던 코드를 사용하는 라이브러리나 인터페이스를 써야 할 필요가 있을 때
  • 독점적이고 소스 공개를 안 하는 어플리케이션을 짜야 할 때(쉘 스크립트는 필연적으로 오픈 소스입니다.)
위에서 얘기한 것중 하나라도 맞는 상황이라면 펄이나 Tcl, 파이썬 같은 다른 스크립팅 언어를 쓰거나 C, C++, 자바 같은 고수준 언어를 고려해 보는게 낫습니다. 어쨌든, 어플리케이션의 프로토타입으로 쉘 스크립트를 쓰는 것은 유용한 개발 단계가 될 것입니다.
우리는 Bash를 사용할 것인데 Bash란 "Bourne-Again Shell"의 앞 글자를 딴 것입니다. 이제는 고전인 된 Stephen Bourne의 Bourne Shell에 대한 말장난 같은 겁니다. Bash는 이제 모든 종류의 유닉스에서 쉘 스크립트에 관한 실질적인 표준(de facto)입니다. 이 문서에서 다루고 있는 거의 대부분의 원리들은 Bash가 몇몇 특징을 이어 받은 Korn 쉘 [2] 이나, C 쉘과 그 변형들에도 동일하게 적용됩니다(C 쉘 프로그래밍은 Tom Christiansen이 1993년 10월에 뉴스 그룹 포스팅을 통해 지적했듯이 타고난 문제점을 갖고 있어서 추천하지 않습니다).
다음부터는 쉘 스크립팅에 대한 튜토리얼입니다. 쉘의 특징들을 설명하기 위해서 최대한 예제들을 통해 접근 했습니다. 예제들은 가능한한 모두 테스트해 보았고, 몇몇은 실제로 쓸 만합니다. 독자 여러분은 이 문서의 소스 아카이브에서 실제 예제를 사용할 수가 있습니다(something-or-other.sh). [3] 실행 권한을 주고(chmod u+rx scriptname), 실행을 시킨 다음 어떤 일들이 일어나는지 살펴보십시오. 소스를 구할 수 없다면 여러분이 보고 있는 HTML이나 pdf, text 버전에서 복사-붙여넣기를 하면 됩니다. 몇몇 예제들은 스크립트들은 설명하기 전에 그 특징을 소개할텐데, 이는 여러분들에게 링크를 따라 이곳 저곳을 왔다 갔다 하게 할 지도 모릅니다.
특별한 언급이 없다면 이 문서에서 쓰인 예제들은 모두 저자가 작성한 것입니다.

주석

[1]
이것들은 내부 명령(builtin)이라고 하는 쉘이 갖고 있는 특징입니다.
[2]
ksh88의 많은 특징들과, 업데이트된 ksh93의 일부분이 Bash로 통합되었습니다.
[3]
관습적으로, 사용자가 작성한 본쉘 호환 스크립트는 보통 .sh 확장자를 갖습니다. 반면에 /etc/rc.d에서 볼 수 있는 시스템 스크립트는 이런 지침을 따르지 않습니다.
 
 
kldp.org 의 bash 스크립팅 가이드
 
 

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, version 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 #! 은 스크립트의 제일 앞에서 이 파일이 어떤 명령어 해석기의 명령어 집합인지를 시스템에게 알려주는 역할을 합니다. #! 은 두 바이트 [1] 의 "매직 넘버"(magic number)로서, 실행 가능한 쉘 스크립트라는 것을 나타내는 특별한 표시자입니다(man magic을 하면 재미있는 주제의 이야기들을 볼 수 있습니다). #! 바로 뒤에 나오는 것은 경로명으로, 스크립트에 들어있는 명령어들을 해석할 프로그램의 위치를 나타내는데 그 프로그램이 쉘인지, 프로그램 언어인지, 유틸리티인지를 나타냅니다. 이 명령어 해석기가 주석은 무시하면서 스크립트의 첫 번째 줄부터 명령어들을 실행시킵니다. [2]
          #!/bin/sh #!/bin/bash #!/usr/bin/perl #!/usr/bin/tcl #!/bin/sed -f #!/usr/awk -f
          
각각의 줄은 기본 쉘인 /bin/sh이나 기본쉘(리눅스에서는 bash), 혹은 다른 명령어 해석기를 부르고 있습니다. [3] 거의 대부분의 상업용 유닉스 변종에서 기본 본쉘인 #!/bin/sh을 쓰면 비록 Bash 만 가지고 있는 몇몇 기능들을 못 쓰게 되겠지만 리눅스가 아닌 다른 머신에 쉽게 이식(port)할 수 있게 해 줍니다(이렇게 작성된 스크립트는 POSIX [4] sh 표준을 따르게 됩니다.
"#!" 뒤에 나오는 경로는 정확해야 합니다. 만약 이를 틀리게 적는다면 스크립트를 돌렸을 때 거의 대부분 "Command not found"라는 에러 메세지만 보게 될 것입니다.
스크립트에서 내부 쉘 지시자를 안 쓰고 일반적인 시스템 명령들만 쓴다면 #!는 안 써도 괜찮습니다. 위의 2번 예제에서는 #!이 필요한데, lines=50이라는 쉘 전용 생성자를 써서 변수에 값을 대입하고 있기 때문입니다. #!/bin/sh이 리눅스에서 기본 쉘 해석기인 /bin/bash을 부르고 있는 것에 주의하십시오.
중요: 이 튜토리얼은 스크립트를 만들 때 모듈별 접근 방식을 사용하도록 유도합니다. 나중에 유용하게 쓸 수 있어 보이고 "자주 등장"(boilerplate)하는 코드 조각들을 모아 두세요. 이렇게 모아두면 나중에 아주 다양하고 매력적인 루틴들을 만들 수 있을 겁니다. 예를 들어, 다음 스크립트 조각은 스크립트 시작 부분에 두어서 원하는 수 만큼의 매개변수를 받았는지 확인하는데 쓰일 수 있습니다.
          if [ $# -ne 원하는_매개변수_갯수 ] then echo "사용법: `basename $0` 어쩌구저쩌구" exit $WRONG_ARGS fi
          

주석

[1]
몇몇 유닉스 버전(4.2BSD 에 기반한)에서는 매직 넘버로 4 바이트를 받아들이기 때문에 ! 다음에 빈 칸이 필요합니다. #! /bin/sh.
[2]
해당 명령어 해석기(sh이나 bash)는 #!이 있는 줄을 처음 해석하려고 할텐데, 이 줄은 이미 명령어 해석기를 부르는 자신의 역할을 수행했고 #으로 시작하기 때문에 주석으로 올바르게 해석될 것입니다.
[3]
그래서 이렇게 멋진 트릭도 가능해 집니다.
          #!/bin/rm # 자기 자신을 지우는 스크립트. # 이 스크립트를 실행시키면 이 파일이 지워지는 것 말고는 아무일도 안 생깁니다. WHATEVER=65 echo "확신하건데, 이 부분은 절대 출력되지 않을 겁니다." exit $WHATEVER # 여기서 exit로 빠져 나가지 못하니까 뭐라고 적든 상관없겠죠.
          
재미있는게 또 있는데, README 파일의 시작 부분에 #!/bin/more 라고 적고 실행 퍼미션을 주면, 자기 스스로 내용을 보여주는 문서 파일이 됩니다.
[4]
Portable Operating System Interface, 유닉스류의 OS들을 위한 표준화 작업

2.1. 스크립트 실행하기

스크립트를 다 만들었고 실행시키려고 한다면 sh scriptname [1] 이나, bash scriptname이라고 치면 됩니다. (sh <scriptname은 스크립트가 표준입력(stdin)에서 읽는 것을 사실상 막기 때문에 별로 권장할 만한 방법이 아닙니다.) 더 편한 방법은 chmod를 써서 스크립트 자체를 실행할 수 있게 만드는 것입니다.
이렇게 하거나:
chmod 555 scriptname (아무나 읽고/실행 할 수 있게) [2]
아니면
chmod +rx scriptname (아무나 읽고/실행 할 수 있게)
chmod u+rx scriptname (스크립트 소유자만 읽고/실행할 수 있게)
이렇게 스크립트를 실행할 수 있게 해 놓았다면, ./scriptname [3] 이라고 쳐서 실험해 볼 수 있습니다. 그 스크립트가 "#!"으로 시작한다면 해당하는 명령어 해석기를 불러서 스크립트를 실행 시키게 됩니다.
끝으로, 테스트와 디버깅이 끝난 다음에 여러분과 다른 사용자들이 그 스크립트를 쓸 수 있게 하려면 /usr/local/bin 디렉토리로 옮기면 됩니다(당연히 루트로). 이렇게 해 놓으면 명령어 줄에서 간단히 scriptname[ENTER]을 치는 것만으로 실행 시킬 수 있습니다.

주석

[1]
주의사항: Bash 스크립트를 sh scriptname이라고 실행 시키게 되면 Bash 전용의 확장된 기능이 꺼져서 실행이 안 될 수도 있습니다.
[2]
쉘이 스크립트를 실행시키려면 스크립트를 읽어야 하기 때문에 실행 퍼미션뿐만 아니라 읽기 퍼미션도 있어야 됩니다.
[3]
왜 간단히 scriptname 이라고 실행 시키지 않을까요? 여러분이 현재 있는 디렉토리($PWD)에 scriptname이 있는데도, 왜 실행되지 않을까요? 왜냐하면, 보안상의 이유로 현재 디렉토리를 나타내는 "." 은 사용자의 $PATH에 들어 있지 않기 때문입니다. 따라서 현재 디렉토리에 있는 스크립트를 실행 시키려면 ./scriptname이라고 강제로 실행 경로를 알려줘야 합니다.

2.2. 몸풀기 연습문제(Preliminary Exercises)

  1. 시스템 관리자들은 일반적인 작업들을 자동으로 하기 위해서 가끔씩 스크립트를 만들어 씁니다. 이런 스크립트로 하기에 좋은 상황들을 몇 개 나열해 보세요.
  2. 시간과 날짜현재 로그인해 있는 모든 사용자들, 시스템 업타임(uptime)을 보여주는 스크립트를 만들어 보세요. 그 다음에는 로그 파일에 그 정보들을 저장하도록 해 보세요.

댓글 없음:

댓글 쓰기