Bash read backspace button behavior problem - bash

When using read in bash, pressing backspace does not delete the last character entered, but appears to append a backspace to the input buffer. Is there any way I can change it so that delete removes the last key typed from the input? If so how?
Here's a short example prog I'm using it with if it's of any help:
#!/bin/bash
colour(){ #$1=text to colourise $2=colour id
printf "%s%s%s" $(tput setaf $2) "$1" $(tput sgr0)
}
game_over() { #$1=message $2=score
printf "\n%s\n%s\n" "$(colour "Game Over!" 1)" "$1"
printf "Your score: %s\n" "$(colour $2 3)"
exit 0
}
score=0
clear
while true; do
word=$(shuf -n1 /usr/share/dict/words) #random word from dictionary
word=${word,,} #to lower case
len=${#word}
let "timeout=(3+$len)/2"
printf "%s (time %s): " "$(colour $word 2)" "$(colour $timeout 3)"
read -t $timeout -n $len input #read input here
if [ $? -ne 0 ]; then
game_over "You did not answer in time" $score
elif [ "$input" != "$word" ]; then
game_over "You did not type the word correctly" $score;
fi
printf "\n"
let "score+=$timeout"
done

The option -n nchars turns the terminal into raw mode, so your best chance is to rely on readline (-e) [docs]:
$ read -n10 -e VAR
BTW, nice idea, although I would leave the end of the word to the user (it's a knee-jerk reaction to press return).

I know the post is old, still this can be useful for someone. If you need specific response to a single keypress on backspace, something like this can do it (without -e):
backspace=$(cat << eof
0000000 005177
0000002
eof
)
read -sn1 hit
[[ $(echo "$hit" | od) = "$backspace" ]] && echo -e "\nDo what you want\n"

Related

Asking for user input in a loop until match found in array of command line arguments

Sometimes I need to find a specific serial in a box with many items, so I wrote a simple Bash script that allows me to use a barcode scanner to scan hundreds of barcodes until a match is found, at which point the screen flashes (so I can see it from the corner of my eyes while looking at the box).
The script works great, but it only checks against one specific serial number provided by the user. Here's the code:
#!/bin/bash
INPUT=''
SCAN=''
SN=''
I='0'
clear
printf "Enter serial\n"
read INPUT
SN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
# Keep comparing scans to needed serial until a match is found
while [[ "${SCAN}" != *"${SN}"* ]];
do
clear
printf "Looking for [ "${SN}" ]\n"
printf "Please scan barcode\n"
read INPUT
SCAN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
done
# Flash screen when match is found
while [[ "${I}" -lt 3 ]];
do
printf '\e[?5h' && sleep 0.3
printf '\e[?5l' && sleep 0.3
I=$[${I}+1]
done
printf "FOUND\n"
Today I spent hours trying to implement a way to pass multiple possible serial numbers as command line arguments, but I can't seem to get it working. I would like to be able to pass a small, manageable number of possible serials, like this:
$ ./script.sh sn1 sn2 sn3 sn4 sn5
And for the script continue asking for input until I come across the item I am looking for.
I've studied the handling of shell arguments, but I can't seem to "massage" the above while loop to get it to check if the scanned serial exists in the array (created from the command line arguments passed):
#!/bin/bash
snList=( "$#" )
INPUT=''
SCAN=''
SN=''
I='0'
clear
#displaying "things" so I can see what each variable contains (debugging)
printf "$#\n"
printf "$0\n"
printf "$*\n"
printf "$0\n"
printf "$1\n"
printf "$2\n"
printf "$3\n"
printf "snList: $snList\n"
printf "snList[#]: ${snList[#]}\n"
printf "snList[*]: ${snList[*]}\n"
# Keep comparing scans to needed serial until a match is found
while [[ ! " ${snList[*]} " =~ "${SCAN}" ]];
do
clear
printf "Looking for [ "$*" ]\n"
printf "Please scan barcode\n"
read INPUT
SCAN=`printf "${INPUT}" | tr '[:lower:]' '[:upper:]'`
done
I've tried using ${snList[#]} in the loop as well, same result, it behaves like a match was found immediately, without even asking for a scan (indicating that the content of the while loop is not being executed).
Any help will be immensely appreciated, I think I am close, but I can't figure out what I am doing wrong.
Thanks in advance!
Something like this maybe?
#!/usr/bin/env bash
to_compare_input=("$#")
exglob_pattern_input=$(IFS='|'; printf '%s' "#(${to_compare_input[*]})")
until [[ $user_input == $exglob_pattern_input ]]; do
read -r user_input
done
Run the script with the the following arguments.
bash -x ./myscript foo bar baz more
Output
+ to_compare_input=("$#")
++ IFS='|'
++ printf %s '#(foo|bar|baz|more)'
+ exglob_pattern_input='#(foo|bar|baz|more)'
+ [[ '' == #(foo|bar|baz|more) ]]
+ read -r user_input
papa
+ [[ papa == #(foo|bar|baz|more) ]]
+ read -r user_input
mama
+ [[ mama == #(foo|bar|baz|more) ]]
+ read -r user_input
baz
+ [[ baz == #(foo|bar|baz|more) ]]
The first user input is empty since the builtinread has not been executed to ask for the user's input. As shown at the debug message.
+ [[ '' == #(foo|bar|baz|more) ]]
The second (assuming the user has entered papa) is papa
The third (assuming the user has entered mama) is mama
The last is baz which breaks out of off the until loop, because it belongs to the $extglob_pattern_input, which is an extglob feature.
A regex is also an alternative using the =~ operator.
#!/usr/bin/env bash
to_compare_input=("$#")
regex_pattern_input=$(IFS='|'; printf '%s' "^(${to_compare_input[*]})$")
until [[ $user_input =~ $regex_pattern_input ]]; do
read -r user_input
done
Run the script same as before.
Using two loops which was suggested in the comments section.
#!/usr/bin/env bash
to_compare_input=("$#")
inarray() {
local n=$1 h
shift
for h; do
[[ $n == "$h" ]] && return
done
return 1
}
until inarray "$user_input" "${to_compare_input[#]}"; do
read -r user_input
done
As for the tr if your version of bash supports the ^^ and ,, for uppercase and lowercase parameter expansion. use ${user_input^^}
until [[ ${user_input^^} == $exglob_pattern_input ]]; do
until [[ ${user_input^^} =~ $regex_pattern_input ]]; do
until inarray "${user_input^^}" "${to_compare_input[#]}"; do
Assuming no spaces in the bar code texts. You can do something like this
while read -r INPUT
do
#Append spaces to prevent substring matching
if [[ $(echo " $# " | grep -i " ${INPUT} " | wc -l) -eq 1 ]]
then
break
fi
done

Would it be possible to change this bash loop into a case statement?

I work at a helpdesk, and I use this script to look up info I need all the time at my job.
This saves me a bit of time, and it has been a great learning experience. I am pretty new to scripting, and any feedback about this script will be well received. What I would really like help with is this loop.
General Feedback welcome also.
# If more than one member in Array Ask for user input.
if [[ ${tLen} -gt '1' ]]; then
for (( i=0; i<${tLen}; i++ ));
do
# adding one so user selection starts at 1
# gobble up everything up to last back slash.
echo "$(($i + 1 ))) ${arr[$i]##*/}"
done
Would it be possible to turn this into a case statement, to protect against erroneous user input? Or another way to handle erroneous user input would be great also. Thanks in advance for any input!
#!/bin/bash
# Asking for serial number
echo -e "Enter serial number:"
read SerialNumber
SerialNumberCount=${#SerialNumber}
if [[ ${SerialNumberCount} > "11" ]]; then
# If Serial is 12 or more characters, set the ModelSerial to four digits
ModelSerial=$(echo $SerialNumber|awk '{ print substr( $0, length($0) - 3, length($0) ) }')
else
# Set serial to three characters
ModelSerial=$(echo $SerialNumber|awk '{ print substr( $0, length($0) - 2, length($0) ) }')
fi
# Set ModelURL based on length of Serial number
# Creating temp dir/file trap for parsing html
TMPDIR=${TMPDIR:-/tmp}
temporary_dir=$(mktemp -d "$TMPDIR/XXXXXXXXXXXXXXXXXXXXXXXXXXXXX") || { echo "ERROR creating a temporary file" >&2; exit 1; }
trap 'rm -rf "$temporary_dir"' 0
trap 'exit 2' 1 2 3 15
temp="$temporary_dir/$RANDOM-$RANDOM-$RANDOM"
# First downloaded html page
lookupUrl=$(echo "http://everymac.com/ultimate-mac-lookup/?search_keywords=${ModelSerial}")
curl -s -o $temp -L $lookupUrl
# Parsing partial links
webRslt=$(cat $temp | grep -o '<a href=['"'"'"][^"'"'"']*['"'"'"]' | sed -e 's/^<a href=["'"'"']//' -e 's/["'"'"']$//'|grep /systems* |grep ".html" )
# Creating Array of partial links
arr=($webRslt)
# Length of Array
tLen=${#arr[#]}
# If more than one member in Array Ask for user input.
if [[ ${tLen} -gt '1' ]]; then
for (( i=0; i<${tLen}; i++ ));
do
# adding one so user selection starts at 1
# gobble up everything up to last back slash.
echo "$(($i + 1 ))) ${arr[$i]##*/}"
done
# Ask user for input
printf "\n"
echo "Please enter 1-${tLen} --> "
read -r sel
# Subtracting one from selection for correct Array member
ArraySel=$((sel - 1))
curl "${arr["$ArraySel"]/#/http://everymac.com}" -s -o "$temp"
else
# If array only has one member do this...
curl "${arr[0]/#/http://everymac.com}" -s -o "$temp"
fi
clear
# Parsing html for screen printing
get_value() { perl -nE 'say $1 if m!>\Q'"$1"'\E</td>\s*<td[^>]+>([^<]+)<!' <"$temp"; }
VAR1=$(get_value "Apple Model No:")
VAR2=$(get_value "Apple Order No:")
VAR3=$(get_value "Model ID:")
VAR4=$(get_value "Processor Speed:")
VAR5=$(get_value "Standard RAM:")
VAR6=$(get_value "Maximum RAM:")
VAR7=$(get_value "RAM Type:")
VAR8=$(get_value "Pre-Installed MacOS:")
VAR9=$(get_value "Maximum MacOS:")
VAR10=$(get_value "Minimum Windows:")
VAR11=$(get_value "Maximum Windows:")
VAR12=$(get_value "Original Price (US):")
tput setaf 2
printf "\033[1mRam & Apple Model Information Information (experimental beta)\033[m\n\n"
printf "Apple Model No:\t\\t\033[1m$VAR1\033[m\n"
printf "Apple Order No::\t\033[1m$VAR2\033[m\n"
printf "Model ID:\t\\t\033[1m$VAR3\033[m\n"
printf "Processor Speed:\t\033[1m$VAR4\033[m\n"
printf "Standard RAM:\t\\t\033[1m$VAR5\033[m\n"
printf "Maximum RAM:\t\\t\033[1m$VAR6\033[m\n"
printf "RAM Type:\t\\t\033[1m$VAR7\033[m\n"
printf "Pre-Installed MacOS:\t\033[1m$VAR8\033[m\n"
printf "Maximum MacOS::\t\t\033[1m$VAR9\033[m\n"
printf "Minimum Windows:\t\033[1m$VAR10\033[m\n"
printf "Maximum Windows:\t\033[1m$VAR11\033[m\n"
printf "Original Price (US):\t\033[1m$VAR12\033[m\n"
printf "\n\n"
printf "See URL below for more details."
printf "\n\n"
# Print page that is being parsed on screen.
if [[ ${tLen} -gt '1' ]]; then
tput setaf 2
echo "${arr[$ArraySel]/#/http://everymac.com}"
else
tput setaf 2
echo "${arr[0]/#/http://everymac.com}"
fi
printf "\n\n"
To be honest, I don't completely understand your question; but it sounds like what you need is the select command (described in ยง3.2.4.2 "Conditional Constructs" of the Bash Reference Manual), which presents a menu of numbered items for users to select from. In your case, you might write:
if (( ${#arr[#]} == 1 )) ; then
url="${arr[0]}"
else
# present the user with all elements of the array, numbered,
# and store the selected element in $url:
select url in "${arr[#]}" ; do break ; done
fi
curl "${url/#/http://everymac.com}" -s -o "$temp"

How to printf a variable length line in fixed length chunks?

I need to to analyze (with grep) and print (with some formatting) the content of an
app's log.
This log contains text data in variable length lines. What I need is, after some grepping, loop each line of this output and print it with a maximum fixed length of 50 characters. If a line is longer than 50 chars, it should print a newline and then continue with the rest in the following line and so on until the line is completed.
I tried to use printf to do this, but it's not working and I don't know why. It just outputs the lines in same fashion of echo, without any consideration about printf formatting, though the \t character (tab) works.
function printContext
{
str="$1"
log="$2"
tmp="/tmp/deluge/$$"
rm -f $tmp
echo ""
echo -e "\tLog entries for $str :"
ln=$(grep -F "$str" "$log" &> "$tmp" ; cat "$tmp" | wc -l)
if [ $ln -gt 0 ];
then
while read line
do
printf "\t%50s\n" "$line"
done < $tmp
fi
}
What's wrong? I Know that I can make a substring routine to accomplish this task, but printf should be handy for stuff like this.
Instead of:
printf "\t%50s\n" "$line"
use
printf "\t%.50s\n" "$line"
to truncate your line to 50 characters only.
I'm not sure about printf but seeing as how perl is installed everywhere, how about a simple 1 liner?
echo $ln | perl -ne ' while( m/.{1,50}/g ){ print "$&\n" } '
Here's a clunky bash-only way to break the string into 50-character chunks
i=0
chars=50
while [[ -n "${y:$((chars*i)):$chars}" ]]; do
printf "\t%s\n" "${y:$((chars*i)):$chars}"
((i++))
done

search through a file in the style of bash reverse-search-history

I'm trying to write a function which will search through a file in the same manner that reverse-search-history works. i.e. user starts typing in, prompt is updated with 1st match, hitting a special key rotates through other matches, hitting another special key selects the current match.
I wrote a bash script to this, but it is awfully slow. Was wondering if I could harness some other unix/bash feature to make this fast. Maybe using awk?
Any ideas would be appreciated.
For this script, TAB rotates through matches, ENTER selects the current match, ESC ends, BACKSPACE removes the last character in the current search. (forgive my dodgy bash script, am relatively new to bash/unix)
#!/bin/bash
do_search()
{
#Record the current screen position
tput sc
local searchTerm
local matchNumber=1
local totalSearchString
local TAB_CHAR=`echo -e "\t"`
#print initial prompt
echo -n "(search) '':"
#-s: means input will not be echoed to screen, -n1 means that one character will be gotten
while IFS= read -r -s -n1 char
do
#If ENTER
if [ "$char" == "" ]; then
if [ "$match" != "" ]; then
eval "$match"
fi
echo ""
return 0
#If BACKSPACE
elif [ "$char" == "" ]; then
if [ "$totalSearchString" != "" ]; then
totalSearchString=${totalSearchString%?}
fi
#If ESCAPE
elif [ "$char" == "" ]; then
tput el1
tput rc
return 0
#If TAB
elif [ "$char" == "$TAB_CHAR" ]; then
matchNumber=$(($matchNumber+1))
#OTHERWISE
else
totalSearchString="$totalSearchString$char"
fi
match=""
if [ "$totalSearchString" != "" ]; then
#This builds up a list of grep statements piping into each other for each word in the totalSearchString
#e.g. totalSearchString="blah" will output "| grep blah"
#e.g. totalSearchString="blah1 blah2" will output "| grep blah1 | grep blah2"
local grepStatements=`echo $totalSearchString | sed 's/\([^ ]*\) */| grep \1 /g'`
local cdHistorySearchStatement="cat $1 $grepStatements | head -$matchNumber | tail -1"
#Get the match
match=`eval "$cdHistorySearchStatement"`
fi
#clear the current line
tput el1
tput rc
#re-print prompt & match
echo -n "(search) '$totalSearchString': $match"
done
return 0
}
do_search categories.txt
I think bash uses readline for this, why don't you look into using it yourself? I don't know much more about it, sorry, but I thought it might help.
I don't think this can be made fast enough for interactive use in pure bash (maybe using the complete builtin?). That said, you can try simplifying the commands you're using. Instead of one grep per word, you can use one for all of them, by doing
grepStatements=$(echo "$totalSearchString" | sed 's/[ ]\+/|/g')
cdHistorySearchStatement="grep '$grepStatements' $1 | ..."
and instead of head -$matchNumber | tail -1 you could use sed -n ${matchNumber}{p;q}.

Bash Shell Scripting - detect the Enter key

I need to compare my input with Enter/Return key...
read -n1 key
if [ $key == "\n" ]
echo "###"
fi
But this is not working.. What is wrong with this code
Several issues with the posted code. Inline comments detail what to fix:
#!/bin/bash
# ^^ Bash, not sh, must be used for read options
read -s -n 1 key # -s: do not echo input character. -n 1: read only 1 character (separate with space)
# double brackets to test, single equals sign, empty string for just 'enter' in this case...
# if [[ ... ]] is followed by semicolon and 'then' keyword
if [[ $key = "" ]]; then
echo 'You pressed enter!'
else
echo "You pressed '$key'"
fi
Also it is good idea to define empty $IFS (internal field separator) before making comparisons, because otherwise you can end up with " " and "\n" being equal.
So the code should look like this:
# for distinguishing " ", "\t" from "\n"
IFS=
read -n 1 key
if [ "$key" = "" ]; then
echo "This was really Enter, not space, tab or something else"
fi
I'm adding below code just for reference if someone will want to use such solution containing countdown loop.
IFS=''
echo -e "Press [ENTER] to start Configuration..."
for (( i=10; i>0; i--)); do
printf "\rStarting in $i seconds..."
read -s -N 1 -t 1 key
if [ "$key" = $'\e' ]; then
echo -e "\n [ESC] Pressed"
break
elif [ "$key" == $'\x0a' ] ;then
echo -e "\n [Enter] Pressed"
break
fi
done
read reads a line from standard input, up to but not including the new line at the end of the line. -n specifies the maximum number of characters, forcing read to return early if you reach that number of characters. It will still end earlier however, when the Return key is pressed. In this case, its returning an empty string - everything up to but not including the Return key.
You need to compare against the empty string to tell if the user immediately pressed Return.
read -n1 KEY
if [[ "$KEY" == "" ]]
then
echo "###";
fi
None of these conditions worked for me and so I've came up with this one:
${key} = $'\0A'
Tested on CentOS with Bash 4.2.46.

Resources