7

As an exercise in understanding my computer better, and as a tool, I'm writing my own shell in C++. Stephen Brennan's article on writing a simple shell was very helpful.

However, what has me flummoxed is how to handle pressing up-arrow and down-arrow to scroll through my command history.

  • I tried ncurses, but that replaces the entire screen, whereas the system-provided shell seems to just continue writing into the Terminal.

  • I tried using tcgetattr to turn off canonical mode, but while that lets me get arrow key presses as they are typed, it also turns off all processing of left/right arrow keys for text navigation, and the backspace key, and Ctrl-C... While I could probably send a signal myself in response to Ctr-C, I have no idea how to get the Terminal to move the cursor back (apart from outputting a "return" and re-writing the start of the line). It also seems to give me different escape sequences for the keys, depending on whether I'm running in Xcode's "dumb" Terminal or in my Mac's Terminal.app.

  • I looked at the sources for fish Shell and bash, but there just seems to be so much going on that I can't find the relevant parts.

How do the standard shells handle receiving keypresses? How do they handle moving the cursor and doing backspace? How do they re-write parts of a line without having to take over the screen? Is there a standard somewhere that defines what a shell needs to do?

PS - I know how to record the previous commands. It's the actually getting the keypresses while they are being typed, as opposed to after someone presses return, that I can't get to work.

Thomas Dickey
  • 51,086
  • 7
  • 70
  • 105
uliwitness
  • 8,532
  • 36
  • 58
  • 1
    Use [GNU readline](https://cnswww.cns.cwru.edu/php/chet/readline/rltop.html) or an equivalent [BSD editline](http://thrysoee.dk/editline/). – n. m. could be an AI May 11 '16 at 05:40
  • 1
    Close-voters: I hope you have read the updated question, it seems fine now :) – Waleed Khan May 11 '16 at 05:45
  • I don't know at all how it is actually implemented, but my suspicion is that the shell is rewriting the entire screen (à la `ncurses`). For example, if I press C-l in `bash`, it clears the entire screen. This suggests that the shell is doing more than just managing one line at a time. I think libraries like `readline` similarly take over the whole terminal. – Waleed Khan May 11 '16 at 05:47
  • @WaleedKhan They don't take over the screen. Terminal.app defaults to bash. If I now type in `ksh` and hit return, I get a Korn Shell, which happily continues writing below the previous bash commands. So there has to be a way for a completely different executable to just take over where the previous one left off. – uliwitness May 11 '16 at 05:51
  • @uliwitness I thought you could just query where the current cursor position is and then continue writing from there. I don't use `ncurses` or similar so I couldn't say for certain. – Waleed Khan May 11 '16 at 05:55
  • @WaleedKhan You can query that, but as far as the docs tell me, I need to have initialized `ncurses` to ask it for the position, and when I initialize it, it clears the screen/creates a new screen that "hides" the previous one until my shell quits. – uliwitness May 11 '16 at 06:08
  • @n.m. Thanks, readline seems to work as a short-term solution and seems to behave right. It still doesn't explain quite what goes on there, but at least now I've narrowed it down from the entirety of some shells to just checking out what the readline source code does. Why don't you leave that as an answer so I can accept it? – uliwitness May 11 '16 at 07:17
  • @uliwitness (1) Questions that ask for help in finding a library or tool are technically offtopic, and (2) as is it would be a link-only answer. – n. m. could be an AI May 11 '16 at 07:32
  • `bash` uses `readline` so it apparently can work as a long-term solution too. As for what's going on, `readline` uses the `ncurses` library or probably just the low-level part of `ncurses` called `terminfo` (on posix systems anyway). You need to read up on those to understand interactions with the terminal. The editing itself is almost trivial as long as you are able to make the terminal do exactly what you want. – n. m. could be an AI May 11 '16 at 07:38
  • @n.m. I never asked for a library or tool, did I? I asked "how does one do X when implementing one's own shell". Now often the answer to a question is "there's this library to do that", but I'm not sure where you got that idea. – uliwitness May 12 '16 at 06:43
  • voted to close as Too Broad regardless of the posted close reason above. Could also be off topic tool request as @n.m. suggests. A wolf in sheep's clothing – Drew May 14 '16 at 15:39

1 Answers1

3

You have to turn off ICANON and ECHO and interpret the escape sequences from the arrow keys yourself.

You have to keep your own “actual” buffer of what's on the screen and where the cursor is. You also need a “desired” buffer of what you want on the screen and where you want the cursor. These buffers don't cover the whole screen, just the lines containing your prompt and the user's input (which you echoed manually because you turned off ECHO). Since you printed everything on these lines, you know their contents.

Just before you wait for the next input byte, you update the screen to match the desired buffer. Back when you were on a 300 (or even 9600) baud connection you cared a lot about making this update as efficient as possible by looking for an optimal sequence of printable bytes and terminal-control sequences to transform the actual buffer into the desired buffer. These days it's a lot less important to be optimal.

These buffers will span lines if the input wraps, so you need to know and track the terminal width (using TIOCGWINSZ and SIGWINCH). You could stick to a single line with horizontal scrolling instead of line wrapping, but you'd still need to know the terminal width.

Theoretically you look up your terminal type (from $TERM) in the termcap or terminfo database. That tells you what escape sequences to expect when the user presses special keys (arrows, home, end, etc.), and what escape sequences to send to move the cursor, clear parts of the screen, insert or delete characters or lines, etc.

These days it's pretty safe to assume everything's fairly xterm-compatible, especially for a hobby project.

For bash, this is all done in the GNU readline library. Updating the screen (called “redisplay”) is done in display.c. Input escape decoding is done in input.c.

However, if you want example code, you should probably take a look at linenoise, which is under 2000 lines. It assumes the terminal is VT100 (and therefore xterm) compatible.

See also “Is there a simple alternative to Readline?”

Community
  • 1
  • 1
rob mayoff
  • 375,296
  • 67
  • 796
  • 848