How to return to bash prompt after printing output from backgrounded function?
Asked Answered
M

4

17

How can I return to my bash prompt automatically after printing output from a function that was put in the background?

For example, when I run the following script in a bash shell:

fn(){
        sleep 10
        echo "Done"
        exit
}
fn &

After running the script, it immediately returns my prompt. After 10 seconds, it prints "Done" and then displays a blinking cursor on a new line:

$ Done
▏

The script isn't running anymore, but I don't get my prompt back until I press Return.

Is there any way to force a return to the bash prompt after printing "Done"?

A related question is: Is there a way for a backgrounded task to inform the terminal to print a new prompt? However, that question asks about a backgrounded program. The answer supplied there applies to a program that is sent to the background, but doesn't seem to work for a function that is sent to the background (as in the example I supplied).

To clarify: I am looking to save the entire code snippet above (e.g., as myscript.sh) and then run it as a foreground script (e.g., as bash myscript.sh).

EDIT: The above is of course just a MWE. The context of this problem is:

  1. User runs script
  2. Script submits PBS job, starts tailing the output file in the background, and calls fn &
  3. User gets prompt back, may start doing other things.
  4. Job output appears on user's terminal when job starts running
  5. fn monitors the queue and kills tail when the job finishes.
  6. Users complain about not getting prompt back (i.e., having to press Enter) after this finishes.

Here's some less minimal code:

watch_queue(){
    until [  `qstat | grep $job | wc -l` -lt 1 ]; do
        sleep 2
    done
    kill -9 $pid
    tput setaf 7
    tput setab 0
    echo "Hit ENTER to return to your command prompt."
    tput sgr0
    exit 0
}

cmd="something complicated that is built at runtime"
outfile="ditto"
queue="selected at runtime, too"

job=`echo "cd \$PBS_O_WORKDIR  && $cmd >> $outfile " | 
     qsub -q $queue -e /dev/null -o /dev/null | 
     awk 'BEGIN { FS="." } { print $1 }'`

echo "Job $job queued on $queue: $cmd"
eval "tail -f -F $outfile 2>/dev/null &"
pid=$!
watch_queue &

Of course it would be a lot easier for me if my users could just pick up the job output from a separate file, or manipulate jobs between foreground and background on their own, but they can't. They can't even follow the instructions in the script to hit Enter to get the "look" of a prompt back... And I can't open another "window" - they do not have a display server.

Mckoy answered 17/4, 2014 at 5:49 Comment(6)
What exactly doesn't work with the zsh solution? Is it that you don't have the function defined in the zsh environment?Invention
It worked fine for me. zsh --version reports zsh 5.0.2 (x86_64-pc-linux-gnu). I used the fn exactly from the question, except with 3 instead of 10.Invention
@Invention I don't want to run "fn &" from the prompt - I want to run the entire thing (definition of fn followed by "fn &") inside a foreground script. That's what doesn't work for me in zsh.Mckoy
Maybe you need to clarify your question, since that wasn't at all clear to me. The issue seems to have to do with the fact that you're firing up another instance of zsh. If you execute the script file with . (or equivalently source), it seems to work as you expect. I tried a few different zsh options, but none seemed to help, and I'm not a zsh expert (or even fan). Good luck.Invention
@Invention I'm actually looking for a bash answer, not zsh - my users are flustered by not getting a prompt back, a new shell might make their heads explode :) I clarified my question, thanks for the suggestion.Mckoy
Do you really want this? Maybe open your background process in another window or write the output in a logfile.Insurmountable
S
3

Compile below code to file a.out

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    /* char buf[] = "date\n"; */
    char buf[] = "\n";  /* Data to write on terminal */
    int i;
    int fd = open(argv[1], O_WRONLY);  /* Open terminal */

    /* printf("fd = %d\n", fd); */
    for (i = 0; i < sizeof buf - 1; i++)  /* Write data */
      ioctl(fd, TIOCSTI, &buf[i]);
    close(fd);  /* Close file descriptor */
    return 0;
}

This program expects a path as command line argument. Program will open the path and write a new line to this path.

If this path happen to contain the file descriptor of a writable terminal running bash script, this would cause bash to catch a new prompt.

Modify your shell script

fn(){
        sleep 10
        echo "Done"
        ./a.out /proc/$PPID/fd/0
}
fn &

This script would do some job (represented with sleep here) and then call the utility written previously with argument as input terminal of parent. Parent terminal would receive a new line and catch a new prompt discarding the stray command on this prompt if any.

/proc contains directories for all processes. Name of folder matches to the pid of process. Inbuild variable PPID contains the parent's pid. Inside the pid directory, there is an fd directory containing open streams. 0 is for input, 1 is for output and 2 is for error. There may be more open streams depending on the process. We are interested in 0 stream here.

Sarge answered 17/4, 2014 at 6:1 Comment(22)
I already know you can press enter to continue - I said so in the question. I asked for a way to do it automatically. Printing a "fake" prompt also would not look the same as returning to my "real" prompt - for one thing, there would still be the blinking cursor on the next line (the "fake" prompt output would be just like any other output from this function).Mckoy
I gave answer 2: Parse $PS1, evaluate it and print prompt by yourself in the next lineSarge
Use echo -ne $FAKE_PROMPTSarge
And make sure you appen FAKE prompt with a spaceSarge
I can't print an accurate fake prompt inside fn - for example, if the prompt includes PWD and user has changed directories since running the script, I don't know about it inside fn.Mckoy
I haven't try this, but you can set parent_pid before calling this script. export PARENT_PID=this terminals pid, and from script send sigint signal to this pid.Sarge
Sent a SIGINT to the terminal and hope my user wasn't doing anything important in the foreground at the time? Nope :)Mckoy
Don't worry as the important work is done in children and not by tty. Secondly send a signal for which the terminal has a default handling of not to exit. I tried -2 and it worked for me except for giving an unwanted ^C before holding prompt. But I am sure, you can find a better signal that can do your job.Sarge
Sending a SIGINT to the terminal can definitely interrupt foreground work; for example if I run echo "hi"; sleep 10; echo "bye" and the SIGINT is sent in middle, the "bye" is never printed. I'm not aware of a signal that doesn't have unfortunate consequences, are you?Mckoy
You can try man kill, man pkill or man signal and find the information appropriate for you.Sarge
There is no signal that will not affect other things going on in the terminal when used in the manner you describe, hence any signal will have unwanted side effects (as far as I can tell).Mckoy
Bash handles sigint gracefully by printing ^C and reprinting the prompt. There is no side effect on other running process. Check man bash heading SIGNALS, it says "SIGINT is caught and handled". Each job is gets a new job number (unique for terminal) and pid (unique for system), so SIGINT (singnal # 2) is handled by bash and does not interact with running jobs. If there is any running job, it won't even print unwanted ^C and will get the prompt gracefully once the job is finished. (tried and tested)Sarge
When I add the kill -2 $PARENT_PID after echo "Done", run the script (./myscript.sh), and then immediately run echo "hi"; sleep 10; echo "bye" at the command prompt - the SIGINT causes the "bye" to not be printed. The SIGINT interrupts the sequence of commands.Mckoy
OK, sorry for wrong interpretation, I thought you are running echo "hi" etc from a script. Running sleep on the same terminal would be interrupted by INT and all the remaining commands after that would be ignored (flushed). There would be some better solution to keep continue with current and remaining commands in pipeline.Sarge
I am impressed by your creative ideas. Sorry none of them worked out for me so far :(Mckoy
Edit3 handles your last requirement also + there is no irritating ^C character. No interruption to sleep. I am only passing an Enter key to the parent.Sarge
Passing an Enter key to the parent returns the shell prompt with no unfortunate side effects. Nice :) :) :)Mckoy
Note: this also can affect something else in the parent shell - for example, if I run this and type a command at the prompt without hitting Enter (i.e., just let it sit there), the command I typed will run when the Enter key is passed to the parent. But I'm willing to tolerate that side effect.Mckoy
If you write about 100 \b before \n, your command would be visible on last prompt, but it won't be executed.Sarge
Could you add some more descriptive information to this post? Walk the reader (me.) through what your solution is actually doing.Incarcerate
Hm. This is a very clever solution; but why do we need the C program to do this? Unfortunately, I don't have a non-Windows machine handy right now (snafu screwed up my Mac work-machine). Can we not, perhaps, redirect a newline directly into that file-descriptor? (Also, this particular approach won't work on OS X, as there's no procfs there.)Incarcerate
@elliottcable Thank you. Main idea is to use ioctl fd TIOCSTI text_buffer. I was comfortable doing this in C so I used C. You can use any other scripting that can let you do the required ioctl may be some scripting or language and if possible in child bash script itself. I have never worked in MAC environment so I can not comment on that. But I believe (not sure) something similar should be possible in unix and OS X also.Sarge
D
5

What is the problem you're trying to solve?

Right now, this is more or less a cosmetic problem. You're still in the shell, and the prompt is still there. Just type another command and it will be executed.

Alternatively, run the function in the foreground, or if you need to do something else in between, use wait:

$ fn & pid=$!
$ : something else
$ wait ${pid}
Disembogue answered 17/4, 2014 at 8:36 Comment(8)
This doesn't answer my question. I realize it's cosmetic; after a few dozen complaints from users about it, I'm looking to fix it anyways.Mckoy
That's why I added the alternative, using wait. Good luck.Disembogue
Well, add some detail to the question about the real issue. You're not really going to run a sleep 10; echo 'Done' in the script, or are you?Disembogue
The context is: 1. User runs script 2. Script submits PBS job, then calls fn &. User gets prompt back 3. fn watches queue, tails output of job to user's terminal when job runs, kills tail when job stops running.Mckoy
That's better. Can you include that context in the original question, and include some of the actual code?Disembogue
Ok, done! added explanation and relevant parts of code.Mckoy
Really, though, "it's just cosmetic" is so unhelpful. You may not care about the cosmetics of a command-line utility, but some of us do. This is a rather sizable issue for me; my users aren't necessarily super-familiar with the command-line; and it's important that the user-experience be as clean-of-flow as possible.Incarcerate
Using wait doesn't do anything to deal with the issue in the question. Did you even bother to try what you are recommending?Cyprinodont
S
3

Compile below code to file a.out

#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    /* char buf[] = "date\n"; */
    char buf[] = "\n";  /* Data to write on terminal */
    int i;
    int fd = open(argv[1], O_WRONLY);  /* Open terminal */

    /* printf("fd = %d\n", fd); */
    for (i = 0; i < sizeof buf - 1; i++)  /* Write data */
      ioctl(fd, TIOCSTI, &buf[i]);
    close(fd);  /* Close file descriptor */
    return 0;
}

This program expects a path as command line argument. Program will open the path and write a new line to this path.

If this path happen to contain the file descriptor of a writable terminal running bash script, this would cause bash to catch a new prompt.

Modify your shell script

fn(){
        sleep 10
        echo "Done"
        ./a.out /proc/$PPID/fd/0
}
fn &

This script would do some job (represented with sleep here) and then call the utility written previously with argument as input terminal of parent. Parent terminal would receive a new line and catch a new prompt discarding the stray command on this prompt if any.

/proc contains directories for all processes. Name of folder matches to the pid of process. Inbuild variable PPID contains the parent's pid. Inside the pid directory, there is an fd directory containing open streams. 0 is for input, 1 is for output and 2 is for error. There may be more open streams depending on the process. We are interested in 0 stream here.

Sarge answered 17/4, 2014 at 6:1 Comment(22)
I already know you can press enter to continue - I said so in the question. I asked for a way to do it automatically. Printing a "fake" prompt also would not look the same as returning to my "real" prompt - for one thing, there would still be the blinking cursor on the next line (the "fake" prompt output would be just like any other output from this function).Mckoy
I gave answer 2: Parse $PS1, evaluate it and print prompt by yourself in the next lineSarge
Use echo -ne $FAKE_PROMPTSarge
And make sure you appen FAKE prompt with a spaceSarge
I can't print an accurate fake prompt inside fn - for example, if the prompt includes PWD and user has changed directories since running the script, I don't know about it inside fn.Mckoy
I haven't try this, but you can set parent_pid before calling this script. export PARENT_PID=this terminals pid, and from script send sigint signal to this pid.Sarge
Sent a SIGINT to the terminal and hope my user wasn't doing anything important in the foreground at the time? Nope :)Mckoy
Don't worry as the important work is done in children and not by tty. Secondly send a signal for which the terminal has a default handling of not to exit. I tried -2 and it worked for me except for giving an unwanted ^C before holding prompt. But I am sure, you can find a better signal that can do your job.Sarge
Sending a SIGINT to the terminal can definitely interrupt foreground work; for example if I run echo "hi"; sleep 10; echo "bye" and the SIGINT is sent in middle, the "bye" is never printed. I'm not aware of a signal that doesn't have unfortunate consequences, are you?Mckoy
You can try man kill, man pkill or man signal and find the information appropriate for you.Sarge
There is no signal that will not affect other things going on in the terminal when used in the manner you describe, hence any signal will have unwanted side effects (as far as I can tell).Mckoy
Bash handles sigint gracefully by printing ^C and reprinting the prompt. There is no side effect on other running process. Check man bash heading SIGNALS, it says "SIGINT is caught and handled". Each job is gets a new job number (unique for terminal) and pid (unique for system), so SIGINT (singnal # 2) is handled by bash and does not interact with running jobs. If there is any running job, it won't even print unwanted ^C and will get the prompt gracefully once the job is finished. (tried and tested)Sarge
When I add the kill -2 $PARENT_PID after echo "Done", run the script (./myscript.sh), and then immediately run echo "hi"; sleep 10; echo "bye" at the command prompt - the SIGINT causes the "bye" to not be printed. The SIGINT interrupts the sequence of commands.Mckoy
OK, sorry for wrong interpretation, I thought you are running echo "hi" etc from a script. Running sleep on the same terminal would be interrupted by INT and all the remaining commands after that would be ignored (flushed). There would be some better solution to keep continue with current and remaining commands in pipeline.Sarge
I am impressed by your creative ideas. Sorry none of them worked out for me so far :(Mckoy
Edit3 handles your last requirement also + there is no irritating ^C character. No interruption to sleep. I am only passing an Enter key to the parent.Sarge
Passing an Enter key to the parent returns the shell prompt with no unfortunate side effects. Nice :) :) :)Mckoy
Note: this also can affect something else in the parent shell - for example, if I run this and type a command at the prompt without hitting Enter (i.e., just let it sit there), the command I typed will run when the Enter key is passed to the parent. But I'm willing to tolerate that side effect.Mckoy
If you write about 100 \b before \n, your command would be visible on last prompt, but it won't be executed.Sarge
Could you add some more descriptive information to this post? Walk the reader (me.) through what your solution is actually doing.Incarcerate
Hm. This is a very clever solution; but why do we need the C program to do this? Unfortunately, I don't have a non-Windows machine handy right now (snafu screwed up my Mac work-machine). Can we not, perhaps, redirect a newline directly into that file-descriptor? (Also, this particular approach won't work on OS X, as there's no procfs there.)Incarcerate
@elliottcable Thank you. Main idea is to use ioctl fd TIOCSTI text_buffer. I was comfortable doing this in C so I used C. You can use any other scripting that can let you do the required ioctl may be some scripting or language and if possible in child bash script itself. I have never worked in MAC environment so I can not comment on that. But I believe (not sure) something similar should be possible in unix and OS X also.Sarge
P
1

similar to Henk Langevelds solution.

Find the pid of your script
wait for it to finish.
echo a line
Prompt is back
Unfortunately you're going to get that blank line still

#!/bin/bash

fn(){
    sleep 1
    echo "Done"

}
fn &
PID=$!
wait $PID
echo -e ''
Pelagias answered 17/4, 2014 at 12:34 Comment(4)
I need the script to end and the shell prompt to return immediately after running fn &, as in the original script. Otherwise I wouldn't need to run fn in the background at all...Mckoy
why would you ever want it to interrupt what you were doing ? Say you were just about to save/close an editing session and suddenly its filled with all the junk from your script and then saved like that ? Surely you would want a separate screen anyway ?Pelagias
The answer is "because my users require it" (learning how to open a second SSH session would be difficult for many of them, among other reasons). Also, output from a background process doesn't get saved if it appears in middle of an editing session.Mckoy
So it doesn't! I hadnt ever tried to save it. Its still quite annoying though if it appears halfway through something.Pelagias
P
0

Rather than just hit ctrl+c (which can accidentally cancel stuff), you can just tell your users to hit return again. That will give you a tidy new prompt.

Bash is really a mess. Developers of bash should not treat the area after the command prompt as a dumping ground for messages. It's friggin horrible UX. lol

Puli answered 17/1, 2021 at 14:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.