Read special keys in bash
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:
- My cursor keys do not work (ncurses FAQ)
dialog
— Script-driven curses widgets (application and library)- keyname/key_name
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:
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.
Related videos on Youtube
user367890
Updated on September 18, 2022Comments
-
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
orinfocmp
and filter by the result given by that. I am however in a snag there as bothtput
andinfocmp
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 notxterm
, which is what is set byTERM
.E.g. arrow left:
tput
/infocmp
:\x1 O D
read
:\x1 [ D
What am I missing?
-
Alessio almost 8 yearsno need to reinvent the wheel, iselect already does this. Alternatively, use one of the
dialog
variants, or use a language with decentncurses
support (perl or python for example, if you want to stick with "scripting" languages). -
Stéphane Chazelas almost 8 yearsNote that
zsh
has builtin curses support (in the zsh/curses module) in addition to basic terminfo querying with itsechoti
builtin and$terminfo
associative array.
- When user press
-
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 almost 8 years… but dialog did not quite seem to meet the needs, or be somewhat cumbersome for my case.
-
user367890 almost 8 yearsThank 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 almost 8 yearsFound 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 frominfocmp
. 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 almost 8 years@user367890 your application sounds like a perfect match for the perl
Curses
,DBI
, andDBD::SQLite
modules. or their python equivalents. -
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 almost 5 yearsIs that the complete C code? I get about a dozen errors when I try compiling this using gcc on Debian 9
-
Marius almost 5 yearsYou probably omitted the
-lncurses
, etc.