I'm a new Linux and Bash user. I wanted to read an INT Number by keyboard input with the read command, but allowing only keys +- at first position and the number keys 0-9, i also need the same for FLOAT Numbers.
I wrote a BASH function, but i'm now asking myself and especially you, if there is not a simplier way.
#!/bin/bash
#-- read out the keystroke repeat rate and calculate the sampling time to get inside the key interval time for one key
#-- to be sure to produce at least one read(command) time out failure to recognize when an ANSI ESCAPE code sequence ends
declare read_samplingTime keystroke_RepeatRate=$(xset -q | grep "repeat rate")
keystroke_RepeatRate=${keystroke_RepeatRate#*rate:}
read_samplingTime=$(bc <<<"scale=5; x=1/(2*$keystroke_RepeatRate); print 0; x")
function read_INTNumber ()
{
#-- inputSTR .. holds the accepted characters for the number (+|- 0-9), key .. the actual key(ASCII char), AESCcode .. ANSI ESCAPE code (a row of ASCII decimal), when pressed an arrow, page down or ..
#-- keyCode .. the ASCII char decimal value for the key, posCursor .. position Cursor, bAESCsequence .. Boolean to indicate an AESCsequence started
local inputSTR="" key="" AESCcode=""
local -i keyCode=-1 posCursor=0 bAESCsequence=0
#-- stop if enter is hit KeyCode 10
until [[ $keyCode -eq 10 ]]
do
read -sn 1 -t $read_samplingTime key
#--check for time out failure, in exit status of read
if [[ $? -eq 0 ]]; then
#-- hiting enter will stop the reading of one character, leaving an empty null string in key
#-- so set keyCode to 10 manually; for other keys printf produces the keyCode
if [[ $key == "" ]]; then keyCode=10
else printf -v keyCode "%d" "'$key"; fi
#-- During AESCsequence add keyCode to AESCcode
if [[ bAESCsequence -eq 1 ]]; then AESCcode+=" $keyCode"
else
#-- numbers 0-9
if ((keyCode >= 48 && keyCode <=57)); then inputSTR+=$key; echo -n "$key"; ((posCursor++))
else
case $keyCode in
#-- AESC sequence started
27) bAESCsequence=1; AESCcode="27";;
#-- backspace - dont allow at cursor position 0
8) if [[ posCursor -gt 0 ]]; then inputSTR=${inputSTR:0:-1}; echo -n "$key $key"; ((posCursor --)); fi;;
#-- +- at cursor position 0
43|45) if [[ posCursor -eq 0 ]]; then inputSTR+=$key; echo -n "$key"; ((posCursor++)); fi;;
esac
fi
fi
elif [[ bAESCsequence -eq 1 ]];
#-- read exited with a time out failure, so check if last key started a bAESCsequence, if so this, sequence already ended,
then echo $AESCcode; AESCcode="", bAESCsequence=0;
fi
done
}
read_INTNumber
and if there is not a simplier way, i need also some editing functionality like backspace, delete, POS-1, POS-END, left|right arrow.
for these keys i get an ANSI ESCAPE sequence (row of ASCII decimal) for example in the gnome terminal:
Are these codes for keyboard keys always the same in each terminal, or at least in the most terminals? - because there also ANSI ESCAPE CODES to set colors but they are different in other terminals
thx in advance
EDIT 09.07.2024: to read a key from keyboard, i now use this
#!/bin/bash
#-- call: readKey [exit keyCode STR] [timeout FLOAT] [timeEC FLOAT]
#-- description: reads the pressed key (or keys, if pressed simultaneous) and converts it to an Unicode charCode (decimal) or to an Ansi Escape Code Decimal Sequence (a series of ASCII charCode decimals)
#-- All decimals separated by space!
#-- calls readKey_action with
#-- 1) key(s) and Unicode charCode(s): readKey_action "$keys" "$keyCodeSequence" - for the most characters (decimal<128), it is the same decimal, as you get for ASCII charCode, for "€" you get 8364.
#-- 2) Ansi Escape Code Sequence(s): readKey_action "$keys" "$keyCodeSequence"
#-- parameters: $1 ... exit keyCode STR - The Unicode charCode(s) or the Ansi Escape Code Decimal Sequence, when to stop reading.
#-- $2 ... timeout FLOAT optional - The read command timeout in seconds for listening at keyboard for a new character, til it breaks and next code line in loop is executed.
#-- There must be a read command timeout between the sent keys, to recognize an Ansi Escape Code Sequence end!
#-- [ Loop execution time for reading the longest Ansi Escape Code Sequence ] + [ timeout time ] + [ executing readKey_action time ] should be smaller than keystroke repeat time
#-- [ Loop execution time ... ] + [ executing readKey_action example ] took average 3-4ms on my computer (CPU Mhz avg: 2203 high: 4196 min/max: 1400/4000)
#-- Standard: 0.010s, 10ms - frequency: 100 - you can use 'xset -q | grep "repeat rate"' to get the key stroke repeat rate.
#-- $3 ... timeEC FLOAT optional - time error correction in seconds - sometimes during keystrokes repeats $'\e' of an Ansi Escape Code Sequence get lost.
#-- If the keystroke interval is lower or equal this time, a correction happens, if the rest of the actual AESC Sequence is equal
#-- to the rest of the last AESC Sequence. Standard: 0.2s
#-- depends on: functions - readKey_action
readKey ()
{
#-- key CHAR ... the actual key in the loop (char as UTF-8) or a part of an Ansi Escape Code Sequence ( \e or [ or ..)
#-- keys STR ... saves the recognized key or keys (pressed simultaneous) or the keys (characters) of an or more Ansi Escape Code Sequence(s)
#-- keyCode INT ... the Unicode charCode decimal value for the key or the ASCII charCode decimal for a part of an Ansi Escape Code Sequence
#-- cC_counter INT ... charCode counter for the charCode decimal values added to keyCodeSequence
#-- to_counter INT ... timeout counter between two sent keys
#-- keyCodeSequence STR ... one or a series of Unicode charCode decimals - e.g when pressed an arrow, page down or ..
local key keys keyCodeSequence="" last_AECS="" timeout=${2:-"0.010"} timeEC=${3:-"0.2"} #-- last_AECS STR ... last Ansi Escape Code Sequence without '\e'
local -i keyCode cC_counter=0 to_counter=0 to_lastCount=0 to_limit wasAECS=0 #-- wasAECS BOOL ... indicates, if last key send an Ansi Escape Code Sequence
to_limit=$(bc <<< "scale=0; $timeEC/$timeout") #-- to_limit INT ... timeEC measured in timeouts - error max (is - real): -1 timeout
stty -echo #-- turn terminal echo off
while :; do
read -N 1 -r -t $timeout key
#--check for time out failure, in exit status of read -> Time Out is End of Sequence!
if (( $? == 0 )); then
printf -v keyCode "%d" "'$key";
keyCodeSequence+="$keyCode "; keys+=$key; ((to_counter ? to_lastCount=to_counter, to_counter=0, cC_counter++ : cC_counter++))
else
((to_counter++))
if ((cC_counter)); then
keyCodeSequence=${keyCodeSequence:0:-1} #-- remove space at end
[[ $keyCodeSequence == "$1" ]] && break #-- break loop
if ((cC_counter>1)); then #-- possible Ansi Escape Code Sequence
if [[ ${keyCodeSequence:0:3} == "27 " ]]; then #-- Ansi Escape Code Sequence
readKey_action "$keys" "$keyCodeSequence" "$to_lastCount"
wasAECS=1; last_AECS=${keys##*$'\e'}
elif ((wasAECS)); then #-- last Sequence was an Ansi Escape Code Sequence
#-- Error Correction of missing $'\e', if time between sent keys is smaller or equal timeEC
if [[ $to_lastCount -le $to_limit && $keys == $last_AECS ]]; then readKey_action $'\e'"$keys" "27 $keyCodeSequence" "$to_lastCount"
else wasAECS=0 #-- if not skip the whole output! and set wasAECS to 0
fi
else
readKey_action "$keys" "$keyCodeSequence" "$to_lastCount" #-- normal keys are pressed simultaneous
fi
else
readKey_action "$keys" "$keyCodeSequence" "$to_lastCount" #-- normal single Unicode key and keyCode
wasAECS=0
fi
keys=""; keyCodeSequence=""; cC_counter=0
fi
fi
done
stty echo #-- turn terminal echo on
}
#-- is called: readKey_action [keys STR] [keyCode STR] [keyStroke interval INT]
#-- description: Is called from function readKey and can combine keys with actions.
#-- In this example, it's just printing out the key, the keyCode for the pressed key and keyStroke interval for the last key
#-- parameters: $1 ... keys STR - The actual key(s), character(s) as UTF8 or an or more Ansi Escape Code Sequences. (keypressed simultaneous)
#-- $2 ... keyCode STR - The Unicode charCode decimal value(s) for the key(s) or an or more Ansi Escape Code Sequences (a series of ASCII Code charCode decimals).
#-- All decimals separated by space!
#-- $3 ... keyStroke interval INT - time between two keystrokes(actual keystroke and previous keystroke) measured in timeouts - error max (is - real): -1 timeout
#-- depends on: functions - readKey
readKey_action ()
{
printf "key(s): %q - keyCode(s): %s - keyStroke interval: %s\n" "$1" "$2" "$3"
}
readKey "10" #-- example - stop at hitting return
and a complete read-editor example for readKey
#!/bin/bash
#-- read-editor example with readKey and readKey_action
#-- supports pos1, end, backspace, del, insert mode, left arrow, right arrow, copy and paste, and resizing terminal
declare editor_text
#-- editor_cursorLine INT ... cursor position line
#-- editor_cursorColumn INT ... cursor position column
#-- editor_textPos INT ... cursor position inside editor text starting with index 1, cursor text position
#-- editor_insertMode BOOL ... indicates, if insertMode is ON or OFF (Standard: ON)
declare -i editor_cursorLine editor_cursorColumn editor_textPos=1 editor_insertMode=1\
editor_columnPosCI editor_clearCI #-- cursor Info
#-- gets the actual cursor position of the terminal
editor_getCursorPos ()
{
local cursorPos
echo -n $'\e[6n'; IFS= read -sd R cursorPos; cursorPos=${cursorPos:2}
editor_cursorLine=${cursorPos%;*}; editor_cursorColumn=${cursorPos#*;}
}
trap '((editor_clearCI)) && { editor_clearCI=0; echo -ne "\e7\e[1;${editor_columnPosCI}H\e[0K\e8"; }; editor_getCursorPos; sleep 0.001' SIGWINCH
editor_readKey ()
{
#-- key CHAR ... the actual key in the loop (char as UTF-8) or a part of an Ansi Escape Code Sequence ( \e or [ or ..)
#-- keys STR ... saves the recognized key or keys (pressed simultaneous) or the keys (characters) of an or more Ansi Escape Code Sequence(s)
#-- keyCode INT ... the Unicode charCode decimal value for the key or the ASCII charCode decimal for a part of an Ansi Escape Code Sequence
#-- cC_counter INT ... charCode counter for the charCode decimal values added to keyCodeSequence
#-- to_counter INT ... timeout counter between two sent keys
#-- keyCodeSequence STR ... one or a series of Unicode charCode decimals - e.g when pressed an arrow, page down or ..
local key keys keyCodeSequence="" last_AECS="" timeout=${2:-"0.010"} timeEC=${3:-"0.2"} #-- last_AECS STR ... last Ansi Escape Code Sequence without '\e'
local -i keyCode cC_counter=0 to_counter=0 to_lastCount=0 to_limit wasAECS=0 #-- wasAECS BOOL ... indicates, if last key send an Ansi Escape Code Sequence
to_limit=$(bc <<< "scale=0; $timeEC/$timeout") #-- to_limit INT ... timeEC measured in timeouts - error max (is - real): -1 timeout
editor_getCursorPos
stty -echo #-- turn terminal echo off
while :; do
read -N 1 -r -t $timeout key
#--check for time out failure, in exit status of read -> Time Out is End of Sequence!
if (( $? == 0 )); then
printf -v keyCode "%d" "'$key";
keyCodeSequence+="$keyCode "; keys+=$key; ((to_counter ? to_lastCount=to_counter, to_counter=0, cC_counter++ : cC_counter++))
else
((to_counter++))
if ((cC_counter)); then
keyCodeSequence=${keyCodeSequence:0:-1} #-- remove space at end
[[ $keyCodeSequence == "$1" ]] && break #-- break loop
if ((cC_counter>1)); then #-- possible Ansi Escape Code Sequence
if [[ ${keyCodeSequence:0:3} == "27 " ]]; then #-- Ansi Escape Code Sequence
editor_readKey_action "$keys" "$keyCodeSequence" "$to_lastCount"
wasAECS=1; last_AECS=${keys##*$'\e'}
elif ((wasAECS)); then #-- last Sequence was an Ansi Escape Code Sequence
#-- Error Correction of missing $'\e', if time between sent keys is smaller or equal timeEC
if [[ $to_lastCount -le $to_limit && $keys == $last_AECS ]]; then editor_readKey_action $'\e'"$keys" "27 $keyCodeSequence" "$to_lastCount"
else wasAECS=0 #-- if not skip the whole output! and set wasAECS to 0
fi
else
editor_readKey_action "$keys" "$keyCodeSequence" "$to_lastCount" #-- normal keys are pressed simultaneous
fi
else
editor_readKey_action "$keys" "$keyCodeSequence" "$to_lastCount" #-- normal single Unicode key and keyCode
wasAECS=0
fi
keys=""; keyCodeSequence=""; cC_counter=0
fi
fi
done
stty echo #-- turn terminal echo on
}
editor_readKey_action ()
{
local cursorInfo #-- [cursorLine]-[Lines] [cursorColumn]-[Columns] [timeouts]
#-- a, b, c, d INT ... for result storage
#-- editor_textL_S INT ... length of text at Start of editor_readKey_action func
#-- editor_textL_E INT ... length of text after End of manipulation in editor_readKey_action
local -i editor_textL_S=${#editor_text} editor_textL_E editor_textCL=${#1} a b c d
#-- for control keys
case $2 in
#-- backspace
*127*) if ((editor_textPos>1 && (editor_cursorColumn!=1 || editor_cursorLine!=1))); then
if ((editor_cursorColumn > 1)); then ((editor_cursorColumn--, editor_textPos--)); echo -ne "\b\e[J\e7${editor_text:editor_textPos}\e8";
else ((editor_cursorLine--, editor_cursorColumn=COLUMNS, editor_textPos--)); echo -ne "\e[$editor_cursorLine;${editor_cursorColumn}H\e[J\e7${editor_text:editor_textPos}\e8"
fi
editor_text=${editor_text:0:editor_textPos-1}${editor_text:editor_textPos}
fi;;&
#-- del
*"27 91 51 126"*) echo -ne "\e[J\e7${editor_text:editor_textPos}\e8";
editor_text=${editor_text:0:editor_textPos-1}${editor_text:editor_textPos};;&
#-- insert
*"27 91 50 126"*) (( editor_insertMode= ! editor_insertMode ));;&
#-- right arrow
*"27 91 67"*) if ((editor_textPos<=editor_textL_S)); then
if ((editor_cursorColumn < COLUMNS)); then echo -n $'\e[1C'; ((editor_cursorColumn++, editor_textPos++))
elif ((editor_cursorLine != LINES)); then ((editor_cursorLine++, editor_cursorColumn=1, editor_textPos++)); echo -ne "\e[$editor_cursorLine;${editor_cursorColumn}H";
fi
fi;;&
#-- left arrow
*"27 91 68"*) if ((editor_textPos>1 && (editor_cursorColumn!=1 || editor_cursorLine!=1))); then
if ((editor_cursorColumn > 1)); then echo -n $'\e[1D'; ((editor_cursorColumn--, editor_textPos--))
else ((editor_cursorLine--, editor_cursorColumn=COLUMNS, editor_textPos--)); echo -ne "\e[$editor_cursorLine;${editor_cursorColumn}H"
fi
fi;;&
#-- pos 1
*"27 91 72"*) if ((d=editor_cursorColumn+(editor_cursorLine-1)*COLUMNS, d>editor_textPos && (editor_cursorColumn!=1 || editor_cursorLine!=1))); then
(( editor_cursorColumn=editor_cursorColumn-editor_textPos+1,
editor_cursorColumn<=0 ? editor_cursorLine+=editor_cursorColumn/COLUMNS-1, editor_cursorColumn=editor_cursorColumn%COLUMNS+COLUMNS : 1, editor_textPos=1))
else ((editor_cursorColumn=1, editor_cursorLine=1, editor_textPos-=d-1))
fi
echo -ne "\e[$editor_cursorLine;${editor_cursorColumn}H";;&
#-- pos end
*"27 91 70"*) (( editor_cursorColumn+=editor_textL_S-editor_textPos+1,
editor_cursorLine+=editor_cursorColumn/COLUMNS, editor_cursorColumn%=COLUMNS, editor_cursorColumn==0 ? editor_cursorColumn=1, editor_cursorLine-- : 1 ))
echo -ne "\e[$editor_cursorLine;${editor_cursorColumn}H"; editor_textPos=editor_textL_S+1;;
esac
#-- for printable keys, copy STRG+SHIFT+C and paste STRG+SHIFT+V
if [[ $2 != "27 "* && $2 != *127* ]]; then
#-- cursor at end position+1 of editor text, build new editor_text, and print pressed keys
if ((editor_textPos>editor_textL_S)); then
editor_text=$editor_text$1; editor_textL_E=${#editor_text}; echo -n "$1"
#-- cursor position inside editor_text and insertMode is ON
elif ((editor_insertMode)); then
#-- print pressed keys
if ((editor_cursorColumn==COLUMNS)); then echo -ne "\e7\e[J$1${editor_text:editor_textPos-1}\e8" #-- cursor moves to next line
else echo -ne "$1\e7\e[J${editor_text:editor_textPos-1}\e8";fi #-- cursor stays in line
#-- build new editor_text
editor_text=${editor_text:0:editor_textPos-1}$1${editor_text:editor_textPos-1}; editor_textL_E=${#editor_text}
#-- if characters are inserted inside the editor_text, check if the end of the editor_text has exceeded the terminal LINES
(( a=editor_cursorColumn+editor_textL_E-editor_textPos, b=a%COLUMNS, c= b == 0 ? editor_cursorLine+a/COLUMNS-1 : editor_cursorLine+a/COLUMNS ))
(( c>LINES )) && { (( c=c-LINES, editor_cursorLine-=c )); echo -ne "\e[${c}A"; }
#-- cursor position inside editor_text and insertMode is OFF, build new editor_text, and print pressed keys
else editor_text=${editor_text:0:editor_textPos-1}$1${editor_text:editor_textPos+editor_textCL-1}; editor_textL_E=${#editor_text}; echo -n "$1"
fi
#-- calculate new cursor position - line and column, and new cursor text position
(( editor_textPos+=editor_textCL, editor_cursorColumn+=editor_textCL,
editor_cursorLine+=(editor_cursorColumn-1)/COLUMNS, editor_cursorLine>LINES ? editor_cursorLine=LINES:1 ))
(( editor_cursorColumn=editor_cursorColumn%COLUMNS, editor_cursorColumn==0 ? editor_cursorColumn=COLUMNS : 1 ))
#-- inserted text ends at column=COLUMNS, move cursor to column 1 at next line
if ((editor_cursorColumn==1)); then
if ((editor_textPos>editor_textL_E)); then echo -ne " \e[1D" #-- cursor at end position+1 of editor text
elif (( !editor_insertMode )); then echo -ne "${editor_text:editor_textPos-1:1}\e[1D" #-- cursor position inside editor_text and insertMode is ON
else echo -ne "${editor_text:editor_textPos-2:2}\e[1D"; fi #-- cursor position inside editor_text and insertMode is OFF
fi
fi
#-- print cursor Info at line 1
cursorInfo="$editor_cursorLine-$LINES $editor_cursorColumn-$COLUMNS $3";
echo -ne "\e[?25l\e7" > /dev/tty
((editor_columnPosCI)) && echo -ne "\e[1;${editor_columnPosCI}H\e[0K" > /dev/tty
((editor_columnPosCI=COLUMNS-${#cursorInfo}))
echo -ne "\e[1;${editor_columnPosCI}H$cursorInfo\e8\e[?25h" > /dev/tty
editor_clearCI=1
}
editor_readKey "10" #-- example - stop at hitting return
clear; echo "Your text:$editor_text"
Yes, these are ANSI escape sequences https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences .
Are these codes for keyboard keys always the same in each terminal
No. There are so many terminals and virtual terminals. Terminfo.
, or at least in the most terminals?
Nowadays universally yes.
because there also ANSI ESCAPE CODES to set colors but they are different in other terminals
That would be odd, 8-bit ones surely are the same everywhere.