3

I have a simple script:

f=1 # initial value
loop(){
...    
    if [[ $f -eq 1 ]]
    then
        echo $msg1
        f=2
        (sleep $interval; loop) &
        exit 1
    elif [[ $f -eq 2 ]]
    then
        echo $msg2
        f=1
        (sleep $interval; loop) &
        exit 2
    fi
...
}

It is intended to keep looping between these two messages one after the other. I am doing this, instead of a normal while loop, because I want the script to be in background. So, what I expected to happen in my terminal is:

gt@gt:~./timer.sh
<msg1>
gt@gt:~<me running some other commands>
<some output>
gt@gt:~<as soon as $interval time gets over>
<msg2>
gt@gt:~<fresh prompt>

As you can see, I would want the msg2 to appear where a normal echo output appears, and immediately after that it should leave a fresh prompt for command input. But what actually happens is:

gt@gt:~./timer.sh
<msg1>
gt@gt:~<me running some other commands>
<some output>
gt@gt:~<msg2>
<same prompt is still waiting for input here>

How to fix this?

You may locally verify this problem by running with #!/bin/bash and setting sleep=3 seconds.

  • The fact that you observed gt@gt:~<msg2> is due to a race condition. It's not waiting for your prompt. Your background processes (which are unnecessarily too many) are connected to the same terminal emulator as your current shell. So, it just happened that your shell printed the prompt (which doesn't end with a line feed) just before one of the background processes printed msg2. – Niko Gambt Jan 25 at 10:13
  • @NikoGambt Ok, I understand that. Could you please also explain how to fix it? – Gaurang Tandon Jan 25 at 10:21
  • I have never done process synchronization in Bash. A dirty hack is to simply print a new line before each message. However, you should be more concerned about your code. It seriously needs to be refactored. – Niko Gambt Jan 25 at 10:25
  • 1
    In general, this won't be possible. Both the script and your shell are two independent processes writing to the same terminal. Bash would have to know that some other process is writing to the terminal for it to create a new prompt. – Olorin Jan 28 at 5:07
  • 1
    @sudodus Hi, I am currently using a text based terminal like zsh. While I'll want the output to appear in the current terminal window itself, I am also open to the notify-send mode of output. – Gaurang Tandon Jan 28 at 8:12
2
+50

Have your timer script send the SIGINT signal to the parent process ($PPID).

Example

#!/bin/bash
echo "Going to interrupt $PPID in 5 seconds"
sleep 5
echo "More output"
kill -SIGINT $PPID
sleep 5

When this script is run in the background (./script &) it should send output, sleep and then send more output followed by forcing the prompt to be redrawn.

The script will sleep again and then exit.

Updating OP's Script

f=1 # initial value
loop(){
...    
    if [[ $f -eq 1 ]]
    then
        echo $msg1
        kill -SIGINT $PPID
        f=2
        (sleep $interval; loop) &
        exit 1
    elif [[ $f -eq 2 ]]
    then
        echo $msg2
        kill -SIGINT $PPID
        f=1
        (sleep $interval; loop) &
        exit 2
    fi
...
}

You will lose your line of pending input when your shell process is interrupted with the signal, but the command prompt will be redrawn after the timer script output.

Edit

To reproduce this behavior in a child script, pass the parent PID environmental variable into the child script from the caller script.

main.sh

...
./timer.sh $PPID
...
#exits

timer.sh

...
# Save the parent pid argument
gp=$1
...
# Replace instances of $PPID with $gp in timer.sh
...
kill -SIGINT $gp
  • Hi, thanks for your answer. So, one problem here I encountered with this is that the code that I have shown above is called from another main script. So, it's like ./main.sh allows user some choices. And if user takes choice X, ./timer.sh is called from within main.sh and that timer.sh contains the sleep logic above. I believe, due to this situation, when I do kill, I get ./timer.sh: line 55: kill: (29423) - No such process and the terminal prompt remains as it was (not redrawn). Is there any way to fix this specific issue? – Gaurang Tandon Feb 1 at 17:47
  • @GaurangTandon So, main.sh calls the timer.sh process and then exits leaving the timer.sh running in the background? – datUser Feb 1 at 18:58
  • Yep, that's what happens. There's no explicit exit statement in main.sh though, just that its last statement to execute is ./timer.sh. – Gaurang Tandon Feb 2 at 2:54
  • @GaurangTandon I added a new section to the answer that covers your use case. – datUser Feb 2 at 15:54
2

I think the following shellscript timer will do what you want or do something rather close to it.

#!/bin/bash

interval=60  # seconds
tips="Skip waiting with Yes|Enter, exit with No|Escape"
msg1="First message ...

$tips"
msg2="Second message ...

$tips"

f=1 # initial value
while true
do
    if [[ $f -eq 1 ]]
    then
#        zenity --notification --text="$msg1" 2> /dev/null
        LANG=C zenity --question --title="${0##*/}" \
        --text="$msg1" --timeout="$interval" --width=400 2> /dev/null
    elif [[ $f -eq -1 ]]
    then
#        zenity --notification --text="$msg2" 2> /dev/null
        LANG=C zenity --question --title="${0##*/}" \
        --text="$msg2" --timeout="$interval" --width=400 2> /dev/null
    fi
    ans=$?
    if [ $ans -eq 0 ] || [ $ans -eq 5 ]
    then
     f=$((-f))
    else
      zenity --info --title="${0##*/}" \
        --text="Exiting now" --timeout="3" 2> /dev/null
     break
    fi
done & pid=$!

echo "kill with
kill $pid       # useful during testing 
--------------------------------------------"
  • I use zenity --question to send the messages, which should be OK, if they come seldom, and you really want to notice them (and probably do something). This way you can also exit gracefully from the background task.

    enter image description here

  • Uncomment the zenity --notification commands, and comment away the zenity --question commands, if you want less intrusive messages.

    You should also enter sleep $interval command lines after the zenity --notification lines.

    But that also means that you have to kill the background process by using its process number,

    kill <process number>
    

    The actual process number is shown, when you start the timer shellscript, and you can find it via ps

    $ ./timer
    kill with
    kill 15585       # useful during testing 
    --------------------------------------------
    $ ps -e |grep timer
    15585 pts/3    00:00:00 timer
    
  • Hi, though I was primarily looking for a non-prompt based approach, I'm glad you posted this option anyway as a fallback just in case the text based approach never works out. Thanks :) – Gaurang Tandon Feb 1 at 17:51
  • @GaurangTandon, You are welcome. I hope you find a solution, that will make you happy in the end :-) – sudodus Feb 1 at 18:00

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.