11

how can I redirect a proc output into a file in tcl, for example, I have a proc foo, and would like to redirect the foo output into a file bar. But got this result

% proc foo {} { puts "hello world" }
% foo
hello world
% foo > bar
wrong # args: should be "foo"
Erik Kaplun
  • 37,128
  • 15
  • 99
  • 111
Yike.Wang
  • 133
  • 1
  • 1
  • 6

8 Answers8

9

If you cannot change your code to take the name of a channel to write to (the most robust solution), you can use a trick to redirect stdout to a file: reopening.

proc foo {} { puts "hello world" }
proc reopenStdout {file} {
    close stdout
    open $file w        ;# The standard channels are special
}

reopenStdout ./bar
foo
reopenStdout /dev/tty   ;# Default destination on Unix; Win equiv is CON:

Be aware that if you do this, you lose track of where your initial stdout was directed to (unless you use TclX's dup to save a copy).

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
5

This should work:

% proc foo {} { return "hello world" }
% foo
hello world
% exec echo [foo] > bar
% exec cat bar
hello world
% exec echo [foo] >> bar
% exec cat bar
hello world
hello world
% 
YM Li
  • 51
  • 1
  • 1
3

Generally, I'd recommend the stdout redirection that Donal Fellows suggests in his answer.

Sometimes this may not possible. Maybe the Tcl interpreter is linked into a complex application that has itself a fancy idea of where the output should go to, and then you don't know how to restore the stdout channel.

In those cases you can try to redefine the puts command. Here's a code example on how you could do that. In plain Tcl, a command can be redefined by renaming it into a safe name, and creating a wrapper proc that calls the original command at the safe name - or not at all, depending on your intended functionality.

proc redirect_file {filename cmd} {
    rename puts ::tcl::orig::puts

    set mode w
    set destination [open $filename $mode]

    proc puts args "
        uplevel \"::tcl::orig::puts $destination \$args\"; return
    "

    uplevel $cmd

    close $destination

    rename puts {}
    rename ::tcl::orig::puts puts
}

You can also redirect text into a variable:

proc redirect_variable {varname cmd} {
    rename puts ::tcl::orig::puts

    global __puts_redirect
    set __puts_redirect {}

    proc puts args {
        global __puts_redirect
        set __puts_redirect [concat $__puts_redirect [lindex $args end]]
        set args [lreplace $args end end]
        if {[lsearch -regexp $args {^-nonewline}]<0} {
            set __puts_redirect "$__puts_redirect\n"
        }
        return
    }

    uplevel $cmd

    upvar $varname destination
    set destination $__puts_redirect
    unset __puts_redirect

    rename puts {}
    rename ::tcl::orig::puts puts
}

The Tcl'ers Wiki has another interesting example of redefining puts in more complex applications. Maybe this is inspiring as well.

Community
  • 1
  • 1
cfi
  • 10,915
  • 8
  • 57
  • 103
1

To accomplish this I wrapped the call to my Tcl proc in a shell script. This works on unix, not sure about other OS.

file - foo.tcl:

proc foo {} {puts "Hi from foo"}

file - foo.csh (any shell script will work, I'm using csh for this example): enter code here

#!/usr/bin/tclsh
source foo.tcl
eval "foo $argv"

file - main.tcl:

exec foo.csh > ./myoutput.txt

Of course these commands can be made 'fancy' by wrapping them in safety measures like catch, etc... for clarity sake I didn't include them, but I would recommend their use. Also I included $argv which isn't needed since proc foo doesn't take args, but typically IRL there will be args. Be sure to use >> if you just want to append to the file rather than overwriting it.

1

Thanks for sharing CFI. I would like to contribute as well. so, i made some changes to redefine puts on dump to file on startlog and end logging on endlog to file when ever we want. This will print on stdout and also redirect stdout to file.

proc startlog {filename } {
    rename puts ::tcl::orig::puts

    set mode w
    global def destination logfilename
    set destination [open $filename $mode]
    set logfilename $filename

    proc puts args "
        uplevel 2 \"::tcl::orig::puts \$args\"
        uplevel \"::tcl::orig::puts $destination \{\$args\}\"; return
    "
}

proc endlog { } {
    global def destination logfilename  
    close $destination
    rename puts {}
    rename ::tcl::orig::puts puts
    cleanlog $logfilename
    puts "name of log file is $logfilename"
}

proc cleanlog {filename } {  
    set f [open $filename]
    set output [open $filename\.log w]
    set line1 [regsub -all {\-nonewline stdout \{} [read $f] ""]
    set line2 [regsub -all {stdout \{} $line1 ""]
    set line3 [regsub -all {\}} $line2 ""]
    set line4 [regsub -all {\{} $line3 ""]
    puts $output $line4
    close $output
    file rename -force $filename.log $filename
}
Sat
  • 11
  • 1
1

We can try in this way also

% proc foo {} { return "hello world" }

% foo

hello world

% set fd [open "a.txt" w]

file5

% set val [foo]

hello world

% puts $fd $val

% close $fd

% set data [exec cat a.txt]

hello world
Daniel
  • 3,541
  • 3
  • 33
  • 46
vishnu
  • 21
  • 5
1

At present when you type foo > bar Tcl is trying to run a foo proc that takes 2 parameter, as this doesn't exist you get the error message. I can think of two ways you could tackle this problem.

You can redirect at the top level, so when you run tclsh tclsh > bar then all of the output will be redirected, however I doubt this is what you want.

You could change foo so that it accepts an open file as a parameter and write to that:

proc foo {fp} {
   puts $fp "some text"
}

set fp [open bar w]
foo $fp
close $fp
Jackson
  • 5,627
  • 2
  • 29
  • 48
-1

you can directly use the command "redirect"