How to reuse current-input-port after piping input from stdin

Consider this test.rkt program:

#lang racket

(let loop ()
  (display "> ")
  (define input (read-line))
  (displayln input)
  (loop))

When invoked as

racket test.rkt

it will just display whatever it gets from stdin:

> hello
hello
> world
world

But when an initial value is passed into stdin, say

echo "hello" | racket test.rkt

It prints the value from stdin but then enters an infinite loop of eof:

> hello
hello
> #<eof>
> #<eof>
> #<eof>
···

Why is this happening, and how do I prevent it? I just want the program to accept input normally in the second case (after getting the first argument from stdin)

1 Like

The pipe created by echo ends, which means that every subsequent call to read-line produces #<eof>. What do you want to happen in that situation?

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.

3 Likes

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.

#lang racket/base

(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))
  (cond
    ((integer? fd)
     ; 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)))
    (else
     (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)
                            fifo-type-bits) 0))))

;; 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)))
(define (ctermid)
  ; Should be enough space anywere; glibc defines L_ctermid as 9!
  (define terminal-name-buffer (make-bytes 256))
  (%ctermid terminal-name-buffer))

(let loop ()
  (when (terminal-port? (current-input-port))
    (display "> ")
    (flush-output))
  (define input (read-line))
  (cond
    ((eof-object? input)
     (cond
       ((terminal-port? (current-input-port))
        ; If we get EOF when reading from a terminal, just exit
        (exit 0))
       ((pipe? (current-input-port))
        ; 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)))
        (loop))))
    (else
     (displayln input)
     (loop))))
5 Likes

@LessThanZero: The solution by @shawnw looks pretty great if you really feel this behavior is the responsibility of your Racket program to provide.

On the other hand, as @samth pointed out, your shell command determines where the input comes from. So you could decide the solution should be on the shell side.

My shell skills are super basic, but if you want input to be the concatenation of a string followed by the terminal, this seems to work in bash:

$ cat <(echo "hello") - | racket input.rkt

IIUC the <(echo "hello") tells cat "pretend I gave you the name of a file containing the output from the command echo "hello". The - means standard input.

3 Likes

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)?

#lang racket

(terminal-port? (current-input-port))
(let loop ()
  (displayln (read-line))
  (loop))

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.

drracket-cmdline-args: Accessible Command-Line Arguments for DrRacket might be handy for using the filename-as-argument approach when running a program directly through DrRacket.

1 Like