Tcl Source Code

View Ticket
Login
Ticket UUID: 0e7c09eb49912d9c58b1d64dea507f64b480e8b2
Title: tclsh in a command pipeline: ::tcl_interactive is 0, but interpreter is in interactive mode (halfway).
Type: Bug Version: 8.6.4
Submitter: pooryorick Created on: 2015-04-14 14:55:42
Subsystem: 69. Other Assigned To: nobody
Priority: 5 Medium Severity: Important
Status: Open Last Modified: 2015-04-14 17:48:43
Resolution: None Closed By: nobody
    Closed on:
Description:

tclsh is schizophrenic about interactive mode. In the case where a file is given as an argument to tclsh, the code path is clear, and tclsh runs in true non-interactive mode, passing the script off to Tcl_FSEvalFileEx. If stdin is a terminal and a script is not provided, ::tcl_interactive is true, and tclsh runs in interactive mode. Prompts and command results are printed to stdout, error messages printed on stderr, errors do not end the REPL, and when the end of the file is reached, the exit status is 0 even if the last command resulted in an error.

Here's the multiple-personality case: If stdin is not a terminal and a script is not provided, ::tcl_interactive is false but tclsh still operates mostly in interactive mode, except that it doesn't print prompts and command results to stdout. What it does do is not halt on errors, and exit with a status of 0 even if the last command in the script returns an error. This means that the exit status of a pipeline like the following is 0:

echo some_command_that_doesnt_exist | tclsh 

It also means that how a script is passed to the interpreter can determine its exit status. Consider a script named myscript with the following contents:

puts too many arguments

If invoked like this, the exit status is 1:

tclsh myscript

But if invoked in these ways, the short error result is displayed, the exit status is 0:

tclsh < myscript

cat myscript | tclsh

Tclsh in a pipeline is an important use case on Unix-like systems. It could behave in a more expected manner by halting on errors and passing a non-zero exit status when script input is via stdin and that channel is not a terminal. The change to accomplish that is trivial:

--- generic/tclMain.c
+++ generic/tclMain.c
@@ -540,10 +540,14 @@
 	    if (code != TCL_OK) {
 		chan = Tcl_GetStdChannel(TCL_STDERR);
 		if (chan) {
 		    Tcl_WriteObj(chan, Tcl_GetObjResult(interp));
 		    Tcl_WriteChars(chan, "\n", 1);
+		}
+		if (!is.tty) {
+		    exitCode = 1;
+		    goto done;
 		}
 	    } else if (is.tty) {
 		resultPtr = Tcl_GetObjResult(interp);
 		Tcl_IncrRefCount(resultPtr);
 		Tcl_GetStringFromObj(resultPtr, &length);

Apart from the modified lines in this page, it can be seen that this code path was already making decisions based on is.tty, even though the whole path is ostensibly already the interactive path. What it really is is the stream path. One additional decision based on is.tty seems reasonable enough. This places the burden of deciding whether stdin is interactive more squarely on the platform-specific parts of Tcl that live off in their own directories.

The potential downside to this change is any disruption it may cause to existing processes that feed scripts to tclsh via an stdin that is not a terminal. This is probably uncommon because this strategy is playing with fire. Such processes would no longer be able to rely on the interpreter to ignore errors. The solution would be to explicitly catch errors in the scripts or to use something like Expect, which could provide a pseudo terminal.

One hint that this fix is good comes from the test suite. The changes required to support this patch are for the most part to use ::tcl_interactive explicitly in order to get the desired outcome, which was the reason for introducing ::tcl_interactive in the first place.


Attachments: