Skip to content

Create a $&readline primitive#265

Merged
jpco merged 6 commits intowryun:masterfrom
jpco:readlineprim
Mar 29, 2026
Merged

Create a $&readline primitive#265
jpco merged 6 commits intowryun:masterfrom
jpco:readlineprim

Conversation

@jpco
Copy link
Copy Markdown
Collaborator

@jpco jpco commented Mar 28, 2026

This PR creates a new primitive $&readline which performs the same readline logic as the shell. It takes one argument which it uses as the prompt, and it reads exactly one line of input, with the same basic return semantics as $&read -- a single string containing a single line of input, or the empty list on an immediate EOF.

$&readline always calls the readline library. Logic around "if we're reading from a TTY, then $&readline; otherwise, $&read" is out of scope for this PR, but something like $&isatty can be added to make that check later.

$&readline respects redirections, setting rl_instream and rl_outstream to fdmap(0) and fdmap(2). Something interesting to note is that apparently readline prints everything to its stdout by default. This PR changes that to stderr for the $&readline primitive, for better consistency with existing shells and es' own non-readline behavior. As a side note: it is, I think, a good sign that moving this behavior to a primitive makes these kinds of behaviors more explicit. It's also possible to change back to stdout with a simple >[2=1]!

The obvious deficiency with $&readline as it is now is that it is really only designed to work as shell input. Somebody trying to read some random text is probably not going to be helped by variable or primitive completion, for example. Improving $&readline's ability to act as non-shell-input is something I'd like to take a stab at in a follow-up.

One question is: why a new $&readline primitive, rather than, say, extending $&read? That comes down to how we model optional behaviors. I think the ideal way to model libraries in es is for them to add additional primitives, which can be tied into the system using normal scripting behavior. This is already how $&limit, $&time, $&readfrom and $&writeto, $&execfailure, and the readline-related primitives work now. Actually, if someone wanted a combined read/readline %read with this PR, I think the best way to do that would be via fn-%read = $&readline.

@jpco
Copy link
Copy Markdown
Collaborator Author

jpco commented Mar 29, 2026

I'm now thinking twice about the isatty check, in the context of this PR being used in #263. The current behavior for how readline is called for shell input is:

  • The shell is interactive if -i is given, OR if es is going to read from stdin for input AND stdin isatty.
  • Readline is called if the shell is interactive AND if the Input is reading from stdin.

This means the shell is perfectly willing to call readline for non-TTY input if you give it -i and input on stdin. For example:

; echo 'echo hello world' | es -i
; echo hello world
hello world
; ; 

This is not obviously the correct behavior: bash, rc, and dash do the same thing, but fish doesn't use its interactive input logic here, zsh seems to just ignore the redirection entirely, and elvish's -i flag is a no-op (but by default elvish seems to treat any invocation where its input comes from stdin as an interactive one.)

With this PR and #263, the behavior would be:

  • The shell is interactive if -i is given, OR if es is going to read from stdin for input AND stdin isatty.
  • Readline is called if the shell is interactive AND if the Input is reading a fd which isatty.

The last bit is the distinction; instead of checking for stdin, we check for isatty.

Technically I think it would work to turn the if (!isatty(input)) check in $&readline into if (input != 0), checking if the fd that's given to $&readline as "stdin" happens to actually be the shell's standard input. That feels like abstraction-breaking behavior to me, though, and unpredictable for someone using $&readline outside of %parse. This would also compose poorly with pipes, where the shell process's actual standard input isn't a terminal.

Another alternative would be to just have $&readline call readline no matter what and provide other mechanisms to pick between $&read and $&readline. This would be the simplest and most predictable behavior for the primitive, which is good, but what would the other mechanisms be? I see two options:

  1. an $&isatty primitive, so users can say fn %read prompt {if {$&isatty 0} {$&readline $prompt} {$&read}}.
  2. a way within $&parse to communicate "you should use readline here". There really shouldn't be an $&fdmap primitive, but this could be an argument that $&parse passes to its reader command (see Separate parsing from reading shell input #263 for what that means).

Technically, both of these could be added simultaneously; the former is (IMO) the best way to get a %read that calls readline when appropriate, while the latter would be (IMO) the best way to get backwards-compatible behavior for es -i. I'm not jazzed about this "actually-stdin" idea, though; I think it's more sensible to break backwards compatibility and switch to checking for isatty.

So, I guess after all this rambling, here's what I want: $&readline should always actually call readline, due to the principle that primitives should be simple and do what they say. We should at some point add an $&isatty primitive to the shell to make a nice %read possible, but also for other uses; for example, we use $&isatty in #79 to automatically decide whether es should be interactive based on its flags. Having $&isatty and $&readline separate is good for generality, flexibility, and orthogonality. In $&parse, $&isatty 0 should be used to determine whether to call readline, since that behavior is significantly easier to communicate and justify than the existing "if stdin is actually stdin" -- though that can be discussed in more detail in #263.

Also do a better job at handling errors setting up the fds for readline.
@jpco jpco merged commit 1e397f3 into wryun:master Mar 29, 2026
1 check passed
@jpco jpco deleted the readlineprim branch March 29, 2026 21:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant