6

Let's say I have the following in my ~/.bash_aliases to ask for confirmation before suspending the system:

function suspend()
{ #
    echo "Please confirm/cancel system suspension:"
    select confirmation in "confirm" "cancel"; do
        case ${confirmation} in
        confirm )
            echo "System suspending..."
            systemctl suspend
            break;;
        cancel )
            echo "Canceled suspension."
            break;;
        esac
    done
}

I would like systemctl suspend to be still executed if no answer is given by the user. For example, after 10 seconds without user input, the content of the "confirm" case would be executed.

I tried the following, with a backgrounded sleep in a subshell:

function suspend()
{ #
    flag_cancel=0
    echo "Please confirm/cancel system suspension:"
    (
    sleep 10 &&
        if [ $flag_cancel -eq 0 ]; then
        echo "System suspending..."
        systemctl suspend
        fi &
    )
    select confirmation in "confirm" "cancel"; do
    case ${confirmation} in
        confirm )
        echo "System suspending..."
        systemctl suspend
        break;;
        cancel )
        flag_cancel=1
        echo "Canceled suspension."
        break;;
    esac
    done
}

but a change of the value of flag_cancel is not taken into account, so the command is always executed after the sleep.

How to achieve what I want?

6

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

| improve this answer | |
  • Wow, I did not expect that so many improvements could be brought to this code. And I really appreciate your great explanations; I learned a lot! :) When you say that "it is easier to catch the timeout case" in the alternative with read, you mean that the timeout case is separate from the "confirm" case, contrary to the alternative with select. Is that correct? – Giuseppe Sep 24 at 15:39
  • 1
    @Giuseppe That's correct, yes. It's easier to "catch it" in the sense that if you want to do anything special upon timing out, this is easier with the second piece of code in my answer. – Kusalananda Sep 24 at 16:31
7

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.

| improve this answer | |
3

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.

| improve this answer | |
1

You can use the t option of the read command.

For example;

read -t 5 -p "Input within 5 seconds:" data
if [[ $data ]]
then
        echo " Your input is $data"
else
        echo " You didn't any input"
fi

Run script;

If no give any input within 5 seconds;

#~ bash scr.sh 
Input within 5 seconds: You didn't any input

If give input in 5 seconds;

#~ bash scr.sh
Input within 5 seconds:new data
 Your input is new data
| improve this answer | |
  • 1
    I do something like read -t 5 -p "Press Enter to cancel execution ... " || run-my-command – Rolf Oct 6 at 8:02
0

You could also do it with at. The advantage: If you have to learn a new tool, learn the most versatile one. at schedules commands at arbitrary times in the future. Scheduled jobs can be remove from the queue with atrm.

In your case:

function suspend()
{ #
    JOBNO=$(echo "systemctl suspend" | at now + 0.2 seconds 2>&1 > /dev/null | awk '{print $2}')
    echo "Please confirm/cancel system suspension:"
    select confirmation in "confirm" "cancel"; do
    case ${confirmation} in
        confirm )
            echo "System suspending..."
            systemctl suspend
            break;;
        cancel )
            echo "Canceled suspension."
        break;;
    esac
    atrm $JOBNO
    done
}
| improve this answer | |
  • Most at implementations have a granularity of 1 minute. I couldn't run at now + 0.2 seconds, nor at now + 60 seconds, but at now + 1 minute works. Does your own at command work as in the answer? – A.B Sep 23 at 11:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.