1
votes

I am trying to use a TCL program to read TCL modulefiles and translate them into another language. This has worked quite well until now. For reasons too complicated explain I have to treat "puts stderr" differently in different parts of the modulefile. I am asking for help in trying to figure out a way to do this.

Below is an extremely abbreviated modulefile called "modfile". This "modfile" is translated or "sourced" by a second tcl program.

    proc ModulesHelp { } {
      puts stderr "(1) This is a help message"
    }
    puts stderr "(2) Here in modfile"

The puts statement inside ModulesHelp has to be treated differently from the second puts statement. Note that any solution CAN NOT CHANGE "modfile". That file is not under my control.

Here is my attempt at a solution:

    #!/usr/bin/env tclsh
    proc myPuts { stream msg } {
        global putMode
        puts stdout "putMode: $putMode"    # <====== HERE 1
        puts stdout $msg
    }

    proc report { message } {
        puts stderr "$message"
    }

    proc execute-modulefile { m } {
        global MODFILE putMode

        set putMode "normal"
        set slave   "__mod"

        interp create $slave
        interp alias  $slave puts {} myPuts
        interp alias  $slave report {} report
        interp eval   $slave {global putMode }
        interp eval   $slave [list "set" "putMode" $putMode]
        interp eval   $slave [list "set" "m"       $m]

        set errorVal [interp eval $slave {
        set sourceFailed [catch {source $m } errorMsg]
            if {[info procs "ModulesHelp"] == "ModulesHelp" } {
                set putMode "InHelp"     # <======= HERE 2
                ModulesHelp
            }
            if {$sourceFailed} {
                report $errorMsg
                return 1
            }
        }]
        interp delete $slave
        return $errorVal
    }

    eval execute-modulefile $argv

To run this I do: $ ./try.tcl modfile where obviously the above script is "try.tcl" and the modulefile is "modfile". I am running this on a linux system with tcl 8.4.

What I would like to have happen is that at the line labelled "HERE 2" I like to somehow change the global variable of "putMode" from "normal" to "InHelp" so that I can change the behavior at the line labelled "HERE 1". No matter what I have tried to do I can not change the value of putMode at "HERE 1" by doing something at "HERE 2". The puts statement at "HERE1 always says "normal".

Using a global variable seems like the easiest solution but if someone could show me how to use namespaces or some other technique, I'll be happy with that as well.

Thanks for any insight.


I greatly appreciate the time that others have looked at my question. I am trying to use the proposed solution and I'm not quite seeing it. Here is my new attempt at a solution (This doesn't work at all). Can someone suggest how I modify this code to change "putMode" to inHelp where "HERE 2" is? Also is there something special that needs to go where "HERE 1" is?

    #!/usr/bin/env tclsh
    proc myPuts { stream msg } {
        global putMode
        puts stdout "putMode: $putMode"  # <=== HERE 1
        puts stdout $msg
    }

    proc report { message } {
        puts stderr "$message"
    }


    proc PutModeTrace {childInterp operation realPutMode} {
        # Alias the main array element for the purposes of this procedure
        upvar \#0 PutMode($childInterp) realPutMode
        if {$operation eq "read"} {
            interp eval $childInterp [list set putMode $realPutMode]
        } elseif {$operation eq "write"} {
            set realPutMode [interp eval $childInterp {set putMode}]
        }
    }
    proc execute-modulefile { m } {
        global MODFILE putMode

        set putMode "normal"
        set slave   [interp create]

        interp alias  $slave puts {} myPuts
        interp alias  $slave report {} report
        interp eval   $slave {global putMode }
        interp eval   $slave [list "set" "putMode" $putMode]
        interp eval   $slave [list "set" "m"       $m]
        interp eval   $slave [list "set" "slave"   $slave]
        interp eval   $slave {trace add variable putMode {read write} PutModeTrace}
        interp alias  $slave PutModeTrace {} PutModeTrace $slave
        set errorVal [interp eval $slave {
        set sourceFailed [catch {source $m } errorMsg]
            if {[info procs "ModulesHelp"] == "ModulesHelp" } {
                set start "help(\[\["
                set end   "\]\])"
                PutModeTrace $slave "write" "inHelp"  # <=== HERE 2
                puts stdout $start
                ModulesHelp
                puts stdout $end
            }
            if {$sourceFailed} {
                report $errorMsg
                return 1
            }
        }]
        interp delete $slave
        return $errorVal
    }

    eval execute-modulefile $argv
3
Just so as you know, 8.4 is very close to the end of its extended support period.Donal Fellows

3 Answers

1
votes

The problem is that the slave and the master are different interpreters. This means that every interpreter has it's own

  • commands
  • variables
  • namespaces
  • channels

You can't simply change a variable in the master from the slave, so the easiest solution would be:

interp alias $slave InHelp {} set ::putMode InHelp

and calling this alias instead.

Some other notes:

  • An other option would be to change the puts alias when InHelp is called. Example

    proc InHelp {slave} {
         interp alias $slave puts {} HelpPuts
    }
    

    and using it with interp alias $slave {} InHelp $slave

  • You don't have to assign a name for the slave. Just do

    set slave [interp create]
    
  • Single words don't have to be quoted, so

    list "a" "b" "c"
    

    is equal to

    list a b c
    
  • If you need argument expansion (and use at least Tcl 8.5) use {*}$argv instead eval.
    But because execute-modfile only accept one argument, execute-modfile [lindex $argv 0] should do the job.

1
votes

As Johannes writes, variables are entirely separate in different interpreters; they're not shared at all.

However, you can use trace and some aliases to couple things together. I'll show how to do it for a simple scalar variable (with the parent having an array of them, presumably one for each child interpreter), under the assumption that you never want to have the setting of the variable in the master interpreter trigger a trace in the child.

interp eval $child {trace add variable putMode {read write} PutModeTrace}
interp alias $child PutModeTrace {} PutModeTrace $child
proc PutModeTrace {childInterp varName elementName operation} {
    # Ignore the elementName argument

    # Alias the main array element for the purposes of this procedure
    upvar \#0 PutMode($childInterp) realPutMode
    if {$operation eq "read"} {
        interp eval $childInterp [list set putMode $realPutMode]
    } elseif {$operation eq "write"} {
        set realPutMode [interp eval $childInterp {set putMode}]
    }
}

This makes it so that whenever the child interpreter reads or writes the putMode variable, the read/write gets reflected into the master.


It's easier to map a command (via an alias) though, and if you were using Tcl 8.6 I'd suggest stacking and unstacking custom transformations on stderr instead. (But that's a massively more sophisticated technique.)

0
votes

Thanks for all the help. It has taken me a while to understand what was being proposed. Here is the code that does what I want:

     #!/usr/bin/env tclsh
    proc myPuts { stream msg } {
        global putMode
        if {$putMode != "inHelp"} {
            puts stderr $msg
        } else {
            puts stdout $msg
        }
    }

    proc report { message } {
        puts stderr "$message"
    }


    proc setPutMode { value } {
        global putMode
        set putMode $value
    }


    proc execute-modulefile { m } {
        global MODFILE putMode

        set putMode "normal"
        set slave   [interp create]

        interp alias  $slave puts {} myPuts
        interp alias  $slave setPutMode {} setPutMode
        interp alias  $slave report {} report
        interp eval   $slave {global putMode }
        interp eval   $slave [list "set" "putMode" $putMode]
        interp eval   $slave [list "set" "m"       $m]
        interp eval   $slave [list "set" "slave"   $slave]
        interp eval   $slave {trace add variable putMode {read write} PutModeTrace}
        interp alias  $slave PutModeTrace {} PutModeTrace $slave
        set errorVal [interp eval $slave {
        set sourceFailed [catch {source $m } errorMsg]
            if {[info procs "ModulesHelp"] == "ModulesHelp" } {
                set start "help(\[\["
                set end   "\]\])"
                setPutMode "inHelp"
                puts stdout $start
                ModulesHelp
                puts stdout $end
                setPutMode "normal"
            }
            if {$sourceFailed} {
                report $errorMsg
                return 1
            }
        }]
        interp delete $slave
        return $errorVal
    }

    eval execute-modulefile $argv

Thanks again.