Execute a command after some time if no input from user

Try with read command to get runtime the input from the user. Since it has timeout option.

From man:

-t timeout Cause read to time out and return failure if a complete line of input is not read within timeout seconds. timeout may be a decimal number with a fractional portion following the decimal point. This option is only effective if read is reading input from a terminal, pipe, or other special file; it has no effect when reading from regular files. If timeout is 0, read returns success if input is available on the specified file descriptor, failure otherwise. The exit status is greater than 128 if the timeout is exceeded.


Your attempt with sleep would not work as the sleep call and the subsequent test on $flag_cancel happens in a background job. Any change to the variable flag_cancel in the main part of the code would not affect the value of the variable in the backgrounded subshell and the code would unconditionally suspend the system after 10 seconds.

Instead, you can use the fact that both read and select times out after $TMOUT seconds in bash.

Here's a variation on the theme of your first piece of code:

suspend_maybe ()
{
        local PS3='Please confirm/cancel system suspension: '
        local TMOUT=10

        local do_suspend=true

        select confirmation in confirm cancel; do
                case $REPLY in
                        1)
                                # default case
                                break ;;
                        2)
                                do_suspend=false
                                break ;;
                        *)
                                echo 'Sorry, try again' >&2
                esac
        done

        if "$do_suspend"; then
                echo 'Suspending...'
                systemctl suspend
        else
                echo 'Will not suspend'
        fi
}

Changes made:

  1. The function is now called suspend_maybe since there is already a built-in suspend utility in bash.
  2. The select loop uses PS3 for its prompt.
  3. The select loop times out after $TMOUT seconds.
  4. We use the digits in the case statement. That way we don't have to type all the strings in twice. The value of $REPLY will be whatever the user types in.
  5. We only need the select loop to tell us whether the user wants to cancel the suspension of the system. We treat suspension as the default action.
  6. Once we're out of the select loop, we suspend the system unless the user chose to cancel that action.

The same thing but with an input loop using read as a drop-in replacement for select:

suspend_maybe ()
{
        local PS3='Confirm system suspension [y]/n: '
        local TMOUT=10

        local do_suspend=true

        while true; do
                if ! read -p "$PS3"; then
                        # timeout
                        break
                fi

                case $REPLY in
                        [yY]*)
                                # default case
                                break ;;
                        [nN]*)
                                do_suspend=false
                                break ;;
                        *)
                                echo 'Sorry, try again' >&2
                esac
        done

        if "$do_suspend"; then
                echo 'Suspending...'
                systemctl suspend
        else
                echo 'Will not suspend'
        fi
}

Here, the user may enter any string starting with n or N to cancel the suspension. Letting the read time out, entering a word starting with y or Y, or pressing Ctrl+D, would suspend the system.

With the loop above, it is easier to catch the timeout case, in case you want to do anything special when the suspension is happening due to a user not responding to the prompt. The break in the if-statement would be triggered whenever the read call fails, which it does upon timing out (or when the input stream is closed).


In general, an alternative you have is timeout from GNU coreutils. Not the best choice here, because we are setting a timeout for a builtin command and an external utility can only do it by spawning a whole shell:

if ! timeout 10 bash -c '
  select conf in yes no
  do
    case $conf in
      (yes) exit 1;;
      (no) exit 0;;
    esac
  done'
then
  echo 'Suspending'
fi

Likely more interesting if you are going to ask for input from the user through some external tool, possibly graphical (several of them implement their own timeout mechanism, though).

Note that timeout needs the --foreground option when not invoked directly from a shell prompt (for instance, if you invoke it in subshell: ( timeout ... )) and it needs to read from the terminal.