stringtclvariable-expansion

Tcl: constructing list with literal `$` in values


I'm trying to construct a (Tcl/)Tk command (to be associated with a widget's -command), that contains a variable that must be expanded at runtime.

In the original code this variable had a fixed name (so everything was simple and we used {...}):

Something like this:

proc addaction {widgid} {
  $widgid add command -label "Action" -command {::actioncmd $::targetid}
}
addaction .pop1      # dynamic target is read from $::targetid
set ::targetid .foo  ## now the action for the .pop1 widget targets .foo
set ::targetid .bar  ## now the action for the .pop1 widget targets .bar

But now I would like to change this so we can replace the to-be-expanded variable with a fixed value in the "constructor". The constraints are:

So I came up with something like this:

proc addaction {widgid {id $::targetid}} {
  $widgid add command -label "Action" -command [list ::actioncmd $id]
}
addaction .pop1      # (as before)
addaction .pop2 .foo # static target for .pop2 is *always* .foo

Unfortunately my replacement code, doesn't work as the the $::targetid variable is no longer expanded. That is, if I trigger the widget's command I get:

$::targetid: no such object

Obviously the problem is with dynamically constructing a list that contains $args. Or more likely: the subtle differences between lists and strings.

At least here's my test that shows that I cannot mimick {...} with [list ...]:

set a bar
set x {foo bar}
set y [list foo $a]
if { $x eq $y } {puts hooray} else {puts ouch}
# hooray, the two are equivalent, and both are 'foo bar'

set b {$bar}
set x {foo $bar}
set y [list foo $b]
if { $x eq $y } {puts hooray} else {puts ouch}
# ouch, the two are different, x is 'foo $bar' whereas y is 'foo {$bar}'

So: how can I construct a command foo $bar (with an expandable $bar) where $bar is expanded from a variable?

A naive solution could be:

proc addaction {widgid {id {}}} {
  if { $id ne {} } {
    set command [list ::actioncmd $id]
  } else {
    set command {::actioncmd $::targetid}
  }
  $widgid add command -label "Action" -command $command
}

But of course, in reality the addaction proc adds more actions than just a single one, and the code quickly becomes less readable (imo).


Solution

  • For cases such as yours, the easiest approach might be:

    proc addaction {widgid {id $::targetid}} {
        $widgid add command -label "Action" -command [list ::actioncmd [subst $id]]
    }
    

    That will be fine as long as those IDs are simple words (up to and including using spaces) but does require that you go in with the expectation that the value is being substituted (i.e., that $, [ and \ are special).

    Alternatively, you could check how many arguments were passed and modify how the script is generated based on that:

    # The value of the default doesn't actually matter
    proc addaction {widgid {id $::targetid}} {
        # How many argument words were passed? Includes the command name itself
        if {[llength [info level 0]] == 2} {
            # No extra argument; traditional code
            $widgid add command -label "Action" -command {::actioncmd $::targetid}
        } else {
            # Extra argument: new style
            $widgid add command -label "Action" -command [list ::actioncmd $id]]
        }
    }