7

I just implemented a Linux command shell in python using only the os library's low level system calls, like fork() and so on.

I was wondering how I can implement a key listener that will listen for key (UP|DOWN) to scroll through the history of my shell.

I want do do this without using any fancy libraries, but I am also wishing that this is not something super complicated. My code is just about 100 lines of code, so far, and I don't want to create a monster just to get a simple feature :D

My thoughts about the problem is, that it should be possible to create a child process with some kind of loop, that will listen for up ^[[A and down ^[[B, key press, and then somehow put the text into my input field, like a normal terminal.

So far the thing I am most interested in is the possibility of the key-listener. But next I will probably have to figure out how I will get that text into the input field. About that I am thinking that I probably have to use some of the stdin features that sys provides.

I'm only interested in making it work on Linux, and want to continue using low-level system calls, preferably not Python libraries that handle everything for me. This is a learning exercise.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
mama
  • 2,046
  • 1
  • 7
  • 24
  • 3
    2 questions: A) This should be Linux/macOS/Windows independent or just one OS? B) This should work for the current process only (your "shell in python"), direct forks or _any normal_ process of the current user? – thoku Sep 28 '20 at 10:12
  • Thanks for you attention :) A) it is for linux, B) I don't understand clearly the question. But I am thinking that I want to do something like zsh and bash. So maybe I need to write my command directly to a history file and load from there when I arrow up. My code for the project is on https://github.com/dat4/os-man-shell – mama Sep 28 '20 at 10:26
  • I made an edit to add your requirement to keep using low-level stuff. If that wasn't what you meant, edit your question yourself. – Peter Cordes Sep 29 '20 at 02:00
  • If you want to be really lazy, just run your program inside `rlwrap`. If you're not doing tab-completion it's probably sufficient. – o11c Oct 04 '20 at 05:08

2 Answers2

9

By default the standard input is buffered and uses canonical mode. This allows you to edit your input. When you press the enter key, the input can be read by Python.

If you want a lower level access to the input you can use tty.setraw() on the standard input file descriptor. This allows you to read one character at a time using sys.stdin.read(1). Note that in this case the Python script will be responsible for handling special characters, and you will lose some of the functionality like character echoing and deleting. For more information take a look at termios(3).

You can read about escape sequences which are used for up and down keys on Wikipedia.

You should be able to replicate the standard shell behavior if you handle everything in one process.

You may also want to try using a subprocess (not referring to the module - you can use fork() or popen()). You would parse the unbuffered input in the main process and send it to stdin (which can be buffered) of the subprocess. You will probably need to have some inter-process communication to share history with the main process.

Here is an example of the code needed to capture the input this way. Note that it is only doing some basic processing and needs more work in order to fit your use-case.

import sys
import tty
import termios


def getchar():
    fd = sys.stdin.fileno()
    attr = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        return sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSANOW, attr)


EOT = '\x04'  # CTRL+D
ESC = '\x1b'
CSI = '['

line = ''

while True:
    c = getchar()
    if c == EOT:
        print('exit')
        break
    elif c == ESC:
        if getchar() == CSI:
            x = getchar()
            if x == 'A':
                print('UP')
            elif x == 'B':
                print('DOWN')
    elif c == '\r':
        print([line])
        line = ''
    else:
        line += c
codeforester
  • 39,467
  • 16
  • 112
  • 140
GitFront
  • 894
  • 3
  • 11
  • Thank you so much for your answer. I think this is what I am looking for ! :D, but I really need to echo the characters, I am trying to see if I can do something with stdout. Also, do you mean that I should create another process and eg. pipe() the history to the stdin/out to get the history.? Do you think it is possible to load the history into the "writing buffer" and be able to edit in the text, in a simple way? or is it gonna be too complicated? If it is too complicated then I will skip it :) – mama Oct 02 '20 at 14:39
  • The output is also buffered so in case of a single process you can turn the buffering off and print characters as they are typed. I don't know what is the best strategy when using two processes or even if it will provide any benefits. You should experiment and find answers to those questions by yourself, especially because this is a learning exercise. – GitFront Oct 02 '20 at 16:35
  • Hello @GitFront, would you mind to check this question please https://stackoverflow.com/questions/69060249/coral-dev-board-direct-connected-keyboard-does-not-control-the-edgetpu-demo or https://github.com/google-coral/edgetpu/issues/462 – Cappittall Sep 08 '21 at 08:22
1

Python has a keyboard module with many features. Install it, perhaps with this command:

pip install keyboard

then use it in code like this:

import keyboard

keyboard.add_hotkey('up', lambda: keyboard.write('write command retrieved from the history of your shell here'))
keyboard.wait('esc')

or you could use the function on_press_key Using the function on_press_key:

keyboard.on_press_key("p", lambda _:print("You pressed p"))

It needs a callback function. I used _ because the keyboard function returns the keyboard event to that function.

Once executed, it will run the function when the key is pressed. You can stop all hooks by running this line:

keyboard.unhook_all()

for detailed information you can see similar post on stackoverflow, hope this helps detect key press in python?

On side Note: you mentioned fork() method above In python we can use the

subprocess builtin module here, so import subprocess and we’re good to go. The run function in particular is used here to execute commands in a subshell. For those coming from C, this saves us from going about forking and creating a child process and then waiting for the child to finish execution, let Python take care of that this one time.

sample code for execute the commands entered by the user

def execute_commands(command):
    try:
        subprocess.run(command.split())
    except Exception:
        print("psh: command not found: {}".format(command))
mudassir ahmed
  • 191
  • 1
  • 1
  • 13
  • 1
    Thank you very much for your answer, but I am looking to do it as low level as possible. The reason I am using fork is because I am doing a scool assignment which is about creating direct system calls to the linux kernel. This is also why I want to find out if it is possible to listen for the keyboard press while I am in my `input()` state without using libraries like `keyboard` but rather using the same way that the guys who made `keyboard` is doing it. But without writing too much code.. :) Thank you very much anyways. – mama Sep 28 '20 at 15:16
  • ImportError: You must be root to use this library on linux. – jaromrax Jan 11 '23 at 13:12