trace — 변수 접근, 명령 사용, 명령 실행 감시하기

trace — 변수 접근, 명령 사용, 명령 실행 감시하기

변수가 읽히거나 쓰일 때, 명령이 이름이 바뀌거나 이름이 지워질 때, 특정 명령이 실행될 때 무언가 반응하고 싶을 때가 있어요. trace는 바로 그런 지점에 끼어들어 지정한 명령을 실행하게 해주는 기능이에요.

출처: Tcl 공식 문서 - trace

본문

trace option ?arg arg ...?

이 명령은 특정 연산이 호출될 때마다 Tcl 명령이 실행되게 해요. 유효한 옵션(축약 가능)은 다음과 같아요.

trace add

  • trace add type name ops ?args?: 여기서 typecommand, execution, variable 중 하나예요.

  • trace add command name ops commandPrefix: name 명령이 ops 목록이 주는 방식 중 하나로 수정될 때마다 commandPrefix(추가 인자 포함)가 실행되도록 준비해요. name은 명령이 쓰는 보통의 네임스페이스 해석 규칙으로 해석돼요. 명령이 없으면 오류가 발생해요. ops는 관심 있는 연산을 나타내며 다음 항목 중 하나 이상의 목록이에요.

    • rename: 추적되는 명령이 이름이 바뀔 때마다 commandPrefix를 호출해요. 빈 문자열로 이름을 바꾸는 것은 삭제로 간주되며 "rename"으로 추적되지 않아요.
    • delete: 추적되는 명령이 삭제될 때 commandPrefix를 호출해요. 명령은 rename 명령으로 빈 문자열에 이름을 바꿔 명시적으로 삭제할 수 있어요. 인터프리터가 삭제될 때도 명령이 삭제되지만, 실행할 인터프리터가 없으므로 trace는 호출되지 않아요.

    trace가 발동하면, 추적되는 연산에 따라 여러 인자가 commandPrefix에 추가되어 실제 명령은 다음과 같아요.

    commandPrefix oldName newName op
    

    oldNamenewName은 추적되는 명령의 현재(이전) 이름과 이름이 바뀌는 대상 이름을 줘요("delete" 연산이면 빈 문자열). op는 명령에 수행되는 연산으로 위에서 정의한 rename 또는 delete 중 하나예요. trace 연산으로 명령이 삭제되는 것을 막을 수는 없어요. Tcl은 trace가 끝나면 항상 명령을 제거해요. 재귀적 이름 바꾸기나 삭제는 같은 유형의 추가 trace 평가를 일으키지 않아요. 따라서 명령을 자체 삭제하는 delete trace나 명령을 자체 이름 바꾸는 rename trace는 추가 trace 평가를 일으키지 않아요. oldNamenewName은 모두 그들이 나타나는 네임스페이스로 완전히 정규화돼요.

  • trace add execution name ops commandPrefix: name 명령이 실행될 때마다, ops 목록이 표시한 지점에서 trace가 발생하도록 commandPrefix(추가 인자 포함)가 실행되게 준비해요. name은 명령이 쓰는 보통의 네임스페이스 해석 규칙으로 해석돼요. 명령이 없으면 오류가 발생해요. ops는 다음 항목 중 하나 이상의 목록이에요.

    • enter: 실제 실행이 일어나기 직전에 name 명령이 실행될 때마다 commandPrefix를 호출해요.
    • leave: 실제 실행이 일어난 직후에 name 명령이 실행될 때마다 commandPrefix를 호출해요.
    • enterstep: 프로시저 name의 실행 시작부터 그 프로시저가 끝날 때까지 실행되는 모든 Tcl 명령에 대해 commandPrefix를 호출해요. 보고되는 Tcl 명령의 실제 실행 직전에 호출돼요. 예를 들어 "proc foo {} { puts \"hello\" }"가 있으면 "puts \"hello\""가 실행되기 바로 직전에 enterstep trace가 호출돼요. 프로시저를 가리키지 않는 명령 이름에 enterstep trace를 설정해도 오류가 나지 않고 그냥 무시돼요.
    • leavestep: 프로시저 name의 실행 시작부터 끝날 때까지 실행되는 모든 Tcl 명령에 대해 commandPrefix를 호출해요. 보고되는 Tcl 명령의 실제 실행 직후에 호출돼요. 프로시저를 가리키지 않는 명령 이름에 leavestep trace를 설정해도 오류가 나지 않고 그냥 무시돼요.

    trace가 발동하면, 추적되는 연산에 따라 여러 인자가 commandPrefix에 추가돼요. enterenterstep 연산의 경우:

    commandPrefix command-string op
    

    command-string은 현재 실행 중인 완전한 명령(enter 연산이면 추적된 명령, enterstep 연산이면 임의의 명령)을 완전히 확장된 인자 형태로 모두 담아요. op는 위에서 정의한 enter 또는 enterstep 중 하나예요. trace 연산으로 해당 명령을 삭제해 실행을 막을 수 있어요. 물론 명령이 나중에 실행되면 "invalid command" 오류가 발생해요.

    leaveleavestep 연산의 경우:

    commandPrefix command-string code result op
    

    command-string은 현재 실행 중인 완전한 명령이에요. code는 그 실행의 결과 코드를, result는 결과 문자열을 줘요. opleave 또는 leavestep 중 하나예요. 많은 enterstep/leavestep trace를 만들면 직관적이지 않은 결과가 나올 수 있다는 점을 주의하세요. 한 trace의 호출된 명령이 다른 trace의 추가 명령 호출로 이어질 수 있기 때문이에요.

    commandPrefix는 추적된 연산을 호출한 코드와 같은 컨텍스트에서 실행돼요. 그래서 프로시저에서 호출된 commandPrefix는 프로시저의 코드와 같은 로컬 변수에 접근할 수 있어요. 이 컨텍스트는 trace가 만들어진 컨텍스트와 다를 수 있어요. commandPrefix가 프로시저를 호출하면(보통 그렇게 함) 그 프로시저는 추적 연산을 호출한 코드의 로컬 변수에 접근하려면 upvaruplevel 명령을 써야 해요.

    실행 trace 중 commandPrefix가 실행되는 동안 name의 trace는 일시적으로 비활성화돼요. 이는 commandPrefix가 본문에서 name을 실행해도 다른 trace가 다시 호출되지 않게 해요. commandPrefix 실행 중 오류가 발생하면 name 명령 전체가 같은 오류를 반환해요.

    name에 여러 trace가 설정되면, enterenterstep 연산의 경우 trace된 명령이 원래 만들어진 순서의 역순으로 호출되고, leaveleavestep 연산의 경우 원래 만들어진 순서대로 호출돼요. 실행 trace의 동작은 다른 네임스페이스로 가져온 명령 이름에 대해서는 현재 정의되지 않아요.

  • trace add variable name ops commandPrefix: 변수 nameops 목록이 주는 방식 중 하나로 접근될 때마다 commandPrefix가 실행되게 준비해요. name은 일반 변수, 배열의 요소, 또는 배열 전체를 가리킬 수 있어요(즉 name은 괄호로 묶인 인덱스 없이 배열 이름만일 수 있음). name이 배열 전체를 가리키면 배열의 어떤 요소가 조작될 때마다 commandPrefix가 호출돼요. 변수가 없으면 만들어지지만 값은 주어지지 않아요. 그래서 네임스페이스 쿼리에는 보이지만 info exists 쿼리에는 보이지 않아요. ops는 다음 항목 중 하나 이상의 목록이에요.

    • array: 변수가 array 명령을 통해 접근되거나 수정될 때마다 commandPrefix를 호출해요. 단 namearray 명령 호출 시점에 스칼라 변수면 그렇지 않아요. name이 스칼라 변수면 array 명령으로의 접근은 trace를 발동시키지 않아요.
    • read: 변수가 읽힐 때마다 commandPrefix를 호출해요.
    • write: 변수가 쓰일 때마다 commandPrefix를 호출해요.
    • unset: 변수가 unset될 때마다 commandPrefix를 호출해요. 변수는 unset 명령으로 명시적으로, 또는 프로시저가 반환할 때(지역 변수 모두 unset됨) 암시적으로 unset될 수 있어요. 인터프리터가 삭제될 때도 변수가 unset되지만, 실행할 인터프리터가 없으므로 trace는 호출되지 않아요.

    trace가 발동하면 세 인자가 commandPrefix에 추가되어 실제 명령은 다음과 같아요.

    commandPrefix name1 name2 op
    

    name1name2는 접근되는 변수의 이름을 줘요. 스칼라면 name1이 변수 이름이고 name2는 빈 문자열. 배열 요소면 name1이 배열 이름, name2가 배열 인덱스. 전체 배열이 삭제되고 trace가 단일 요소가 아닌 배열 전체에 등록되어 있으면 name1이 배열 이름이고 name2는 빈 문자열. name1name2trace variable 명령에 쓰인 이름과 반드시 같지는 않아요. upvar 명령이 프로시저가 다른 이름으로 변수를 참조하게 하기 때문이에요. op는 변수에 수행되는 연산으로 위에서 정의한 read, write, unset 중 하나예요.

    commandPrefix는 추적된 연산을 호출한 코드와 같은 컨텍스트에서 실행돼요. 프로시저의 일부로 변수에 접근했다면 commandPrefix는 프로시저의 코드와 같은 로컬 변수에 접근할 수 있어요. 이 컨텍스트는 trace가 만들어진 컨텍스트와 다를 수 있어요. commandPrefix가 프로시저를 호출하면 추적된 변수에 접근하려면 upvaruplevel을 써야 해요. name1이 변수에 trace를 설정할 때 쓴 이름과 반드시 같지는 않다는 점도 주의하세요. upvar 명령으로 정의된 변수를 통해 접근하면 차이가 생길 수 있어요.

    readwrite trace의 경우 commandPrefix가 변수를 수정해 추적 연산의 결과에 영향을 줄 수 있어요. read나 write trace 중 commandPrefix가 변수 값을 수정하면 새 값이 추적 연산의 결과로 반환돼요. commandPrefix의 반환 값은 무시되는데, 단 어떤 종류의 오류라도 반환하면 추적된 연산도 trace 명령이 반환한 것과 같은 오류 메시지로 오류를 반환해요(이 메커니즘으로 읽기 전용 변수를 구현할 수 있어요). write trace의 경우 commandPrefix가 변수 값이 바뀐 뒤에 호출돼요. 변수에 새 값을 써서 write 연산에 지정된 원래 값을 덮어쓸 수 있어요. 읽기 전용 변수를 구현하려면 commandPrefix가 변수의 이전 값을 복원해야 해요.

    read나 write trace 중 commandPrefix가 실행되는 동안 변수의 trace는 일시적으로 비활성화돼요. 즉 commandPrefix가 수행하는 읽기·쓰기는 commandPrefix(나 다른 trace)를 다시 호출하지 않고 직접 일어나요. 다만 commandPrefix가 변수를 unset하면 unset trace는 호출돼요.

    unset trace가 호출되면 변수는 이미 삭제되어 있어서 trace 없이 정의되지 않은 것처럼 보여요. 프로시저 반환 때문에 unset이 발생하면 trace는 반환되는 프로시저의 변수 컨텍스트에서 호출돼요. 반환하는 프로시저의 스택 프레임은 더 이상 존재하지 않아요. unset trace 동안 trace는 비활성화되지 않아서, unset trace 명령이 새 trace를 만들고 변수에 접근하면 그 trace가 호출돼요. unset trace의 오류는 무시돼요.

    변수에 여러 trace가 있으면 최근 것부터 만들어진 순서대로 호출돼요. 한 trace가 오류를 반환하면 그 변수에 대해 추가 trace는 호출되지 않아요. 배열 요소에 trace가 있고 배열 전체에도 trace가 있으면 배열 전체의 trace가 요소의 trace보다 먼저 호출돼요.

    만들어진 trace는 아래의 trace remove variable 명령으로 제거되거나, 변수가 unset되거나, 인터프리터가 삭제될 때까지 유효해요. 배열 요소를 unset하면 그 요소의 trace는 제거되지만 배열 전체의 trace는 제거되지 않아요. 이 명령은 빈 문자열을 반환해요.

trace remove

  • trace remove type name opList commandPrefix: 여기서 typecommand, execution, variable 중 하나예요.
  • trace remove command name opList commandPrefix: 명령 name에 연산 opList와 명령 commandPrefix로 설정된 trace가 있으면 그 trace를 제거해 commandPrefix가 다시는 호출되지 않게 해요. 빈 문자열을 반환해요. name이 없으면 오류를 발생해요.
  • trace remove execution name opList commandPrefix: 실행 trace name에 연산 opList와 명령 commandPrefix로 설정된 trace가 있으면 제거해요. 빈 문자열을 반환해요. name이 없으면 오류를 발생해요.
  • trace remove variable name opList commandPrefix: 변수 name에 연산 opList와 명령 commandPrefix로 설정된 trace가 있으면 제거해요. 빈 문자열을 반환해요.

trace info

  • trace info type name: 여기서 typecommand, execution, variable 중 하나예요.
  • trace info command name: 명령 name에 현재 설정된 각 trace에 대해 하나의 요소를 담은 리스트를 반환해요. 각 요소는 그 trace와 연관된 opListcommandPrefix를 담은 두 요소짜리 리스트예요. name에 설정된 trace가 없으면 결과는 빈 문자열이에요. name이 없으면 오류를 발생해요.
  • trace info execution name: 실행 trace와 관련해 명령 name에 현재 설정된 각 trace에 대해 하나의 요소를 담은 리스트를 반환해요. 설정된 trace가 없으면 빈 문자열, name이 없으면 오류를 발생해요.
  • trace info variable name: 변수 name에 현재 설정된 각 trace에 대해 하나의 요소를 담은 리스트를 반환해요. name이 없거나 설정된 trace가 없으면 빈 문자열이에요.

하위 호환 서브컴마드

하위 호환을 위해 세 개의 추가 서브컴마드가 있어요.

  • trace variable name ops command: trace add variable name ops command와 동일해요.
  • trace vdelete name ops command: trace remove variable name ops command와 동일해요.
  • trace vinfo name: trace info variable name과 동일해요.

이 서브컴마드들은 더 이상 쓰이지 않으며(비권장) 향후 Tcl 버전에서 제거될 가능성이 있어요. 그들은 array, read, write, unset을 각각 a, r, w, u로 대체하고, ops 인자가 리스트가 아니라 rwua 같은 단순한 문자열 연결인 옛 문법을 사용해요.

예제

전역 변수 foobar가(그 시점에 upvar 명령으로 만든 다른 로컬 이름을 가져도) 갱신될 때마다 메시지를 출력해요.

proc tracer {varname args} {
    upvar #0 $varname var
    puts "$varname was updated to be \"$var\""
}
trace add variable foo write "tracer foo"
trace add variable bar write "tracer bar"

전역 변수 foobar가 항상 전역 변수 foobar의 곱을 담도록 해요.

proc doMult args {
    global foo bar foobar
    set foobar [expr {$foo * $bar}]
}
trace add variable foo write doMult
trace add variable bar write doMult

Tcl 프로시저 처리 중 실행되는 명령을 추적해 출력해요.

proc x {} { y }
proc y {} { z }
proc z {} { puts hello }
proc report args {puts [info level 0]}
trace add execution x enterstep report
x
  → report y enterstep
    report z enterstep
    report {puts hello} enterstep
    hello

더 알아보기

  • set — 변수 쓰기·읽기
  • unset — 변수 삭제