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.

Community
  • 1
  • 1
ff524
  • 387
  • 4
  • 19
  • What exactly doesn't work with the `zsh` solution? Is it that you don't have the function defined in the `zsh` environment? – rici Apr 17 '14 at 05:55
  • 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. – rici Apr 17 '14 at 06:04
  • @rici 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. – ff524 Apr 17 '14 at 06:08
  • 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. – rici Apr 17 '14 at 06:30
  • @rici 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. – ff524 Apr 17 '14 at 06:38
  • Do you really want this? Maybe open your background process in another window or write the output in a logfile. – Walter A Apr 17 '14 at 07:46

4 Answers4

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}
Henk Langeveld
  • 8,088
  • 1
  • 43
  • 57
  • 1
    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. – ff524 Apr 17 '14 at 08:40
  • That's why I added the alternative, using `wait`. Good luck. – Henk Langeveld Apr 17 '14 at 08:41
  • 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? – Henk Langeveld Apr 17 '14 at 08:45
  • 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. – ff524 Apr 17 '14 at 08:48
  • That's better. Can you include that context in the original question, and include some of the actual code? – Henk Langeveld Apr 17 '14 at 08:50
  • 1
    Ok, done! added explanation and relevant parts of code. – ff524 Apr 17 '14 at 09:01
  • 7
    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. – ELLIOTTCABLE Jun 24 '14 at 14:06
  • Using `wait` doesn't do anything to deal with the issue in the question. Did you even bother to try what you are recommending? – CodeClown42 Aug 19 '16 at 14:35
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.

Mohit Jain
  • 30,259
  • 8
  • 73
  • 100
  • 1
    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). – ff524 Apr 17 '14 at 06:03
  • I gave answer 2: Parse $PS1, evaluate it and print prompt by yourself in the next line – Mohit Jain Apr 17 '14 at 06:09
  • Use echo -ne $FAKE_PROMPT – Mohit Jain Apr 17 '14 at 06:15
  • And make sure you appen FAKE prompt with a space – Mohit Jain Apr 17 '14 at 06:16
  • 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. – ff524 Apr 17 '14 at 06:22
  • 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. – Mohit Jain Apr 17 '14 at 06:39
  • Sent a SIGINT to the terminal and hope my user wasn't doing anything important in the foreground at the time? Nope :) – ff524 Apr 17 '14 at 06:52
  • 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. – Mohit Jain Apr 17 '14 at 07:29
  • 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? – ff524 Apr 17 '14 at 07:33
  • You can try man kill, man pkill or man signal and find the information appropriate for you. – Mohit Jain Apr 17 '14 at 07:35
  • 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). – ff524 Apr 17 '14 at 07:37
  • 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) – Mohit Jain Apr 17 '14 at 10:51
  • 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. – ff524 Apr 17 '14 at 10:57
  • 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. – Mohit Jain Apr 17 '14 at 11:07
  • I am impressed by your creative ideas. Sorry none of them worked out for me so far :( – ff524 Apr 17 '14 at 11:09
  • 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. – Mohit Jain Apr 17 '14 at 11:50
  • Passing an Enter key to the parent returns the shell prompt with no unfortunate side effects. Nice :) :) :) – ff524 Apr 17 '14 at 13:54
  • 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. – ff524 Apr 17 '14 at 14:56
  • If you write about 100 `\b` before `\n`, your command would be visible on last prompt, but it won't be executed. – Mohit Jain Apr 18 '14 at 05:06
  • Could you add some more descriptive information to this post? Walk the reader (me.) through what your solution is actually doing. – ELLIOTTCABLE Jun 17 '14 at 10:12
  • 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.) – ELLIOTTCABLE Jun 19 '14 at 10:57
  • @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. – Mohit Jain Jun 19 '14 at 13:10
0

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 ''
  • 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... – ff524 Apr 17 '14 at 13:55
  • 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 ? –  Apr 17 '14 at 14:39
  • 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. – ff524 Apr 17 '14 at 14:48
  • So it doesn't! I hadnt ever tried to save it. Its still quite annoying though if it appears halfway through something. –  Apr 17 '14 at 15:13
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

C.Rogers
  • 441
  • 4
  • 5