5

I'm trying my best on creating a simple shell on linux. Just something I can create to learn how to use basic system calls with.

Scenario: user types in the command, presses tab (so the shell auto-completes his command), the auto-completed command pops out (or suggestions), user presses enter, command evals and executes.

Just like in bash.

I have figured out how to make evals, convert command into tokens, execute it with pipes and stuff. What I can't figure out is the input part. Namely those tab keystrokes.

I know what options I have:

  • getc() - get each character separately, store it in a buffer. Can't figure out how to get tab keystrokes, because it pauses execution until it sees '\n' or Ctrl+D. Kinda expensive, since there will be 1 getc() for every character in the command. Also, I will have to deal with buffer reallocation, amortization... boo...
  • scanf("%s") - too much worrying about buffer overflow. Can't get those tab keystrokes I wan't. Pauses execution
  • read() (from unistd.h) - could be something I wan't. But I've seen people here that said that it is a real pain to use it to do this. I checked. It is.
  • getline() - can't get tab keystrokes.

I looked into bash source code, to see how it deals with input, and OH MY GOD. There are 450 lines of code dedicated to do this one simple thing (input.c file).

Are there really no simpler solutions than this? I don't want to use ncurses, I don't care about portability, I just wan't to achieve one goal: getting user input and knowing when he pressed tab key. Do it elegantly, with as little effort as possible.

Błażej Michalik
  • 4,474
  • 40
  • 55
  • 1
    `if(fgetc(stdin) == '\t') { // launch }` – Ryan Dec 26 '15 at 21:44
  • @self : Pauses execution. The "launch" part is executing only after I press enter. I need to get this character before I press enter, so I can perform autocompletion for what has been already written to input stream. – Błażej Michalik Dec 26 '15 at 21:46
  • use it in a `for(;;)` loop, once you get the `\t` just discard all other input – Ryan Dec 26 '15 at 21:47
  • @self : the command itself still pauses the control flow. If you need to press enter to resume it, it doesn't work like in bash. In bash you don't need to press enter to launch autocompletion code. You only need to press the tab key. – Błażej Michalik Dec 26 '15 at 21:51
  • Have you considered posting your code? Bash is open source software where the source is available. check it out. – Ryan Dec 26 '15 at 21:51
  • 1
    See: [setvbuf not able to make stdin unbuffered](http://stackoverflow.com/questions/10247591/setvbuf-not-able-to-make-stdin-unbuffered) – kaylum Dec 26 '15 at 21:57
  • Just wonder reading from a keyboard device like in http://www.thelinuxdaily.com/2010/05/grab-raw-keyboard-input-from-event-device-node-devinputevent/ – mon Dec 26 '15 at 21:58
  • @monta That usually requires root permissions. And it may also not fit the problem exactly as it captures *all* keyboard input and not just from the program's terminal. – kaylum Dec 26 '15 at 22:00

2 Answers2

4

In order to get individual keystrokes from the terminal without any delay or buffering, you must change its mode from cooked to raw. You can do this with the tcsetattr() function defined in <termios.h>. Look at the man page for details.

Once you have set the terminal to the appropriate mode, it is wise to use the read() system call to read data from the terminal handle.

Be aware that you will have to deal with echoing user input and if you start doing advanced stuff like TAB expansion, you will need to implement most of a line editor... Not to mention handling character composition and other oddities the terminal gives you for free. As Basile says, there is no simple handcrafted solution for this, but it will be very instructive to dive into this mess!

If you are pressed for time and this is an assignent, just use readline() and implement the rest of the shell first. This will be a lot of work already. You can always get back to this later if you still dare.

chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • I just realized what you've said in your edit. Yes, diving into this is fun. And at the same time, it is something so insolently basic that simply touching it "insults" me. I will have to go with Basile's answer, because when it comes to the raw mode I'm starting to have second thoughts on my career as software engineer. And suicide ones too. I will have to get back to the challange at some point though. – Błażej Michalik Dec 26 '15 at 22:32
  • 1
    No offense ;-) Delving into this low level stuff feels like peeling on onion: every time you think you found the core, there is another layer of unexpected fluff to scrape off, and its makes you cry too. – chqrlie Dec 26 '15 at 22:49
  • 1
    Ah, but the `termios.h` onion is a necessary spice in any good I/O dish. Just light a candle while peeling to minimize the discomfort, mix with a pinch of `select` and `switch` and a nice basic I/O handler can be baked in less than an hour. A good recipe can probably be found in the S.O. cookbook by searching `'kbhit'`. You will need to modify to taste... – David C. Rankin Dec 26 '15 at 23:32
  • @DavidC.Rankin : I know I can do it, but it just seems like unnecessary and not elegant enough. I'm teaching people in my free time. I know that at some point I will wan't to use it as example, and show it to them. How will they react, when they'll see 200 lines of code, and then when asking for explanation they'll hear: "This? Oh, right, this is responsible for grabbing that one line from the input.". Thing is, I can deal with high level of complexity that I can't comprehend yet. I'm comfortable with it. I know I won't give up. People who I wan't to get interested in this stuff - not so much. – Błażej Michalik Dec 27 '15 at 02:12
  • 1
    You have 'hit-the-nail-on-the-head' so to speak in framing the issue of code size for a simple task versus having the students eyes roll back in their heads over apparent complexity. But it is just like anything else, at first blush it may look overly complicated and daunting, once you become familiar with it, those problems fade away. In your case the basic termios code for switching from *buffered* to *raw* mode is just a handful of lines (both links in the original comments are good). Whether you do it in curses or termios, you must still code the input handler - that code is about equal. – David C. Rankin Dec 27 '15 at 02:30
4

To get some specific auto-completion, you could use the GNU readline library, which is used by bash.

If you care about terminal full screen I /O (à la vi or emacs) consider GNU ncurses.

Terminals are quite complex and arcane things (because they want to emulate weird physical teletypes from the past century). Often, some of the line processing is done in the kernel for the line discipline of the tty. Read the tty demystified webpage. Hence, low level functions à la termios(3) are arcane to use, and you should prefer libraries like readline or ncurses.

So, no, there are no simple solutions for terminal I/O for autocompletion, because ttys are complex stuff. See also tty(4) & tty_ioctl(4) & pty(7)

You could also use strace(1) to understand all the complex system calls done by e.g. an interactive shell.

See also this & that answers.

Community
  • 1
  • 1
Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • You can definitely look at the source code for this function to understand how to interact with the terminal to suit your needs. – chqrlie Dec 26 '15 at 21:59
  • I could use an external library for this, but for now it is on the second place when it comes to being "elegant enough". I found out this will work, but right now I wan't to check @chqrlie's answer. It just seems to be more... "pure"... – Błażej Michalik Dec 26 '15 at 22:06
  • @BłażejMichalik: good luck with this approach, it is not as difficult as it looks, just very quirky. – chqrlie Dec 26 '15 at 22:07
  • 1
    @BasileStarynkevitch: good pointer for tty internals, quite a trip through arcane details. – chqrlie Dec 26 '15 at 22:13