如何在Python中检测控制台是否支持ANSI转义码?

44

为了检测控制台是否正确使用了sys.stderr或者sys.stdout,我进行了如下测试:

if hasattr(sys.stderr, "isatty") and sys.stderr.isatty():
   if platform.system()=='Windows':
       # win code (ANSI not supported but there are alternatives)
   else:
       # use ANSI escapes
else:
   # no colors, usually this is when you redirect the output to a file

当在IDE(例如PyCharm)中运行此Python代码时,问题变得更加复杂。最近,PyCharm添加了对ANSI的支持,但第一个测试失败了:它具有isatty属性,但设置为False

我想修改逻辑以便正确检测输出是否支持ANSI着色。其中一个要求是,在输出被重定向到文件时,我不应该输出任何东西(对于控制台,这将是可以接受的)。

更新

https://gist.github.com/1316877上添加了更复杂的ANSI测试脚本。

3个回答

27

Django用户可以使用django.core.management.color.supports_color函数。

if supports_color():
    ...

他们使用的代码是:

def supports_color():
    """
    Returns True if the running system's terminal supports color, and False
    otherwise.
    """
    plat = sys.platform
    supported_platform = plat != 'Pocket PC' and (plat != 'win32' or
                                                  'ANSICON' in os.environ)
    # isatty is not always implemented, #6223.
    is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
    return supported_platform and is_a_tty

请查看https://github.com/django/django/blob/master/django/core/management/color.py


这似乎在某些Windows终端中无法正常工作。 - Aabesh Karmacharya

13

我可以告诉你别人是如何解决这个问题的,但这并不美观。如果你以ncurses为例(它需要能够在各种不同的终端上运行),你会发现它使用终端能力数据库来存储每种终端及其能力。重点是,即使他们从来没有能够自动“检测”这些事情。

我不知道是否有跨平台的termcap,但找一找可能是值得的。即使有,也可能没有你的终端列表,并且你可能需要手动添加它。


PyCharm团队报告称,目前没有办法发现您的代码是否在PyCharm中执行。解决方法是自己添加一个环境变量,但几乎没有用处。 - sorin
1
这个PyCharm的票证讨论了这个问题--https://youtrack.jetbrains.net/issue/PY-4853(它引用了这个SO的讨论,我认为有一些相同的参与者)。它记录了他们添加了`PYCHARM_HOSTED=1`环境变量,可以用来检测pycharm。 - Eli Collins

6

\x1B[6n 是一个标准(据我所知)的 ANSI 转义码,用于查询用户光标的位置。如果发送到 stdout,则终端应该将 \x1B[{line};{column}R 写入 stdin。如果实现了这个结果,就可以假定支持 ANSI 转义码。主要问题在于检测此回复。

Windows

msvcrt.getch 可以用于从 stdin 中检索字符,而无需等待按下回车键。这与 msvcrt.kbhit 结合使用,后者检测是否正在等待读取 stdin,可产生本文“带注释的代码”部分中找到的代码。

Unix/with termios

警告:我(不明智地)没有测试过这个特定的 tty/select/termios 代码,但我知道类似的代码过去曾经有效。 getchkbhit 可以使用 tty.setrawselect.select 进行复制。因此,我们可以定义这些函数如下:

from termios import TCSADRAIN, tcgetattr, tcsetattr
from select import select
from tty import setraw
from sys import stdin

def getch() -> bytes:
    fd = stdin.fileno()                        # get file descriptor of stdin
    old_settings = tcgetattr(fd)               # save settings (important!)

    try:                                       # setraw accomplishes a few things,
        setraw(fd)                             # such as disabling echo and wait.

        return stdin.read(1).encode()          # consistency with the Windows func
    finally:                                   # means output should be in bytes
        tcsetattr(fd, TCSADRAIN, old_settings) # finally, undo setraw (important!)

def kbhit() -> bool:                           # select.select checks if fds are
    return bool(select([stdin], [], [], 0)[0]) # ready for reading/writing/error

这可以与下面的代码一起使用。

带注释的代码

from sys import stdin, stdout

def isansitty() -> bool:
    """
    The response to \x1B[6n should be \x1B[{line};{column}R according to
    https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797. If this
    doesn't work, then it is unlikely ANSI escape codes are supported.
    """

    while kbhit():                         # clear stdin before sending escape in
        getch()                            # case user accidentally presses a key

    stdout.write("\x1B[6n")                # alt: print(end="\x1b[6n", flush=True)
    stdout.flush()                         # double-buffered stdout needs flush 

    stdin.flush()                          # flush stdin to make sure escape works
    if kbhit():                            # ANSI won't work if stdin is empty
        if ord(getch()) == 27 and kbhit(): # check that response starts with \x1B[
            if getch() == b"[":
                while kbhit():             # read stdin again, to remove the actual
                    getch()                # value returned by the escape

                return stdout.isatty()     # lastly, if stdout is a tty, ANSI works
                                           # so True should be returned. Otherwise,
    return False                           # return False

完整代码(无注释)

如果您需要,这里是原始代码。

from sys import stdin, stdout
from platform import system


if system() == "Windows":
    from msvcrt import getch, kbhit

else:
    from termios import TCSADRAIN, tcgetattr, tcsetattr
    from select import select
    from tty import setraw
    from sys import stdin

    def getch() -> bytes:
        fd = stdin.fileno()
        old_settings = tcgetattr(fd)

        try:
            setraw(fd)

            return stdin.read(1).encode()
        finally:
            tcsetattr(fd, TCSADRAIN, old_settings)

    def kbhit() -> bool:
        return bool(select([stdin], [], [], 0)[0])

def isansitty() -> bool:
    """
    Checks if stdout supports ANSI escape codes and is a tty.
    """

    while kbhit():
        getch()

    stdout.write("\x1b[6n")
    stdout.flush()

    stdin.flush()
    if kbhit():
        if ord(getch()) == 27 and kbhit():
            if getch() == b"[":
                while kbhit():
                    getch()

                return stdout.isatty()

    return False

来源

没有特定的顺序:


这在 Windows 10 的 ConEmu64.exe(支持 ANSI 码)和 cmd.exe(不支持 ANSI 码)上都可以正常工作。但是,在 cmd.exe 中测试时,控制台中会出现额外的“垃圾”字符:←[6nThis TTY supports ANSI codes: False。虽然这不会影响功能,但我想知道是否可以详细说明一下(在不支持 ANSI 码的终端上,在测试终端功能时避免打印额外的垃圾符号)? - lospejos
2
我已经发现,在第一个 print() 之前插入 stdout.write('\b\b\b\b') 可以防止控制台中出现额外的垃圾字符。我不确定这是否是一种正常的方法,还是一种“肮脏的技巧”。 - lospejos

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