Updated 2017-10-10 22:43:53 by xk2600

xk2600 hope others find this useful... I needed the ability to process c-stype preprocessing directives. The added advantage (or disadvantage depending on use) is since TCL utilizes # for comments, if you write directives using this style, they are simply ignored by TCL when this facility is disabled.

Usage: preprocessor eval body
# preprocessor.tcl --
# 
# This file provides preprocessing directives and a limited macro facility.
# 
# Copyright (c) 2016-2017, Christopher M. Stephan <chris.stephan@live.com>
# Free to use, no warranty implied, use at your own risk.

namespace eval ::preprocessor {

  namespace export eval
  namespace ensemble create -command ::preprocessor -subcommands eval

  variable DEFINES
  array set DEFINES {}

  proc eval {body} {
    variable DEFINES

    set result     {}
    set defname    {}
    set defval     {}
    set breadcrumb {}
    set linenum    0
    set includecontent true
    set exprFilter {[^ @_A-Za-z0-9=<>()&|/%*^+-]*}
    set defFilter  {@[_A-Za-z0-9]+}

    foreach line [split $body "\n"] {
    
      incr linenum
      puts "#### line $linenum : $line      (bc:$breadcrumb)"
      switch -exact -- [lindex $line 0] {

        \#DEFINE {
          # process line
          set line [lassign $line instruction defname defval]

          # validate arguments
          if {[string length $line]>0} {
            error [format {preprocessor-define: too many arguments in %s on line %s: "%s"} $instruction $linenum $line]
          }
          if {[string equal $defval {}]} {
            error [format {preprocessor-define: define value not specified for "%s" on line %s:%s "%s"} $instruction $linenum "\n       " $line]
          }

          set DEFINES(@$defname) $defval

          unset defname
          unset defval
        }

        \#IFNDEF -
        \#IFDEF {
          #### PROCESS FOR @IFDEF AND @IFNDEF
          # single function checks for undefined and defined variables
          # by using setting invert variable to ! if this is IFNDEF 
          
          # make sure we're not already processing an IF or IFDEF
          if {![string equal $breadcrumb {}]} {
            set errmsg    {preprocessor-ifdef: recursive preprocessor scripts unsupported, currently processing:} 
            foreach stack_frame $breadcrumb {
              append errmsg "\n   --> $stack_frame"
            }
            error $errmsg
          }
          # process line...
          set line [lassign $line instruction defname]
          if {[string length $line]>0} {
            error [format {preprocessor-ifdef: too many arguments in %s on line %s: "%s"} $instruction $linenum $line]
          }
          # set breadcrumb
          lappend breadcrumb "${instruction}(${defname}) linenum:$linenum"
          # deal with inversion... (NDEF)
          set INVERT [expr {[string equal $instruction IFNDEF] ? {!} : {}}]
          if {[expr ${INVERT} [info exist DEFINES($defname)]]} {
            # passes condition... all lines unil else or elif are kept.
            set includecontent true
          } else { 
            # failes condition... all lines until else or elif are dropped.
            set includecontent false
          }
          
          unset instruction
          unset defname
        }

        \#IF {
          #### PROCESSES @IF

          # make sure we're not already processing an IF or IFDEF
          if {![string equal $breadcrumb {}]} {
            set errmsg    {preprocessor-if: recursive preprocessor scripts unsupported, currently processing:} 
            foreach stack_frame $breadcrumb {
              append errmsg "\n   --> $stack_frame"
            }
            error $errmsg
          }

          # process line
          set line [lassign $line instruction expression]
          if {[string length $line]>0} {
            error [format {preprocessor-if: too many arguments in @%s on line %s: "%s"} $instruction $linenum $line]
          }

          # check expression character use for invalid characters
          if {[regsub -all -- $exprFilter $expression {} filteredExpression] > 0} {
            set errmsg     {preproceossor: expression in \"%s\" on line $linenum contains illegal symbols.}
            lappend errmsg "\n        submitted"
            lappend errmsg "\n   [string map [list \n {} \r {}] [string trim $expression]]\n"
            lappend errmsg "\n        passed through filter:"
            lappend errmsg "\n   $filteredExpression\n"
          } 

          set ppexpr {}
          # substitute @defines
          foreach {full_match expr var} [regexp -inline -all -- {([^@]*)(@[_A-Za-z0-9]+)} $expression] {
            append ppexpr ${expr}$DEFINES($var)
          }

          # attempt evaluation
          if {[catch {expr $ppexpr} res]} {
            error [format {preprocessor: expression evaluation failure: "%s" on line %s} $expression $linenum]
          }
          # set breadcrumb
          lappend breadcrumb "${instruction}(${expression}) linenum:$linenum"
          # handle result
          if {$res} {
            # passes condition... all lines unil else or elif are kept.
            set includecontent true
          } else { 
            # failes condition... all lines until else or elif are dropped.
            set includecontent false
          }

          unset ppexpr
          unset instruction
          unset expression
          unset full_match
          unset expr
          unset var
          unset res
        }

        \#ELSE {
          #### PROCESSES @ELSE

          # make sure we're already processing an IF/IFDEF/IFNDEF
          if {[string equal $breadcrumb {}]} {
            error [format {preprocessor-else: ELSE before IF, IFDEF, or IFNDEF on line %s} $linenum]
          }

          # process line
          set line [lassign $line instruction expression]

          # invert current action as we're now operating on the ELSE
          set includecontent [expr {$includecontent ? false : true }]            
        }

        \#ENDIF {
          #### PROCESSES @ENDIF

          # make sure we're already processing an IF/IFDEF/IFNDEF
          if {[string equal $breadcrumb {}]} {
            error [format {preprocessor-endif: ENDIF before IF, IFDEF, or IFNDEF on line %s} $linenum]
          }

          set breadcrumb {}
          set includecontent true
        }

        default {
          #### CONTROLLED CONTENT

          if {$includecontent} {
            append result $line\n
          }
        }
      }
    }

    return [uplevel $result]
  }
}