6

I am trying to find a method to save the output of a command to variable but also print it in real time. I initially found an example with tee that seemed to work;

VARIABLE=$(./build.sh | tee /dev/tty)

However the problem with this is that if I run it on a system with virtual console (i.e. git hub actions) I get an error tee: /dev/tty: No such device or address. I just want to find a way to save output to a variable and do a normal print in real time (i.e. just like echo, printf, etc.)

2
  • 3
    What is wrong with printf '%s\n' "$VARIABLE" just after? Commented Nov 7, 2022 at 22:44
  • I rather see it live, and I also have set set -e -o pipefail so if the build fails I will not get any outputs Commented Nov 8, 2022 at 14:02

2 Answers 2

5

The problem with using /dev/tty is it assumes that stdout was originally attached to a tty - this is not necessarily so as demonstrated by GitHub Actions. It would also be a problem if you were redirecting stdout of the script elsewhere (a log file for instance).

What you want to pass to tee is what was stdout before the process substitution (the $(...) bit) - process substitution takes stdout in order to capture the output. Bash and other shells have the ability to manipulate file descriptors for your use case:

exec 3>&1  # Open FD 3 as a duplicate of stdout (fd 1)
# Run ./build.sh but make sure it does not have FD 3 open and tee to FD 3
VARIABLE=$(./build.sh 3>&- | tee /dev/fd/3)
exec 3>&-  # Close FD 3

Doing it this way allows the stdout of the script to be manipulated outside the script (redirecting to a logger, or /dev/null, etc), preserving the correct behaviour WRT stdout.

As pointed out by @SOUser in the comments, there is an issue when writing the output of a script to a file. The problem is that tee /dev/fd/3 truncates the file when it opens it, discarding anything written to it already. Attempting to fix this with tee -a /dev/fd/3 causes even stranger behaviour - the line to be captured gets lost entirely for some reason.

A solution to this is to write to a pipe and not a file, so instead of ./script > test.log, use ./script | cat > test.log. When done this way, tee cannot truncate the file because it is not attached to it - it is attached to a pipe to cat which cannot be truncated.

4
  • Would I be able to simply tee to stdout or stderror as recommended below ? Or I would need to take this path ? Also I have set -e -o pipefail set, if the command fails and exits will exec will be closed properly ? Commented Nov 8, 2022 at 14:01
  • @TorqueNoFriction You can tee to stderr if that's what you want. My answer is how you tee to stdout, since the command substitution alters stdout so it can capture the output. Exiting early will not be a problem. When a process exits, all its files are closed.
    – camh
    Commented Nov 8, 2022 at 20:06
  • @camh Using this way, the output before this section is lost, if the entire script is redirected to a file. For example, a test.sh has echo "hello 1" exec 3>&1 # Open FD 3 as a duplicate of stdout (fd 1) # Run ./build.sh but make sure it does not have FD 3 open and tee to FD 3 VARIABLE=$(echo "hello 2" 3>&- | tee /dev/fd/3) exec 3>&- # Close FD 3 echo "hello 3" , though /bin/bash test.sh gives all three, but /bin/bash test.sh > test.log gives only two. Could you help to suggest what is the cause and how to fix ?
    – SOUser
    Commented Mar 29, 2024 at 10:32
  • 1
    @SOUser I have updated the question addressing your issue. Thanks for raising it. It is a strange one, but luckily the fix is easy. I could not find a way to fix it within the script though.
    – camh
    Commented Apr 11, 2024 at 11:58
3

You could tee to stderr:

VARIABLE=$(./build.sh | tee /dev/stderr)

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.