我能用Java找到控制台宽度吗?

47

有没有一种方法可以找到Java程序运行的控制台的宽度?

如果可能的话,我希望这是跨平台的...

我不想改变缓冲区或窗口的宽度,我只想知道它的宽度,以便正确格式化正在打印到屏幕上的文本。


不是答案,但换行对我来说是个问题,因为我想往返移动光标,而换行是一个问题。虽然可以通过计算控制台宽度来解决这个问题,但这很困难,因此我的解决方案至少在 Mac 上(并且可能也适用于 Linux)是使用 tput rmam 禁用换行和 tput smam 启用换行(https://itectec.com/askdifferent/macos-disable-line-wrapping-for-output-in-the-terminal/)。可以直接在控制台中执行,或者作为从 Java 直接执行的 bash 命令。 - user1300214
8个回答

32

没有可靠的跨平台解决这个问题的方案。实际上,有些情况下无法知道真正的控制台宽度。

(查看其他答案以了解在某些情况和/或某些平台上有效的方法。但要注意这些方法的限制...)

例如,在Linux系统上,通常可以从LINES和COLUMNS环境变量中找到理论终端尺寸。虽然这些变量在调整某些“终端仿真器”窗口大小时会自动更新,但并非总是如此。实际上,在通过telnet协议连接的远程控制台的情况下,无法将实际终端尺寸传递给用户的shell。

编辑:只是为了补充说明,如果用户在启动Java应用程序后更改了Linux上xterm的尺寸,则Java应用程序不会被通知,并且它不会在其LINESCOLUMNS环境变量的副本中看到新的尺寸!

编辑2:我的错误: LINESCOLUMNSbash shell变量,默认情况下不导出到环境中。您可以在运行Java应用程序之前运行export COLUMNS LINES来“修复”这个问题。


1
我希望有些东西可以返回宽度/高度等,或者如果无法确定则返回-1...哦,好吧。欢迎硬编码常量! - masher
3
在Linux下,我使用一个脚本来调用我的Java应用程序,并在调用Java之前通过执行eval \resize`; export COLUMNS`来更改终端的大小。 - Ogre Psalm33
我找到了一个库,已经确认在Linux和Windows cmd下可以工作。请查看我的详细回复。 - Michael Scheper
根据你在回答中所说的,“有时候有效”可能更为准确。 - Stephen C
你有权发表自己的观点 :-) - Stephen C
显示剩余8条评论

25

实际上,在Java中已经存在一个Java库可以做到这一点:JLine 2。(在SourceForce上有一个旧版本,但是该链接到GitHub似乎是最新的。)

我在Linux(Mint 14)和Windows(我不知道是哪个版本)下使用JLine 2.11测试过这个方法:

terminalWidth = jline.TerminalFactory.get().getWidth();

JLine承诺在Mac上也能工作。

我发现它在Eclipse控制台下返回错误的宽度(比如1!),甚至Java的本地控制台也不能在Eclipse下工作!而且,不幸的是,在Cygwin下也有这个问题。但是我写了一些代码来检查不合理的值(< 10),并在这些情况下只使用80。

JLine 3的更新(根据Mark提供的信息——谢谢你,老兄!):

terminalWidth = org.jline.terminal.TerminalBuilder.terminal().getWidth()

6
在Unix上,JLine只是在TerminalLineSetting中调用 stty -a ,而在Windows上,它通过org.fusesource.jansi.internal.WindowsSupport钩入了JAnsi库中的封闭源代码部分。 - dimo414
1
对于JLine3,它应该是"org.jline.terminal.TerminalBuilder.terminal().getWidth()"。 - Mark
看起来 JLine3 可以配置使用 Jansi,那么如果我已经安装了 Jansi,有没有一种方法可以仅使用 Jansi 确定控制台,而不需要 JLine Terminal 的开销? - Garret Wilson

16

有一个技巧,基于ANSI转义代码。它们不提供直接查询控制台大小的方式,但它们有一个请求当前光标位置的命令。通过将光标移动到一个非常高的行和列,然后请求光标位置,可以获得准确的测量值。

将此与存储/恢复光标位置的命令结合使用,例如以下示例:

发送以下序列到终端(stdout)

"\u001b[s"             // save cursor position
"\u001b[5000;5000H"    // move to col 5000 row 5000
"\u001b[6n"            // request cursor position
"\u001b[u"             // restore cursor position

现在观察标准输入,你应该收到一个看起来像\u001b[25;80R的序列,其中25是行数,80是列数。

我最初在Lanterna库中看到它被使用。

更新:

实际上有四种不同的方法可以实现此目的,但它们都对程序运行的环境或终端设备/模拟器做出了某些假设。

  • 使用VT100协议。这就是这个解决方案所做的,它假定您正在通过stdin/stdout与支持这些转义代码的终端仿真器通信。对于CLI程序来说,这似乎是一个相对安全的假设,但例如,如果有人正在使用cmd.exe,这可能不起作用。
  • terminfo/termcap。这些是终端信息数据库,您可以使用tput进行查询。操作系统相关,并假设您连接到TTY设备。例如,无法在ssh上工作。
  • 使用telnet协议。Telnet具有自己的查询屏幕尺寸的功能,但当然,这只适用于人们通过telnet客户端连接到您的应用程序的情况,这在大多数情况下并不是一个选项。
  • 依靠shell(例如bash),这是使用COLUMNS/ROWS变量的解决方案所做的。远非普遍存在,但如果您为应用程序提供确保必要的环境变量被导出的包装脚本,它可能非常有效。


1
这种方法可能在应用程序的 stdinstdout 连接到终端时有效。但是,它假定它总是有效的。用户可能会将输出导入文件中,这将打破该假设。在这种情况下,转义代码将被写入文件中。应用程序在使用此技术之前应检查是否连接到终端。不幸的是,我怀疑没有标准的Java API来检查。在C中,可能类似于 isatty - JojOatXGME
这种方法的问题在于许多终端以规范模式工作,提供基于行的输入。因此,除非您找到一种设置ICANON标志的方法,否则此解决方案需要用户按Enter键才能启用Java从System.in读取。 - Björn Zurmaar

12

编辑:请参考@dave_thompson_085的评论,因为那很可能是更好的方法,使用ProcessBuilder

另一个答案提到在脚本中运行tput cols,然后再启动命令。但是如果你想在Java已经启动后使用Runtime.getRuntime().exec()来运行它,你会发现tput无法与你的终端通信,因为Java已经重定向了stdout和stderr。作为一种不太可移植的解决方法,你可以使用神奇的/dev/tty设备,它指的是当前进程的终端。这样你就可以运行像这样的东西:

Process p = Runtime.getRuntime().exec(new String[] {
    "bash", "-c", "tput cols 2> /dev/tty" });
// Read the output of this process to get your terminal width

这对我在Linux上有效,但我不指望它能在任何地方都有效。希望它能在Mac上工作。但它肯定不会在Windows上工作,尽管在Cygwin上可能会。


2
自Java7(2011年)以来,ProcessBuilder可以将Java自己的stdin/out/err传递给子进程,而不是默认的重定向管道;请参见inheritIO()redirect*(Redirect.INHERIT)--如果用户已经重定向了java本身,这并没有什么帮助,但在这种情况下,为什么要关心你不使用的I/O设备的宽度呢? - dave_thompson_085
遗憾的是,这种技术似乎与IntelliJ的默认终端不兼容。我看到其他人抱怨Eclipse也有同样的限制。对于终端和IDE如何处理这个问题有什么见解吗?通常我们在一个上编写/调试,但支持另一个。 - tresf

4
Java 6有一个java.io.Console类,但它不幸地缺少您要求的功能。使用标准的Java库和纯跨平台的Java是无法获取控制台窗口宽度的。
这里有一个替代方案Java控制台库,可以让您获取屏幕大小,但其中包含一个特定于Windows的DLL。您可能能够获取源代码并将C部分编译为Linux或Mac OS X共享库,以便在这些平台上也能正常工作。

我发现了Console API并感到很兴奋,但随后却不是这样... 对于我想做的事情来说,学习C语言以及如何交叉编译就像用一颗拆迁球锤钉一颗钉子。 - masher
@Jesper - 这并不是Java的错。在Linux / Unix机器上,有很多因素使得无法在所有情况下获取当前控制台大小成为可能。 - Stephen C

3

我之前一直在解决这个问题。我使用了几种不同的技术。但是要实现真正的跨平台解决方案很困难。

我尝试过像这样的方法:

String os = System.getProperty("os.name").toLowerCase();

    //Windows
    if(os.contains("win")){
        System.out.append("Windows Detected");
        //set Windows Dos Terminal width 80, height 25
        Process p = Runtime.getRuntime().exec("mode 80, 25");
    }
    //Mac
    if(os.contains("mac")){
        System.out.println("Macintosh Detected");
        //... I dont know...try Google
    }
    //Linux
    if(os.contains("linux")){
        System.out.println("Linux Detected");

你可以使用String.contains("export COLUMNS")方法和user.dir属性,在每个Linux用户的主目录中读取/测试并将"export COLUMNS"添加到.bashrc文件中。这样可以使列在每次Java应用程序启动时加载。
然后,你可以将其传递给一个临时文件。就像这样:
try {
    ProcessBuilder pb = new ProcessBuilder("bash","-c","echo $COLUMNS >/home/$USER/bin/temp.txt" );  
        pb.start();

    }catch (Exception e) {
        System.out.println("exception happened - here's what I know: ");
        e.printStackTrace();
        System.exit(-1);
    }
}

另一种选择是在启动时使用bash脚本执行您的Java.jar。在脚本内,您可以使用"tput cols"获取宽度。然后将该值作为String[] arg传递给您的Java应用程序。
例如:
//#!/bin/bash

//#clear the screen

clear 

//#get the console width and height

c=$[$(tput cols)]

l=$[$(tput lines)]

//#pass the columns, lines and an optional third value as String[] args.

java -jar ~/bin/Plus.jar $c $l $1

为什么使用Java实现这个任务会很困难?显然,编写一个良好的API是个好的开始。我想我们也可以尝试使用Apache.commons.exec吗?

2
如果在Java进程启动之前调用了export COLUMNS,则可以简单地使用System.getenv("COLUMNS");无需使用ProcessBuilder启动子进程。 - dimo414

1

对我来说,了解终端窗口(当窗口大小改变时仍然不正确)的唯一方法是使用类似于以下命令的命令:

ProcessBuilder pb = new ProcessBuilder("cmd.exe", "/c", "mode con");
pb.redirectError(ProcessBuilder.Redirect.INHERIT);

当没有运行cmd.exe部分时,会显示找不到命令。还要注意redirectError部分。如果不使用它,则将使用Java输出大小而不是实际大小。只有使用这种组合才能获取实际大小。

0

1
正如shutil文档所提到的,这会回退到os.get_terminal_size(),它是特定于操作系统并且是用C语言实现的。使用Jython可能适用于某些用例,但这是一个相对较重的工具。 - dimo414
这似乎根本不是一个“好的解决方案”。 - snorbi

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