0

I have a variable, let's say xx, with a list of index 0 and index 1 values. I want to modify a script (not mine) which previously defines a function, pptable, i.e.,

proc pptable {l1 l2} {
    foreach i1 $l1 i2 $l2 {
        puts " [format %6.2f $i1]\t[format %6.2f $i2]"
    }
}

so that it displays the output into two columns using

pptable [lindex $xx 1] [lindex $xx 0]

However, I want to write the output directly to a file. Could you tell me how I can send the data to a file instead to the display?

Peter Lewerin
  • 13,140
  • 1
  • 24
  • 27

2 Answers2

2

One of the neatest ways of doing this is to stack on a channel transform that redirects stdout to where you want it to go. This works even if the write to stdout happens from C code or in a different thread as it plugs into the channel machinery. The code is a little bit long (and requires Tcl 8.6) but is reliable and actually mostly very simple.

package require Tcl 8.6;  # *REQUIRED* for [chan push] and [chan pop]

proc RedirectorCallback {targetHandle op args} {
    # The switch/lassign pattern is simplest way of doing this in one procedure
    switch $op {
        initialize {
            lassign $args handle mode
            # Sanity check
            if {$mode ne "write"} {
                close $targetHandle
                error "this is just a write transform"
            }
            # List of supported subcommands
            return {initialize finalize write}
        }
        finalize {
            lassign $args handle
            # All we need to do here is close the target file handle
            close $targetHandle
        }
        write {
            lassign $args handle buffer
            # Write the data to *real* destination; this does the redirect
            puts -nonewline $targetHandle $buffer
            # Stop the data going to *true* stdout by returning empty string
            return ""
            # If we returned the data instead, this would do a 'tee'
        }
        default {
            error "unsupported subcommand"
        }
    }
}

# Here's a wrapper to make the transform easy to use
proc redirectStdout {file script} {
    # Stack the transform onto stdout with the file handle to write to
    # (which is going to be $targetHandle in [redirector])
    chan push stdout [list RedirectorCallback [open $file "wb"]]

    # Run the script and *definitely* pop the transform after it finishes
    try {
        uplevel 1 $script
    } finally {
        chan pop stdout
    }
}

How would we actually use this? It's really very easy in practice:

# Exactly the code you started with
proc pptable {l1 l2} {
    foreach i1 $l1 i2 $l2 {
        puts " [format %6.2f $i1]\t[format %6.2f $i2]"
    }
}

# Demonstrate that stdout is working as normal
puts "before"

# Our wrapped call that we're capturing the output from; pick your own filename!
redirectStdout "foo.txt" {
    pptable {1.2 1.3 1.4} {6.9 6.8 6.7}
}

# Demonstrate that stdout is working as normal again
puts "after"

When I run that code, I get this:

bash$ tclsh8.6 stdout-redirect-example.tcl 
before
after
bash$ cat foo.txt 
   1.20   6.90
   1.30   6.80
   1.40   6.70

I believe that's precisely what you are looking for.


You can do this with less code if you use Tcllib and TclOO to help deal with the machinery:

package require Tcl 8.6
package require tcl::transform::core

oo::class create WriteRedirector {
    superclass tcl::transform::core

    variable targetHandle
    constructor {targetFile} {
        set targetHandle [open $targetFile "wb"]
    }
    destructor {
        close $targetHandle
    }

    method write {handle buffer} {
        puts -nonewline $targetHandle $buffer
        return ""
    }

    # This is the wrapper, as a class method
    self method redirectDuring {channel targetFile script} {
        chan push $channel [my new $targetFile]
        try {
            uplevel 1 $script
        } finally {
            chan pop $channel
        }
    }
}

Usage example:

proc pptable {l1 l2} {
    foreach i1 $l1 i2 $l2 {
        puts " [format %6.2f $i1]\t[format %6.2f $i2]"
    }
}

puts "before"
WriteRedirector redirectDuring stdout "foo.txt" {
    pptable {1.2 1.3 1.4 1.5} {6.9 6.8 6.7 6.6}
}
puts "after"
Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • Note that the code intercepts `stdout` at the byte level and needs no further encoding transformation or end-of-line processing; that's why the text file is opened in `wb` mode and the internal puts uses `-nonewline`. – Donal Fellows Mar 01 '18 at 12:00
0

I assume you don't want or can't modify the existing script and proc pptable, correct?

If so, there are different options, depending on your exact situation:

  • Redirect stdout: tclsh yourscript.tcl > your.out
  • Redefine puts (for a clearly defined scope):

    rename ::puts ::puts.orig 
    proc puts args { 
        set fh [open your.out w]; 
        ::puts.orig $fh $args; 
        close $fh
    }
    # run pptable, source the script
    

    This theme has been covered before, e.g., tcl stop all output going to stdout channel?

  • Rewire Tcl's stdout channel (not necessarily recommended):

    close stdout
    open your.out w
    # run pptable, source the script
    

    This has also been elaborated on before, e.g. Tracing stdout and stderr in Tcl

mrcalvin
  • 3,291
  • 12
  • 18