如何在bash脚本中保存/ dev / stdout目标位置?


12

我有一个特定的bash脚本,它想要/dev/stdout在将第一个文件描述符替换为其他位置之前保留原始位置。

所以,自然地,我写了类似

old_stdout=$(readlink -f /dev/stdout)

而且它没有用。我很快就知道问题出在哪里:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

显然,$()它在子外壳中运行,并通过管道传输到父外壳。

因此,问题是:是否存在一种可靠的方法(范围仅限于Linux发行版之间的可移植性),以便将/dev/stdout位置另存为bash脚本中的字符串?


这听起来有点像XY问题。潜在的问题是什么?
库萨兰达

根本的问题是某个安装脚本会以两种模式运行-静默模式,在静默模式下,所有输出都记录到文件中;而冗长的模式,不仅将日志记录到文件中,而且还将所有内容打印到终端上。但是在两种模式下,脚本都希望与用户进行交互,即打印到终端并读取用户响应。因此,我认为保存/dev/stdout将解决在静默模式下打印消息的问题。另一种方法是重定向其他所有可产生输出的动作,其中有很多。大约是用户交互消息的100倍。
alexey.e.egorov '16

与用户互动的标准方式是打印到stderr。例如,这就是为什么stderr默认情况下会显示提示的原因。
库萨兰达

不幸的是,stderr由于脚本调用了许多外部程序,并且必须收集和记录所有可能的错误消息,因此必须也必须重新保存该命令。
alexey.e.egorov '16

Answers:


14

要保存文件描述符,请在另一个fd上复制它。仅保存指向相应文件的路径是不够的,您需要保存打开模式,打开标志,文件中的当前位置等。当然,对于匿名管道或套接字,这是行不通的,因为它们没有路径。您要保存的是fd所引用的打开文件描述,而复制fd实际上是将新fd返回到相同的打开文件描述

若要使用类似Bourne的shell将文件描述符复制到另一个文件描述符,语法为:

exec 3>&1

在上面,将fd 1复制到fd 3上。

之前已经开放的fd 3将会被关闭,但是请注意,fds 3至9(通常更多,最多有99个yash)用于该目的(并且没有与0、1或2相反的特殊含义), Shell知道不将其用于自己的内部业务。事先打开fd 3的唯一原因是因为您在脚本1中进行了操作,或者它被调用方泄漏。

然后,您可以将stdout更改为其他内容:

exec > /dev/null

然后,恢复stdout:

exec >&3 3>&-

3>&-正在关闭不再需要的文件描述符)。

现在,问题在于,除了在ksh中,在此之后运行的每个命令都exec 3>&1将继承fd3。这就是fd泄漏。通常来说没什么大不了的,但这会引起问题。

ksh在那些fds 设置close-on-exec标志(对于2以上的fds),但是其他外壳程序和其他外壳程序没有任何手动设置该标志的方法。

其他shell的解决方法是关闭每个命令的fd 3,例如:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

笨拙的。在这里,最好的方法是根本不使用exec,而是重定向命令组:

{
  ls
  uname
} > file.log

在那里,是shell负责保存stdout并随后将其还原(并且它是通过在fd上将其复制(设置为9,高于99,为yash)在内部完成的,并设置了close-on-exec标志)。

1

现在,如果您广泛地使用fds 3至9,或者在函数中使用它们,则这些fds 3至9的管理可能会变得麻烦且成问题,特别是如果您的脚本使用某些第三方代码,而这些第三方代码又会使用这些fds。

一些炮弹(zshbashksh93,所有添加的功能(由奥利弗的Kiddle建议zsh)在2005年大约在同一时间它自己的开发人员之间的讨论后)有特殊语法来分配高于10,而不是这有助于在这种情况下,第一自由FD:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}

另外,在某种程度上说您的代码是错误的,因为fd 3可能已被采用,例如在从rc.local服务运行脚本时发生的情况,例如,所以您真的应该使用了诸如此类的exec {FD}>&1东西。但这仅在bash 4中得到支持,这实在令人遗憾。因此,这并不是真正的可移植性。
alexey.e.egorov '16

@ alexey.e.egorov,请参阅编辑。
斯特凡Chazelas

Bash 3. *不支持此功能,Centos 5中仍使用此版本,该版本仍受支持并仍在使用。然后,eval "exec $i>&1"我想避免找到免费的描述符,因为它很麻烦。我真的可以依靠9以上的fds免费吗?
alexey.e.egorov '16

@ alexey.e.egorov,不,您正在向后看。fds 3到9可以免费使用(并且您可以根据需要管理它们),并且用于此目的。外壳内部可能会使用高于9的fds,关闭它们可能会造成严重后果。大多数外壳不会让您使用它们。bash会让你在脚上开枪。
斯特凡Chazelas

2
@ alexey.e.egorov,如果启动时脚本在(3..9)中打开了某些fds,那是因为调用者忘记了关闭它们或在它们上设置了close-on-exec标志。这就是我所说的FD泄漏。现在,调用者可能打算将这些fds传递给您,以便您可以从它们读取数据和/或向它们写入数据,但是随后您便知道了。如果您对它们一无所知,则不必担心,则可以随意关闭它们(请注意,它只是关闭脚本的进程fd,而不是调用者的进程)。
斯特凡Chazelas

3

如您所见,bash脚本编写不同于可以分配文件描述符的常规编程语言。

最简单的解决方案是使用子外壳程序来运行要重定向的内容,以便可以将处理还原到具有完整标准I / O的顶部外壳程序。

一种替代解决方案是用于tty标识TTY设备并控制脚本中的I / O。例如:

dev=$(tty)

然后你可以..

echo message > $dev

>另一种解决方案是使用tty来识别TTY设备并控制脚本中的I / O。这个怎么办?
alexey.e.egorov '16

1
我只是在回答中包含一个例子。
Julie Pelletier

1

$$ 如果是交互式外壳程序或脚本相关外壳程序PID,将为您提供当前的进程PID。

因此,您可以使用:

readlink -f /proc/$$/fd/1

例:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33

1
虽然功能正常,但依赖于特定的/proc结构会导致可移植性问题,就像问题/dev/stdout中提到的那样。
朱莉·佩勒

1
@JuliePelletier依靠特殊/proc结构吗?它可以在任何具有procfs..
heemayl的

1
没错,因此我们可以像procfs通常出现的那样对Linux进行概括,但是我们经常看到可移植性问题,一个好的开发方法包括考虑可移植到其他系统的问题。 bash可以在多种操作系统上运行。
Julie Pelletier
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.