shell prompt seemingly does not reappear after run

2020-04-21 06:04发布

问题:

I have a shell script which writes all output to logfile and terminal, this part works fine, but if I execute the script a new shell prompt only appear if I press enter. Why is that and how do I fix it?

#!/bin/bash

exec > >(tee logfile)
echo "output"

回答1:

First, when I'm testing this, there always is a new shell prompt, it's just that sometimes the string output comes after it, so the prompt isn't last. Did you happen to overlook it? If so, there seems to be a race where the shell prints the prompt before the tee in the background completes.

Unfortunately, that cannot fixed by waiting in the shell for tee, see this question on unix.stackexchange. Fragile workarounds aside, the easiest way to solve this that I see is to put your whole script inside a list:

{
your-code-here
} | tee logfile


回答2:

If I run the following script (suppressing the newline from the echo), I see the prompt, but not "output". The string is still written to the file.

#!/bin/bash

exec > >(tee logfile)
echo -n "output"

What I suspect is this: you have three different file descriptors trying to write to the same file (that is, the terminal): standard output of the shell, standard error of the shell, and the standard output of tee. The shell writes synchronously: first the echo to standard output, then the prompt to standard error, so the terminal is able to sequence them correctly. However, the third file descriptor is written to asynchronously by tee, so there is a race condition. I don't quite understand how my modification affects the race, but it appears to upset some balance, allowing the prompt to be written at a different time and appear on the screen. (I expect output buffering to play a part in this).

You might also try running your script after running the script command, which will log everything written to the terminal; if you wade through all the control characters in the file, you may notice the prompt in the file just prior to the output written by tee. In support of my race condition theory, I'll note that after running the script a few times, it was no longer displaying "abnormal" behavior; my shell prompt was displayed as expected after the string "output", so there is definitely some non-deterministic element to this situation.



回答3:

@chepner's answer provides great background information.

Here's a workaround - works on Ubuntu 12.04 (Linux 3.2.0) and on OS X 10.9.1:

#!/bin/bash

exec > >(tee logfile)
echo "output"

  # WORKAROUND - place LAST in your script.
  # Execute an executable (as opposed to a builtin) that outputs *something*
  # to make the prompt reappear normally.
  # In this case we use the printf *executable* to output an *empty string*.
  # Use of `$ec` is to ensure that the script's actual exit code is passed through.
ec=$?; $(which printf) ''; exit $ec

Alternatives:

@user2719058's answer shows a simple alternative: wrapping the entire script body in a group command ({ ... }) and piping it to tee logfile.

An external solution, as @chepner has already hinted at, is to use the script utility to create a "transcript" of your script's output in addition to displaying it:

script -qc yourScript /dev/null > logfile   # Linux syntax

This, however, will also capture stderr output; if you wanted to avoid that, use:

script -qc 'yourScript 2>/dev/null' /dev/null > logfile

Note, however, that this will suppress stderr output altogether.



回答4:

As others have noted, it's not that there's no prompt printed -- it's that the last of the output written by tee can come after the prompt, making the prompt no longer visible.

If you have bash 4.4 or newer, you can wait for your tee process to exit, like so:

#!/usr/bin/env bash
case $BASH_VERSION in ''|[0-3].*|4.[0-3]) echo "ERROR: Bash 4.4+ needed" >&2; exit 1;; esac

exec {orig_stdout}>&1 {orig_stderr}>&2       # make a backup of original stdout
exec > >(tee -a "_install_log"); tee_pid=$!  # track PID of tee after starting it

cleanup() {             # define a function we'll call during shutdown
  retval=$?
  exec >&$orig_stdout  # Copy your original stdout back to FD 1, overwriting the pipe to tee
  exec 2>&$orig_stderr # If something overwrites stderr to also go through tee, fix that too
  wait "$tee_pid"      # Now, wait until tee exits
  exit "$retval"       # and complete exit with our original exit status
}
trap cleanup EXIT       # configure the function above to be called during cleanup

echo "Writing something to stdout here"


标签: linux bash shell