我一直在自学 Bash 脚本,遇到了一个问题。我编写了一个脚本,使用“read”命令从用户那里获取输入,并将该输入设为变量,以便稍后在脚本中使用。该脚本可以运行,但是……
我希望能够使用“对话框”进行设置。我发现
“dialog --inputbox” 会将输出定向到“stderr”,为了将该输入作为变量获取,您必须将其定向到文件然后检索它。我找到的解释这一点的代码是:
#!/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
我看到它使用 2> 将 sdterr 发送到 /tmp/inputbox.tmp.$$,但输出文件看起来像“inputbox.tmp.21661”。当我尝试 cat 该文件时,它给出了一个错误。所以我仍然无法从 --inputbox 获取用户输入作为变量。
示例脚本:
echo " What app would you like to remove? "
read dead_app
sudo apt-get remove --purge $dead_app
如您所见,这是一个基本脚本。是否可以从中获取变量作为单词dialog --inputbox
?
答案1
:我没法解释!如果你能理解他们在说什么高级 Bash 脚本指南:第 20 章 I/O 重定向,写一个新的答案,我会给你50次:
exec 3>&1; result=$(dialog --inputbox test 0 0 2>&1 1>&3); exitcode=$?; exec 3>&-; echo $result $exitcode;
^ 回答来自@Sneetsher(2014 年 7 月 4 日)
根据要求,我将尝试逐行解释此代码片段的作用。
请注意,我将通过;
省略行尾的所有分号来简化它,因为如果我们每行写一个命令,它们就没有必要了。
I/O-流:
首先,您需要了解通信流。共有 10 个流,编号从 0 到 9:
流 0 (“STDIN”):
“标准输入”,默认的输入流是从键盘读取数据。流 1 (“STDOUT”):
“标准输出”,用于在终端中显示普通文本的默认输出流。流 2 (“STDERR”): “标准错误”,用于在终端中显示错误或其他特殊用途的文本的默认输出流。
第 3-9 组:
额外的、可自由使用的流。默认情况下不使用它们,并且只有在尝试使用它们时才存在。
请注意,所有“流”在内部都由文件描述符表示/dev/fd
(这是一个符号链接,其中/proc/self/fd
包含每个流的另一个符号链接……这有点复杂,而且对它们的行为并不重要,所以我就此打住。)。标准流还有/dev/stdin
,/dev/stdout
和/dev/stderr
(它们又是符号链接,等等……)。
剧本:
exec 3>&1
Bash 内置函数
exec
可用于将流重定向应用于 shell,这意味着它会影响所有后续命令。有关更多信息,请help exec
在终端中运行。在这种特殊情况下,流 3 会被重定向到流 1(STDOUT),这意味着我们稍后发送到流 3 的所有内容都会出现在我们的终端中,就像它正常打印到 STDOUT 一样。
result=$(dialog --inputbox test 0 0 2>&1 1>&3)
此行由许多部分和句法结构组成:
result=$(...)
此结构执行括号中的命令并将输出 (STDOUT) 分配给 bash 变量result
。它可通过读取$result
。所有这些都以某种方式在非常长的时间内描述man bash
。dialog --inputbox TEXT HEIGHT WIDTH
此命令显示一个 TUI 框,其中包含给定的 TEXT、文本输入字段和两个按钮 OK 和 CANCEL。如果选择 OK,命令将以状态 0 退出并将输入的文本打印到 STDERR,如果选择 CANCEL,它将以代码 1 退出并且不打印任何内容。有关更多信息,请阅读man dialog
。2>&1 1>&3
这是两个重定向命令。它们将从右到左进行解释:1>&3
将命令的流 1 (STDOUT) 重定向到自定义流 3。2>&1
然后将命令的流 2 (STDERR) 重定向到流 1 (STDOUT)。这意味着命令打印到 STDOUT 的所有内容现在都出现在流 3 中,而原本应该显示在 STDERR 上的所有内容现在都被重定向到 STDOUT。
因此,整行显示一个文本提示符(在 STDOUT 上,它被重定向到流 3,shell 最终再次将其重定向回 STDOUT - 参见命令
exec 3>&1
)并将输入的数据(通过 STDERR 返回,然后重定向到 STDOUT)分配给 Bash 变量result
。exitcode=$?
dialog
此代码通过保留的 Bash 变量$?
(始终保存最后一个退出代码)检索先前执行的命令的退出代码(此处来自),并将其简单地存储在我们自己的 Bash 变量中exitcode
。它可以再次读取$exitcode
。您可以在中搜索有关此内容的更多信息man bash
,但这可能需要一段时间...exec 3>&-
Bash 内置函数
exec
可用于将流重定向应用于 shell,这意味着它会影响所有后续命令。有关更多信息,请help exec
在终端中运行。在这个特殊情况下,流 3 被重定向到“流 -”,这意味着它应该被关闭。从现在起,发送到流 3 的数据将不再被重定向到任何地方。
echo $result $exitcode
这个简单的
echo
命令(更多信息见)只是将两个 Bash 变量和man echo
的内容打印到 STDOUT。由于这里不再有显式或隐式的流重定向,它们实际上会出现在 STDOUT 上,因此只会显示在终端上。真是个奇迹!;-)result
exitcode
概括:
首先,我们设置 shell 将发送到自定义流 3 的所有内容重定向回 STDOUT,以便它显示在我们的终端中。
然后我们运行dialog
命令,将其原始 STDOUT 重定向到我们的自定义流 3,因为它最终需要显示,但我们暂时需要将 STDOUT 流用于其他用途。
之后,我们将命令的原始 STDERR(对话窗口的用户输入返回的位置)重定向到 STDOUT。
现在我们可以捕获 STDOUT(其中包含从 STDERR 重定向的数据)并将其存储在我们的变量中$result
。它现在包含所需的用户输入!
我们还需要dialog
命令的退出代码,它告诉我们是单击了“确定”还是“取消”。此值显示在保留的 Bash 变量中$?
,我们只需将其复制到我们自己的变量中即可$exitcode
。
之后我们再次关闭流 3,因为我们不再需要它,以停止对它的进一步重定向。
$result
最后,我们正常地将两个变量(对话窗口的用户输入)的内容和$exitcode
(0 表示 OK,1 表示 CANCEL)输出到终端。
答案2
使用对话框自己的工具:--output-fd 标志
如果您阅读对话框的手册页,就会发现有一个选项--output-fd
,它允许您明确设置输出到哪里(STDOUT 1,STDERR 2),而不是默认转到STDERR。
下面您可以看到我运行示例dialog
命令,明确指出输出必须发送到文件描述符 1,这允许我将其保存到 MYVAR 中。
MYVAR=$(dialog --inputbox "THIS OUTPUT GOES TO FD 1" 25 25 --output-fd 1)
使用命名管道
另一种方法有很多隐藏的潜力,就是使用一种被称为命名管道。
#!/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
更深入的概述user.dz 的回答采用替代方法
user.dz 的原始答案和ByteCommander 的解释两者都提供了很好的解决方案,并概述了它的作用。然而,我相信更深入的分析可能有助于解释为什么有用。
首先,重要的是要了解两件事:我们要解决的问题是什么,以及我们正在处理的 shell 机制的底层工作原理是什么。任务是通过命令替换捕获命令的输出。在每个人都知道的简单概述下,命令替换捕获stdout
命令的并让其他东西重用它。在这种情况下,该result=$(...)
部分应该将指定的任何命令的输出保存...
到名为的变量中result
。
在底层,命令替换实际上是作为管道实现的,其中有一个子进程(实际运行的命令)和一个读取进程(将输出保存到变量)。这可以通过简单的系统调用跟踪来证明。请注意,文件描述符 3 是管道的读取端,而 4 是写入端。对于echo
写入其stdout
文件描述符 1 的子进程,该文件描述符实际上是文件描述符 4 的副本,它是管道的写入端。请注意,stderr
这里不起作用,因为它只是一个管道连接stdout
。
$ 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 +++
让我们回到最初的答案。既然现在我们知道将dialog
TUI 框写入stdout
,答案写入stderr
,并且命令替换 stdout
被管道传输到其他地方,我们已经有了部分解决方案 - 我们需要重新连接文件描述符,以便将其stderr
管道传输到读取器进程。这是2>&1
答案的一部分。但是,我们如何处理 TUI 框?
这就是文件描述符 3 的作用所在。dup2()
系统调用允许我们复制文件描述符,使它们有效地引用同一个位置,但我们可以单独操作它们。连接了控制终端的进程的文件描述符实际上指向特定的终端设备。如果你这样做,这一点很明显
$ 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
/dev/pts/5
我当前的伪终端设备在哪里。因此,如果我们能以某种方式保存这个目的地,我们仍然可以将 TUI 框写入终端屏幕。这就是它的exec 3>&1
作用。例如,当您使用重定向调用命令时command > /dev/null
,shell 会传递它的 stdout 文件描述符,然后使用dup2()
将该文件描述符写入/dev/null
。该exec
命令执行类似于dup2()
整个 shell 会话的文件描述符,从而使任何命令都继承已重定向的文件描述符。 也一样exec 3>&1
。文件描述符3
现在将引用/指向控制终端,并且在该 shell 会话中运行的任何命令都将知道它。
因此当result=$(dialog --inputbox test 0 0 2>&1 1>&3);
发生时,shell 会为对话框创建一个管道来写入,但也会2>&1
首先将命令的文件描述符 2 复制到该管道的写入文件描述符上(从而使输出转到管道的读取端并进入变量),而文件描述符 1 将被复制到 3。这将使文件描述符 1 仍然引用控制终端,并且 TUI 对话框将显示在屏幕上。
现在,实际上有一个进程当前控制终端的简写,即/dev/tty
。因此,解决方案可以简化,无需使用文件描述符,只需将其简化为:
result=$(dialog --inputbox test 0 0 2>&1 1>/dev/tty);
echo "$result"
要记住的关键事项:
- 每个命令都从 shell 继承文件描述符
- 命令替换作为管道实现
- 重复的文件描述符将引用与原始文件描述符相同的位置,但我们可以分别操作每个文件描述符
也可以看看
答案3
:D我解释不了!!!如果你能理解他们在参考文献中说了什么:高级 Bash 脚本指南:第 20 章 I/O 重定向,写一个新的答案,我会给你50次
悬赏已发放,详情请参阅ByteCommander 的回答。:)这是历史的一部分。
exec 3>&1;
result=$(dialog --inputbox test 0 0 2>&1 1>&3);
exitcode=$?;
exec 3>&-;
echo $result $exitcode;
答案4
Sneetsher 提供的答案稍微优雅一些,但我可以解释哪里出了问题:反引号中的值$$
不同(因为它启动了一个新的 shell,并且$$
是当前 shell 的 PID)。您需要将文件名放在变量中,然后在整个过程中引用该变量。
#!/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
在这种情况下,避免使用临时文件会是一个更好的解决方案,但在很多情况下您无法避免使用临时文件。