如何阻止 ANSI 颜色代码弄乱 printf 对齐?

Posted

技术标签:

【中文标题】如何阻止 ANSI 颜色代码弄乱 printf 对齐?【英文标题】:How to stop ANSI colour codes messing up printf alignment? 【发布时间】:2011-04-13 00:18:16 【问题描述】:

我在使用 ruby​​ printf 时发现了这一点,但它也适用于 C 的 printf。

如果您在输出字符串中包含 ANSI 颜色转义码,则会打乱对齐方式。

鲁比:

ruby-1.9.2-head > printf "%20s\n%20s\n", "\033[32mGreen\033[0m", "Green"
      Green          # 6 spaces to the left of this one
               Green # correctly padded to 20 chars
 => nil

C 程序中的同一行产生相同的输出。

有没有办法让 printf(或其他东西)对齐输出而不为非打印字符添加空格?

这是一个错误,还是有充分的理由?

更新:由于在存在 ANSI 代码和宽字符时无法依赖 printf 来对齐数据,是否有最佳实践方法在 ruby​​ 中的控制台中排列彩色表格数据?

【问题讨论】:

【参考方案1】:

这不是错误:ruby 不可能知道(至少在 printf 中,对于像 curses 这样的东西会是另一回事)它的标准输出将进入一个可以理解 VT100 转义序列的终端。

如果您不调整背景颜色,这样的做法可能会更好:

GREEN = "\033[32m"
NORMAL = "\033[0m"
printf "%s%20s%s\n", GREEN, "Green", NORMAL

【讨论】:

我不确定我是否遵循您的回答——终端理解转义序列,因为文本显示为绿色。它只是神秘地添加了 9 个空格(我不明白 - 转义序列中只有 7 个字符。) @Stewart,终端 可能理解它们,但不是终端在进行格式化。 printf 唯一知道的是字符,而不是转义序列。您在两个不同的抽象级别上工作。 杰克,我什至会做#define green "\033[32m"(或任何红宝石等价物)并直接在printf中使用greenprintf "%s%20s%s\n", green, "Green", normal。这将使代码更具可读性。 我明白你现在在说什么。我不明白的是 printf 如何将 7 个转义序列字符转换为 9 个空格字符。 @Stewart Johnson: printf 只看到一个 14 个字符的序列,你想在 20 个字符的字段中打印,所以它会输出 6 个空格,然后是字符串。它只是没有意识到转义序列将是“不可见的”。转义序列在字符串中的哪个位置也无关紧要 - 开头、中间或结尾,效果都是一样的。【参考方案2】:

printf 字段宽度说明符对于对齐表格数据、界面元素等没有用处。除了您已经发现的控制字符问题之外,您的程序还必须处理非间距和双宽字符如果您不想将事情限制为遗留字符编码(许多用户认为已弃用),请处理。

如果你坚持这样使用printf,你可能需要这样做:

printf("%*s\n%*s\n", bytestopad("\033[32mGreen\033[0m", 20), "\033[32mGreen\033[0m", bytestopad("Green", 20), "Green");

其中bytestopad(s,n) 是您编写的一个函数,它计算需要多少总字节数(字符串加上填充空格)以导致字符串s 占用n 终端列。这将涉及解析转义和处理多字节字符并使用工具(如 POSIX wcwidth 函数)来查找每个终端列的数量。请注意使用* 代替printf 格式字符串中的恒定字段宽度。这允许您将 int 参数传递给 printf 以获得运行时可变字段宽度。

【讨论】:

谢谢 - 那么在 ruby​​ 控制台中排列表格数据的最佳做法是什么? FWIW,您也可以使用 * 代替精度,例如"%.*s" 将字符串截断为最多由参数指定的字节数,或%.*f 用于运行时可配置的浮点​​精度。您可以同时使用两者,如%*.*f,在这种情况下首先传递宽度参数。【参考方案3】:

我不同意您对“绿色绿色之后的 9 个空格”的描述。我使用 Perl 而不是 Ruby,但如果我使用您的语句的修改,在字符串后打印一个管道符号,我得到:

perl -e 'printf "%20s|\n%20s|\n", "\033[32mGreen\033[0m", "Green";'
      Green|
               Green|

这向我表明printf() 语句在字符串中计算了 14 个字符,因此它在前面加上 6 个空格以产生 20 个右对齐字符。然而,终端吞下了其中的 9 个字符,将它们解释为颜色变化。因此,输出看起来比您想要的短 9 个字符。但是,printf() 在第一个“绿色”之后没有打印 9 个空格。


关于对齐输出(带有着色)的最佳实践,我认为您需要让每个大小对齐的字段都被处理着色的简单“%s”字段包围:

printf "%s%20.20s%s|%s%-10d%s|%s%12.12s%s|\n",
       co_green, column_1_data, co_plain,
       co_blue,  column_2_data, co_plain,
       co_red,   column_3_data, co_plain;

很明显,co_XXXX 变量(常量?)包含用于切换到指定颜色的转义序列(co_plain 可能比co_black 更好)。如果事实证明您不需要在某些字段上着色,则可以使用空字符串代替 co_XXXX 变量(或称其为 co_empty)。

【讨论】:

很好看。感谢@caf 的解释,我现在可以看到发生了什么。【参考方案4】:

我会从实际文本中分离出任何转义序列以避免整个问题。

# in Ruby
printf "%s%20s\n%s%20s\n", "\033[32m", "Green", "\033[0m", "Green"

/* In C */
printf("%s%20s\n%s%20s\n", "\033[32m", "Green", "\033[0m", "Green");

由于 ANSI 转义序列不是 Ruby 或 C 的一部分,因此它们都不认为需要对这些字符进行特殊处理,这是理所当然的。

如果你打算做很多终端颜色的东西,那么你应该研究 curses 和 ncurses,它们提供了对许多不同类型的终端都有效的颜色更改功能。它们还提供更多功能,例如基于文本的窗口、功能键,有时甚至是鼠标交互。

【讨论】:

你的 C 示例中有一个额外的引号,它使语法突出显示变得很奇怪。【参考方案5】:

这是我最近想出的一个解决方案。这允许您在printf 语句中使用color("my string", :red)。我喜欢对标题和数据使用相同的格式字符串——DRY。这使这成为可能。另外,我使用rainbow gem 来生成颜色代码;它并不完美,但可以完成工作。 CPAD 散列包含每种颜色的两个值,分别对应于左右填充。当然,应该扩展此解决方案以促进其他颜色和修饰符,例如粗体和下划线。

CPAD = 
  :default => [0, 2],
  :green   => [0, 3],
  :yellow  => [0, 2],
  :red     => [0, 1],


def color(text, color)
  "%*s%s%*s" % [CPAD[color][0], '', text.color(color), CPAD[color][1], '']
end

例子:

puts "%-10s   %-10s   %-10s   %-10s" % [
  color('apple',  :red),
  color('pear',   :green),
  color('banana', :yellow)
  color('kiwi',   :default)
]

【讨论】:

以上是关于如何阻止 ANSI 颜色代码弄乱 printf 对齐?的主要内容,如果未能解决你的问题,请参考以下文章

为啥ANSI颜色转义码不起作用[重复]

在 Python 中为具有 ANSI 颜色代码的字符串获取正确的字符串长度

ANSI 颜色转义序列列表

VIM 中的 ANSI 颜色代码

如何使用 ANSI Escape 代码在控制台上输出彩色文本

在最新的VScode中运行Minitest - 显示Ansi颜色代码