After the pipe created by echo ends, I want read-line to resume taking interactive input as usual. It seems like if there is no echo pipe, Racket initializes a different kind of current-input-port that is terminal-port?. I know I can use parameterize to supply a new current-input-port, but I don't see how to make one that is terminal-port?
The behavior you're seeing is about what the shell does, not what Racket is doing. When you start your program without echo, standard input is connected to your terminal. But when you start it with echo, standard input is the pipe that results from the use of | at the shell prompt. You can't get back to your terminal that way, at least not easily.
As already noted, the behavior you're looking for is not following normal usage or expectations, so it's going to confuse people and programs, but it is possible (At least on Linux and possibly other Unixish OSes). The gist is figuring out if current-input-port is an OS-level pipe or not, and if so, reassigning current-input-port to the controlling terminal of the process upon EOF.
(require racket/file ffi/unsafe ffi/unsafe/port)
;; Like file-or-directory-stat but for a file-stream-port
(define (file-stream-port-stat port)
(define fd (unsafe-port->file-descriptor port))
; This isn't as portable as using the FFI to call fstat(2),
; but a struct statbuf is more complicated to use with the ffi
; than I can be bothered with right now.
(file-or-directory-stat (format "/proc/self/fd/~S" fd)))
(raise-argument-error 'file-stream-port-stat "file-stream-port?" port))))
;; Check if a given port is a pipe.
(define (pipe? port)
(and (file-stream-port? port)
(not (= (bitwise-and (hash-ref (file-stream-port-stat port) 'mode)
;; Wrap the C ctermid(3) function to get controlling terminal.
;; (Just opening /dev/tty might work too)
(define libc-ffi (ffi-lib #f))
(define %ctermid (get-ffi-obj "ctermid" libc-ffi (_fun _bytes -> _path)))
; Should be enough space anywere; glibc defines L_ctermid as 9!
(define terminal-name-buffer (make-bytes 256))
(let loop ()
(when (terminal-port? (current-input-port))
(display "> ")
(define input (read-line))
; If we get EOF when reading from a terminal, just exit
; If we get EOF when reading from a pipe, change the
; current input port to the controlling terminal
(current-input-port (open-input-file (ctermid)))
So if I take this suggestion, how do I get consistent behavior at the terminal and within DrRacket? If I start an interactive session at the terminal, then (terminal-port? (current-input-port)) is #true. But if I start an interactive session within DrRacket, (terminal-port? (current-input-port)) is #false.
I suppose this question could be rephrased as: is there a test like terminal-port? that can tell me whether I am running in DrRacket, so I can handle that condition the same way (because they are both interactive prompts)?
Consistent, conventional behavior would be to stop reading from standard input when you get end of file, and if you need to read from two different sources of input (Like a file and standard input), open the file as its own port, passing the name as a command line argument, probably. Shells like bash and zsh let you tie the output of a command to a filename that can be used that way (Process Substitution (Bash Reference Manual)). Or subprocess* from in Racket.