Read special keys in bash

8,949

Solution 1

What you are missing is that most terminal descriptions (linux is in the minority here, owing to the pervasive use of hard-coded strings in .inputrc) use application mode for special keys. That makes cursor-keys as shown by tput and infocmp differ from what your (uninitialized) terminal sends. curses applications always initialize the terminal, and the terminal data base is used for that purpose.

dialog has its uses, but does not directly address this question. On the other hand, it is cumbersome (technically doable, rarely done) to provide a bash-only solution. Generally we use other languages to do this.

The problem with reading special keys is that they often are multiple bytes, including awkward characters such as escape and ~. You can do this with bash, but then you have to solve the problem of portably determining what special key this was.

dialog both handles input of special keys and takes over (temporarily) your display. If you really want a simple command-line program, that isn't dialog.

Here is a simple program in C which reads a special key and prints it in printable (and portable) form:

#include <curses.h>

int
main(void)
{   
    int ch;
    const char *result;
    char buffer[80];

    filter();
    newterm(NULL, stderr, stdin);
    keypad(stdscr, TRUE);
    noecho();
    cbreak();
    ch = getch();
    if ((result = keyname(ch)) == 0) {
        /* ncurses does the whole thing, other implementations need this */
        if ((result = unctrl((chtype)ch)) == 0) {
            sprintf(buffer, "%#x", ch);
            result = buffer;
        }
    }
    endwin();
    printf("%s\n", result);
    return 0;
}

Supposing this were called tgetch, you would use it in your script like this:

case $(tgetch 2>/dev/null) in
KEY_UP)
   echo "got cursor-up"
   ;;
KEY_BACKSPACE|"^H")
   echo "got backspace"
   ;;
esac

Further reading:

Solution 2

Have you tried using dialog? It comes standard with most Linux distros and can create all kinds of text-based dialogs, including checklists.

For example:

exec 3>&1 # open temporary file handle and redirect it to stdout

#                           type      title        width height n-items    
items=$(dialog --no-lines --checklist "Title here" 20    70     4 \
          1 "Item 1" on \
          2 "Item 2" off \
          3 "Item 3" on \
          4 "Item 4" off \
            2>&1 1>&3) # redirect stderr to stdout to catch output, 
                       # redirect stdout to temporary file
selected_OK=$? # store result value
exec 3>&- # close new file handle 

# handle output
if [ $selected_OK = 0 ]; then
    echo "OK was selected."
    for item in $items; do
        echo "Item $item was selected."
    done
else
    echo "Cancel was selected."
fi

You'll get something like this:

enter image description here

And the output will be:

 OK was selected.
 Item 1 was selected.
 Item 3 was selected.

(or whichever items you selected).

man dialog will get you information on the other kinds of dialogs you can create, and how to customize the appearance.

Share:
8,949

Related videos on Youtube

user367890
Author by

user367890

Updated on September 18, 2022

Comments

  • user367890
    user367890 over 1 year

    I am playing with a script that, among other things, list a selection-list. As in:

    1) Item 1              # (highlighted)
    2) Item 2
    3) Item 3              # (selected)
    4) Item 4

    • When user press down-arrow next items is highlighted
    • When user press up-arrow previous items is highlighted
    • etc.
    • When user press tab item is selected
    • When user press shift+tab all items are selected / deselected
    • When user press ctrl+a all items are selected
    • ...

    This works fine as of current use, which is my personal use where input is filtered by my own setup.

    Question is how to make this reliable across various terminals.


    I use a somewhat hackish solution to read input:

    while read -rsn1 k # Read one key (first byte in key press)
    do
        case "$k" in
        [[:graph:]])
            # Normal input handling
            ;;
        $'\x09') # TAB
            # Routine for selecting current item
            ;;
        $'\x7f') # Back-Space
            # Routine for back-space
            ;;
        $'\x01') # Ctrl+A
            # Routine for ctrl+a
            ;;
        ...
        $'\x1b') # ESC
            read -rsn1 k
            [ "$k" == "" ] && return    # Esc-Key
            [ "$k" == "[" ] && read -rsn1 k
            [ "$k" == "O" ] && read -rsn1 k
            case "$k" in
            A) # Up
                # Routine for handling arrow-up-key
                ;;
            B) # Down
                # Routine for handling arrow-down-key
                ;;
            ...
            esac
            read -rsn4 -t .1 # Try to flush out other sequences ...
        esac
    done
    

    And so on.


    As mentioned, question is how to make this reliable across various terminals: i.e. what byte sequences define a specific key. Is it even feasible in bash?

    One thought was to use either tput or infocmp and filter by the result given by that. I am however in a snag there as both tput and infocmp differ from what I actually read when actually pressing keys. Same goes for example using C over bash.

    for t in $(find /lib/terminfo -type f -printf "%f\n"); { 
        printf "%s\n" "$t:"; 
        infocmp -L1 $t | grep -E 'key_(left|right|up|down|home|end)';
    }
    

    Yield sequences read as defined for for example linux, but not xterm, which is what is set by TERM.

    E.g. arrow left:

    • tput / infocmp: \x1 O D
    • read: \x1 [ D

    What am I missing?

    • Alessio
      Alessio almost 8 years
      no need to reinvent the wheel, iselect already does this. Alternatively, use one of the dialog variants, or use a language with decent ncurses support (perl or python for example, if you want to stick with "scripting" languages).
    • Stéphane Chazelas
      Stéphane Chazelas almost 8 years
      Note that zsh has builtin curses support (in the zsh/curses module) in addition to basic terminfo querying with its echoti builtin and $terminfo associative array.
  • user367890
    user367890 almost 8 years
    +1 for effort, but Dickey was more to the point of what I'm asking. For one to what the issue described was – in a more general sense, the list was merely to give some context. Secondly I have had a quick look at dialog – and admittedly I have not looked at it thoroughly, my case, to expand on it, is a front for a sqlite database with several thousand records where I have for example Page-Up/Down to scroll trough selection. Scroll-region, scroll-buffer, a status line, a ex line with modal input, sub functions for filtering etc. In short it might sound complex, but is rather simple …
  • user367890
    user367890 almost 8 years
    … but dialog did not quite seem to meet the needs, or be somewhat cumbersome for my case.
  • user367890
    user367890 almost 8 years
    Thank you. Yes, inputrc was indeed the culprit I was looking for. Have to look at it some more. Have considered going python or C, but find it fun to hack on as a bash script as well. I also tried to have a look at the ncurses source to see if I could extract the bits I need – but after quite some time digging the source I left it on ice. The "project" started out as a simple command, then became a simple interactive script, and then extended on that again. Somewhere along the way I should have gone other language,but got a bit stubborn (and as mentioned it is fun to hack on in bash 2 :)
  • user367890
    user367890 almost 8 years
    Found the sequences in, among others, /usr/share/doc/readline-common/inputrc.arrows. As I already have a generic "read_key" function that I use across the script I hoped there was a easier way to define the sequences (in the script) from what is actually presented when a key is pressed. I.e. similar to extracting definitions from infocmp. But guess not and either have to leave it as is or move on to another language. A compromise could of course be to use your, nice, C-snippet. But then I can write the whole thing in C instead. (Sorry for oversharing.)
  • Alessio
    Alessio almost 8 years
    @user367890 your application sounds like a perfect match for the perl Curses, DBI, and DBD::SQLite modules. or their python equivalents.
  • user367890
    user367890 almost 8 years
    @cas: Yes. Have written similar applications using python and C earlier – though I have to re-learn a lot of it. This "project" is more of an adventure into bash possibilities and "for the fun of it" :) Though I am getting close to abandoning it or port it to another language. Thanks for input.
  • InterLinked
    InterLinked almost 5 years
    Is that the complete C code? I get about a dozen errors when I try compiling this using gcc on Debian 9
  • Marius
    Marius almost 5 years
    You probably omitted the -lncurses, etc.