bashansi-escape

Terminal read command - read an INT Number and only allow keys +- 0-9


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"

Solution

  • 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.