3

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

  • timeout after 10 seconds, if there is no user input at all
  • the user has infinite time to finish his answer if the first character was typed within the first 10 sec.

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?

Olli
  • 1,621
  • 15
  • 18
  • 1
    I'm pretty sure all these problems can be solved, but the code to handle each, and especially *all*, is nontrivial. You might find some helpful tips in [this slightly related article](https://stackoverflow.com/questions/24016046/shell-script-respond-to-keypress). – Paul Hodges Apr 18 '19 at 14:14
  • [@paul-hodges](https://stackoverflow.com/users/8656552/paul-hodges): the problems can be solved by adding a `-s` option on the first read command and a `-ei` option on the second read command. More info see below in the ansers of [@chepner](https://stackoverflow.com/users/1126841/chepner) and a more elaborate anwer further below. Thanks for the link, which has lead us to the `-s` option. Therefore, I have marked your comment as helpful. – Olli Apr 23 '19 at 13:24

4 Answers4

1

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

chepner
  • 497,756
  • 71
  • 530
  • 681
  • I tested your solution, and there is a weird effect: the first character is doubled. And the caveat still persists, i.e. the first of the doubled characters cannot be deleted. E.g. I type "Oliver", then I will see "OOliver". I use backspace 10 times, but the first O cannot be deleted. If I then type "Michael", I get "OMichael". – Olli Apr 18 '19 at 14:56
  • The answer was still helpful for me, since I have learned of the useful `-ei` read option. Therefore, I will press the corresponding triangle. – Olli Apr 18 '19 at 15:16
  • Oh, sorry. `b` itself is the name you want, since `a` is now used only to initialize the input to `read`: `echo "Your name is $b"`. – chepner Apr 18 '19 at 15:54
  • Hi @chepner, this still does not do the trick. If you type "Oliver", the O is still duplicated and there is no way to remove the initial "O": `What is your name? > OOliver`. After your correction, the program will type `Your name is Oliver`, though. I have marked your answer as helpful, since the the `-ei` option is very helpful in creating the full solution. – Olli Apr 18 '19 at 16:24
  • You just need to add a `-s` option in the first read command. If you do so, I will accept your answer, even though I have created my own answer, which grew a little bit too elaborated, I fear. ;-) – Olli Apr 18 '19 at 16:30
1

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.

sjsam
  • 21,411
  • 5
  • 55
  • 102
  • 1
    Hi [sjsam](https://stackoverflow.com/users/1620779/sjsam), this is an interesting answer and works as well (therefore, +1). Instead of using the `-s` (silent) option and not printing the furst read character, you are just returning to charater 1 of the same line with the `echo -en "\r"` and re-writing the same prompt "Enter Name : " a second time. – Olli Apr 23 '19 at 13:45
  • @Olli That's right ! The first `echo` is the key here. – sjsam Apr 23 '19 at 14:33
0

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

timtj
  • 94
  • 2
  • 11
  • I should also mention that currently, unedited, the `$rg` variables allows all uppercase and lowercase letters, as well as hypens. – timtj Apr 18 '19 at 15:49
0

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.

Olli
  • 1,621
  • 15
  • 18