Version 20 of An indentation syntax for Tcl

Updated 2012-10-03 18:11:54 by LarrySmith

Here's a little Tcl script which lets you use indentation as optional subsitute for braces, as pioneered by Python -jcw
See Also [L1 ]


First a sample of the new syntax (note that this is NOT standard Tcl!):


  # A little package to deal with an alternate Tcl source code syntax,
  # using optional indentation as a substitute for braces.
  #
  # by Jean-Claude Wippler, June 2002

  package provide indentcl 0.3

  namespace eval indentcl
    namespace export iconvert ieval isource

    namespace eval v
      variable lb \{
      variable rb \}

    proc emit {line}
      if {[llength $v::last] > 0}
        lappend v::result [join $v::last " "]
      foreach x $v::keep
        lappend v::result $x
      set v::keep {}
      set v::last [list $line]

    proc dedent {i}
      while {$i < [lindex $v::levels end]}
        set v::levels [lreplace $v::levels end end]
        lappend v::last $v::rb

    proc iconvert {text}
      set v::last {}
      set v::levels 0
      set v::keep {}
      set v::result {}

      foreach x [split $text \n]
        regexp {^(\s*)(.*)$} $x - a b
        if {$b == "" || [string index $b 0] == "#"}
          lappend v::keep $x
          continue
        set i 0
        foreach y [split $a ""]
          switch $y
            " "  { incr i }
            "\t" { incr i [expr {8 - $i%8}] }
        if {$i < [lindex $v::levels end]}
          dedent $i
          if {$i != [lindex $v::levels end] || [regsub {^(\s*)\\:} $x {\1} x]}
            lappend v::last \\
        \:elseif {$i > [lindex $v::levels end]}
          lappend v::last $v::lb
          lappend v::levels $i
        emit $x

      dedent 0
      emit ""
      set r [join $v::result "\n"]
      while {[llength $r] == 1 && [lindex $r 0] ne $r}
        set r [lindex $r 0] ;# strip top level indentation
      return $r

    proc ieval {script}
      uplevel 1 [iconvert $script]

    proc isource {file}
      set fd [open $file]
      set data [read $fd]
      close $fd
      set prev [info source]
      info source $file
      set result [uplevel 1 [iconvert $data]]
      info source $prev
      return $result

  if {$argv0 == [info script]}
    if {[llength $argv] != 2}
      puts "usage: $argv0 infile outfile"
      exit

    set fd [open [lindex $argv 0]]
    set data [read $fd]
    close $fd

    set data [indentcl::iconvert $data]

    set fd [open [lindex $argv 1] w]
    puts -nonewline $fd $data
    close $fd

And here's the Tcl script that can deal with the above input file:

  # A little package to deal with an alternate Tcl source code syntax,
  # using optional indentation as a substitute for braces.
  #
  # by Jean-Claude Wippler, June 2002

  package provide indentcl 0.3

  namespace eval indentcl {
    namespace export iconvert ieval isource

    namespace eval v {
      variable lb \{
      variable rb \} }

    proc emit {line} {
      if {[llength $v::last] > 0} {
        lappend v::result [join $v::last " "] }
      foreach x $v::keep {
        lappend v::result $x }
      set v::keep {}
      set v::last [list $line] }

    proc dedent {i} {
      while {$i < [lindex $v::levels end]} {
        set v::levels [lreplace $v::levels end end]
        lappend v::last $v::rb } }

    proc iconvert {text} {
      set v::last {}
      set v::levels 0
      set v::keep {}
      set v::result {}

      foreach x [split $text \n] {
        regexp {^(\s*)(.*)$} $x - a b
        if {$b == "" || [string index $b 0] == "#"} {
          lappend v::keep $x
          continue }
        set i 0
        foreach y [split $a ""] {
          switch $y {
            " "  { incr i }
            "\t" { incr i [expr {8 - $i%8}] } } }
        if {$i < [lindex $v::levels end]} {
          dedent $i
          if {$i != [lindex $v::levels end] || [regsub {^(\s*)\\:} $x {\1} x]} {
            lappend v::last \\ } } \
        elseif {$i > [lindex $v::levels end]} {
          lappend v::last $v::lb
          lappend v::levels $i }
        emit $x }

      dedent 0
      emit ""
      set r [join $v::result "\n"]
      while {[llength $r] == 1 && [lindex $r 0] ne $r}  {
        set r [lindex $r 0] ;# strip top level indentation }
      return $r }

    proc ieval {script} {
      uplevel 1 [iconvert $script] }

    proc isource {file} {
      set fd [open $file]
      set data [read $fd]
      close $fd
      set prev [info source]
      info source $file
      set result [uplevel 1 [iconvert $data]]
      info source $prev
      return $result } }

  if {$argv0 == [info script]} {
    if {[llength $argv] != 2} {
      puts "usage: $argv0 infile outfile"
      exit }

    set fd [open [lindex $argv 0]]
    set data [read $fd]
    close $fd

    set data [indentcl::iconvert $data]

    set fd [open [lindex $argv 1] w]
    puts -nonewline $fd $data
    close $fd }

If you run that second script with the first as input file... you get the second script again :o)

14apr03 jcw - Adjusted to deal with "re-indentation" by starting a line with "\:". There is one example of it in the above. It is needed to support mixed indents/dedents of the form:

  if {...} {
    ...
  } else {
    ...
  }

This can now be written as:

  if {...}
    ...
  \:else
    ...

In essence, "\:" means: "dedent, but keep on continuing last block". Ugly, but this gets around the issue. Note that "\:" is not just for else's.

Larry Smith Might I suggest the method I like to use with Modula?

  if { }
    then
      s1
      s2
      ...
      sn
    else
      s1
      s2
      ...
      sn
  end

Having a closing marker for such things makes syntax a lot cleaner. This is why all the Wirth languages after Pascal were designed this way. The various members of the Modula family, Oberon and its spin-offs, and finally Component Pascal all use this method. CP has a non-Wirthian successor called Zonnon that also uses this feature.


How's this for a twisted example:

  package require indentcl
  indentcl::ieval {
    package require critcl

    critcl::ccode fraction {int n} double
      double f = 1;
      while (--n >= 0)
        f /= 2;
      return f;

    puts [fraction 6]
  }

08nov05 jcw - Heh, interesting one :)

Here's another example I toyed with recently, using htmlgen to generate HTML code:

  proc do_html {}
    global info cmds objs this desc sect
    html
      head
        meta http-equiv=Content-type {content=text/html; charset=utf-8} - ""
        title - $info(name) $info(version)
        style type=text/css +
          <!-- var {color:#44a} pre {background-color:#eef} -->
      body
        do_sect name
          p - [b $info(name) - $info(title)]
        do_sect synopsis
          p ! $info(preamble)
          foreach {type name} $info(calls)
            do_call $type $name
            br -
        do_sect description
          do_text $desc(~)
          dl
            foreach {type name} $info(calls)
              dt
                do_call $type $name
              dd
                do_text $desc($name)
        foreach name $::info(sections)
          do_sect $name
            do_text $sect($name)
        if {$info(examples) ne ""}
          do_sect examples
            foreach {x y} $info(examples)
              set x [string trim $x]
              if {$x ne ""}
                do_text $x
              regsub {^\n} $y {} y
              set y [string trimright $y]
              if {$y ne ""}
                pre width=81n - "&nbsp; [esc $y]"
        foreach x {author copyright bugs website see-also keywords}
          if {$info($x) ne ""}
            do_sect [string map {- " "} $x]
              set y $info($x)
              switch $x
                author    { p - Written by $y. }
                copyright { p - Copyright {&copy;} $y }
                keywords  { p - [join $y ", "] }
                website   { p - See [a href=$y $y]. }
                default   { p - $y }

IL Simply amazing. jblz I freakin' love tcl.