Tcl/Tk 문법 - 1부 Tcl

Development 2008. 6. 9. 09:34
출처 : http://terzeron.net/wp/?p=244

1999/05/15

소개

Tcl은 Tool Command Language, Tk는 Toolkit의 약자로서, 응용프로그램을 제어하고 확장하는 것을 돕는 프로그래밍 언어의 하나이다. Tcl의 장점은 Tk와 함께 사용되어 응용프로그램이 X Window를 손쉽게 다룰 수 있도록 할 수 있다는 데 있다. 물론 요즘에는 Scheme, Lisp, Perl 등의 강력한 경쟁자들이 생기긴 했지만, 아직도 많은 개발자들이 Tcl/Tk에 대해서 친근감을 느끼고 있다. 게다가 Tcl/Tk는 C source로 embed하여 사용할 수 있다는 강점을 가지고 있다.

Tcl/Tk는 [티클티케이]라고 읽는다고 개발자 J. K. Ousterhout가 말했다. Ousterhout는 Tcl의 장점으로 쉽고, 강력한 스크립트 언어이며, 다른 패키지와 연결하기 쉽다는 점을 밝히고 있다.

필자가 꼽는 여타의 프로그래밍 언어에 대한 Tcl/Tk의 장점은 쉽다는 것이다. 아주 간단하게 문법을 익힐 수 있어서 초보자들이 X Window 상에서 실행되는 프로그램을 개발하는 것이 아주 용이하다. 그러나 이 장점은 역으로 단점이 되기도 한다. Tcl자체만으로는 시스템 프로그래밍에 적합하지 않다는 점이 다. 그러나 프로그래밍 언어는 모든 기능을 가질 수도 없고, 가진다고 해도 널리 쓰일 수 있는 것은 아니기 때문에 Tcl 나름대로 유용하게 쓰일 곳이 있기 마련이다. 그러나 미리부터 실망할 필요는 없다. Tcl을 C로 embed시켜 서 시스템 프로그램을 개발한다면 Tcl의 장점을 살리면서 C의 강력한 기능 을 지원받을 수 있으니 말이다. 자, 이제부터 티클티케이가 당신의 개발도 구가 될 수 있도록 가볍게 시작하도록 하자.

설치 및 실행

Tcl/Tk에 관련된 거의 모든 패키지는 http://www.scriptics.com/software에서 구할 수 있다. 윈도우즈(Windows)와 매킨토시(Macintosh)환경에서 사용할 수 있는 바이너리 배포본도 제공된다. 필자가 권하는 버전은 Tcl/Tk 8.1b3이다.

ftp://ftp.scriptics.com/pub/tcl/tcl8_1/tcl8.1b3.tar.gz

ftp://ftp.scriptics.com/pub/tcl/tcl8_1/tk8.1b3.tar.gz

에서 소스를 직접 내려받을 수도 있다. 컴파일 과정은 아주 간단하다. Tcl과 Tk 소스를 모두 풀어놓고 tcl8.1b3/unix 디렉토리에서 configure;make를 실행하면 컴파일까지 끝난다. 수퍼유저 권한으로 make install을 실행하면 /usr/local/bin에서 tclsh과 wish를 실행할 수 있을 것이다. 이전 Tcl/Tk버전을 이미 설치했다면 tclsh과 wish가 최신 버전(tclsh8.1과 wish8.1)을 링크하고 있지 않을 테니, 확인을 해야 하는 번거로움이 있다. tclsh은 Tcl 명령만을 실행할 수 있는 인터프리터이고, wish은 Tcl/Tk 명령을 실행할 수 있는 인터프리터인데, 1부에서는 tclsh로도 충분히 모든 기능을 확인할 수 있을 것이다.

이 문서는 총 3부로 구성되어 있고, 1부는 Tcl의 문법, 2부는 Tk의 문법, 3부는 Tcl/Tk의 확장에 대해 다루고 있다.

1부 Tcl의 문법


1장 기초 문법과 변수, 표현식, 리스트

1) 기초 문법과 변수

가장 처음에 하고 싶은 일은 출력일 것이다. 모든 프로그래밍 언어가 그렇 게 하듯이 세상에 인사부터 하는 게 예의일 것이다.

puts "Hello, World!"

인사를 여러 번 할 수 있도록 문자열을 변수에 저장하려면,

set hello "Hello, World!"
put $hello

csh과 비슷하게 대입에는 set, 변수의 값 사용(dereference)에는 $를 사용한다. 더욱 재미있는 점은 csh처럼 unset 명령도 제공된다는 점이다.

다소 불편한 점이라면, 표현식을 처리하기 위해서는 반드시 expr을 사용해 야 한다는 것이다. 이것은 Tcl이 변수 타입에 대해서 느슨한 태도를 취하기 때문인데, 만약 다음과 같은 명령을 준다면,

set j 3; set i $j+1

i에는 문자열로 $j+1이 대입될 것이다. 이것은 원하는 바가 아닐 수도 있다. $j+1을 표현식(expression)으로 간주하고 처리하기 위해서는 앞에서 밝힌 바와 같이 expr을 써서,

set i [expr $j+1]

이라고 명령을 주어야만 한다. 다소 번거로운 일이긴 하지만, 거꾸로 생각해보면 표현의 자유로움과 다양성을 보장받을 수 있다는 것을 의미하기도 한다. 이 예에서 우리는 ;이 Tcl문장의 구분자(separator)로 사용된다는 것과 [ ]이 하나의 Tcl명령의 결과값을 반환한다는 것을 덤으로 알게 되었다.

Tcl에서 사용되는 특수문자는 일반적으로 셸(shell)에서 사용하는 a, ,
, f 등을 그대로 사용할 수 있다. 예를 들어, 다음의 실행 결과는 My name is “Young-il Cho”. 가 될 것이다.

set firstname "Young-il"
set lastname "Cho"
set myname "My name is "$firstname $lastname".
";
puts $myname

사실 Tcl의 각 문장 하나하나는 결과값을 매번 반환하기 때문에 마지막 줄의 puts는 필요하지 않을 수도 있다. 그렇기 때문에, puts $myname 대신에 set myname 이라고 입력해도 같은 결과를 얻을 수 있다.

변수는 타입이 정해지지 않았기 때문에, 다른 타입의 값을 대입할 수 있을 뿐만 아니라, 일반 변수를 배열처럼 사용할 수도 있다. 다음은 1월과 2월의 이름을 배열에 넣은 후에, 1월의 이름을 구하는 예제이다. 배열이라고는 하지만, 마치 Perl의 해시(hash) 변수처럼 사용할 수 있다.

set month(1) Jan
set month(2) Feb
set month(1)

다차원 배열도 가능하다. 다음은 4월 3일부터 5일까지의 요일을 다차원 배열에 대입해보는 아주 간단한 예이다.

set mday(4,3) Sat
set mday(4,4) Sun
set mday(4,5) Mon
puts $mday(4,4)

정수값을 가지고 있는 변수는 incr 명령을 이용하여 증감시킬 수 있는데, 그 변수에 들어 있는 값이 정수값으로 읽을 수 있는 값이기만 하면 된다.

set x "1"
incr x

여기에 다시 100배를 해서 3을 빼는 작업을 해보도록 하자. 100를 하기 위해서는 앞에서와 같이 expr $x*100도 가능하겠지만, append 명령을 이용하여 정수값 뒤에 0을 두 개 붙이는 것도 가능하다. 다음 예의 결과값은 2 * 100 - 3이니까 197이 될 것이다.

append x "00"
incr x -3
puts $x

강력하게 타입을 검사하는 컴파일 프로그래밍 언어에서는 상상도 하지 못할 엉뚱한 작업들을 태연하게 해내고 있다. 그만큼 융통성이 있다고 볼 수도 있고 거꾸로 사용자들이 에러를 내기 쉬울 수도 있다고 볼 수도 있다.

변수 중에서 예약되어 있는 변수 두 가지가 있다. 하나는 프로그램으로 넘겨지는 argument 리스트에 관련된 argv0, argv, argc이고, 또 하나는 환경 변수를 해시 형태로 가지고 있는 env이다. argv0은 Tcl로 작성된 스크립트의 이름을 가리키고, argv는 argument 리스트 전체를 배열로 가지고, argc는 argument의 개수를 정수값으로 가지고 있다. 다음의 예를 실행파일로 만들어서 argument로 hello 1 2 world를 사용해 실행해보면 이해가 어렵지 않을 것이다.

#!/usr/local/bin/tclsh
puts "This program $argv0 has $argc arguments : $argv
";
puts "User's ID is $env(USER).
";

2) 표현식

표현식은 수식이나 문자열 처리에 관한 것이다. Tcl에서 제공하는 연산자는 ANSI C의 것과 거의 동일하다. 사칙 연산자(+, -, *, -)와 나머지 연산자(%)를 비롯하여 관계연산자(>, <, >=. <=, !=, ==)와 논리연산자(!, &&, ||), 비트 연산자(!, ^, |, ~, <<, >>), 3항 연산자(? :)가 모두 지원된다.

또한 C에서 제공하는 수학 함수(삼각함수, 지수함수 등)를 거의 모두 사용할 수 있다. 기본적으로 프로그래밍의 경험이 조금이라도 있는 독자라면 여기에서 자세히 다루지는 않고 이름만 나열해도 쉽게 man page를 이용해 쉽게 찾아볼 수 있을 것으로 간주하고 넘어가도록 하겠다.

abs, acos, asin, atan, ceil, cos, cosh, double, exp, floor, fmod, hypot, int, log, log10, pow, round, sin, sinh, sqrt, tan, tanh

tclsh 실행 도중, man abs를 입력해보면 C의 man page를 볼 수 있다. abs, labs, llabs가 C에서 제공되고 있지만, Tcl에서는 long 타입의 변수를 사용할 수 없는 관계로 labs와 llabs는 사용할 수 없다. 물론 Tcl을 C에서 라이브러리로 사용할 때는 long이 사용되어야 하므로 long를 다루는 방법이 제공된다.

리스트나 문자열 형태의 값을 Tcl 명령으로 인식하여 작업을 수행하는 eval이라는 명령이 있다. eval {puts $tcl_version}라고 입력하면 puts $tcl_version이 실행될 것이다. 그러면 굳이 eval 명령을 사용해야 하는 이유에 대한 의구심이 생길 지도 모르겠다. 다음의 예를 보자.

set y 3
foreach f {set unset} {
eval $f y
}
puts $y

우선 변수 y에 3을 대입하고 foreach 문에서 변수 f에 set과 unset을 대입하면서 매번 eval $f y를 실행하기 때문에, set y와 unset y가 실행된다. unset에 의해 y를 변수로 사용할 수 없게 되었으므로 마지막 명령 puts는 에러를 발생하게 되어 있다.

문자열 처리에 관련된 명령들은 뒤에서 다시 설명하도록 하겠다.

3) 리스트

리스트는 [, ] 또는 “, “을 이용해 만들 수 있다.

set x {hello world}
set x "hello world"

는 같은 결과를 가지게 된다. 주의할 것은 set x [hello world]는 hello를 명령으로 인식하여 에러를 발생하게 된다는 점이다.

리스트를 다루기 위해 제공되는 명령에는 리스트를 붙이거나 인덱스를 이용해 특정 아이템을 꺼내거나 쪼개거나 검색하는 명령이 있다. 리스트 안에 리스트를 둘 수도 있는데, 이런 경우 lindex를 두번 사용하여 특정 단어를 지정할 수 있다. 다음은 New York의 York를 가리키는 예를 든 것이다.

set x {California Chicago {New York} Washington Texas}
lindex [lindex $x 2] 1

리스트를 붙이는 방법에는 리스트에 존재하는 모든 아이템을 모아서 붙이는 concat 명령과 각 리스트를 하나의 아이템으로 유지시키면서 붙이는 list 명령이 있는데, 다음을 비교해보자.

set y {Miami {Salt-lake City} Alaska}
concat $x $y
list $x $y

이 명령들의 결과는 다음과 같다.

California Chicago {New York} Washington Texas Miami {Salt-lake City} Alaska
{California Chicago {New York} Washington Texas} {Miami {Salt-lake City} Alaska}

이제는 리스트를 쪼개고 합치고 하는 작업을 하기로 한다. passwd파일의 일부을 변수 a에 담고, :을 구분자(delimiter)로 삼아서 : 단위로 쪼개어 변수 b에 리스트로 저장한다. 그리고는 다시 하나의 아이템으로 합쳐보도록 하자. 결과적으로 변수 c는 a와 같은 내용을 포함하게 될 것이다.

set a root:x:0:1:Super-User:/:/sbin/sh
set b [split $a :]
set c [join $b :]

이번에는 리스트의 일부를 찾거나 치환하거나 리스트를 정렬하는 명령에 대해 알아보도록 하자. 아이템을 찾는 것은 lsearch, 치환하는 것은 lreplce와 lrange, 정렬하는 것은 lsort이다. 리스트가 포함하고 있는 아이템 수를 반환하는 llength명령은 지나가도록 한다. 치환하거나 아이템을 찾을 때 사용되는 인덱스(index 또는 offset)를 설명하는 일은 필자에게나 독자에게나 모두 머리 아픈 일이 될 테니, 간단한 예로 대신하고자 한다. 다음은 리스트 변수 x의 2번째 위치부터 1 빼기 1개(0개)의 아이템과 Detroit라는 새로운 아이템을 바꿔치기 하고 2번째 위치부터 3 빼기 1개(2개)의 아이템을 Detroit와 Boston으로 치환하는 것이다. 그 다음은 3번째 공백 위치에 Illinois과 Detroit를 끼워넣는 것이다. 리스트의 일부를 꺼내는 lrange 명령도 있으나 여기서는 설명을 생략하기로 한다.

lreplace $x 2 1 Detroit
lreplace $x 2 3 Detroit Boston
linsert $x 3 Illinois Detroit
lrange $x 1 2

주의할 것은 이런 명령이 리스트 변수 x에 대해서 물리적인 효과를 가지지는 못한다는 점이다. x에 물리적인 효과를 주려면 set x [linsert $x 3 Illinois Detroit] 처럼 새로 대입해야만 한다. 위 명령의 결과는 다음과 같다.

California Chicago Detroit {New York} Washington Texas
California Chicago Detroit Boston Texas
California Chicago {New York} Illinois Detroit Washington Texas
Chicago {New York}

lappend는 위의 명령들과는 조금 다르다. 변수를 새로운 아이템을 추가한 결과로 바꾸어주기 때문이다.

lappend x Hawaii

이제 정렬해보자. lsort에 사용되는 옵션(option)에는 -ascii, -dictionary, -integer, -real, -command, -increasing, -decreasing, -index가 있는데, 디폴트(default) 값은 -ascii와 -increasing이다. Tcl/Tk가 버전 8로 올라오면서 -dictionary와 -index가 추가되었는데, -dictionary 옵션은 -ascii 옵션과 거의 동일하지만, 대소문자가 구분되지만 비교시에는 무시되고 단어 중간에 나오는 숫자는 문자로서 취급되지 않고 수치로 취급된다는 점이다. man page에 나오는 예를 적어보았다.

lsort -dictionary {bigBoy bigbang bigboy}
lsort -dictionary {x10y x9y x11y}

직접 실행하여 결과를 얻어보길 바란다. -index 옵션은 다음과 같은 예를 보면 쉽게 이해할 수 있을 것이다. 각각의 아이템은 리스트 형태로 되어있고, 그 리스트 중에서 index가 가리키는 아이템이 정렬할 때 키(key)로 사용되는 것이다.

lsort -integer -index 1 {{First 24} {Second 18} {Third 30}}

2장 제어문, 프러시져

1) 제어문

제어문에는 if, while, for, foreach, switch, continue, break 등이 있다. source나 이전에 다루었던 eval등이 제어문에 속하기는 하지만, source의 설명은 다음 장으로 미룬다. 제어문은 대체로 프로그래밍의 기본에 속하기 때문에 프로그래밍의 경험이 조금이라도 있는 독자라면 쉽게 이해할 것으로 간주하여 간단히 설명하고 다음으로 넘어가도록 하겠다. 다만 Tcl에서 주의해야 할 점은 C와는 달리 비교문은 {과 }로 둘러싸야 한다는 것이다.

if {$x < $y} {
puts "X is less than Y."
} else if {$x == $y} {
puts "X is equal to Y."
} else {
puts "X is greater than Y."
}

위의 예제는 x와 y의 수치값을 비교하여 각각의 경우에 대해 설명을 출력한다. 다음의 while, for, foreach는 모두 동일한 작업을 수행한다.

set i 0
while {$i > 5} {
puts $i
if {$i == 3} {
break;
}
incr i
}

for {set i 0} {$i > 5} {incr i} {
puts $i
if {$i == 3} {
break;
}
}

foreach i {0 1 2 3 4 5} {
puts $i
if {$i == 3} {
break;
}
}

2) 프러시져(procedure)

프러시져는 C의 함수(function)과 같다고 할 수 있다. 문법은 “proc 프러시져이름 매개변수리스트 프러시져본체”로 되어 있다. 명시적으로 return을 이용해서 결과를 반환할 수도 있고, return을 사용하지 않으면 마지막으로 사용된 표현식의 결과가 return값으로 사용된다.

proc average parameters {
set sum 0.0
foreach i $parameters {
set sum [expr $sum + $i]
}
return [expr $sum / [llength $parameters]]
}

프러시져의 호출은 다음과 같은 방법으로 할 수 있다.

average {1 2 3 4 5 6 7 8 9 10}

{1 2 3 4 5 6 7 8 9 10}이 parameters라는 리스트형 변수로 넘겨지는 것이다. 그러나 매개변수로 하나만 사용될 수 있는 것은 아니고 여러 개를 지정하거나 기본값을 지정할 수도 있다.

proc sum {x {y 3}} {
return [expr $x + $y]
}

변수 y의 기본값으로 3이 정해져 있기 때문에, sum 3 4 과 같은 형태의 호출이 가능하고, sum 3 과 같은 형태로도 호출을 할 수 있다. 그 결과값은 7과 6일 것이다.

프러시져 내부로 제어가 넘어가면 기본적으로는 외부나 상위의 프러시져의 변수를 사용할 수 없다. 매번 매개변수를 사용할 수는 없는 노릇이므로 global이나 upvar를 사용하여 상위레벨의 변수에 접근하게 된다. global 명령은 외부의 변수들을 전역변수로 지정하여 현재 프러시져 내부에서 지역변수처럼 자연스럽게 사용할 수 있게 한다. upvar는 Tcl의 “참조에 의한 호출”(call-by-reference)을 지원하기 위해 사용되는 명령으로 상위레벨의 프러시져에서 사용되는 변수를 새로운 이름으로 지정해서 지역변수로 사용하게 한다.

set virtual_average 30.0
proc average parameters {
global virtual_average
set sum 0.0
foreach i $parameters {
set sum [expr $sum + $i]
}
return [expr $virtual_average + $sum / [llength $parameters]]
}

이 예제는 평균을 구하는 위의 예제에 가평균을 사용하기 위해 약간 손을 본 프러시져이다. virtual_average가 외부에서 정의되었기 때문에 proc average에서는 사용할 수가 없어서 global 명령으로 virtual_average를 지역변수화한 것이다. 다음의 예는 upvar를 이용해서 새로운 지역변수로 상위레벨의 변수를 참조하는 것을 보이는 것이다.

proc print array_name {
upvar $array_name arr
foreach item [lsort $arr] {
puts $item
}
}

upvar는 정수값을 이용해서 참조의 대상이 되는 변수를 어느 레벨까지 찾을 것인가를 결정할 수 있는데, #0을 쓰면 무한대로 거슬러 상위로 올라갈 수 있다. 다음의 예는 바로 상위의 프러시져의 array_name이나 혹은 그 프러시져를 호출한 더 상위의 프러시져에서 사용된 array_name을 arr로 참조하겠다는 의미이다. 그 다음 줄은 상위로 계속 거슬러 올라가면서 array_name이라는 변수를 찾겠다는 의미이다.

upvar 2 $array_name arr
upvar #0 $array_name arr

upvar와 비슷한 명령으로 uplevel이라는 명령이 제공되는데, 이 명령은 eval과 upvar를 합쳐놓은 기능을 한다. 다음 명령이 프러시져 내부에서 사용되면 이 프러시져를 호출한 프러시져의 x변수를 참조해다가 43을 대입하는 것을 실행할 수 있게 한다.

uplevel 1 {set x 43}

3장 문자열 처리

Tcl은 8 bit-clean하다고 한다. 이 말은 ASCII 코드 이외의 문자를 다룰 수 있다는 의미이다. 그래서 tclsh에서 한글 출력이 가능하다. wish의 경우에는 조금 사정이 달라서, 폰트만 지정하면 출력은 문제가 없지만,아직까지도 입력서버에 관련된 사항이 미비하여 한글입력은 가능하지 않다.

문자열을 다루는 것은 리스트를 다루는 것과 비슷한 형태의 명령을 통해 가능하다. 다만 문자열 처리 명령은 string과 기타 명령을 조합한 형태이다. 예를 들면, string length는 문자열의 길이를 출력하는 명령이고, string index는 문자열의 몇번째 글자를 지정하는 명령이다.

1) 정규 표현식(regular expression)

문자열에 대해서 설명하기 전에 정규 표현식(regular expression)에 대해서 설명을 해야겠다. 다소 안타깝지만, Tcl의 정규 표현식은 Perl의 정규 표현식이 만들어내는 것보다는 표현력이 약하다. 그러나 사용하는데 별 무리는 없을 것이다.

[HTML]

. 임의의 한 글자
* 바로 앞 글자 또는 부분문자열(substring)이 0번 이상 반복될 수 있음
+ 바로 앞 글자 또는 부분문자열이 1번 이상 반복될 수 있음
? 바로 앞 글자 또는 부분문자열이 존재하거나 존재하지 않을 수 있음
^ 문자열의 처음 위치(첫 글자 자체를 가리키는 것은 아님)
$ 문자열의 마지막 위치(마지막 글자 아님)
특수문자 특수문자를 정규 표현식에서의 의미가 아닌 문자 그 자체로 인식
(정규표현식) ()를 이용하여 표현식과 일치하는 문자열을 부분문자열로 간주
[문자열] []안의 문자들 중 임의의 한 글자(예제 참고)
정규표현식|정규표현식 두 정규 표현식 중 임의의 한 표현식

[/HTML]

정규 표현식은 regexp나 regsub와 같은 명령을 통해 사용될 수도 있고, 문자열 관련 명령 중의 패턴(pattern)으로도 사용될 수 있다. 다음에 나올 예는 regexp와 regsub가 각각 패턴의 일치성을 검사하거나 패턴을 치환하는 것이다.

regexp {[^a-z].*} The
regexp {[^a-z].*} them
regexp {[^a-z].*} 123

[] 내부 문자열의 첫 글자로 ^가 나오면 [] 내부의 문자열을 제외한 모든 글자가 해당되도록 되어 있으므로 위의 예는 a부터 z까지의 소문자를 제외한 글자로 시작하는 단어만 참(true)로 간주하여 패턴 매치(pattern match)의 결과를 1이나 0으로 돌려주게 된다. 그러므로 두번째 단어를 제외한 두 단어는 패턴 매치가 성공하여 1이 반환된다. 매치된 부분문자열을 변수에 저장할 수도 있는데, 다음의 예를 보자.

regexp {([A-Z][a-z]+) ([a-z]+)} “King died!” line noun verb

line에는 King died가 noun에는 King이 verb에는 died가 저장될 것이다. 옵션으로 -nocase와 -indices가 지원되는데, -nocase는 대소문자를 구별하지 않으면서 패턴 매치를 수행하는 옵션이고, -indices는 일치하는 부분문자열을 변수에 저장하는 대신, 부분문자열의 첫 글자와 마지막 글자의 인덱스(index 또는 offset)을 대입해준다. 이 옵션을 써서 위의 예를 실행하게 되면 line은 0 8, noun은 0 3, verb는 5 8을 가지게 될 것이다.

다음은 regsub를 이용해 잘못된 스펠링 teh를 the로 바꾸고 그 바뀐 문자열을 x에 저장하는 것이다.

regsub teh "I can't believe teh truth of teh events" the x
puts $x

결과는 예상과는 달리 앞 쪽의 teh만 제대로 고쳐질 것이다. 매번 teh가 나올 때마다 고치려면 -all옵션을 사용하면 된다.

2) format

C의 printf는 너무 유명해서 모르는 프로그래머가 없을만한 함수이다. printf의 최대 장점은 문자열의 format을 정해서 프로그래머가 원하는 형태로 출력이 가능하다는 점인데, Tcl에서는 이런 format을 위해 format명령과 scan명령을 제공한다. format 명령은 printf와 비슷하고 scan은 sscanf과 비슷하다.

puts [format "My name is %s and my age is %d." Terzeron 25]

이런 식으로 해서 ANSI C의 printf가 제공하는 모든 기능을 Tcl에서도 사용 가능하다. 거꾸로 sscanf도 가능한데, 다음의 예를 보자. scan의 결과는 매치되는 부분문자열의 개수이고, 마지막에 사용된 변수에 매치되는 부분문자열이 저장된다.

scan "My name is Terzeron and my age is 25." "My name is %s" name

3) 문자열 명령

드디어 문자열 자체를 다루는 명령들에 대해서 알아볼 시간이다. 앞에서도 언급했다시피 string 명령은 compare(비교), first(첫번째 매치되는 글자의 index), last(마지막으로 매치되는 글자의 index), index(그 인덱스에 위치한 글자), length(문자열의 길이), match(패턴 매치 결과), range(부분문자열), tolower(소문자로 바꾸기), toupper(대문자로 바꾸기), trim, trimleft, trimright(해당 글자를 문자열 앞쪽이나 뒤쪽에서 잘라내기), 등의 명령과 조합하여 사용해야 한다.

string length "hello"
string range "hello" 3 4
string compare "hello" "hello"
string compare "hello" "Hello"
string index "hello" 4
string first l "hello"
string last l "hello"
string match {[a-z]*} “hello”
string trim aaabbcccaaa a
string trimleft aaabbcccaaa a
string trimright aaabbcccaaa a
string toupper hello
string tolower Hello

위 예제를 한 줄씩 설명하기는 시간 낭비일테니까, 각자 직접 실행하여 결과를 얻어보길 바란다. 필자는 string match가 잘 되지 않아서 다소 애를 먹은 경험이 있다. 위 예제의 결과를 차례대로 나열해보면 이렇다. 5, lo, 0, 1, o, 2, 3, 1, bbccc, bbcccaaa, aaabbccc, HELLO, hello

4장 파일 입출력, 프로세스 관리

이전까지는 어떤 시스템에서도 잘 실행되었을 법한 명령들에 대해서 다루어 보았다. 그러나 파일 입출력이나 프로세스 문제는 시스템마다 다르기 때문에, 지금부터는 POSIX를 따르는 유닉스 시스템에서 실행되는 명령들에 대해서 다루어보기로 한다.

1) 시스템

디렉토리를 지정하는 방법으로 C shell에서 사용하는 ~도 사용가능하다. ~terzeron이라라고 하면 terzeron이라는 사용자의 홈 디렉토리를 의미하고, ~라고 하면 지금 프로그램의 실행자의 홈 디렉토리를 의미한다. cd 명령으로 디렉토리를 옮길 수 있고, pwd 명령은 현재 디렉토리를 확인할 수 있고, file 명령으로 디렉토리에 관한 문자열 작업을 가능하게 돕는다. file 명령의 옵션으로는 다음과 같은 것들이 있다.

[HTML]

dirname 파일이 위치한 디렉토리 이름
rootname 파일의 확장자를 제외한 이름 전체
extension 파일의 확장자
tail 파일의 전체 경로에서 디렉토리 부분을 제외한 나머지
atime 최종 접근 시간(access time)
mtime 최종 변경 시간(modified time)
size 파일의 바이트(byte) 크기
type 파일의 타입
readlink 심볼릭 링크(symbolic link)가 가리키는 파일의 이름
exists 파일이 존재하는가?
isdirectory 파일이 디렉토리인가?
isfile 파일이 일반 파일인가?
executable 실행가능한가?
readable 읽기가능한가?
writable 쓰기가능한가?
owned 프로그램 실행자의 소유인가?
stat 파일의 atime, ctime, dev, gid, ino, mode, mtime, nlink, size, uid를 키로 가지는 배열을 반환한다.
lstat stat과 같으나 심볼릭 링크에 대해서는 링크 자체의 정보를 반환한다.

[/HTML]

glob을 이용하면 shell에서처럼 *이나 ?같은 메타 문자를 사용할 수 있다. glob 명령으로 파일이름 리스트를 구하고 file 명령으로 각 파일에 대한 정보를 구하거나 테스트를 수행할 수 있다. 다음의 예는 /usr/include/sys 디렉토리에 존재하는 헤더 파일 중 x로 시작하는 이름을 가진 파일들을 읽어서 그 크기를 조사해보는 것이다.

set filelist [glob /usr/include/sys/x*.h]
foreach f $filelist {
puts “$f [file size $f]”
}

2) 파일 입출력
파일을 열고 닫고 읽고 쓰기 위해 제공되는 명령에는 open, close, gets, puts, read, flush, eof, seek, tell 등이 있다. 파일을 다루기 위해서 가장 먼저 해야 할 일은 파일을 여는(open) 작업이다. 파일을 열면 파일id가 반환되고 이 id를 가지고 읽고 쓰는 작업을 수행할 수 있는 것이다.

set filename /usr/include/limits.h
set f [open $filename r]
while {[gets $f line] >= 0} {
puts $line
}
close $f

이 예제는 가장 간단한 파일 입출력을 보여주고 있다. 한 줄씩 읽어서 line이라는 변수에 저장하고 다시 출력하고 있다. 이제 다른 파일로 복사하는 프로그램으로 고쳐보자. 위의 예에서는 읽기 위해서 모드(mode)를 r(읽기)로 주었는데, 이번에는 강제적으로 덮어쓰기 위해 w+(강제로 덮어쓰기) 모드로 열어야 한다.

set infile /usr/include/limits.h
set outfile ~/limits.h
set inf [open $infile r]
set outf [open $outfile w+]
while {[gets $inf line] >= 0} {
puts $outf $line
}
close $outf
close $inf

flush는 puts가 버퍼링을 하기 때문에 출력이 지연되는 것을 강제적으로 막기 위해 사용되는 명령이다. 위의 예제에서 puts 명령 다음에 flush $outf라고 써주게 되면 puts가 버퍼를 다 채우지 못했더라도 강제적으로 버퍼를 비워서 출력이 빠르게 진행된다. 이 밖에 파일의 변위(offset)를 가지고 특정 위치로 이동할 수 있도록 seek 명령이 제공되고, 현재 파일의 어느 부분을 읽거나 쓰고 있는지 알기 위해 tell 명령이 제공된다. 이렇게 랜덤한 접근을 하는 경우 파일의 끝을 알아야 하는데, 이럴 때 eof 명령으로 파일의 끝인가를 검사할 수 있다.
3) 프로세스 관리

프로세스는 파일 상태로 디스크에 존재하는 프로그램이 실행되어서 메모리에 올라와 있는 것을 말한다. 프로세스를 만들기 위해서는 exc 명령으로 프로그램을 실행시켜야 한다. 다음 예는 stdio.h에서 printf라는 단어를 포함하고 그 중에서 FILE이라는 단어를 포함하지 않는 줄을 찾아주는 프로세스를 만드는 것이다.

exec grep printf /usr/include/stdio.h | grep -v FILE

현재 실행되고 있는 Tcl 스크립트의 출력을 다른 프로그램(필터)으로 넘길 수 있는데, 이것은 파일 입출력에서 다루었던 open 명령을 이용하는 것이다. 다음 예는 stdio.h에서 printf 라는 단어가 나오는 모든 줄을 test라는 파일로 저장하는 것을 보인다.

set in [open /usr/include/stdio.h r]
set out [open {| grep printf > test} w]
while {[gets $in line] >= 0} {
puts $out $line
}
close $out
close $in

open 명령에 사용될 수 있는 파이프(pipe)나 입출력 재지정(redirection)을 위해 사용할 수 있는 기호에는 >>파일명(출력을 기존 파일에 덧붙임), >@파일id(파일id로 출력), >&파일명(표준출력과 표준에러를 파일로 보냄), 2>파일명(파일로 표준에러를 보냄), <파일명(파일로부터 표준입력을 받음), <<값(값으로부터 표준입력을 받음), <@파일id(파일id를 통해 표준입력을 받음) 이 있다. 이 기호들은 exec에서도 사용할 수 있다.

exec find / -name core -exec rm -f "{}" ; 2> /dev/null > result

find를 실행하게 되면 유저 권한으로는 들어갈 수 없는 디렉토리가 종종 있다. 그래서 이럴 때 뜨는 에러 메시지를 모아서 /dev/null로 보내는 방법을 이용한다. find의 실행 결과는 result라는 파일로 모은다.

close하기 전에 pid $out을 실행해보면 grep 프로세스의 프로세스 id가 출력된다. 그리고 pid라고 실행하면 현재 Tcl 스크립트나 tclsh의 프로세스 id가 출력된다.

프로세스를 종료할 때, exit를 통해서 상태값(status)을 반환할 수 있는데, 기본 상태값은 0이다. 물론 exit 명령 자체의 결과값은 없다.

4) 스크립트 실행하기

source 명령을 이용하면 C shell의 source 명령처럼 Tcl 스크립트를 불러다가 실행할 수 있다.

source /usr/local/lib/tcl8.1/history.tcl

5장 에러와 예외사항(exception)

에러(error)라는 표현은 독자들도 많이 알고 있을 것이지만, 예외사항(exception)은 Java가 널리 퍼지기 전에는 프로그래밍 이론 과목에서나 접했을 법한 용어이다. 예외사항은 아주 간단하게 말하면 에러의 상위집합(superset)이다. 다시 말해서 에러는 예외사항의 부분집합이다. 에러라고 하기에는 큰 문제가 발생한 것은 아니고 그렇다고 정상 종료도 아닌 결과가 예외사항이 되는 것이다.

예외사항은 Java에서처럼 발생시키는 것과 그것을 잡아서 처리한다는 개념이 존재하는데, 에러나 예외사항을 발생시키기 위해서는 error 명령와 return 명령을 사용할 수 있다. 반대로 발생한 예외사항을 감지해내기 위해서는 catch 명령을 사용해야 한다.

unset zzz

은 에러가 발생할 것이다. zzz라는 변수를 이전에 선언한 적이 없기 때문이다. 그러나 이렇게 발생한 에러를 잡으려면 다음과 같이 할 수 있다. message 변수를 지정하면 변수에 에러 메시지가 저장된다.

catch {unset x}
catch {unset x} message

에러를 발생시킬 때, 에러 메시지는 반드시 지정해야 하지만, 에러 정보와 에러 코드는 선택하게 되면 Tcl에서 지정해서 사용하는 errorInfo나 errorCode 변수에 저장할 수 있다. error 명령의 문법은 “error 에러메시지 에러정보 에러코드”이다.

error "can't unset the variable: no such variable"

예외사항을 발생시키는 것은 return 명령을 이용하는 것이다. return도 error 명령처럼 에러코드와 에러정보, 에러메시지등을 지정할 수 있는데, 문법은 “return -code 결과값 -errorinfo 에러정보 -errorcode 에러코드 에러메시지”이다. 결과값의 기본값은 0이다. 다음의 예는 전역 변수인 errorInfo와 errorCode를 이용해 return하는 것이다. 프러시져 내에서 실행되는 경우에는 errorInfo와 errorCode를 global 명령을 이용해 지역 변수처럼 사용할 수 있도록 처리해 주어야 한다는 사실을 잊지 않아야 한다.

return -code 4 -errorinfo $errorInfo -errorcode $errorCode

6장 기타

1) 배열

앞에서 Tcl에서의 배열이란 Perl에서의 해시 변수와 비슷하다고 언급한 적이 있다. 배열을 다루는 명령은 문자열 처리 명령처럼 array 명령에 다양한 옵션을 결합시켜서 사용해야 한다. 배열의 키를 모두 찾기 위해서는 names라는 옵션을, 배열의 키의 수를 구하기 위해서는 size 옵션을 사용한다.

array names my_array
array size my_array

배열에 대해서 검색을 할 수도 있는데, 이 때는 다음과 같은 형태로 가능하다. 배열을 만들고, search id를 구해서 배열에 원소가 아직 남아 있는지 검사하고 남아 있으면 매번 키를 구해다가 키를 이용해 배열을 다루게 된다. 검색이 끝나면 search에 관련된 정보를 반환한다.

set my_arr(1) hello
set my_arr(2) world
set my_arr(name) terzeron
set my_arr(age) 25
set search_id [array startsearch my_arr]
while {[array anymore my_arr $search_id]} {
set key [array nextelement my_arr $search_id]
puts $my_arr($key)
}
array donesearch my_arr $search_id

2) 정보(info)와 내부 명령

현재 Tcl 스크립트와 현재 버전의 Tcl/Tk의 정보를 볼 구할 수 있는 명령은 info이다. info 명령의 옵션을 다음과 같이 정리해 보았다.

[HTML]

args 프러시져명 프러시져의 아규먼트(argument)를 리스트로 반환한다.
body 프러시져명 프러시져의 몸체 부분을 반환한다.
cmdcount 현재 인터프리터(interpreter)에서 사용된 명령의 수를 반환한다.
commands 패턴 실행가능한 명령을 리스트로 반환한다. 패턴을 지정하는 경우에는 매치되는 명령만 반환할 수도 있다.
complete 명령 버전 8에서 추가된 옵션으로 명령이 완전한 명령인가를 검사한다.
default 프러시져명 아규먼트명 변수명 프러시져의 해당 아규먼트가 기본값을 가지는지 검사하여 변수에 결과값을 써준다.
exists 변수명 변수가 존재하는지 검사한다.
globals 패턴 전역 변수의 리스트를 반환한다. 패턴 매치도 가능하다.
hostname 현재 프로세스가 실행되고 있는 컴퓨터의 이름을 반환한다.
level 번호 현재 스택(stack)의 레벨를 반환한다.
library 현재 Tcl의 라이브러리의 절대 경로를 반환한다.
locals 패턴 지역 변수의 리스트를 반환한다.
nameofexecutable 현재 실행 프로그램(tclsh)의 절대경로를 반환한다.
patchlevel 패치된 레벨을 반환한다.
procs 패턴 현재 정의되어 있는 프러시져를 패턴 매치하면서 반환한다.
script 스크립트의 이름을 반환한다.
sharedlibextension 공유라이브러리의 확장자명을 반환한다.
tclversion Tcl의 버전을 반환한다.
vars 패턴 현재 사용가능한 변수명의 리스트를 반환한다. 패턴 매치도 가능하다.

[/HTML]

내부명령으로 사용할 수 있는 것은 rename, time, trace 등이 있다. rename은 변수나 프로시져의 이름을 바꿀 수 있는데, 바꾸는 것 뿐만 아니라 {}로 바꾸어서 사용하지 못하게 제거할 수도 있다. time은 스크립트을 반복 수행하여 그 평균 수행시간을 계산해준다. 다음과 같이 사용할 수 있다.

time test.tcl 300

trace 명령은 변수의 값 변화를 추적할 수 있는 명령이다. 다음의 예를 보면서 설명하도록 하자.

trace variable age w p_age1
trace vinfo age
trace vdelete age w p_age1

이 예는 age라는 변수에 대해서 쓰기 작업이 이루어지면 p_age1이라는 프러시져를 매번 호출하고, age에 대한 trace 정보를 구하고, 추적 작업을 종료하는 것이다.

3) 히스토리(history)

C shell에서와 같이 history 명령을 실행하면 여태까지 실행했던 명령들을 볼 수가 있다.

history

history 명령에 keep 옵션을 주어 특정 개수만큼 히스토리를 유지할 수도 있고, nextid 옵션을 주어 다음 명령이 히스토리에서 몇 번째 명령이 될 것인가를 알아볼 수도 있다. 또한 redo 옵션으로 저장된 명령을 다시 실행할 수도 있다. 히스토리는 substitute 옵션을 통해 아이템을 변경할 수 있다.

history redo
history redo 3

set x helo
history substitute lo llo -1

마지막 명령은 바로 이전 명령행에서 lo를 llo로 바꾸게 하는 것이다. 조심해야 할 것이 하나 있다. 버전 8에서는 이 옵션이 없어지고 대신 change 옵션이 추가되었는데, 이 옵션은 명령행 전체를 치환한다. 비슷한 기능을 하는 add 옵션도 추가되었다. 히스토리를 지우는 clear 옵션도 제공된다.

set x helo
history change {set x hello} -1

!!는 history redo의 단축어로 제공된다. !번호 같은 형식으로도 히스토리를 이용할 수가 있다. substitute대신에 ^old^new와 같은 형식을 이용할 수도 있다. 이런 단축어들은 C shell에 기반한 것이다.

4) 매뉴얼(manual)

tclsh에서는 매뉴얼을 볼 수가 있다. 다른 키워드와 겹치지만 않는다면 man 키워드 로 매뉴얼을 볼 수 있다. 그러나 history만 해도 유닉스의 명령과 같기 때문에 man history로는 Tcl명령인 history를 공부할 수가 없다. 이럴 때는

man -s n history

로 문제를 해결할 수 있을 것이다.

서울대학교 전산과학과 데이타베이스 연구실 조영일

: