3

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:

  • to keep the signature of addaction (therefore id must be optional)
  • not to touch ::actioncmd at all.

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).

umläute
  • 28,885
  • 9
  • 68
  • 122
  • Does targetId change during the execution of the program? – Shawn Feb 02 '23 at 10:58
  • yes. (that's what my `set ::targetid ...` lines in the first code snippet were meant to say) – umläute Feb 02 '23 at 11:57
  • 1
    With the signature `proc addaction {widgid {id $::targetid}}` you can _possibly_ do `-command "::actioncmd $id"` but that precludes $id being a string with whitespace in it. IMO the if-else "naive" solution is the safest way to go. – glenn jackman Feb 02 '23 at 13:56

1 Answers1

3

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]]
    }
}
Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • how's the 2nd solution better than my "naive solution" (i know you know your stuff in Tcl, so this question is really out of interest) – umläute Feb 05 '23 at 23:43