2

I recently asked a question about reopening stdin in C after passing EOF and now want the same behavior when using Tcl.

I can't seem to find a Tcl commmand doing what C clearerr would do. How can I pass ctrl+d to stdin at one time and later "reopen" stdin from the Tcl script? (Compiling an external library using C is cheating!)

Currently using Windows and thus ctrl+z but I assume they work similarly enough not to make a difference in this case. Here is some sample code:

set var {}; # declare var to hold the line
gets stdin var; # read a line
if {[string length $var]>0} {puts $var}; # print if read
if {[eof stdin]} { # if end-of-file reached
  puts {read from stdin was canceled. reopening just for fun}; # some debug message
  puts -nonewline "eof reached for stdin. enter something more to echo: "; flush stdout
  # clearerr() ???
  gets stdin var
  if {[string length $var]>0} {puts $var}
}

EDIT: Reading about fileevent I believe I can come up with a solution where user does not enter EOF at all to transition between stdin and GUI control.

Andreas
  • 5,086
  • 3
  • 16
  • 36
  • `set stdin [open /dev/tty r]` might work on Linux. I don't know if it is possible to reopen stdin on windows. – Brad Lanam Jul 17 '18 at 20:42

2 Answers2

1

How can I pass ctrl+d to stdin at one time and later "reopen" stdin from the Tcl script?

I am not sure whether this expectation makes sense from a Tcl POV. If [eof] is caught on a channel, the Tcl channel for stdin is not closed (unless done so explicitly using [close], or Tcl shuts down completely), so there is no need to reopen it. Watch:

proc isReadable { f } {
  # The channel is readable; try to read it.
  set status [catch { gets $f line } result]
  if { $status != 0 } {
    # Error on the channel
    puts "error reading $f: $result"
    set ::DONE 2
  } elseif { $result >= 0 } {
    # Successfully read the channel
    puts "got: $line"
  } elseif { [eof $f] } {
      # End of file on the channel
      puts "end of file; just continue working"
      # set ::DONE 1
  } elseif { [fblocked $f] } {
    # Read blocked.  Just return
  } else {
    # Something else
    puts "can't happen"
    set ::DONE 3
  }
}

fconfigure stdin -blocking false
fileevent stdin readable [list isReadable stdin]

# Launch the event loop and wait for the file events to finish
vwait ::DONE

This is just a standard snippet from Tcl documentation, also used in How to check if stdin is readable in TCL?. Aside, some comments from the answers and comments to your question at How to restart stdin after Ctrl+D? apply to Tcl as well. See Brad's comment using open or seek stdin 0 end, provided that the source of stdin is seekable.

mrcalvin
  • 3,291
  • 12
  • 18
  • edited the code sample. my problem is that once stdin has gone eof, I can't find a way to reset it to not being eof. that is what `clearerr` would have done, which is nowhere to be found in tcl docs. the second `gets stdin var` in my sample does not block since it is still eof. – Andreas Jul 18 '18 at 12:52
  • for me (macOS), the second `gets stdin var` blocks just fine. I cannot verify on Win, I am afraid. Whatever the behaviour, blocking or non-blocking should not be affected by the (internal) EOF flag. You would have to provide more details on your setup. In any case, using the event loop (`fileevent`) is probably a better design choice, but I cannot tell from the few details shimmering through (GUI vs. CLI etc.) – mrcalvin Jul 18 '18 at 17:40
  • besides: when you place the `[eof]` test in an appropriate (re-entering) branch in my snippet, you will see that the eof flag (the one Tcl maintains) will have been reset. – mrcalvin Jul 18 '18 at 17:43
  • as for "the second gets stdin var in my sample does not block since it is still eof". turns out that windows default console works differently than their siblings under *nix and macOS (unless in raw mode). [gets] assumes line buffering, but on windows default console, ctrl-z (eof) does not produce a newline character along with it. therefore, the second [gets] does not show any effect. (kudos to sebres @ Tclers chat for explaining to me). – mrcalvin Jul 19 '18 at 10:51
  • I also learnt that you can experience the same, no-newline behavior on *nix/ macOS using ctrl-d-d ... this will show the (unwanted) behaviour you are experiencing on windows also on those environments. – mrcalvin Jul 19 '18 at 14:29
  • thanks! that would suggest clearerr is called internally on read, maybe... gonna check the tclers chat out and ask the experts myself :) – Andreas Jul 20 '18 at 09:54
  • `clearerr` and `eof` have no play in this, if I understand this correctly; tclers chat is a good place to hang out. – mrcalvin Jul 20 '18 at 10:25
1

I believe I have a found a pure-TCL way around this problem: change the EOF character to something other than Ctrl-Z, read a dummy line (to remove the Ctrl-Z from the input buffer) and then reset the EOF character back to Ctrl-Z. Wrapped up in a procedure:

proc clearEOF {} {
    fconfigure stdin -eofchar { "\x01" "" }
    gets stdin dummy
    fconfigure stdin -eofchar { "\x1a" "" }
}

The choice of \x01 is somewhat arbitrary: essentially anything that is not likely to be in the input buffer alongside the Ctrl-Z should do.

Note: This has only been tested on Windows 10 with TCL 8.6.9.


Original Test Program

puts "Enter lines then Ctrl-Z <RETURN> to end"
while { [ gets stdin line ] >= 0 } {
    puts "Read: $line"
}
puts "Reached EOF"
puts "eof=[eof stdin]"
puts "Enter another line"
puts "gets=[gets stdin line]"
puts "Read: $line"

The wish is that after having read a number of lines, terminated by the EOF-marker (Ctrl-Z), you can then read another line. In practice, the EOF-state is not cleared, and the second call to gets does not wait for input and immediately returns -1 (=EOF):

Enter lines then Ctrl-Z <RETURN>
Line1
Read: Line1
Line2
Read: Line2
^Z
Reached EOF
eof=1
Enter another line                <-- This does not wait
gets=-1
Read:

Note: despite the TCL documentation including (my emphasis):

read ?-nonewline? fileID

Reads all the remaining bytes from fileID, and returns that string. If -nonewline is set, then the last character will be discarded if it is a newline. Any existing end of file condition is cleared before the read command is executed.

replacing the gets with something like set line [ read stdin ] makes no difference. Both commands return immediately. Having multiple repetitions of either command makes no difference: once TCL (and/or Windows1) thinks we've hit EOF, we stay at EOF!


My Solution

After some playing around, trying every file-manipulation command I could find that TCL posses, I came up with the following:

puts "Enter lines then Ctrl-Z <RETURN>"
while { [ gets stdin line ] >= 0 } {
    puts "Read: $line"
}
puts "Reached EOF"
puts "eof=[eof stdin]"
puts "Reset EOF char"
fconfigure stdin -eofchar { "\x01" "" }
puts "eof=[eof stdin]"
puts "Reading dummy line"
puts "gets=[gets stdin line]"
fconfigure stdin -eofchar { "\x1a" "" }
puts "Enter another line"
puts "gets=[gets stdin line]"
puts "Read: $line"

The output of this version does wait for more input:

Enter lines then Ctrl-Z <RETURN>
Line 1
Read: Line 1
Line 2
Read: Line 2
^Z
Reached EOF
eof=1
Reset EOF char
eof=0                             <-- EOF has been cleared
Reading dummy line
gets=1                            <-- Read 1 character: the Ctrl-Z
Enter another line
More text                         <-- Waits for this to be typed
gets=9
Read: More text

My assumption of what's happening is that changing the EOF-character does reset the EOF status (whether this happens "in TCL" or "in Windows" I'm unsure). With a different EOF-marker in place, we can read the line containing the Ctrl-Z that has been left in the input buffer. (Depending on what you entered either side of the Ctrl-Z, this would normally also contain an end-of-line marker). With the Ctrl-Z disposed of, we can reset the EOF-character back to Ctrl-Z and carry on reading from stdin as normal.


1 This issue on Microsoft's WSL GitHub page suggests that it could be Windows that is at fault: once the Ctrl-Z in the buffer, it always returns EOF, even when clearerr() is used. My reading of "Another bane for xplat programmers for the last 30 years, Ctrl-D on Unix and Ctrl-Z on Windows don't work the same." is that although the issue is against WSL, the problem is in Windows itself. Interestingly, the final comment (at time of writing) states "Fixed in Windows Insider Build 18890", but one might still need to call clearerr().

TripeHound
  • 2,721
  • 23
  • 37