1. Introduction
Variables are often vital to dynamic software and scripts. By knowing how to store and change variable values, we can direct the execution flow according to the current conditions. While we can assign a value directly, generating input and redirecting that to variables is usually more convenient. Also, direct assignments generally provide more options.
In this tutorial, we explore ways to get the output of a command directly into a variable by using tee as the main example. First, we discuss the basic behavior of the tee command. After that, we go over two ways to store the output of tee in a variable instead of a file.
We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4. It should work in most POSIX-compliant environments unless otherwise specified.
2. Basic tee and Piping Behavior
Normally, we use the tee command to redirect stdout to a file or files without hiding any data from the terminal. In other words, we clone the tee input as several output streams to different targets, one of which can again be stdout.
In fact, we can leverage this behavior to read, modify, and store back the changed contents of a file within a single command:
$ cat file | head | tee file
Here, we pipe the contents of file as returned by cat to the head command, which extracts the first 10 lines by default. At the end of the pipeline, we use tee to store the result back into file. In such cases, we may have to consider that tee doesn’t use buffering.
Moreover, when it comes to getting the output of tee or other commands to go into a variable, we might need to employ more complex methods.
3. Assign tee Output to Variable
There are different ways to get the output of a command into a variable. What’s more, tee is a special case, as it modifies streams.
3.1. Using Command Substitution
Perhaps the most basic way to get data out of a given command or pipeline is the $() command substitution syntax:
$ var=$(echo 'data')
$ echo $var
data
The code above assigns the result of [echo]ing data to the variable $var by using command substitution.
Similarly, we can do the same when piping to tee:
$ var=$(echo 'data' | tee output)
$ echo $var
data
In this case, we also split the stream to the file output, yet $var still acquires the proper value.
Moreover, by replacing the output file path with /dev/tty, we can preserve the console output while also storing the data within the variable:
$ var=$(echo 'data' | tee /dev/tty)
data
$ echo $var
data
Here, we get the same values on the screen and in the variable.
However, we can modify the data after getting it to stdout:
$ var=$(echo 'data' | tee /dev/tty | tr --delete 'a')
data
$ echo $var
dt
In this example, we –delete (-d) the character a via tr after using tee to get the original string to the screen. As a result, we see data as the output of the assignment command, but $var holds dt, i.e., data without the two a characters.
3.2. Using Streams and File Descriptors
Like other POSIX shells, Bash supports both stream creation and redirecting into streams.
In addition, running processes can access their own file descriptors via a couple of paths:
- /proc/self/fd
- /dev/fd
By combining command substitution with these two concepts within {} curly braces, we can achieve our goal:
$ { var=$(echo 'data' | tee /dev/fd/3); } 3>&1
data
$ echo $var
data
Again, we see the same output. In fact, the main difference between this option and using /dev/tty is the control over the exact stream and location that the data is going to – stream 3 at /dev/fd/3. In particular, the 3>&1 syntax ensures we get what was directed to that stream into stdout (1). Of course, $() only captures the latter, so we can get stream 3 even outside the subshell.
Critically, in both cases above, we can get the exit status code of only the last command in the pipeline. This is a result of the subshell pipeline that masks the special $? question mark variable.
4. Summary
In this article, we talked about tee and ways to assign command output to a variable.
In conclusion, as with other commands, we use command substitution with or without stream redirection to get data to both the screen and a variable in the same command.