如何欺骗一个应用程序,使其认为其stdout是终端而不是管道

199
我正在尝试做 "检测stdin是终端还是管道?" 的相反操作。
我运行的应用程序会改变其输出格式,因为它在STDOUT上检测到一个管道,而我希望它认为它是一个交互式终端,以便在重定向时获得相同的输出。
我想过用expect脚本包装它或在PHP中使用proc_open(),但都不行。
有什么想法吗?

8
http://empty.sf.net有帮助吗? - ephemient
1
@ephemient:本应该是一个答案。顺便说一下,这是一个很棒的工具... - neuro
这个问题谈论的是标准输出(stdout),但标题提到了标准输入(stdin)。我认为标题是错误的。 - Piotr Dobrogost
stdin也一样吗? - theonlygusti
10个回答

207

哦!

script 命令可以实现我们想要的功能...

script --return --quiet -c "[executable string]" /dev/null

做到了!
Usage:
 script [options] [file]

Make a typescript of a terminal session.

Options:
 -a, --append                  append the output
 -c, --command <command>       run command rather than interactive shell
 -e, --return                  return exit code of the child process
 -f, --flush                   run flush after each write
     --force                   use output file even when it is a link
 -q, --quiet                   be quiet
 -t[<file>], --timing[=<file>] output timing data to stderr or to FILE
 -h, --help                    display this help
 -V, --version                 display version

1
+1: 刚刚遇到一个与库进行静态初始化的问题。在 Fedora 12 的最近更改中,当启动的 exe 不在 tty 中时,会导致初始化失败。您的技巧完美地解决了这个问题。我喜欢它胜过 unbuffer,因为脚本默认已安装! - neuro
12
如果您想将某个命令的输出传输到交互式的终端程序中,比如 less -R ,并让终端程序对颜色进行解释,那么需要进行一些额外的处理。比如,我想要一个彩色版本的 git status | less 。你需要给less传递 -R 以使其支持颜色,并使用 script 来让 git status 输出彩色。但是我们不希望 script 占用键盘,我们希望输入传输到 less 。所以我现在使用了以下命令,运行良好:0<&- script -qfc "git status" /dev/null | less -R。这几个字符会关闭此命令的标准输入。 - Aaron McDaid
3
注意:在检查交互性的组件查找 "$-" shell 变量中是否包含 "i" 的情况下,此方法无效。 - Jay Taylor
1
这太棒了。我需要它来处理一个极其罕见的用例,其中包含在Wine中运行的可执行文件中的嵌入式Python库。当我在终端中运行时,它可以正常工作,但是当我运行我创建的.desktop文件时,它总是会崩溃,因为Py_Initialize没有看到正确的stdin/stderr。 - Tatsh
3
有没有一种不受操作系统限制的方式来做这件事?我喜欢macOS上的script命令,因为你不需要用引号将命令包起来。该命令运行并将输出发送到tty,然后将其复制到提供的文件中,但是我似乎无法让Linux版本以相同的方式运行...我可能做错了什么。所以在macOS上,这个命令的Linux等价命令是什么:script -q -t 0 tmp.out perl -e 'print "Test\n"' Test cat tmp.out Test - hepcat72
显示剩余10条评论

82

根据Chris的解决方案,我提出了如下小助手函数:

faketty() {
    script -qfc "$(printf "%q " "$@")" /dev/null
}

为了正确扩展脚本参数中的$@,同时保护命令可能带引号的部分(请参见下面的示例),需要使用看起来有些古怪的printf

用法:

faketty <command> <args>

例子:

$ python -c "import sys; print(sys.stdout.isatty())"
True
$ python -c "import sys; print(sys.stdout.isatty())" | cat
False
$ faketty python -c "import sys; print(sys.stdout.isatty())" | cat
True

10
如果您使用的script版本支持,您可能希望使用“--return”选项来保留子进程的退出代码。 - jwd
6
我建议将这个函数改成这样: function faketty { script -qfc "$(printf "%q " "$@")" /dev/null; } 否则,每次运行命令时都会创建一个名为 typescript 的文件,在许多情况下这是不必要的。 - w0rp
5
似乎在MacOS上无法工作,我收到了script: illegal option -- f的错误信息。 - Alexander Mills

32

unbuffer脚本是由Expect提供的,应该可以很好地处理这个问题。如果不能,那么该应用程序可能会查看与其输出连接不同的内容,例如TERM环境变量的设置。


23

参考之前的回答,在Mac OS X上,"script"可以像下面这样使用...

script -q /dev/null commands...

但是,由于它可能会在标准输出中使用"\r\n"替换"\n",因此您可能还需要像这样的脚本:

script -q /dev/null commands... | perl -pe 's/\r\n/\n/g'

如果这些命令之间有管道,则需要刷新标准输出。例如:

script -q /dev/null commands... | ruby -ne 'print "....\n";STDOUT.flush' |  perl -pe 's/\r\n/\n/g'

1
感谢提供 OS X 语法,但是根据你的 Perl 语句判断,你想要说它将 "\r\n" 的实例替换为 "\n",而不是反过来,对吗? - mklement0

19

我不知道从PHP是否可行,但如果你确实需要子进程看到TTY,则可以创建一个PTY

在C语言中:

#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>
#include <unistd.h>
#include <pty.h>

int main(int argc, char **argv) {
    int master;
    struct winsize win = {
        .ws_col = 80, .ws_row = 24,
        .ws_xpixel = 480, .ws_ypixel = 192,
    };
    pid_t child;

    if (argc < 2) {
        printf("Usage: %s cmd [args...]\n", argv[0]);
        exit(EX_USAGE);
    }

    child = forkpty(&master, NULL, NULL, &win);
    if (child == -1) {
        perror("forkpty failed");
        exit(EX_OSERR);
    }
    if (child == 0) {
        execvp(argv[1], argv + 1);
        perror("exec failed");
        exit(EX_OSERR);
    }

    /* now the child is attached to a real pseudo-TTY instead of a pipe,
     * while the parent can use "master" much like a normal pipe */
}

实际上,我认为expect自身确实创建了一个PTY。


你知道如何在Mac OS X上将nettop作为子进程运行吗?我想在我的应用程序中获取nettop的输出。我尝试使用forkpty,但仍然无法成功运行nettop。 - Vince Yuan

17

更新 @A-Ron 的答案,以便 a)适用于Linux和MacOs b)间接传播状态码(因为MacOs script不支持它)

faketty () {
  # Create a temporary file for storing the status code
  tmp=$(mktemp)

  # Ensure it worked or fail with status 99
  [ "$tmp" ] || return 99

  # Produce a script that runs the command provided to faketty as
  # arguments and stores the status code in the temporary file
  cmd="$(printf '%q ' "$@")"'; echo $? > '$tmp

  # Run the script through /bin/sh with fake tty
  if [ "$(uname)" = "Darwin" ]; then
    # MacOS
    script -Fq /dev/null /bin/sh -c "$cmd"
  else
    script -qfc "/bin/sh -c $(printf "%q " "$cmd")" /dev/null
  fi

  # Ensure that the status code was written to the temporary file or
  # fail with status 99
  [ -s $tmp ] || return 99

  # Collect the status code from the temporary file
  err=$(cat $tmp)

  # Remove the temporary file
  rm -f $tmp

  # Return the status code
  return $err
}

例子:

$ faketty false ; echo $?
1

$ faketty echo '$HOME' ; echo $?
$HOME
0

embedded_example () {
  faketty perl -e 'sleep(5); print "Hello  world\n"; exit(3);' > LOGFILE 2>&1 </dev/null &
  pid=$!

  # do something else
  echo 0..
  sleep 2
  echo 2..

  echo wait
  wait $pid
  status=$?
  cat LOGFILE
  echo Exit status: $status
}

$ embedded_example
0..
2..
wait
Hello  world
Exit status: 3

我使用非标准的 SHELL,因此这个函数在我的(Linux)环境中失败了。script 命令使用 SHELL 来运行 /bin/sh -c ...,然后运行 $cmd。通过显式设置 SHELL=/bin/sh,似乎可以省略其中的一个阶段。我用更简单的 SHELL=/bin/sh script -qfc "$cmd" /dev/null 替换了 Linux 的 script 行,它看起来具有相同的功能。 - Ben Mares
在Mac上:你的脚本似乎没有处理标准输入,是吗?例如,当uniq被包裹在faketty中并从管道中获取标准输入时,它似乎无法正常工作:grep --line-buffered ... | faketty uniq。(我需要faketty,因为我需要将uniq的输出无缓冲地传递给另一个命令。) - undefined

9

对于具体答案,我评论不了,但我想跟进一下ingomueller-net上发布的faketty函数,因为它最近帮了我一个忙。

我发现这正创建一个typescript文件,而我不想/不需要,所以我将/dev/null作为脚本目标文件添加:

function faketty { script -qfc "$(printf "%q " "$@")" /dev/null ; }


3

书籍《UNIX环境高级编程第二版》的样例代码中还包括一个pty程序!

以下是在Mac OS X上编译pty的方法:

man 4 pty  #  pty -- pseudo terminal driver

open http://en.wikipedia.org/wiki/Pseudo_terminal

# Advanced Programming in the UNIX Environment, Second Edition
open http://www.apuebook.com

cd ~/Desktop

curl -L -O http://www.apuebook.com/src.tar.gz

tar -xzf src.tar.gz

cd apue.2e

wkdir="${HOME}/Desktop/apue.2e"

sed -E -i "" "s|^WKDIR=.*|WKDIR=${wkdir}|" ~/Desktop/apue.2e/Make.defines.macos

echo '#undef _POSIX_C_SOURCE' >> ~/Desktop/apue.2e/include/apue.h

str='#include   <sys/select.h>'
printf '%s\n' H 1i "$str" . wq | ed -s calld/loop.c

str='
#undef _POSIX_C_SOURCE
#include <sys/types.h>
'
printf '%s\n' H 1i "$str" . wq | ed -s file/devrdev.c

str='
#include <sys/signal.h>
#include <sys/ioctl.h>
'
printf '%s\n' H 1i "$str" . wq | ed -s termios/winch.c

make

~/Desktop/apue.2e/pty/pty ls -ld *

真的很奇怪的错误,而且是这样的:Fastly 错误:未知域名:codesnippets.joyent.com。请检查该域名是否已添加到服务中。 - i336_

3

我试图在Linux上运行shellcheck <file> | less时获取颜色,因此我尝试了以上建议,但它们会产生这种奇怪的效果,即文本水平偏离正确位置:

In ./all/update.sh line 6:
                          for repo in $(cat repos); do
                                                                  ^-- SC2013: To read lines rather than words, pipe/redirect to a 'while read' loop.

(对于不熟悉shellcheck的人,警告所在的行应该与问题所在的位置保持一致。)
为了使上面的答案能够与shellcheck一起使用,我尝试了评论中的一个选项:
faketty() {                       
    0</dev/null script -qfc "$(printf "%q " "$@")" /dev/null
}

这样可以运行。我还添加了--return并使用长选项,使得该命令更加通俗易懂:

faketty() {                       
    0</dev/null script --quiet --flush --return --command "$(printf "%q " "$@")" /dev/null
}

适用于Bash和Zsh。


相当确定这在 BSD 系统上的 bash 和 zsh 都不起作用。 - oligofren
@oligofren,我鼓励您提交一个解决方案。 - Nick ODell
不需要,另一个答案已经涵盖了那个。我只是想说,你的答案的主要驱动因素取决于操作系统,而不是shell。我可以编辑一下来澄清 :) - oligofren

-2

如果你正在寻找一种使用tee高亮显示Ansible输出的颜色,那么你也可以尝试这个方法。

export ANSIBLE_FORCE_COLOR=True

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接