如何检查Java程序的输入/输出流是否连接到终端?

53

我希望一个Java程序可以根据其用途设置不同的默认设置(详细程度,可能会有支持彩色输出)。在C语言中,有一个isatty()函数,如果文件描述符连接到终端则返回1,否则返回0。在Java中是否有类似的功能?在InputStream或PrintStream的JavaDoc中没有看到相关内容。


我认为在Java中没有这样的等效物。对于其余的设置,您可以尝试在Java中使用此curses实现:http://javacurses.sourceforge.net - rogeriopvl
6个回答

51

System.console() vs isatty()

如@Bombe所述,System.console()适用于简单的检查连接到控制台的情况。然而,System.console()的问题在于它不让您确定是STDIN还是STDOUT(或两者都没有)与控制台连接。

Java的System.console()和C的isatty()之间的区别可以在以下情况下进行说明(我们将数据传输到/从一个假想的Foo.class):

1) STDIN和STDOUT均为tty

%> java Foo
System.console() => <Console instance>
isatty(STDIN_FILENO) => 1
isatty(STDOUT_FILENO) => 1

2)STDOUT 是 tty

%> echo foo | java Foo
System.console() => null
isatty(STDIN_FILENO) => 0
isatty(STDOUT_FILENO) => 1

3) STDIN 是 tty

%> java Foo | cat
System.console() => null
isatty(STDIN_FILENO) => 1
isatty(STDOUT_FILENO) => 0

4) STDIN和STDOUT都不是tty

%> echo foo | java Foo | cat
System.console() => null
isatty(STDIN_FILENO) => 0
isatty(STDOUT_FILENO) => 0

我无法告诉你为什么Java不支持更好的tty检查。我想知道是否有一些Java的目标操作系统不支持它。

使用JNI调用isatty()

技术上讲,通过一些相当简单的JNI方法,在Java中实现这一点是可能的(正如stephen-c@所指出的),但这将使您的应用程序依赖于C代码,该代码可能不可移植到其他系统。我可以理解有些人可能不想那样做。

下面是JNI的一个快速示例(省略了很多细节):

Java:tty/TtyUtils.java

public class TtyUtils {
    static {
        System.loadLibrary("ttyutils");
    }
    // FileDescriptor 0 for STDIN, 1 for STDOUT
    public native static boolean isTty(int fileDescriptor);
}

C: ttyutils.c(假定有匹配的ttyutils.h 文件),编译成libttyutils.so

#include <jni.h>
#include <unistd.h>

JNIEXPORT jboolean JNICALL Java_tty_TtyUtils_isTty
          (JNIEnv *env, jclass cls, jint fileDescriptor) {
    return isatty(fileDescriptor)? JNI_TRUE: JNI_FALSE;
}

其他语言:

如果你可以选择使用另一种语言,我能想到的大多数其他语言都支持tty检查。但是,既然你已经问了这个问题,你可能已经知道了。除了C/C++之外,我首先想到的是RubyPythonGolangPerl


4
这里是Console用来决定其是否存在的本地代码 - 必须同时为TTY才能使用stdinstdout创建Console实例。 - dimo414
请注意,在JDK 22及更高版本中,System.console() == null是不正确的,替代方法是System.console().isTerminal(),请参阅https://bugs.openjdk.org/browse/JDK-8309155。 - Liam Miller-Cushon

32

System.console() 方法将返回你的应用程序所连接的控制台,如果没有连接则返回 null。(请注意,该方法只在 JDK 6 及以后版本中可用。)


1
@Bombe:这并没有回答核心问题...即如何判断一个已存在的流是否连接到控制台。 - Stephen C
1
现有流是什么?如何将流连接到终端但不使其存在?或者您是在尝试检测进程何时失去其控制终端? - Bombe
1
@Bombe:啊,我明白Console对象的作用了。但是我仍然认为这不是isatty所做的事情。具体来说,它不能告诉你一个给定的流是否连接到控制台。 - Stephen C
7
如果 stdin 或者 stdout 被重定向了,那么 System.console() 将会是空值,但它并不会告诉你哪一个被重定向了。这很重要,因为有时候你需要知道哪一个流(如果有)连接到了控制台。例如,如果 stdout 被重定向到了日志文件,你可能想要去掉 ANSI 转义码,但如果是 stdin 被重定向了,就不是这样。 - Gili

9
简短的回答是,在标准Java中没有“isatty”的直接等效物。自从1997年,Java Bug数据库中就有类似于此的RFE,但只有一个微不足道的投票。
理论上,您可以使用JNI魔术来实现'isatty'。但这会引入各种潜在问题。我甚至不会考虑自己去做这件事……
注1:投票Java错误以进行修复在Oracle接管Sun时消失了。

感谢提供RFE链接。 - Zilk
链接已损坏,已移至https://bugs.java.com/bugdatabase/view_bug.do?bug_id=4099017。 - binarynoise

7
你可以使用 jnr-posix 库来从 Java 中调用本地 posix 方法:

jnr-posix

import jnr.posix.POSIX;
import jnr.posix.POSIXFactory;
import java.io.FileDescriptor;

POSIX posix = POSIXFactory.getPOSIX();

posix.isatty(FileDescriptor.out);

6

如果您不想自己编译C源代码,可以使用Jansi库。它比jnr-posix小很多。

<dependency>
  <groupId>org.fusesource.jansi</groupId>
  <artifactId>jansi</artifactId>
  <version>1.17.1</version>
</dependency>

...

import static org.fusesource.jansi.internal.CLibrary.isatty;

...

System.out.println( isatty(STDIN_FILENO) );

1
有另一种方法。我偶然发现了这个方法,因为我需要使用/dev/tty。如果Java程序不是TTY的一部分(如Gradle守护进程),当程序尝试从tty设备文件创建InputStream时,会引发FileSystemException。但是,如果任何一个标准输入、标准输出或标准错误连接到终端,此代码将不会引发带有以下消息的异常:

  • 在macOS上为(Device not configured)
  • 在Linux上为No such device or address

不幸的是,检查/dev/tty是否存在且可读将返回true。只有在实际尝试读取文件而没有读取它时,才会出现此FSE。

// straw man check to identify if this is running in a terminal
// System.console() requires that both stdin and stdout are connected to a terminal
// which is not always the case (eg with pipes).
// However, it happens that trying to read from /dev/tty works
// when the application is connected to a terminal, and fails when not
// with the message
//     on macOS '(Device not configured)'
//     on Linux 'No such device or address'
//
// Unfortunately Files::notExists or Files::isReadable don't fail.
//noinspection EmptyTryBlock
try (var ignored = Files.newInputStream(Path.of("/dev/tty"))) {
  return "in a tty"
} catch (FileSystemException fileSystemException) {
  return "not in a tty";
}

虽然这种方法不太美观,但它避免了使用第三方库。不过这并没有回答哪个标准流连接到终端的问题,对此最好依赖于终端库,比如Jansi或JLine 3。


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