tcl

Tcl uplevel and substitution


I would like to have a procedure with a single argument that either executes the argument using uplevel 1 (as if no procedure call) or print the argument using uplevel 1 (to have the right context for variable substitutions).

proc exec_or_print {cmd} {
    if 0 {
        uplevel 1 $cmd
    } else {
        uplevel 1 puts $cmd
    }
}

set a "temp"
exec_or_print {
    puts "something $a"
}

results in

can not find channel named "puts"
    while executing
"puts puts "something $a""

uplevel 1 [list puts $cmd] prints puts "something $a", which is missing the var substitution.

uplevel 1 puts {$cmd} prints ::yaml::_setAnchor, which is missing the procedure var substitution

uplevel 1 puts \"$cmd\" results in

extra characters after close-quote
    while executing
"puts "

What would be the right syntax to express the intended functionality?


Solution

  • The uplevel command, when presented with multiple non-level arguments, will concat those arguments to form the script to call in the other stack level. This is often not what you want, but is retained for backward compatibility. (The more-modern tailcall command does not work that way.)

    The first-level workaround is to use list to built the script to pass to uplevel when there are several arguments otherwise:

    proc exec_or_print {cmd} {
        if 0 {
            uplevel 1 $cmd
        } else {
            uplevel 1 [list puts $cmd]
        }
    }
    

    This doesn't show the values that would be substituted though. In that case, you need to call subst in the correct context, and that can have non-trivial side effects (because it can call command substitutions and variables could be traced):

    proc exec_or_print {cmd} {
        if 0 {
            uplevel 1 $cmd
        } else {
            puts [uplevel 1 [list subst $cmd]]
        }
    }
    

    The puts itself can be in the procedure; it doesn't care what stack frame it is called from.