Problem: Two way Communications using Pipes, a Tk GUI and terminal Application.

WJG (25/12/23) I'm familiar with the use of Tcl/Tk to act as a GUI front end for console applications: set a command and parameters string, exec/open the application and then gather its output. But, I'm having ongoing issues with both talking and listening to a console application.

The example I have below is using hoichess, a simple Linux chess engine available from most repositories. When the script in run in a console, hoichess executes and any ouput that it writes to stdout is caught and displayed in the text widget.

If a valid chess move (lets say e4) is entered from the console hoichess reads it and a couple of seconds or later its move is displayed in the textbox. However, if a move is typed in the entry widget and returned, the contents are put to the console but these are ignored by hoichess. What am I missing out on? I've tried every which way that I can think of but to no avail.

Anyone got any ideas?

# !/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

package require Tk
text .t -yscrollcommand {.s set} -state disabled
entry .e -textvar e

scrollbar .s -orient vertical -command {.t yview}
pack .s -side right -fill y
pack .t -side left -fill both -expand 1
pack .e  

proc receive {chan} {
    .t configure -state normal
    .t insert end [read $chan]
    .t configure -state disabled
    .t see end
    if {[eof $chan]} { close $chan }
}


# Run hoichess and capture output
set command {hoichess -x off } 

set chan [open |[concat $command 2>@1]] ;# 0 = stdin 1 = stdout 2 = stderr
fconfigure $chan -blocking 0
fileevent $chan readable [list receive $chan]

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# running from terminal is fine, imput read, output loaded by script
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# neither one of these output types appear to be read by hoichess, why?
bind .e <Return> { 
        puts stderr $e ; flush stderr
        puts stdout $e ; flush stdout 
        }

chw 2023-12-25: the command channel (pipeline) you've opened is unidirectional, i.e. capable of reading the output of the command but you do not have a way to send data to its standard input. For full bidirectional communication you would need expect or the mechanism of e.g. https://github.com/lawrencewoodman/pty_tcl

HE 2024-01-02: I only agree partly with the answer of chw. It is simply not my experience of the last 25 years that I need Expect or pty.tcl to control an interactive command line tool. That doesn't mean that there could be cases where these both libraries are needed. For example some programs need for some reason their own PTY which the both named packages would provide.

I agree with that the opened command pipeline is uni-directional. But, this is because it is opened in the wrong mode and not because there is no way!

Therefore, I took the challenge and tried by myself. As a base I used the code from the original post and tried it under Manjaro with a self compiled Tcl 8.6.13.

Analyzing the code I could see directly two issues (beside some GUI related items I possibly would do different).

The first issue is the wrong mode used by "open |concat $command 2>@1". The mode is missing here. In the man page it is named as access and its default value is 'r'. That means from the command pipeline it is only possible to read. With 'w' or 'a' we would only be able to write to the pipeline. It is needed to use one of 'r+', 'w+', 'a+' or 'RDWR' (from https://www.tcl-lang.org/man/tcl/TclCmd/open.htm ). The code would now look like:

set chan [open |[concat $command 2>@1] a+] ;# 0 = stdin 1 = stdout 2 = stderr

Next issue is the code of the binding. There, the contents of variable 'e' will be printed to stdout and stderr. Both are standard channels of tclsh and not the standard channels of the client. In general if a process creates a child process three channels are established from child point of view. STDIN, STDOUT and STDERR. With other words the three standard channels always points to the parent. That means if the channel stderr or stdout is used inside the controlling tclsh it points into the direction of the parent, in my case bash. From our tclsh the child (hoichess) can be reached by writing to the channel we opened with the command pipeline which is stored in variable 'chan'. The code would now look like:

bind .e <Return> { 
        puts $chan; flush $chan
}

If we try the code with these changes, tclsh starts hoichess and can communicate with it. At least as long as no error is happening. Because I don't know hoichess I simply send 'e4' a second time and get an error message after which the client died. Double check with a direct start of hoichess from a bash showed that the program survived the error and we can enter other commands to it until we terminate it with 'quit'. So, I assume hoichess is one of these picky programs.

Like WJG figured out by himself on Two Way Pipe - Communicating with gnuchess. gnuchess is another chess program which will work fine with the fixed and extended code:

# !/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

# A small script to control tool_fragment.tcl or hoichess

package require Tk
text .t -yscrollcommand {.s set} -state disabled
entry .e -textvar e

scrollbar .s -orient vertical -command {.t yview}
pack .s -side right -fill y
pack .t -side left -fill both -expand 1
pack .e 

set debug 1 ;# '0' means Deactivate output to STDOUT of this script 

proc writeToTWandStdout {text} {
        global debug
    .t configure -state normal
    .t insert end "$text\n"
    .t configure -state disabled
    .t see end
        if {$debug} {
                puts $text
        }
        return
}

proc receive {std chan} {
        if {[catch {
                set tmp "Received from $std of client: '[gets $chan]'"
        } err]} {
                writeToTWandStdout "Get error with $std of client: $err"
        }
        writeToTWandStdout $tmp
    if {[eof $chan]} {
                writeToTWandStdout "Received EOF from $std of client"
                terminate
        }
}

proc terminate {} {
        global chan
        global rderr
        global wrerr
        writeToTWandStdout {Terminating}
        catch {puts $chan {exit}; flush $chan}
        catch {close $chan}
        catch {close $rderr}
        catch {close $wrerr}
        writeToTWandStdout {Terminated}
        exit
}

# Run gnuchess
set command {gnuchess} 

lassign [chan pipe] rderr wrerr
set chan [open |[concat $command [list 2>@ $wrerr]] a+]
fconfigure $rderr -blocking 0  -buffering line
fileevent $rderr readable [list receive stderr $rderr]
fconfigure $chan -blocking 0  -buffering line
fileevent $chan readable [list receive stdout $chan]

bind .e <Return> {
        writeToTWandStdout "Send to stdin of client:        '$e'"
        puts $chan $e; flush $chan
        set e {}
}
wm protocol . WM_DELETE_WINDOW terminate
writeToTWandStdout {Control: Started}
#vwait forever ;# Not needed here because we use package Tk which goes by itself into the event loop