Bash scripting, using arrow keys [duplicate] - bash

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?

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

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

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

# 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

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:
Linux Keycode Table (comptechdoc.org)
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

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

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

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
}

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

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

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.

Related

Using bash, check to see if the entered value is in a list

I'm not familiar with bash scripting. In bash script, I'm attempting to validate a prompted value against a defined list, so that when I call:
./test.bash [caseyear] [sector]
caseyear and sector will be validated in the following attempted bash script:
Validating Year
#1 Check the value of the caseyear Parameter
if [ "$1" -eq "$1" 2> /dev/null ]; then
if [ $1 -ge 2009 ]; then
export caseyear=$1
else
echo -e "\n\tSpecify [caseyear] value greater than or equal to 2009.\n"
exit 4
fi
else
echo -e "\n\tSpecify [caseyear] value greater than or equal to 2009.\n"
exit 3
fi
I'm having trouble validating that the entered value must be in the sector list, so I tried the following script:
Validating Sector
#sector list
list_of_sectors="11 40 44_45 52 10_11"
#1 Check if sector value is in the sector list
$ function exists_in_list() {
LIST=$1
DELIMITER=$2
VALUE=$3
LIST_WHITESPACES=`echo $LIST | tr "$DELIMITER" " "`
for x in $LIST_WHITESPACES; do
if [ "$x" = "$VALUE" ]; then
return 0
fi
done
return 1
}
#2 Check if sector is null
if [ -z "$2" ]; then
echo -e "\n\tSpecify [caseyear] sector value.\n"
exit 2
else
#export omitflag="$(echo $2 | tr '[a-z]' '[A-Z]')" #Convert to upper case
#3 Check if sector value is in the sector list
export sector
#------------------->Problem Area
#How do I pass the entered $sector value to exists_in_list function that matches with the list, $list_of_sectors?
if [ $(exists_in_list $list_of_sectors) -ne 0 ] ; then
echo -e "\n\tSpecify [sector] sector value.\n"
exit 1
fi
fi
echo -e "\nYou Specified - CaseYear:$caseyear, Sector:$sector"
Thank you!
Here's a concise way to portably test if a word appears in a string containing space separated words:
list_of_sectors="11 40 44_45 52 10_11"
sector="11"
case " $list_of_sectors " in
(*" $sector "*) echo yes;;
(*) echo no;;
esac
No loops, no pipes, pure POSIX shell. Neat.
Use an array
#sector list
list_of_sectors=( 11 40 44_45 52 10_11 )
#1 Check if sector value is in the sector list
function exists_in_list() {
local value="$1"
for s in "${list_of_sectors[#]}"; do
if [[ "$s" == "${value}" ]]; then
return 0
fi
done
return 1
}
if exists_in_list "$sector"; then
...
fi

How to distinguish whitespace in string after using "read" in bash shell

I'm trying to writing a vim-like editor using shell. And I find that I cannot distinguish whitespace. I read 4 character because some special characters like arrows have 3 bytes. But it will be a mess if you enter keys at a fast speed.
So how can I distinguish space? (If you can tell me a better way to deal with the "entering too fast" problem will be nicer.)
while read -rsN1 key
do
if [ $key==$'\e' ];then
read -sN1 -t 0.0001 k1
read -sN1 -t 0.0001 k2
read -sN1 -t 0.0001 k3
key+=${k1}${k2}${k3}
fi
case $key in
$'\e[A')
echo "up" ;;
' ')
echo "space" ;; #It doesn't work here!!!
i)
echo "insert" ;;
esac
done
You can try this.
Instead of trying that there are a lot of things you can learn when using vim/vi/vim.exe/vi.exe in any operating system (Windows cygwin/mingw, AIX, Linux HP-UX/OSF1/IRIX/SunOS/UNIX)
Hence try learning vi.
However I have updated your code only for my knowledge transfer:
Here goes my coding:
#!/bin/bash
echo "Use any one of the following keys:"
UP=$(echo $'\U2190')
#echo -n $'\U2190' | od -bc
#echo -e "\0342\0206\0220" # LEFT
LEFT=$(echo $'\U2191')
#echo -e "\0342\0206\0221" # UP
RIGHT=$(echo $'\U2192')
#echo -e "\0342\0206\0222" # RIGHT
DOWN=$(echo $'\U2193')
#echo -e "\0342\0206\0223" # DOWN
echo SPACEBAR
while read -rsN1 key
do
if [ $'\e' == "$key" ]
then
read -sN1 -t 0.0001 k1
read -sN1 -t 0.0001 k2
read -sN1 -t 0.0001 k3
key+=${k1}${k2}${k3}
fi
case $key in
$'[A')
echo -n "$UP"
;;
$'[B')
echo -n "$DOWN"
;;
$'[C')
echo -n "$RIGHT"
;;
$'[D')
echo -n "$LEFT"
;;
' ')
echo -n " "
;;
#It doesn't work here!!!
i)
echo "insert"
;;
esac
done
My output:
$ 73172247.sh
Use any one of the following keys:
SPACEBAR
↑↓←→↓↑ ↑↑ ↑ →←↑
Example pdf:
https://www.jics.utk.edu/files/images/csure-reu/PDF-DOC/VI-TUTORIAL.pdf
Textpad.exe/notepad++.exe at windows
gedit at Linux.

Bash: Masking user input for a password, with * support for backspace and special characters

I have this snippet below based on the idea to mask the input for a password by #SiegeX and #mklement0 from this question.
It's great and my only desired addition was to delete for length of entered chars only, so we're not wiping out the entire line.
I don't understand this very well, so have run into bugs.
With below, entering "12345" and backspacing, the numbers don't "backspace"; no error.
Entering "123FourFive" and backspacing, produces error: line 9: [[: 123FourFiv: value too great for base (error token is "123FourFiv")
Entering "OneTwo345" and backspacing, seems to work fine.
Entering symbols one might expect in a password and then backspacing produces error: line 9: [[: OneTwo./?: syntax error: invalid arithmetic operator (error token is "./?")
Also pressing arrow keys during input creates wild screen behaviour after backspacing...
How to improve this so we're masking user input, and only deleting what has been entered?
Or are we "reinventing the wheel"? Is there something else out there that will do what I'm trying to do already (which is mask user input to obtain a password in a bash script to put it in a variable)?
User environment is Linux Mint 19.3 with Cinnamon.
#!/bin/bash
printf "\n\tPlease enter password: "
# mask the input for the password by #SiegeX and #mklement0 (https://stackoverflow.com/questions/4316730)
while IFS= read -r -s -n1 char; do
[[ -z "${char}" ]] && { printf '\n'; break; } # ENTER pressed; output \n and break.
if [[ "${char}" == $'\x7f' ]]; then # backspace was pressed
# #nooblag, only delete for length of entered chars?
if [[ "${password}" -lt "${#password}" ]]; then printf '\b \b'; fi # erase one '*' to the left.
[[ -n $password ]] && password=${password%?} # remove last char from output variable
else
# add typed char to output variable
password+="${char}"
# print '*' in its stead
printf '*'
fi
done
printf "\tPassword: ${password}\n\n"
Update: askpass as suggested here nearly does what I'm after, but if the user tries to abort/kill it with Ctrl+C it messes up the terminal...
This might be the solution! Taken from here.
#!/bin/bash
#
# Read and echo a password, echoing responsive 'stars' for input characters
# Also handles: backspaces, deleted and ^U (kill-line) control-chars
#
unset PWORD
PWORD=
echo -n 'password: ' 1>&2
while true; do
IFS= read -r -N1 -s char
# Note a NULL will return a empty string
# Convert users key press to hexadecimal character code
code=$(printf '%02x' "'$char") # EOL (empty char) -> 00
case "$code" in
''|0a|0d) break ;; # Exit EOF, Linefeed or Return
08|7f) # backspace or delete
if [ -n "$PWORD" ]; then
PWORD="$( echo "$PWORD" | sed 's/.$//' )"
echo -n $'\b \b' 1>&2
fi
;;
15) # ^U or kill line
echo -n "$PWORD" | sed 's/./\cH \cH/g' >&2
PWORD=''
;;
[01]?) ;; # Ignore ALL other control characters
*) PWORD="$PWORD$char"
echo -n '*' 1>&2
;;
esac
done
echo
echo $PWORD

Casing arrow keys in bash

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?
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
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
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
# 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
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:
Linux Keycode Table (comptechdoc.org)
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
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
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
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
}
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
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
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.

Bash Menu: Return to menu after selection made and executed?

I've got a bash script that writes to a file. at the end of the script I want to display a menu, with line numbers - and have the user be able to select 1 or 2, (etc. up to the number of lines in the file) then have it execute that line.
Up to here is perfect.
However, after the line is executed (say for example it displays another file.) I'd like to return to the menu and let the user select another number. Including zero for exiting the menu.
Once the menu is displayed I have the following. (dumpline being the line of the file read)
dresult=`sed -n "$dumpline"p "$PWD"/"$myday1"_"$myjob".txt`
$dresult
But right now - after running the variable $dresult - it exits the shell (where instead I'd like the menu displayed.
Any thoughts?
thank you in advance.
Here's another way to do a menu which relies on cat having the ability to number the lines of a file (some versions of cat may not have that - see the second example if this is the case). Both examples are for a simple four-item menu:
while [[ 1 ]]
do
cat -n menufile
read -p "Make a selection " choice
case $choice in
1|2)
echo "A or B"
;;
3)
echo "C"
;;
4)
break
;;
*)
echo "Invalid choice"
;;
esac
done
This doesn't require cat -n:
saveIFS="$IFS"
IFS=$'\n'
read -d '' -a menuarray < menufile
IFS="$saveIFS"
for (( i=0; i<${#menuarray[#]}; i++ ))
do
menu=$menu"$(($i+1))) ${menuarray[i]}"$'\n'
done
while [[ 1 ]]
do
echo "$menu"
read -p "Make a selection " choice
case $choice in
1|2)
echo "A or B"
;;
3)
echo "C"
;;
4)
break
;;
*)
echo "Invalid choice"
;;
esac
done
My comments on dz's answer are too long for a comment, so I'm posting them here:
Using seq with select would make a redundant-looking menu, with no correlation between it and the display of the lines in $dumpfile:
ls
echo 'hello'
1) 1
2) 2
etc.
You could do something like this instead:
saveIFS=$IFS
IFS=$'\n'
menu=$(< $dumpfile)
PS3="Make a selection: "
select ACTION in $menu QUIT
do
IFS=$saveIFS
case ...
I think, you need something like a loop. Here is a small skelleton for executing a selected line from file:
#!/bin/bash
dumpfile="bla.txt"
echo "ls
echo 'hello'" > ${dumpfile}
function do_action {
line="$( sed -n "${1},1p" "$dumpfile" )"
(
eval "${line}"
)
}
cat -n $dumpfile
nr=$( cat "${dumpfile}" | wc -l )
PS3="select line nr or nr for QUIT: "
select ACTION in $( seq "$nr" ) QUIT
do
case $ACTION in
QUIT) echo "exit" ; break ;; #EXIT
*) do_action "$ACTION" ;;
esac
done
But be aware of the following:
Using eval might be not allways be a good idea (escaping is hard). Sometimes $line should be sufficient.
Using a subshell prevents changing variables by executing a line of the file. It also does prevent exiting the script from lines that do normally exits a shell.

Resources