Using dialog's own tools: --output-fd flag
If you read man page for dialog, there is option --output-fd
, which allows you to explicitly set where the output goes (STDOUT 1 , STDERR 2), instead of by default going to STDERR.
Bellow you can see me running sample dialog
command , with explicitly stating that output must go to file descriptor 1, which allows me to save it into MYVAR.
MYVAR=$(dialog --inputbox "THIS OUTPUT GOES TO FD 1" 25 25 --output-fd 1)

Using named pipes
Alternative approach which has a lot of hidden potential, is to use something known as named pipe.
#!/bin/bash
mkfifo /tmp/namedPipe1 # this creates named pipe, aka fifo
# to make sure the shell doesn't hang, we run redirection
# in background, because fifo waits for output to come out
dialog --inputbox "This is an input box with named pipe" 40 40 2> /tmp/namedPipe1 &
# release contents of pipe
OUTPUT="$( cat /tmp/namedPipe1 )"
echo "This is the output " $OUTPUT
# clean up
rm /tmp/namedPipe1

A more in-depth overview of user.dz's answer with alternate approach
The original answer by user.dz and ByteCommander's explanation of that both provide a good solution and overview of what it does. However, I believe a deeper analysis could be beneficial to explain why it works.
First of all, it is important to understand two things: what is the problem we're trying to solve and what are the underlying workings of shell mechanisms with which we're dealing. The task is to capture output of a command via command substitution. Under simplistic overview that everyone knows, command substitutions capture the stdout
of a command and let it be reused by something else. In this case, the result=$(...)
part should save the output of whatever command is designated by ...
into a variable called result
.
Underneath the hood, command substitution is actually implemented as pipe, where there is a child process (the actual command that runs) and reading process (which saves output to variable). This is evident with a simple trace of system calls. Notice that file descriptor 3 is the read end of the pipe, while 4 is the write end. For the child process of echo
, which writes to its stdout
- the file descriptor 1, that file descriptor is actually copy of file descriptor 4, which is the write-end of the pipe.
Notice that stderr
isn't playing a role here, simply because it's a pipe connecting stdout
only.
$ strace -f -e pipe,dup2,write,read bash -c 'v=$(echo "X")'
...
pipe([3, 4]) = 0
strace: Process 6200 attached
[pid 6199] read(3, <unfinished ...>
[pid 6200] dup2(4, 1) = 1
[pid 6200] write(1, "X\n", 2 <unfinished ...>
[pid 6199] <... read resumed> "X\n", 128) = 2
[pid 6200] <... write resumed> ) = 2
[pid 6199] read(3, "", 128) = 0
[pid 6200] +++ exited with 0 +++
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=6200, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
+++ exited with 0 +++
Let's go back to the original answer for a second. Since now we know that dialog
writes the TUI box to stdout
, answer to stderr
, and within command substitution stdout
gets piped somewhere else, we already have part of the solution - we need to rewire file descriptors in such way that stderr
will be piped to the reader process. This is the 2>&1
part of the answer. However, what do we do with TUI box ?
That's where file descriptor 3 comes in. The dup2()
syscall allows us to duplicate file descriptors, making them effectively refer to the same place, yet we can manipulate them separately. File descriptors of processes that have controlling terminal attached actually point to specific terminal device. This is evident if you do
$ ls -l /proc/self/fd
total 0
lrwx------ 1 user1 user1 64 Aug 20 10:30 0 -> /dev/pts/5
lrwx------ 1 user1 user1 64 Aug 20 10:30 1 -> /dev/pts/5
lrwx------ 1 user1 user1 64 Aug 20 10:30 2 -> /dev/pts/5
lr-x------ 1 user1 user1 64 Aug 20 10:30 3 -> /proc/6424/fd
where /dev/pts/5
is my current pseudo-terminal device. Thus, if we can somehow save this destination, we can still write the TUI box onto terminal screen. That's what exec 3>&1
does. When you call a command with redirection command > /dev/null
for example, the shell passes it's stdout file descriptor and then uses dup2()
to write that file descriptor to /dev/null
. The exec
command performs something similar to dup2()
file descriptors for the whole shell session, thus making any command inherit already redirected file descriptor. Same with exec 3>&1
. The file descriptor 3
will now refer to/point to the controlling terminal, and any command that runs in that shell session will know about it.
So when result=$(dialog --inputbox test 0 0 2>&1 1>&3);
occurs, the shell creates a pipe for dialog to write, but also 2>&1
will first make the command's file descriptor 2 be duplicated onto the write file descriptor of that pipe (thus making output go to read end of the pipe and into the variable), while file descriptor 1 will be duplicated onto 3. This will make file descriptor 1 still refer to the controlling terminal, and the TUI dialog will show up on the screen.
Now, there's actually a short-hand for the current controlling terminal of the process, which is /dev/tty
. Thus, the solution can be simplified without use of file descriptors, simply into:
result=$(dialog --inputbox test 0 0 2>&1 1>/dev/tty);
echo "$result"
Key things to remember:
- file descriptors are inherited from shell by each command
- command substitution is implemented as pipe
- duplicated file descriptors will refer to same place as original one, but we can manipulate each file descriptor separately
See also
mktemp
command to create a temporary file.