I want to send output of a shell script, including user-entered text, to the terminal and a logfile.

I thought some combination of tee and exec might do it, but I’ve had no luck so far. I know tee by itself can echo and capture what the user enters in the terminal:

$ tee logfile
Hello  (I entered this at runtime)
Hello  (I entered this at runtime)

$ cat logfile
Hello  (I entered this at runtime)

But I need to see (on both terminal and in the logfile) what the user enters in response to commands invoked within the shell script.

tee doesn’t seem to be able to do that consistently.

For example:

$ read message 2>&1 | tee logfile
Hello  (I entered this at runtime)

$ cat logfile

Nothing was captured there. I expected to see Hello (I entered this at runtime) in the file just like before.

I also tried combining tee with exec in the shell script like so:

$ cat test.bash
# Note: in this simplified version of this file, I’m not looking at $1, $2, or anything else passed in, but will need to eventually

rm -f logfile
exec &> >(tee -a logfile)
echo “Say \”Hello\”” 2>&1
read -p “> “ 2>&1

Unfortunately, adding exec did not help:

$ ./test.bash
Say “Hello”
> Hello  (I entered this at runtime)

$ cat logfile
Say “Hello”

As you can see, it captured the output of the echo command and the read command, but not what I entered into the terminal in response to the read command.

Is there a way to do it?

I know the script command (“make typescript of terminal session”) can capture everything on the screen and put it in a logfile. But the script command can’t be invoked in a useful way from within a shell script. (Can it?)

script needs to be invoked first, and then the user has to invoke the desired shell script. But I want the user to only have to invoke one command, with its parameters, and then have the command take care of running everything else and logging everything.

Then there’s all that “extra” information (e.g. color codes, backspaces) script captures that makes it hard to read the resulting logfile in an arbitrary text editor.

I just want to see the “human-readable” characters in the logfile. And I don’t want to see if the user corrected a spelling error. I just want to see that they had “Hello” on the screen when they finished editing and hit Enter. Although I suppose the extra information could be stripped out after capture.

  • 1
    "all shell script output […] including any text entered by the user" – There's a misunderstanding in the title: the text entered by the user does not belong to the output of the script. Usually it's echoed by the terminal device line discipline. It's not even the input for the script until the user hits Enter or Ctrl+D. The script can reconfigure the terminal, read raw and echo back by itself (e.g. read -e in Bash), then what you see comes from the script (not necessarily to stdout though). "Output", "output+input" and "what you see in the terminal" – these are three non-equivalent things. Sep 29 at 5:16
  • @KamilMaciorowski surely you could create a log of everything a process (and its descendants) writes and reads from a terminal. strace can do just that, but in a verbose and unparsable way ;-) Sep 29 at 5:28
  • Pipe to tee, as seen below. This is what that command is made to do. It is part of coreutils, I believe. Regardless, it should be on most "middle of the road" distributions.
    – Nate T
    Sep 29 at 8:52
  • make a wrapper script that runs the actual script under script.
    – ilkkachu
    Sep 29 at 10:33
  • You can't do that by just redirecting stdout and stderr to the logfile (via tee), since the stdin is distinct from those.
    – ilkkachu
    Sep 29 at 10:49

The script implementation from util-linux at least is scriptable.

You could do for instance:

SHELL=/bin/sh script -qec '
  # any sh code here
  echo Whatever
  cat # user input' file.log

And file.log will capture all that is written to the terminal, including the echo of what you type. That also includes the transformations performed by the tty line discipline like the conversion of LF to CRLF.

script also adds some

Script started on 2021-09-29 14:58:59+01:00 [TERM="screen.xterm-256color" TTY="/dev/pts/8" COLUMNS="191" LINES="54"]

header and:

Script done on 2021-09-29 14:58:59+01:00 [COMMAND_EXIT_CODE="0"]

footer which from a shell with ksh-style process substitution support, you can remove with:

SHELL=/bin/sh script -qec '...' >(sed '1d;$d' > file.log)

Or, as suggested by @zevzek, tell script to write the log including header/footer to /dev/null, but redirect script's output (which with -q doesn't have header nor footer) to tee to do the logging.

(set -o pipefail; SHELL=/bin/sh script -qec '...' /dev/null | tee file.log)

Or with zsh with multi_ios on (as it is by default):

SHELL=/bin/sh script -qec '...' /dev/null >&1 > file.log

To disable the output post-processing of the tty line discipline of the pseudo-tty started by script, you could disable it there and at least reenable the NL -> CRNL conversion on the host tty with something like:

SHELL=/bin/sh HOST_TTY="$(tty)" script -qec '
  stty -opost && stty < "$HOST_TTY" opost onlcr || exit
  ...' file.log

(assuming the commands in ... do not restore that output processing).

  • OK, this does appear to do almost exactly what I want. And thanks for throwing in the tip to redirect the output through sed to remove the script header. I noticed that my script command is not adding the footer. I think it is because of the -q option passed to script. From the man page: -q, --quiet Be quiet (do not write start and done messages to either standard output or the typescript file). Although my version of script is still including the header even when the -q option is specified. Must be a bug in the version of script I have.
    – user494791
    Sep 30 at 3:10
  • Now I just need to find the right expression to use with sed to remove all the backspaces, linefeeds, color codes, etc... captured by script.
    – user494791
    Sep 30 at 3:10
  • @zevzek, for less to handle colour escapes, you need the -R option though. I'm incorporated your suggestion to pipe to tee. Sep 30 at 9:54

To log what the application reads and writes to the tty device, another approach could be to use strace -P "$(tty)" to log all the system calls it does to the tty, and include a hex dump for the reading and writing ones, which we can then decode with xxd:

strace -f -e read=all -e write=all \
       -e trace='read,write' \
       -P "$(tty)" \
       -e status=successful -o '|
          grep "^ "|cut -b11-60 | xxd -p -r > file.log
       ' -qqs0 -a0 cat

Use teewith this syntax.

read message
echo "${message}" | tee file

I can promise you that your input will be in file when you finish..

  • @KamilMaciorowski better?
    – Nate T
    Sep 29 at 9:24
  • For now I think you missed the point. I wonder what the asker will say. Sep 29 at 9:33
  • @KamilMaciorowski The op seems to be new to shellscripting. The third sentence of the question is a false inference. Btw, I appreciate you leaving a comment. I have only myself to blame...
    – Nate T
    Sep 29 at 9:41
  • 1
    @NateT Thanks for the suggestion. I should have specified that I don't want to have to individually collect the user input that gets echoed back to the terminal. Although it works in the simple case of a single read message (my own example), I won't be able to anticipate all the sub-commands that end up getting run that request input from the user. There's too many for it to be practical to catch them all individually.
    – user494791
    Sep 30 at 2:32

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.