14

Is it possible to case arrow keys in a bash script to run a certain set of commands if the up/left arrow keys are pressed, and a certain set if the down/right arrow keys are pressed? I am trying to have a way to switch between users quickly by using the arrow keys while it is displaying the data, using this script to read the data from.

function main()  # The main function that controls the execution of all other functions
{
  mkdir -p ~/usertmp  # Make a new temporary user directory if it doesn't exist
  touch ~/last_seen_output.txt  # Create the output file if it doesn't exist
  cat /dev/null > ~/last_seen_output.txt  # Make sure that the output file is empty
  gather  # Call the "gather" function
  total=$((`wc -l ~/usertmp/user_list.txt|awk '{print $1}'`-1))  # Calculate the total amount of lines and subtract 1 from the result
  echo Current Time: `date +%s` > ~/last_seen_output.txt  # Print the current time to the output file for later reference
  echo "" > ~/last_seen_output.txt  # Print a blank line to the output file
    if [ $log -eq 1 ]
      then
        # If it is enabled, then delete the old backups to prevent errors
        while [ $line_number -le $total ]
          do

            line_number=$((line_number+1))  # Add 1 to the current line number
            calculate # Call the "calculate" function
            hms  # Call the "hms" function to convert the time in seconds to normal time
            log
        done
      else
        while [ $line_number -le $total ]
          do
            line_number=$((line_number+1))  # Add 1 to the current line number
            calculate # Call the "calculate" function
            hms  # Call the "hms" function to convert the time in seconds to normal time
            echo "Displaying, please hit enter to view the users one by one."
            read  # Wait for user input
            if [ "$log_while_displaying" ]
              then
                log
                display
              else
                display
            fi
        done
    fi
}

https://github.com/jbondhus/last-seen/blob/master/last-seen.sh is the complete script.

The read command commented as "wait for user input" is the command that you hit enter to go to the next user for. Basically, what this script does it list users and the time that has passed since each user logged in. I am trying to switch between each user being displayed by using the arrow keys. I figured that it might be possible to use a case statement to case the key input. To reiterate my point, I'm not sure if this is possible. If it isn't, can anyone think of another way to do this?

Jonathan Bondhus
  • 181
  • 1
  • 1
  • 4

11 Answers11

25

You can read arrow keys as well as other keys without any unusual commands; you just need to conditionally add a second read call:

escape_char=$(printf "\u1b")
read -rsn1 mode # get 1 character
if [[ $mode == $escape_char ]]; then
    read -rsn2 mode # read 2 more chars
fi
case $mode in
    'q') echo QUITTING ; exit ;;
    '[A') echo UP ;;
    '[B') echo DN ;;
    '[D') echo LEFT ;;
    '[C') echo RIGHT ;;
    *) >&2 echo 'ERR bad input'; return ;;
esac
JellicleCat
  • 28,480
  • 24
  • 109
  • 162
  • very nice, thx! Not sure why this is not higher. Easy, readable working. – Martin Mucha Jan 05 '20 at 09:51
  • nearly perfect... if you made the second read -rsn4, you can also recognise F* keys but you'll need to add ~ to the cases - see my extended answer – Paul Hedderly Apr 28 '20 at 14:58
  • Paul Hedderly, I'm just not sure that it's worth adding a read timer (`-t 0.001`) just to support something that OP didn't ask for. It's an easy enhancement to code, but the introduction of one more dimension where failure can occur seems like it doesn't bring the solution closer to perfection :shrug: – JellicleCat Jan 06 '21 at 19:32
18

As mentioned before, the cursor keys generate three bytes - and keys like home/end even generate four! A solution I saw somewhere was to let the initial one-char read() follow three subsequent one-char reads with a very short timeout. Most common key sequences can be shown like this f.e.:

#!/bin/bash
for term in vt100 linux screen xterm
  { echo "$term:"
    infocmp -L1 $term|egrep 'key_(left|right|up|down|home|end)'
  }

Also, /etc/inputrc contains some of these with readline mappings.. So, answering original question, here's a snip from that bash menu i'm just hacking away at:

while read -sN1 key # 1 char (not delimiter), silent
do
  # catch multi-char special key sequences
  read -sN1 -t 0.0001 k1
  read -sN1 -t 0.0001 k2
  read -sN1 -t 0.0001 k3
  key+=${k1}${k2}${k3}

  case "$key" in
    i|j|$'\e[A'|$'\e0A'|$'\e[D'|$'\e0D')  # cursor up, left: previous item
      ((cur > 1)) && ((cur--));;

    k|l|$'\e[B'|$'\e0B'|$'\e[C'|$'\e0C')  # cursor down, right: next item
      ((cur < $#-1)) && ((cur++));;

    $'\e[1~'|$'\e0H'|$'\e[H')  # home: first item
      cur=0;;

    $'\e[4~'|$'\e0F'|$'\e[F')  # end: last item
      ((cur=$#-1));;

    ' ')  # space: mark/unmark item
      array_contains ${cur} "${sel[@]}" && \
      sel=($(array_remove $cur "${sel[@]}")) \
      || sel+=($cur);;

    q|'') # q, carriage return: quit
      echo "${sel[@]}" && return;;
  esac                  

  draw_menu $cur "${#sel[@]}" "${sel[@]}" "$@" >/dev/tty
  cursor_up $#
done
eMPee584
  • 1,945
  • 20
  • 20
  • +1 although this doesn't really solve the problem. I'd go with @DennisWilliamson's final suggestion. – tripleee May 03 '13 at 06:01
  • For Mac users, `-sN1` needs to be `-sn1`. Also, `-t 0.0001` doesn't work. It throws `read: 0.0001: invalid timeout specification` error. @eMPee584 do you know how to resolve the fractional timeout issue in Mac? – Moon Jul 22 '21 at 15:26
12
# This will bind the arrow keys

while true # Forever = until ctrl+c
do
    # r: backslash is not for escape
    # s: silent
    # "n x": read x chars before returning
    read -rsn 1 t
    case $t in
        A) echo up ;;
        B) echo down ;;
        C) echo right ;;
        D) echo left ;;
    esac
done
Balaco
  • 12
  • 1
  • 8
user3229933
  • 129
  • 1
  • 2
11

You can use read -n 1 to read one character then use a case statement to choose an action to take based on the key.

On problem is that arrow keys output more than one character and the sequence (and its length) varies from terminal to terminal.

For example, on the terminal I'm using, the right arrow outputs ^[[C. You can see what sequence your terminal outputs by pressing Ctrl-V Right Arrow. The same is true for other cursor-control keys such as Page Up and End.

I would recommend, instead, to use single-character keys like < and >. Handling them in your script will be much simpler.

read -n 1 key

case "$key" in
    '<') go_left;;
    '>') go_right;;
esac
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • @JonathanBondhus: They are placeholders for function names or other commands or statements that you would provide. – Dennis Williamson May 21 '12 at 14:24
  • One more thing, I updated my script, again seen [here](https://github.com/jbondhus/last-seen/blob/master/last-seen.sh ) and it now has , assigned to go back a user and . assigned to go forward. I did this so that the user doesn't have to hold down the shift key. So far, I am using [this](pastebin.com/MiPA5jJD) as the function that parses the key input, and I want to change it so that instead of , for back and . for forward it uses the arrow keys. I tried using the keycode, ^[[D and ^[[C as the left and right keys, like '^[[D') and '^[[C'), but it still doesn't work. – Jonathan Bondhus May 21 '12 at 15:52
  • Yeah, don't try to use the arrow keys. To do that well, you need to use a cursed module in a Python script or something similar instead of trying to use a Bash script. Some comments on your script: You can improve readability and reduce the size of the code by using `((var += value))` or `((var++))` instead of `var=$((var+value))`. For example: `((total_time -= mo * 2629746))`. Doing `var=$((0))` isn't necessary, just use `var=0`. Integer comparisons: `while (( line_number <= total ))`. Use `$()` instead of backticks for command substitution. You don't need awk: `wc -l < filename` – Dennis Williamson May 21 '12 at 16:04
  • Thanks, I'm 17 and I've only been writing bash for about 3 months. – Jonathan Bondhus May 22 '12 at 13:02
  • 1
    You're off to a very good start. In my previous comment I said "cursed module". That was a typo. It should have said "curses module". – Dennis Williamson May 22 '12 at 13:45
  • meh, you could handle arrow keys perfectly in bash ...as any another escape sequence – Reishin Aug 23 '22 at 17:49
9

Not sure if this answer the question directly, but I think it's related - I was wandering where do those codes come from, and I finally found:

It's a bit difficult to read at first; for left arrow, lookup "LEFT 4" in the "Key" column, and for the sequence that bash sees, look up the 5th ("keymap" - "normal") column, where it is written as "[D 1b 5b 44" - which are the three bytes (27, 91, 68) representing this key.

Finding the thread How to read arrow keys on really old bash? - The UNIX and Linux Forums, inspired me to write a short one-liner which dumps the key codes of keys pressed. Basically, you press a key, then Enter (to trigger ending of read), and then use hexdump to output what read has saved (and finally hit Ctrl-C to exit the loop):

$ while true; do read -p?; echo -n $REPLY | hexdump -C; done
?^[[D     
00000000  1b 5b 44                                          |.[D| # left arrow
00000003
?^[[C
00000000  1b 5b 43                                          |.[C| # right arrow
00000003
?^[[1;2D
00000000  1b 5b 31 3b 32 44                                 |.[1;2D| # Shift+left arrow
00000006
?^[[1;2C
00000000  1b 5b 31 3b 32 43                                 |.[1;2C| # Shift+right arrow
00000006
?^C

So, while arrow keys require 3 bytes - Shift+arrow keys require 6! However, seemingly all these sequence start with 0x1b (27), so one could possibly check for this value for read -n1, before reading any more bytes; also 5b remains a second byte in multi-byte sequence for the "normal" and "shift/NUM-Lock" columns of the table above.


Edit: much easier and proper way to scan for terminal codes of pressed keys in Linux is via showkey:

$ showkey 
Couldn't get a file descriptor referring to the console

$ showkey -h
showkey version 1.15

usage: showkey [options...]

valid options are:

    -h --help   display this help text
    -a --ascii  display the decimal/octal/hex values of the keys
    -s --scancodes  display only the raw scan-codes
    -k --keycodes   display only the interpreted keycodes (default)

$ sudo showkey -a

Press any keys - Ctrl-D will terminate this program

^[[A     27 0033 0x1b
         91 0133 0x5b
         65 0101 0x41
^[[B     27 0033 0x1b
         91 0133 0x5b
         66 0102 0x42
^[[A     27 0033 0x1b
         91 0133 0x5b
         65 0101 0x41
^[[D     27 0033 0x1b
         91 0133 0x5b
         68 0104 0x44
^[[C     27 0033 0x1b
         91 0133 0x5b
         67 0103 0x43
^C       3 0003 0x03
^M       13 0015 0x0d
^D       4 0004 0x04
sdaau
  • 36,975
  • 46
  • 198
  • 278
5

Using eMPee584 answer I think I came up a good solution for you. Its output is much the same as user3229933 answer but will not be triggered by shift keys and will work in most terminals.

It has UP DOWN LEFT RIGHT HOME and END Keys Press 'q' to quit Most of this is thanks to eMPee584

you may need to change '-sn1' to '-sN1' if you get an error like illegal option n.

#!/bin/bash

while read -sn1 key # 1 char (not delimiter), silent
do

  read -sn1 -t 0.0001 k1 # This grabs all three symbols 
  read -sn1 -t 0.0001 k2 # and puts them together
  read -sn1 -t 0.0001 k3 # so you can case their entire input.

   key+=${k1}${k2}${k3} 

  case "$key" in
    $'\e[A'|$'\e0A')  # up arrow
        ((cur > 1)) && ((cur--))
        echo up;;

    $'\e[D'|$'\e0D') # left arrow
        ((cur > 1)) && ((cur--))
        echo left;;

    $'\e[B'|$'\e0B')  # down arrow
        ((cur < $#-1)) && ((cur++))
        echo down;;

    $'\e[C'|$'\e0C')  # right arrow
        ((cur < $#-1)) && ((cur++))
        echo right;;

    $'\e[1~'|$'\e0H'|$'\e[H')  # home key:
        cur=0
        echo home;;

    $'\e[4~'|$'\e0F'|$'\e[F')  # end key:
        ((cur=$#-1))
        echo end;;

    q) # q: quit
        echo Bye!
        exit;;

   esac                  

done
djorborn
  • 151
  • 1
  • 7
0

To extend JellicleCat's answer:

#!/bin/bash
escape_char=$(printf "\u1b")
read -rsn1 mode # get 1 character
if [[ $mode == $escape_char ]]; then
    read -rsn4 -t 0.001 mode # read 2 more chars
fi
case $mode in
    '') echo escape ;;
    '[a') echo UP ;;
    '[b') echo DOWN ;;
    '[d') echo LEFT ;;
    '[c') echo RIGHT ;;
    '[A') echo up ;;
    '[B') echo down ;;
    '[D') echo left ;;
    '[C') echo right ;;
    '[2~') echo insert ;;
    '[7~') echo home ;;
    '[7$') echo HOME ;;
    '[8~') echo end ;;
    '[8$') echo END ;;
    '[3~') echo delete ;;
    '[3$') echo DELETE ;;
    '[11~') echo F1 ;;
    '[12~') echo F2 ;;
    '[13~') echo F3 ;;
    '[14~') echo F4 ;;
    '[15~') echo F5 ;;
    '[16~') echo Fx ;;
    '[17~') echo F6 ;;
    '[18~') echo F7 ;;
    '[19~') echo F8 ;;
    '[20~') echo F9 ;;
    '[21~') echo F10 ;;
    '[22~') echo Fy ;;
    '[23~') echo F11 ;;
    '[24~') echo F12 ;;
    '') echo backspace ;;
    *) echo $mode;;
esac
Paul Hedderly
  • 3,813
  • 2
  • 18
  • 12
0

None of the above answers have worked for me! I had to grab bits and pieces from many answers in this thread, as well as other searches via google. It took me about an hour to concoct this.

I am running Ubuntu 20.04 LTS and this works for me (although, it may not be perfect, as I had to 'hack' at it):

waitkey() {
  local end=""
  local key=""

  echo
  echo "   Press ESC ... "

  while [ "$end" == "" ]; do
    read -rsn1 key
    case "$key" in
      $'\x1b')
        local k=""
        # I'm not sure why I have to do this if statement,
        # but without it, there are errors.  took forever
        # to figure out why 'read' would dump me outta the script
        if [ "$IFS" ]; then
          read -rsn1 -t 0.1 holder && k="$holder"
        else
          IFS=read -rsn1 -t 0.1 holder && k="$holder"
        fi 

        if [ "$k" == "[" ]; then
          read -rsn1 -t 0.1 holder && kk="$holder"

          ##############################
          # you put your arrow code here
          #
          # eg:
          #  case "$kk" in
          #    "A") echo "up arrow!" ;; # do something ...
          #  esac
          ##############################
        elif [ "$k" == "O" ]; then
          read -rsn1 -t 0.1 holder && kk="$holder"

          # I am honestly not knowing what this is for
        elif [ "$k" == "" ]; then
          end=1
        fi
    esac
  done
}
Jam Roll
  • 13
  • 3
0

For anyone looking for a Mac compatible version that also handles holding down the shift key as well as enter and space:

#!/bin/bash

ESC=$'\033'
SHIFT=$'[1;2'
# distinguish between enter and space
IFS=''

while true; do
    read -rsn1 a
    # is the first character ESC?
    if [[ $ESC == $a ]]; then
        read -rsn2 b
        # does SHIFT start with the next two characters?
        if [[ $SHIFT == "$b"* ]]; then
            read -rsn3 c
        fi
    fi

    input=$a$b$c
    unset b c

    case $input in
        $ESC[A) echo UP ;;
        $ESC[B) echo DOWN ;;
        $ESC[C) echo RIGHT ;;
        $ESC[D) echo LEFT ;;

        $ESC$SHIFT'A') echo SHIFT UP ;;
        $ESC$SHIFT'B') echo SHIFT DOWN ;;
        $ESC$SHIFT'C') echo SHIFT RIGHT ;;
        $ESC$SHIFT'D') echo SHIFT LEFT ;;

        '') echo ENTER ;;
        ' ') echo SPACE ;;

        q) break ;;
    esac
done
Chris Redford
  • 16,982
  • 21
  • 89
  • 109
0

Here is yet another variant of readkey for bash.

But first the notes(related to Linux):

read -rsN 1 vs read -rsn 1 - the second variant will not distinguish between escape, enter or space. It's visible from @Paul Hedderly answer.

The tactics:

  • scanning depends on the first and the third and fifth bytes variations
  • we scan extended keycodes until we hit "~"/ dec: 126 byte
  • there are exceptions for third byte dec 49, it should be processed differently
  • due to way we getting input, it's hard to track ESC button itself, as workaround we doing it, if we getting sequence "27 27", what means 2 presses of ESC button

Notes:

  • This code should able to return scan codes for any button.
    • Shift + Key
    • Alt + Key
  • keyCode is an array, which would contain the whole button scan sequence
  • uncomment # echo "Key: ${keyCode[*]}" to see the button scan code
  • This is not something ideal or perfect, but it works.
  • Code utilizes bash and could be not compatible with dash, sh, etc
  • Tested in Linux environment from Windows Terminal ssh.
    #!/bin/bash
    
    readKey(){
        read -rsN 1 _key
        printf %d "'${_key}"   # %x for hex
    }
    
    demo(){
        local keyCode=(0)
    
        while [[ ${keyCode[0]} -ne 10 ]]; do  #exit on enter
            local keyCode=("$(readKey)") # byte 1
            if [[ ${keyCode[0]} -eq 27 ]]; then # escape character 
                local keyCode+=("$(readKey)") # byte 2
                if [[ ${keyCode[-1]} -ne 27 ]]; then # checking if user pressed actual
                    local keyCode+=("$(readKey)")  # byte 3
                    
                    if [[ "51 50 48 52 53 54" =~ (^|[[:space:]])"${keyCode[2]}"($|[[:space:]]) ]]; then
                        while [[ ${keyCode[-1]} -ne 126 ]]; do
                            local keyCode+=("$(readKey)")    
                        done
                    fi
                    if [[ "49" =~ (^|[[:space:]])"${keyCode[2]}"($|[[:space:]]) ]]; then
                        local keyCode+=("$(readKey)")  # byte 4
                        [[ ${keyCode[-1]} -ne 126 ]] && local keyCode+=("$(readKey)") # byte 5
                        [[ ${keyCode[-1]} -eq 59 ]] && local keyCode+=("$(readKey)") # byte 5 check
                        [[ ${keyCode[-1]} -ne 126 ]] && local keyCode+=("$(readKey)")
                    fi
                fi
            fi
            # echo "Key: ${keyCode[*]}"
            case "${keyCode[*]}" in 
                "27 91 65") echo "UP";;
                "27 91 66") echo "DOWN";;
                "27 91 67") echo "RIGHT";;
                "27 91 68") echo "LEFT";;
            esac
        done
    }

demo
Reishin
  • 1,854
  • 18
  • 21
0

Here's a function I made to convert special keys into readable keywords.

For example :

  • < -> LEFT
  • Shift + > -> SHIFT_RIGHT
  • Ctrl + Alt + Home -> CTRL_ALT_HOME
  • Ctrl + Alt + Shift + Page Up -> CTRL_ALT_SHIFT_PAGEUP
  • .....
#!/usr/bin/env bash

shopt -s extglob

#keyOf <KEY>
keyOf() {
  local key=
  local mod=

  case "$1" in
    $'\177' ) key=BACKSPACE ;;
      $'\b' ) key=CTRL_BACKSPACE ;;
  $'\E\177' ) key=ALT_BACKSPACE ;;
    $'\E\b' ) key=CTRL_ALT_BACKSPACE ;;

    $'\t' ) key=TAB ;;
  $'\E[Z' ) key=SHIFT_TAB ;;

  $'\E' ) key=ESCAPE ;;
    ' ' ) key=SPACE ;;
  esac

  if [ -z "${key:+x}" ] && [ "${1:0:1}" = $'\E' ]; then
    case "${1#$'\E'}" in
    \[??(?)\;2? ) mod=SHIFT_ ;;
    \[??(?)\;3? ) mod=ALT_ ;;
    \[??(?)\;4? ) mod=ALT_SHIFT_ ;;
    \[??(?)\;5? ) mod=CTRL_ ;;
    \[??(?)\;6? ) mod=CTRL_SHIFT_ ;;
    \[??(?)\;7? ) mod=CTRL_ALT_ ;;
    \[??(?)\;8? ) mod=CTRL_ALT_SHIFT_ ;;
    esac

    case "${1#$'\E'}" in
    OA | \[?(1\;?)A ) key="${mod}UP" ;;
    OB | \[?(1\;?)B ) key="${mod}DOWN" ;;
    OC | \[?(1\;?)C ) key="${mod}RIGHT" ;;
    OD | \[?(1\;?)D ) key="${mod}LEFT" ;;

    OP | \[?(1\;?)P ) key="${mod}F1" ;;
    OQ | \[?(1\;?)Q ) key="${mod}F2" ;;
    OR | \[?(1\;?)R ) key="${mod}F3" ;;
    OS | \[?(1\;?)S ) key="${mod}F4" ;;
        \[15?(\;?)~ ) key="${mod}F5" ;;
        \[17?(\;?)~ ) key="${mod}F6" ;;
        \[18?(\;?)~ ) key="${mod}F7" ;;
        \[19?(\;?)~ ) key="${mod}F8" ;;
        \[20?(\;?)~ ) key="${mod}F9" ;;
        \[21?(\;?)~ ) key="${mod}F10" ;;
        \[23?(\;?)~ ) key="${mod}F11" ;;
        \[24?(\;?)~ ) key="${mod}F12" ;;

      \[?(?(1)\;?)F ) key="${mod}END" ;;
      \[?(?(1)\;?)H ) key="${mod}HOME" ;;

         \[2?(\;?)~ ) key="${mod}INSERT" ;;
         \[3?(\;?)~ ) key="${mod}DELETE" ;;

         \[5?(\;?)~ ) key="${mod}PAGEUP" ;;
         \[6?(\;?)~ ) key="${mod}PAGEDOWN" ;;
    esac
  fi
  
  printf '%s' "${key:-$1}"
}



while read -rsN1 c; do
  d=
  read -t 0.001 -rsd $'\0' d
  c+="$d"
  printf "%q \t%q\n" "$c" "$(keyOf "$c")"
done

NOTE: Only tested on Windows (MSYS2/MingW64) but can easily be modified.

Elie G.
  • 1,514
  • 1
  • 22
  • 38