29

I have been teaching myself bash scripting and have run into an issue. I have written a script to take input from the user, using the 'read' command, and make that input a variable to use later in the script. The script works, but....

I would like to be able to get it setup using 'dialog'. I found out that

'dialog --inputbox' will direct the output to 'stderr' and in order to get that input as a variable you have to direct it to a file and then retrieve it. The code I found to explain this is:

#!/bin/bash
dialog --inputbox \

"What is your username?" 0 0 2> /tmp/inputbox.tmp.$$

retval=$?

input=`cat /tmp/inputbox.tmp.$$`

rm -f /tmp/inputbox.tmp.$$

case $retval in
0)

echo "Your username is '$input'";;
1)

echo "Cancel pressed.";;

esac

I see that it is sending the sdterr to the /tmp/inputbox.tmp.$$ with 2>, but the output file looks like 'inputbox.tmp.21661'. When I try and cat the file it gives me an error. So I am still unable to get the user input from the --inputbox as a variable.

Example Script:

echo "  What app would you like to remove? "

read dead_app

sudo apt-get remove --purge $dead_app

So as you can see it is a basic script. Is it even possible to get the variable as a word from dialog --inputbox?

1
  • In my experience the script works fine, if you remove the empty line after the 2nd line. Alternatively, you could use mktemp command to create a temporary file.
    – jarno
    Jul 16, 2016 at 20:52

6 Answers 6

27
+300

:D I can't explain it!!! If you can understand what they are saying in Advanced Bash-Scripting Guide: Chapter 20. I/O Redirection, write a new answer and I will give you 50rep:

exec 3>&1;
result=$(dialog --inputbox test 0 0 2>&1 1>&3);
exitcode=$?;
exec 3>&-;
echo $result $exitcode;

Reference: Dialog in bash is not grabbing variables correctly

^ answer from @Sneetsher (Jul 4, 2014)

As requested, I will try to explain what this snippet is doing line by line.

Note that I will simplify it by omitting all the ; semicolons at the line ends, because they're not necessary if we write one command per line.

I/O - Streams:

First, you need to understand the communication streams. There are 10 streams, numbered from 0 to 9:

  • Stream 0 ("STDIN"):
    "Standard input", the default input stream to read data from the keyboard.

  • Stream 1 ("STDOUT"):
    "Standard output", the default output stream used to show normal text in the terminal.

  • Stream 2 ("STDERR"): "Standard error", the default output stream used to display errors or other text for special purposes in the terminal.

  • Streams 3-9:
    Additional, freely usable streams. They're not used by default and do not exist until something attempts to use them.

Note that all "streams" are internally represented by file descriptors in /dev/fd (which is a symbolic link to /proc/self/fd which contains another symbolic link for every stream... it's a bit complicated and not important for their behaviour, so I stop here.). The standard streams also have /dev/stdin, /dev/stdout and /dev/stderr (which are symbolic links again, etc...).

The script:

  • exec 3>&1
    

    The Bash built-in exec can be used to apply a stream redirection to the shell, that means it affects all following commands. For more info, run help exec in your terminal.

    In this special case, the stream 3 gets redirected to stream 1 (STDOUT), that means everything we send to stream 3 later will appear in our terminal as if it was normally printed to STDOUT.

  • result=$(dialog --inputbox test 0 0 2>&1 1>&3)
    

    This line consists of many parts and syntactical structures:

    • result=$(...)
      This structure executes the command in the brackets and assigns the output (STDOUT) to the bash variable result. It's readable through $result. All this is described somehow in the veeeery looong man bash.

    • dialog --inputbox TEXT HEIGHT WIDTH
      This command shows a TUI box with the given TEXT, a text input field and two buttons OK and CANCEL. If OK gets selected, the command exits with status 0 and prints the entered text to STDERR, if CANCEL gets selected, it will exit with code 1 and print nothing. For more info, read man dialog.

    • 2>&1 1>&3
      These are two redirection commands. They will be interpreted from right to left:

      1>&3 redirects the command's stream 1 (STDOUT) to the custom stream 3.

      2>&1 redirects afterwards the command's stream 2 (STDERR) to stream 1 (STDOUT).

      That means that everything the command prints to STDOUT now appears in stream 3, while everything that was intended to show up on STDERR now gets redirected to STDOUT.

    So the entire line displays a text prompt (on STDOUT, which got redirected to stream 3, which the shell again redirects back to STDOUT in the end - see the exec 3>&1 command) and assigns the entered data (returned through STDERR, then redirected to STDOUT) to the Bash variable result.

  • exitcode=$?
    

    This code retrieves the previously executed command's exit code (here from dialog) through the reserved Bash variable $? (always holds the last exit code) and simply stores it in our own Bash variable exitcode. It can be read through $exitcode again. You can search for more info on this in man bash, but that might take a while...

  • exec 3>&-
    

    The Bash built-in exec can be used to apply a stream redirection to the shell, that means it affects all following commands. For more info, run help exec in your terminal.

    In this special case, the stream 3 gets redirected to "stream -", which just means it should be closed. Data sent to stream 3 will not get redirected anywhere any more from now on.

  • echo $result $exitcode
    

    This simple echo command (more info on man echo) just prints the content of the two Bash variables result and exitcode to the STDOUT. As we have no explicit or implicit stream redirections here any more, they will really appear on STDOUT and therefore simply get displayed in the terminal. What a miracle! ;-)

Summary:

First, we set the shell up to redirect everything we send to the custom stream 3 back to STDOUT, so that it shows up in our terminal.

Then we run the dialog command, redirect its original STDOUT to our custom stream 3, because it needs to get displayed in the end, but we temporarily need to use the STDOUT stream for something else.
We redirect the original STDERR of the command, where the dialogue window's user input gets returned, to STDOUT afterwards.
Now we can capture the STDOUT (which holds the redirected data from STDERR) and store it in our variable $result. It contains the wanted user input now!

We also want the dialog command's exit code, which shows us whether OK or CANCEL was clicked. This value is presented in the reserved Bash variable $? and we just copy it to our own variable $exitcode.

After that we close stream 3 again, as we don't need it any more, to stop further redirections of it.

Finally, we normally output the contents of both variables $result (the user input of the dialogue window) and $exitcode (0 for OK, 1 for CANCEL) to the terminal.

5
  • I think using exec is unnecessarily complicated. Why not just us --stdout option for dialog or redirect its output by 2>&1 >/dev/tty?
    – jarno
    Aug 8, 2016 at 8:46
  • 1
    Please see my answer.
    – jarno
    Aug 8, 2016 at 8:47
  • 5
    Great answer! However, I believe you have one note that is incorrect - you say that "They will be interpreted from right to left" but I believe that is not true. From the bash manual gnu.org/software/bash/manual/html_node/Redirections.html it indicates that redirections take place as they are encountered (ie left to right) Oct 3, 2017 at 21:48
  • Pretty nice answer, it helped me to understand stream channels once and for all!
    – artu-hnrq
    Feb 21, 2020 at 21:01
  • Are you sure that redirection commands are interpreted from right to left? Making more research it seems not, for example: { echo stdout; echo stderr >&2; } 2>&1 1>/dev/null prints something, whereas { echo stdout; echo stderr >&2; } 1>/dev/null 2>&1 does not
    – piertoni
    Oct 5, 2021 at 15:55
22

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)

enter image description here

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 

enter image description here

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

9
  • The manpage also says that --stdout option can be dangerous and is easily failing on some systems, and I think --output-fd 1 is doing the same: --stdout: Direct output to the standard output. This option is provided for compatibility with Xdialog, however using it in portable scripts is not recommended, since curses normally writes its screen updates to the standard output. If you use this option, dialog attempts to reopen the terminal so it can write to the display. Depending on the platform and your environment, that may fail. - However, the named pipe idea is cool!
    – Byte Commander
    Dec 1, 2015 at 15:51
  • @ByteCommander "May fail" isn't a very convincing , as this doesn't provide examples. In addition, they don't mention anything about --output-fd , which is the option I used here, not --stdout. Second, the dialog is being drawn on stdout first, the output returned is second. We don't do these two things at the same time. However, --output-fd doesn't specifically require one to use fd 1 (STDOUT). It can easily be redirected to another file descriptor Dec 1, 2015 at 15:58
  • I am not sure, maybe it works everywhere, maybe it works only on most systems. It works on mine and the manpage says to use a similar option with caution is all I know for sure. But as I already said, the +1 is deserved for the named pipes anyway.
    – Byte Commander
    Dec 1, 2015 at 16:10
  • 1
    What is the advantage of using named pipe instead of regular file here? Don't you want to delete the pipe after use?
    – jarno
    Jul 16, 2016 at 20:38
  • 1
    @jarno The big advantage is that you can reuse named pipe over and over : send stuff there, release it into variable, repeat. Especially that is useful with multiple dialogs involved . If you write all data to file, you later have to parse that text file. In addition, this is convenient because you can use it as regular pipe , but with more flexibility. What if you want to store that data from dialog A , do some other stuff, and use that data in dialog B ? Well, you can do that with named pipe Jul 17, 2016 at 3:48
10

:D I can't explain it!!! If you can understand what they are saying in the reference:Advanced Bash-Scripting Guide: Chapter 20. I/O Redirection, write a new answer and I will give you 50rep

Bounty was given, for explanation see ByteCommander's answer. :) This is a part of the history.

exec 3>&1;
result=$(dialog --inputbox test 0 0 2>&1 1>&3);
exitcode=$?;
exec 3>&-;
echo $result $exitcode;

Source: Dialog in bash is not grabbing variables correctly
Reference:Advanced Bash-Scripting Guide: Chapter 20. I/O Redirection

4
  • is that offer still valid? I think I could explain what you found there one and a half years ago... :-)
    – Byte Commander
    Dec 1, 2015 at 13:15
  • @ByteCommander, but however if you can provide that, I will give you that, I will be at my words :D.
    – user.dz
    Dec 1, 2015 at 13:25
  • @ByteCommander, please , ping me after you post it.
    – user.dz
    Dec 1, 2015 at 13:41
  • 1
    Finished! askubuntu.com/a/704616/367990 I hope you understand everything and enjoy the "Eureka!" moment. :-D Leave a comment if anything was left unclear.
    – Byte Commander
    Dec 1, 2015 at 14:29
6

This works for me:

#!/bin/bash
input=$(dialog --stdout --inputbox "What is your username?" 0 0)
retval=$?

case $retval in
${DIALOG_OK-0}) echo "Your username is '$input'.";;
${DIALOG_CANCEL-1}) echo "Cancel pressed.";;
${DIALOG_ESC-255}) echo "Esc pressed.";;
${DIALOG_ERROR-255}) echo "Dialog error";;
*) echo "Unknown error $retval"
esac

The manual page of dialog tells about --stdout:

Direct output to the standard output. This option is provided for compatibility with Xdialog, however using it in portable scripts is not recommended, since curses normally writes its screen updates to the standard output. If you use this option, dialog attempts to reopen the terminal so it can write to the display. Depending on the platform and your environment, that may fail.

Can anyone tell in which platform or environment it does not work? Does redirecting dialog output to 2>&1 >/dev/tty instead work better then?

3

The answer provided by Sneetsher is somewhat more elegant, but I can explain what's wrong: The value of $$ is different inside the backticks (because it starts a new shell, and $$ is the PID of the current shell). You'll want to put the file name in a variable, then refer to that variable throughout instead.

#!/bin/bash
t=$(mktemp -t inputbox.XXXXXXXXX) || exit
trap 'rm -f "$t"' EXIT         # remove temp file when done
trap 'exit 127' HUP STOP TERM  # remove if interrupted, too
dialog --inputbox \
    "What is your username?" 0 0 2>"$t"
retval=$?
input=$(cat "$t")  # Prefer $(...) over `...`
case $retval in
  0)    echo "Your username is '$input'";;
  1)    echo "Cancel pressed.";;
esac

In this case, avoiding the temporary file would be a better solution, but there will be many situations where you cannot avoid a temp file.

3

In case somebody else too landed here from Google, and although this question asks specifically for bash, here is another alternative:

You can use zenity. Zenity is a graphical utility that can be used inside bash scripts. But of course this would require an X server as user877329 rightfully pointed out.

sudo apt-get install zenity

Then in your script:

RETVAL=`zenity --entry --title="Hi" --text="What is your username"`

Useful link.

3
  • 3
    Unless there is no X server
    – user877329
    Dec 1, 2015 at 20:23
  • 3
    OP wants to know about dialog. It's like I come and ask you "How do I write this and that in python ? " , but you give me bash - i'm very happy this can be done different way, but that's not what I am asking Jul 17, 2016 at 4:01
  • @Serg your comment is invalid, my answer is not: The utility provides a perfectly valid and simple alternative to the solution asked for by the OP.
    – Wtower
    Mar 16, 2017 at 17:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .