[[TableOfContents]] ---- = 들어가면서 = 쉘(shell)이라는 건 일종의 명령 해석기입니다. OS 커널과 사용자 사이의 중간자적 역할 이상으로 프로그래밍 언어로서의 기능 또한 강력하죠. 스크립트(script)라고하는 쉘 프로그램은 시스템 콜, 유틸리티, 이진파일 등을 서로 붙이고 연결해서 손쉽게 응용프로그램을 개발하도록 도와주는 툴이라고 보시면 됩니다. 사실상 모든 UNIX 명령어, 유틸리티, 툴 등이 스크립트의 재료가 될 수 있습니다만 이것으로도 충분치 않다면 쉘 내부의 조건문, 반복문 등 여러 구문들을 쓰신다면 스크립트가 좀더 강력하고 유연해지겠네요. 쉘 스크립트는 시스템 관리(admin) 작업이나 덩치 큰 프로그래밍 언어까지는 필요하지 않은 루틴 반복적인 작업에 특히 능력을 발휘합니다. == 왜 쉘 프로그래밍인가? == 숙련된 시스템 관리자에게 있어서 쉘 스크립팅 지식은 필수입니다. 스크립트 작성할 일이 없다고 생각하는 관리자에게도 예외없죠. 아시다시피 리눅스 시스템이 부팅할 때 {{{/etc/rc.d}}} 에 있는 스크립트들을 실행해서 시스템 설정을 복원하고 서비스들을 세팅합니다. 시스템의 동태를 분석하고 수정하려면 이런 시동(startup) 스크립트들을 세부적으로 이해해야겠죠. 쉘 스크립트를 작성하는 것은 그리 어렵지 않습니다. 스크립트는 한입거리의 부분들로 짜집기 되어 있고 쉘 고유의 연산자나 옵션들[[Footnote(11장. 내부 명령어와 내장함수 에서 설명합니다.)]]도 많지 않기 때문이죠. 문법도 단순하고 직관적이며 명령행 상에서 유틸리티들을 실행하고 연결하는 방식과 비슷합니다. 익혀야 할 규칙(rule)도 그리 많지 않아요. 복잡한 응용프로그램의 프로토타입(prototype)을 작성하는 데 "quick and dirty"한 방법으로 쉘 스크립트가 이용되기도 합니다. 프로젝트 개발의 첫 단계로서 부분적으로 기능을 구현하는 데 쉘스크립트를 이용하여 테스트하면서 C, C++, Java 나 Perl 등으로 최종 코딩되기 전에 루틴의 구조적 혹은 논리적 결함이 발견되죠. 쉘 스크립팅은 복잡한 프로젝트는 더 단순한 요소들로 나눠서 작업하고 다시 결합한다는 고전적인 UNIX 철학을 잘 이행하는 방법입니다. 많은 사람들이 차세대의 올인원(all-in-one) 고급 언어를 사용하는 것보다는 이러한 방식이 더 좋거나 매력적이라고 생각하죠. 모든 사람에게 모든 일이 가능하도록 기획된 그런 언어를 사용하려면 자신의 사고방식과 절차를 그 도구에 맞게 고쳐야 하는 부담은 감수해야 합니다. 다음의 작업을 할 생각이라면 쉘 스크립트는 적당하지 않습니다. * 자원(resource)집약적인 작업 : 특히 속도가 생명인 작업 (Sorting, Hashing 등) * 중요한 수치 연산 : 특히 부동 소수점, 임의의 정밀도, 복소수 계산 등 (C++ 이나 포트란을 쓰세요) * 플랫폼간 이식성이 요구되는 작업 (C를 쓰세요) * 구조화된 프로그래밍이 필요한 작업 (함수형, 변수형 등을 확인해야 하는 작업) * 회사의 미래가 걸려있는 중대한 응용프로그램 * 보안이 중요시되는 상황 : 시스템의 무결성을 보장해야 하거나 해킹이나 크래킹으로부터 보호해야 하는 상황 * 규모가 큰 파일 연산이 요구되는 작업 (Bash는 연속적인 파일 접근에 제한적입니다) * 다차원 배열이 필요한 작업 * 연결리스트나 트리 같은 자료구조가 필요한 작업 * 그래픽이나 GUI를 생성해야 하는 작업 * 하드웨어에 직접 접근해야 하는 작업 * 포트(port)나 소켓I/O를 필요로 하는 작업 * 구식 코드로 라이브러리나 인터페이스를 사용해야 하는 작업 * 소유권이 있거나 소스가 공개되어서는 안되는 프로그램 위의 작업을 해야겠으면 좀더 강력한 스크립트 언어(Perl, Tcl, Python, Ruby 등)나 컴파일 언어(C, C++, Java 등)를 쓰는게 좋습니다. 물론 이 때에도 부분 기능을 구현하는 데 쉘 스크립트로 프로토타입을 작성해보는 건 쓸모가 있겠죠. 우리가 앞으로 함께 배워볼 쉘은 Bash입니다. Bash는 "Bourne-Again shell"의 머릿글자이고 Stephen Bourne의 Bourne Shell에서 유래했죠. Bash는 사실상 거의 모든 UNIX(-like)에서 쉘스크립팅의 표준이 되었습니다. 앞으로 얘기할 대부분의 원리들은 Korn Shell[[Footnote(Many of the features of ksh88, and even a few from the updated ksh93 have been merged into Bash.)]]이나 C Shell과 같은 다른 쉘에서의 스크립팅에도 적용해 볼 수 있어요. (C Shell 프로그래밍은 추천하지 않습니다. 1993년 10월에 Tom Christiansen 이 C Shell의 타고난 결함에 대해 유즈넷에 포스트했었네요.) 이어질 내용은 쉘 스크립팅 튜토리얼(tutorial)입니다. 쉘의 다양한 특징들을 설명하기 위해 많은 예제들을 곁들였는데 몇몇은 그대로 가져다 써도 유용한 것들이죠. 예제 스크립트의 소스파일({{{scriptname.sh}}} 또는 {{{scriptname.bash}}})[[Footnote(사용자가 작성한 스크립트는 확장자를 .sh 로 하는 것이 관례입니다. /etc/rc.d 에 있는 것과 같은 시스템 스크립트는 이 명명법을 따르지 않아요.)]]을 실행하려면 먼저 {{{chmod u+rx scriptname}}} 과 같이 실행퍼미션을 주어야 한다는 것 잊지 마세요. == Sha-Bang(#!)으로 시작하자 == 가장 간단한 스크립트를 보면, 스크립트라는 것은 시스템 명령어들을 파일 안에 나열한 것에 지나지 않구나 하는 생각이 들겁니다. 그렇다고 해도 실행할 때마다 동일한 명령어들을 다시 타이핑하는 수고는 덜어주는 거죠. '''예제 2-1. cleanup : /var/log 안에 있는 로그파일 청소하는 스크립트''' {{{#!vim sh # Cleanup # Run as root, of course. cd /var/log cat /dev/null > messages cat /dev/null > wtmp echo "Logs cleaned up."}}} 보시는 것처럼 그리 특별한 건 없습니다. 콘솔(console)이나 ''xterm'' 의 명령행에서 한줄 한줄 실행할 수 있는 명령어들을 묶어놓은 것 뿐이니까요. 그래도 이 작업을 할 때마다 다시 타이핑하지 않아도 되는 이점이 있으니 이 스크립트는 우리에게 유용한 툴이고, 필요에 따라 쉽게 수정하고 새로운 기능을 추가할 수 있습니다. '''예제 2-2. cleanup : 개선된 clean-up 스크립트''' {{{#!vim sh #!/bin/bash # Proper header for a Bash script. # Cleanup, version 2 # Run as root, of course. # Insert code here to print error message and exit if not root. LOG_DIR=/var/log # Variables are better than hard-coded values. cd $LOG_DIR cat /dev/null > messages cat /dev/null > wtmp echo "Logs cleaned up." exit # The right and proper method of "exiting" from a script.}}} 이제야 진짜 스크립트처럼 보이기 시작하네요. 좀더 가봅시다... '''예제 2-3. cleanup : 위 스크립트를 강화하고 일반화한 버전''' {{{#!vim sh #!/bin/bash # Cleanup, version 3 # Warning: # ------- # This script uses quite a number of features that will be explained #+ later on. # By the time you've finished the first half of the book, #+ there should be nothing mysterious about it. LOG_DIR=/var/log ROOT_UID=0 # Only users with $UID 0 have root privileges. LINES=50 # Default number of lines saved. E_XCD=66 # Can't change directory? E_NOTROOT=67 # Non-root exit error. # Run as root, of course. if [ "$UID" -ne "$ROOT_UID" ] then echo "Must be root to run this script." exit $E_NOTROOT fi if [ -n "$1" ] # Test if command line argument present (non-empty). then lines=$1 else lines=$LINES # Default, if not specified on command line. fi # Stephane Chazelas suggests the following, #+ as a better way of checking command line arguments, #+ but this is still a bit advanced for this stage of the tutorial. # # E_WRONGARGS=65 # Non-numerical argument (bad arg format) # # case "$1" in # "" ) lines=50;; # *[!0-9]*) echo "Usage: `basename $0` file-to-cleanup"; exit $E_WRONGARGS;; # * ) lines=$1;; # esac # #* Skip ahead to "Loops" chapter to decipher all this. cd $LOG_DIR if [ `pwd` != "$LOG_DIR" ] # or if [ "$PWD" != "$LOG_DIR" ] # Not in /var/log? then echo "Can't change to $LOG_DIR." exit $E_XCD fi # Doublecheck if in right directory, before messing with log file. # far more efficient is: # # cd /var/log || { # echo "Cannot change to necessary directory." >&2 # exit $E_XCD; # } tail -$lines messages > mesg.temp # Saves last section of message log file. mv mesg.temp messages # Becomes new log directory. # cat /dev/null > messages #* No longer needed, as the above method is safer. cat /dev/null > wtmp # ': > wtmp' and '> wtmp' have the same effect. echo "Logs cleaned up." exit 0 # A zero return value from the script upon exit #+ indicates success to the shell.}}} 이 버전은 시스템 로그를 몽땅 지우지 않고 로그의 마지막 부분을 조금 남겨둡니다. 이후에 스크립트 효율을 높인다든지 다른 필요가 생긴다든지 할 때마다 여러분이 스크립트를 다듬어 가세요. 스크립트 맨 처음에 등장하는 ''sha-bang''(#!)은 아래의 스크립트는 지정한 명령 해석기를 통해서 수행되어야 한다는 걸 시스템에 알려주는 역할을 합니다. #! 는 사실 2바이트[[Footnote(4.2BSD 기반의 몇몇 UNIX에서는 4바이트 매직넘버를 쓰기도 합니다. #! /sh/bash 처럼 ! 다음에 빈칸을 넣어주어야 하죠.)]] 매직넘버(''magic number'', 자세한 내용은 '''man magic'''을 쳐보세요)입니다. ''sha-bang'' 다음에는 곧바로 ''경로명''이 와야 하는데, 스크립트에 쓰인 명령들을 해석할 프로그램 경로이고 쉘, 프로그래밍 언어 또는 유틸리티 등이 올 수 있어요. ''sha-bang''라인의 다음 줄부터는 호출된 명령 해석기가 순차적으로 처리를 하고, 이때 주석(comments)은 무시합니다.[[Footnote(sh나 bash와 같은 명령 해석기가 스크립트 처음에 등장하는 #!라인에서부터 해석을 시작합니다만 이 줄은 #로 시작했기 때문에 주석으로 보고 무시합니다. 물론 #!라인은 명령 해석기 호출이라는 본래의 목적을 달성한 이후이기 때문에 상관은 없는거죠. 만약에 스크립트 중간에 또 #!라인이 나타난다고 해도 bash는 이를 주석으로 처리합니다.)]] {{{#!/bin/sh #!/bin/bash #!/usr/bin/perl #!/usr/bin/tcl #!/bin/sed -f #!/usr/awk -f}}} 위의 ''sha-bang''라인들은 각기 다른 명령 해석기를 호출하니까 필요한 대로 하나만 선택하세요. 대부분의 상업적인 UNIX류의 운영체제에서 기본(default)으로 채택하고 있는 '''#!/bin/sh'''을 사용한다면 스크립트를 비리눅스 머신에 이식할 수 있게 됩니다. 물론 Bash 고유의 특징들은 못 쓰겠지만 스크립트는 POSIX[[Footnote(Portable Operating System Interface. UNIX-like OS들을 표준화하기 위한 시도로, POSIX 스펙은 Open Group 사이트에 잘 나와 있습니다.)]] '''sh'''표준을 따르게 되는 거죠. ''sha-bang''라인의 경로명은 유효한 것이어야 합니다. 그렇지 않으면 에러 메세지(보통 "Command not found")만 딸랑 내보내거든요. 쉘 내부의 명령들을 사용하지 않고 일반적인 시스템 명령들만 쭉 나열한 스크립트라면(위의 첫번째 예제처럼) #!라인을 생략할 수 있습니다. 위의 두번째 예제에서는 쉘 고유의 구문인 변수 할당문('''lines=50''')을 사용했기 때문에 반드시 #!라인을 써야 하죠. 또 하나 명심할 것은 '''#!/bin/sh'''은 기본(default) 쉘을 실행합니다. 리눅스 머신에서는 {{{/bin/bash}}}가 기본 쉘이에요. (!) 스크립트를 작성하실 때 모듈방식의 접근을 권장합니다. 반복적으로 사용되는 토막 코드(snippets)는 나중에 재사용할 수 있도록 모아두세요. 이렇게 하다보면 마침내는 멋진 라이브러리가 완성되겠죠. 예를 들어, 아래의 토막 코드는 스크립트가 실행될 때 넘겨받은 파라메터(parameter)의 수가 올바른지 검사하는 겁니다. {{{#!vim sh E_WRONG_ARGS=65 script_parameters="-a -h -m -z" # -a = all, -h = help, etc. if [ $# -ne $Number_of_expected_args ] then echo "Usage: `basename $0` $script_parameters" # `basename $0` is the script's filename. exit $E_WRONG_ARGS fi}}} 처음에는 예제 2-1처럼 한가지 특정 작업만 수행하는 스크립트를 작성하겠지만, 나중에는 비슷하거나 조금 다른 작업에도 이용할 수 있도록 스크립트를 일반화하게 될 겁니다. 그럴려면 먼저, 반복된 코드를 함수로 묶는 것처럼, 문자 상수를 변수로 바꾸세요. (예제 2-1에서 2-2로 변화된 점을 살펴보세요.) === 스크립트 실행하기 === 스크립트를 작성했으면 '''sh scriptname'''[[Footnote(주의: "sh scriptname"으로 Bash 스크립트를 실행하면 Bash 고유의 기능은 꺼집니다. 따라서 실행이 안될 수도 있어요.)]]나 '''bash scriptname'''을 쳐서 실행해 보세요. (Not recommended is using sh 주석라인 안에 명령문을 쓰시면 마찬가지로 주식처리 되니까 다음 명령문은 새로운 줄에 쓰세요. #로 한번 시작한 주석라인을 그 줄에서 종료시킬 방법은 없습니다. (./) 물론, '''echo'''문에서 이스케이프 처리한 #은 주석으로 보지 않습니다. 파라메터 치환식이나 숫자 상수 표현식, 패턴 매칭 연산에서 쓰이는 #도 마찬가지죠. {{{#!vim sh echo "The # here does not begin a comment." echo 'The # here does not begin a comment.' echo The \# here does not begin a comment. echo The # here begins a comment. echo ${PATH#*:} # Parameter substitution, not a comment. echo $(( 2#101011 )) # Base conversion, not a comment. # Thanks, S.C.}}} #를 이스케이프 처리하는 데 " ' \ 와 같은 문자가 사용됩니다. ; '''명령 구분자 ![semicolon]'''. 한줄에 둘 이상의 명령문이 가능하게 하죠. {{{#!vim sh echo hello; echo there if [ -x "$filename" ]; then # Note that "if" and "then" need separation. # Why? echo "File $filename exists."; cp $filename $filename.bak else echo "File $filename not found."; touch $filename fi; echo "File test complete."}}} {{{;;}}} '''__case__ 구문에서 선택문 종결자 ![double semicolon]'''. {{{#!vim sh case "$variable" in abc) echo "\$variable = abc" ;; xyz) echo "\$variable = xyz" ;; esac}}} . '''"dot" command ![period]'''. Equivalent to source (see Example 11-20). This is a bash builtin. . '''"dot", 파일명에서'''. 파일명이 "."으로 시작하면 숨김파일이 됩니다. {{{bash$ touch .hidden-file bash$ ls -l total 10 -rw-r--r-- 1 bozo 4034 Jul 18 22:04 data1.addressbook -rw-r--r-- 1 bozo 4602 May 25 13:58 data1.addressbook.bak -rw-r--r-- 1 bozo 877 Dec 17 2000 employment.addressbook bash$ ls -al total 14 drwxrwxr-x 2 bozo bozo 1024 Aug 29 20:54 ./ drwx------ 52 bozo bozo 3072 Aug 29 20:51 ../ -rw-r--r-- 1 bozo bozo 4034 Jul 18 22:04 data1.addressbook -rw-r--r-- 1 bozo bozo 4602 May 25 13:58 data1.addressbook.bak -rw-r--r-- 1 bozo bozo 877 Dec 17 2000 employment.addressbook -rw-rw-r-- 1 bozo bozo 0 Aug 29 20:54 .hidden-file}}} 디렉토리명을 대신해 쓸 수도 있는데, "."은 현재 디렉토리를, ".."은 상위 디렉토리를 의미합니다. {{{bash$ pwd /home/bozo/projects bash$ cd . bash$ pwd /home/bozo/projects bash$ cd .. bash$ pwd /home/bozo/}}} "."은 파일을 이동시키는 명령에서 목적지(디렉토리)를 대신해서 자주 쓰입니다. {{{bash$ cp /home/bozo/current_work/junk/* . }}} . '''"dot", 정규표현식에서'''. 정규표현식에서 "."은 문자 하나에 매치됩니다. " '''partial quoting ![double quote]'''. "STRING" preserves (from interpretation) most of the special characters within STRING. See also Chapter 5. ' '''full quoting ![single quote]'''. 'STRING' preserves all special characters within STRING. This is a stronger form of quoting than using ". See also Chapter 5. , '''콤마(comma) 연산자'''. 콤마 연산자는 일련의 수치 연산을 연결합니다. 연결된 연산식 모두를 평가하지만 마지막 것만 반환해요. {{{#!vim sh let "t2 = ((a = 9, 15 / 3))" # Set "a = 9" and "t2 = 15 / 3"}}} \ '''이스케이프 문자 ![backslash]'''. A quoting mechanism for single characters. \X "escapes" the character X. This has the effect of "quoting" X, equivalent to 'X'. The \ may be used to quote " and ', so they are expressed literally. See Chapter 5 for an in-depth explanation of escaped characters / '''파일명 경로 구분자 ![forward slash]'''. {{{/home/bozo/projects/Makefile}}}처럼 파일명에서 구분자 역할을 하는데, 산술에서 나눗셈 연산자로도 쓰이죠. ` '''command 치환 [backquote, backtick]'''. `로 감싼 command는 command의 출력물을 변수에 대입할 때 이용합니다. : '''null command ![colon]'''. "NOP" (no op, a do-nothing operation)와 같습니다. 쉘 내장의 true와 동의어로 보시면 됩니다. ":" 은 그 자체로 쓰일 수 있고 exit값은 "true" (0)입니다. {{{#!vim sh : echo $? # 0}}} 무한 루프: {{{#!vim sh while : do operation-1 operation-2 ... operation-n done # Same as: # while true # do # ... # done}}} Placeholder in if/then test: {{{#!vim sh if condition then : # Do nothing and branch ahead else take-some-action fi}}} Provide a placeholder where a binary operation is expected, see Example 8-2 and default parameters. {{{#!vim sh : ${username=`whoami`} # ${username=`whoami`} Gives an error without the leading : # unless "username" is a command or builtin...}}} ---- [[Footnote]]