Updated 2006-12-05 13:38:08

GPS: I'm sick of the discussion over the expand vs. {} vs. {*} and so on, so I came up with this. Comments are welcome...

GPS: To help the reader understand this (after a comment in Tcl'ers chat). The ~ token is discarded in a safe.eval call. ~ is like the proposed ` except it must be preceded and followed by whitespace. Any list or string after a ~ token is expanded. Simple no mess solution to a simple problem. The character is easily changed if you like.

I have added tests to demonstrate how it solves the problems of whitespace in widget pathnames, and potentially dangerous commands being executed.

GPS: Sep 2, 2003 -- I have improved version 5 of safe.eval in several ways. Rather than having to use two tildes like: ~SPACE~SPACE to pass a single tilde you may now use ~~. This fits in nicely with other tools that react to %% as meaning a literal %. I have left version 4 because the tests are pretty much the same, and I thought someone might find the difference interesting.
 #Copyright 2003 George Peter Staplin
 #You may use this under the same terms as Tcl.
 #Thanks to RS and AM for comments via the Tcl'ers Chat.
 #rev 5
 proc safe.eval args {
  set cmd [list]
  set expand 0
  foreach arg $args {
   if {"~~" == $arg} {
    lappend cmd ~
    set expand 0
   } elseif {"~" == $arg} {
    set expand 1
   } elseif {$expand} {
    foreach a $arg {
     lappend cmd $a
    }
    set expand 0
   } else {
    lappend cmd $arg
    set expand 0
   }
  }
  uplevel 1 $cmd
 }

 #Copyright 2003 George Peter Staplin
 #You may use this under the same terms as Tcl.
 #Thanks to RS and AM for comments via the Tcl'ers Chat.
 #rev 4
 proc safe.eval args {
  set expand 0
  set cmd [list]
  foreach arg $args {
   if {"~" == $arg} {
    if {$expand} {
     #we have ~ ~
     lappend cmd ~
     set expand 0
     continue
    }
    set expand 1
   } else {
    if {$expand} {
     set cmd [concat $cmd $arg]
    } else {
     lappend cmd $arg
    }
    set expand 0
   }
  }
  uplevel 1 $cmd
 }

 proc A {a b c} {
  puts A:
  foreach v [list a b c] {
   puts "  $v: [set $v]"
  }
 }

 proc B {a b c d} {
  puts B:
  foreach v [list a b c d] {
   puts "  $v: [set $v]"
  }
 }

 proc C {a b c} {
  puts C:
  foreach v [list a b c] {
   puts "  $v: [set $v]"
  }
 }

 proc main {} {
  #Let's do some simple expansion
  #A expects 3 arguments
  safe.eval A ~ [list 1 2 3]

  #Now let's use a potentially dangerous command
  set l [list hello world bye]
  #We want $l to expand, but not [dangerous]
  #B expects 4 arguments
  safe.eval B {[dangerous]} ~ $l

  set l [list hi world]
  #exec shouldn't be called
  safe.eval C ~ $l "\[exec\]"

  puts "Now testing with a typical Tk usage..."
  package require Tk
  button .b
  button .b2
  button .b3
  puts BEFORE:[winfo children .]
  safe.eval destroy ~ [winfo children .]
  puts AFTER:[winfo children .]

  #Now for a commonly complained about issue...
  #The case of a window pathname with a space in it.
  #In the normal eval this would be clobbered, but not with safe.eval.
  set flags "-text Exit -command exit "
  append flags "-bg orange -fg yellow"
  set win ".b ob"
  safe.eval button $win ~ $flags -bd 0
  pack $win
 }
 main