Olli
Olli

Reputation: 1651

bash read timeout only for the first character entered by the user

I have searched for a simple solution that will read user input with the following features:

I have found a solution to a similar request (timeout after each typed character) on Linux Read - Timeout after x seconds *idle*. Still, this is not exactly the feature, I was looking for, so I have developed a two line solution as follows:

read -N 1 -t 10 -p "What is your name? > " a
[ "$a" != "" ] && read b && echo "Your name is $a$b" || echo "(timeout)"

In case the user waits 10 sec before he enters the first character, the output will be:

What is your name? > (timeout)

If the user types the first character within 10 sec, he has unlimited time to finish this task. The output will look like follows:

What is your name? > Oliver
Your name is Oliver

However, there is following caveat: the first character is not editable, once it was typed, while all other characters can be edited (backspace and re-type).

Do you have any ideas for a resolution of the caveat or do you have another simple solution to the requested behavior?

Upvotes: 3

Views: 681

Answers (4)

Olli
Olli

Reputation: 1651

Short Answer:

Add a -s option on the first read command and a -ei option on the second read command:

read -s -N 1 -t 10 -p "What is your name? > " a
[ "$a" != "" ] && read -ei "$a" b && echo "Your name is $b" || echo "(timeout)"

Or with better handling of empty input:

read -s -N 1 -t 10 -p "What is your name? > " a || echo "(timeout)" \
  && [ -n "$a" ] && read -ei "$a" b || echo \
  && echo "Your name is \"$b\""

Elaborate Answer:

With the help of @chepner's answer (thanks for the -ei option!) and a comment of @paul-hodges, which has lead me to an article promoting the -s read option, I was able to create a working solution very similar to my original 2-liner:

read -N 1 -t 10 -s -p "What is your name? > " a
[ "$a" != "" ] && read -ei "$a" b && echo "Your name is $b" || echo "(timeout)"

Some of you might like a more elaborate version of the same functionality:

if read -N 1 -t 10 -s -p "What is your name? " FIRST_CHARACTER; then
  read -ei "$FIRST_CHARACTER" FULL_NAME
  echo "Your name is $FULL_NAME"
else
  echo "(timeout)"
fi

Explanation:

  • the -s option in the first read command will make sure the FIRST_CHARACTER is not printed out while typing.
  • the -N 1 or -n1 option will make sure that only the first character is read into the FIRST_CHARACTER variable
  • the -ei option will read $FIRST_CHARACTER into the FULL_NAME before the user continues to write the characters 2 to n.
  • the user is able to reconsider his answer and he can remove the whole input including the first character with the backspace.

I have testet it, and the combination of those options seems to do the trick.

Resolving a Caveat with empty input

However, there is still a small caveat: if the user just types <enter>: the second read command will wait for an input until the user is pressing <enter> a second time. This can be fixed like follows:

if read -N 1 -t 10 -s -p "What is your name? " FIRST_CHARACTER; then
  if [ -n "$FIRST_CHARACTER" ]; then
    read -ei "$FIRST_CHARACTER" FULL_NAME
  else
    echo
  fi
  echo "Your name is \"$FULL_NAME\""
else
  echo "(timeout)"
fi

In the style of the two-liner, this will get us a three-liner as follows:

read -N 1 -t 10 -s -p "What is your name? > " a || echo "(timeout)" \
  && [ -n "$a" ] && read -ei "$a" b || echo \
  && echo "Your name is \"$b\""

Test

The code of both versions (the nested if version and the three-liner) will behave as follows:

  • If the user does not do anything for 10 sec, the output will yield
What is your name? (timeout)
  • If the user writes Oliver<enter> the output will be
What is your name? Oliver
Your name is "Oliver"
  • if the user starts to write "Oliver", then considers, that he want to be called "Michael", he can completely remove the "Oliver" with the backspace key and replace it accordingly. The output will be:
What is your name? Oliver

after entering the name "Oliver". Then, after pressing the backspace key 6 or more times:

What is your name?

And after entering Michael<enter>:

What is your name? Michael
Your name is "Michael"

Hope that helps.

Upvotes: 0

sjsam
sjsam

Reputation: 21965

This solution may do.

read -n1 -t 10 -p "Enter Name : " name && echo -en "\r" &&
read -e -i "$name" -p "Enter Name : " name || echo "(timeout)"

Note: The second read uses the text captured from the first(-i option) to provide an editable buffer. The carriage return and the same prompt gives the user an impression that he is entering the same value.

Upvotes: 1

chepner
chepner

Reputation: 531480

Enable readline and add $a as the default value for the second read.

# read one letter, but don't show it
read -s -N 1 -t 10 -p "What is your name? > " a

if [ -n "$a" ]; then
  # Now supply the first letter and let the user type
  # the rest at their leisure.
  read -ei "$a" b && echo "Your name is $b"
else
  echo "(timeout)"
fi

This still displays a second prompt after the first letter is answered, but I don't think there's a better way to handle this; there's no way to "cancel" a timeout for read. The ideal solution would be to use some command other than read, but you would have to write that yourself (probably as a loadable built-in, in C).

Upvotes: 1

timtj
timtj

Reputation: 94

Test Conditions: GNU bash, version 4.4.19(1)-release Ubuntu 18.04.2 LTS

I created a function to solve your caveat of the first letter not being edittable, as shown below. I have only tested this with my local linux server, and I make no assumptions that this will work elsewhere or with newer/older versions of BASH (or read for that matter, but I was unable to tell what version I was running)

__readInput(){
    str="What is your name? > "
    tput sc                       # Save current cursor position
    printf "$str"
    read -n 1 -t 10 a             # Wait 10 seconds for first letter
    [[ $? -eq 0 ]] || return 1    # Return ErrorCode "1" if timed_out
    while :; do                   # Infinite Loop
        tput rc                   # Return cursor to saved position
        printf "$str$a"           # Print string (including what is saved of the user input)
        read -n 1 b               # Wait for next character
        if [[ $? -eq 0 ]]; then
            # We had proper user input
            if [[ ${#b} -eq 0 ]]; then
                # User hit [ENTER]
                n=$a$b
                break             # End our loop
            fi
            rg="[A-Za-z-]"        # REGEX for checking user input... CAVEAT, see below
            if ! [[ $b =~ $rg ]] ;then
                # We have an unrecognisied character return, assume backspace
                [[ ${#a} -gt 0 ]]&&a=${a:0:(-1)}   # Strip last character from string
                tput rc           # Return cursor to saved position
                printf "$str$a   " # This removes the ^? that READ echoes on backspace
                continue          # Continue our loop
            fi
            a=$a$b                # Append character to user input
        fi
    done
}

You can call this function similar to the following:

declare n=""
__readInput
if [[ $? -eq 0 ]] || [[ ${#n} -eq 0 ]] ;then
    echo "Your name is $n"
else
    echo "I'm sorry, I didn't quite catch your name!"
fi

CAVEAT MENTIONED ABOVE EXPLAINED So, you have a caveat that I fixed, perhaps you (or our friends) can fix this one. ANY character entered that isn't included in the $rg REGEX variable will be treated as BACKSPACE. This means your user could hit F7, =, \, or literally any character other than those specified in $rg and it will be treated as a backspace

Upvotes: 0

Related Questions