为什么printf“缩小”变音符号?


54

如果我执行以下简单脚本:

#!/bin/bash
printf "%-20s %s\n" "Früchte und Gemüse"   "foo"
printf "%-20s %s\n" "Milchprodukte"        "bar"
printf "%-20s %s\n" "12345678901234567890" "baz"

它打印:

Früchte und Gemüse foo
Milchprodukte        bar
12345678901234567890 baz

也就是说,带有变音符号的文字(例如ü)会被每个变音符号“缩小”一个字符。

当然,我在某个地方有一些错误的设置,但是我无法弄清楚可能是哪个。

如果文件的编码为UTF-8,则会发生这种情况。

如果我将其编码更改为latin-1,对齐方式是正确的,但是变音符号却被错误地呈现:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

14
您希望printf知道UTF-8和其他多字节字符集吗?
弗罗斯特斯

16
看起来它是在计算字节而不是字符。看到echo Früchte und Gemüse | wc -c -m差异。
Stephen Kitt

7
@frostschutz Zsh printf是。
史蒂芬·基特

10
是的,我希望printf确实(至少)知道UTF-8。
勒内Nyffenegger

12
好吧,不是。倒霉。;-)
弗罗斯特斯

Answers:


87

POSIX 需要 printf%-20s计算这20来讲字节不是字符,即使是没有什么意义,因为printf是打印文本,格式化(见讨论在奥斯汀集团(POSIX)和bash邮件列表)。

POSIX shell和其他大多数POSIX shell 的printf内置组件都可以bash证明这一点。

zsh忽略了这个愚蠢的要求(即使在sh仿真中也是如此),因此printf可以按您期望的那样工作。printf内建的相同fish(不是POSIX类的shell)。

ü以UTF-8编码时,字符(U + 00FC)由两个字节(0xc3和0xbc)组成,这说明了差异。

$ printf %s 'Früchte und Gemüse' | wc -mcL
    18      20      18

该字符串由18个字符组成,宽18列(-L是GNU wc扩展,用于报告输入中最宽行的显示宽度),但编码为20个字节。

zsh或中fish,文本将正确对齐。

现在,有些字符的宽度为0(例如,组合字符,例如U + 0308,组合偏斜),或者像许多亚洲脚本中一样,是全角字符(更不用说控制字符,例如Tab),甚至zsh不能对齐那些正确的。

例如,在zsh

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
 ü|
  ᄀ|

bash

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
 ü|
ü|
ᄀ|

ksh93具有一种%Ls格式规范,可以根据显示宽度来计算宽度。

$ printf '%3Ls|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
  ü|
 ᄀ|

如果文本包含TAB之类的控制字符,这仍然行不通(怎么可能?printf必须知道制表位在输出设备中相距多远以及开始在哪个位置打印)。尽管确实考虑到所有控制字符的宽度均为,但它确实偶然地使用了退格字符(如在roff输出中,X(粗体X)写为X\bX)。ksh93-1

作为其他选择,您可以尝试:

printf '%s\t|\n' u ü $'u\u308' $'\u1100' | expand -t3

这适用于某些expand实现(虽然不是GNU的)。

在GNU系统中,你可以使用GNU awkprintf计数字符(不是字节,而不是显示宽度,所以仍然不能确定为0,宽度或2角字符,但确定为您的样品):

gawk 'BEGIN {for (i = 1; i < ARGC; i++) printf "%-3s|\n", ARGV[i]}
     ' u ü $'u\u308' $'\u1100'

如果输出到终端,则还可以使用光标定位转义序列。喜欢:

forward21=$(tput cuf 21)
printf '%s\r%s%s\n' \
  "Früchte und Gemüse"    "$forward21" "foo" \
  "Milchprodukte"         "$forward21" "bar" \
  "12345678901234567890"  "$forward21" "baz"

2
那是不对的。的ü卡拉科特可以由作为u+ ¨,其是3个字节。在问题的情况下,它被编码为2个字符,但并非所有字符ü均等地创建。
Ismael Miguel

6
@IsmaelMiguel, u\u308是一个字形/字形/字形簇的两个字符wc -m至少在Unix / 意义上是两个字符),并且已经提到并包含在此答案中。
斯特凡Chazelas

“对于printf来说,打印文本意义不大。”好吧,有人可能会说printf处理C字符(字节)。它不应处理文本语言环境,也不应承担理解(可能是多字节)字符集编码的负担。但是,此防线与(ISO C99)要求“%s”字节截断不应导致“无效”文本(截断的字符)的要求相冲突。在这种情况下,Glibc甚至会失败(不打印任何内容)。真是一团糟。postgresql.org/message-id/…–
leonbloy

@leonbloy,这也许可以理解C的printf(3)含义(在您提到C99要求之后就没什么意义了,谢谢),但是不是该printf(1)实用程序,因为每个Shell运算符或其他文本实用程序都处理字符(或者被修改为处理字符)例如wc,哪一个-m-c保留了byte)或之后cut有一个字节,可能意味着不是字节。-b-c
斯特凡Chazelas

即使使用字符而不是字节,它仍然不适合对齐列。您需要知道每个字符占用多少个终端单元,这些终端单元因字符(0-2)而异。
R.,

10

如果我将其编码更改为latin-1,对齐方式是正确的,但是变音符号却被错误地呈现:

Frchte und Gemse   foo
Milchprodukte        bar
12345678901234567890 baz

实际上,不,但是您的终端不会讲latin-1,因此您会收到垃圾邮件而不是变音符号。

您可以使用iconv解决此问题:

printf foo bar | iconv -f ISO8859-1 -t UTF-8

(或仅运行通过管道传输到iconv的整个shell脚本)


3
这是一个有用的评论,但不能回答核心问题。
Gerrit

1
@gerrit怎么回事?如果在latin1中进行打印时printf做正确的事,那么是否可以在latin1中进行打印并将其转换为UTF-8?似乎对我的核心问题是正确的解决方法。
Wouter Verhelst,2017年

1
核心问题是“为什么变小变音”,答案(与其他答案一样)是“因为它不支持utf-8”。这不是在问为什么变符号渲染错误如何修复变音符号渲染。无论哪种方式,您的建议对于utf-8子集(都可以表示为iso8859-1(仅))很有用。
Gerrit

4
@WouterVerhelst,是的,尽管它仅适用于可以以单字节字符集编码的文本。
斯特凡Chazelas

3
我也读过这样一个问题:“我怎样才能正确获得输出”,而不是“只要知道为什么我就不会介意错误的输出”。
李斯特先生,2017年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.