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

Ren*_*ger 58 bash unicode printf

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

#!/bin/bash
printf "%-20s %s\n" "Früchte und Gemüse"   "foo"
printf "%-20s %s\n" "Milchprodukte"        "bar"
printf "%-20s %s\n" "12345678901234567890" "baz"
Run Code Online (Sandbox Code Playgroud)

它打印:

Früchte und Gemüse foo
Milchprodukte        bar
12345678901234567890 baz
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

Fr?chte und Gem?se   foo
Milchprodukte        bar
12345678901234567890 baz
Run Code Online (Sandbox Code Playgroud)

Sté*_*las 93

POSIX要求 printf's%-20s字节数而不是字符数来计算这 20字符,尽管这printf与打印格式化的文本毫无意义(请参阅Austin Group (POSIX) 和bash邮件列表中的讨论)。

和大多数其他 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
Run Code Online (Sandbox Code Playgroud)

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

zshor 中fish,文本将正确对齐。

现在,也有宽度为 0 的字符(如组合字符,如 U+0308,组合分音符)或双倍宽度,如许多亚洲文字(更不用说控制字符如 Tab),甚至zsh不会对齐那些正确的。

例如,在zsh

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
 u?|
  ?|
Run Code Online (Sandbox Code Playgroud)

bash

$ printf '%3s|\n' u ü $'u\u308' $'\u1100'
  u|
 ü|
u?|
?|
Run Code Online (Sandbox Code Playgroud)

ksh93有一个%Ls格式规范来计算显示宽度方面的宽度。

$ printf '%3Ls|\n' u ü $'u\u308' $'\u1100'
  u|
  ü|
  u?|
 ?|
Run Code Online (Sandbox Code Playgroud)

如果文本包含 TAB 之类的控制字符,这仍然不起作用(怎么可能?printf必须知道制表位在输出设备中的距离以及它开始打印的位置)。它确实与退格字符一起意外工作(例如在roff输出中X(粗体X)写为X\bX),尽管ksh93将所有控制字符视为宽度为-1.

其他选项

在 中zsh,您可以使用其填充参数扩展标志(l用于左填充,r用于右填充),当与m标志结合使用时,会考虑字符的显示宽度(与字符串中的字符数相反):

$ () { printf '%s|\n' "${(ml[3])@}"; } u ü $'u\u308' $'\u1100'
  u|
  ü|
  ü|
 ?|
Run Code Online (Sandbox Code Playgroud)

expand

printf '%s\t|\n' u ü $'u\u308' $'\u1100' | expand -t3
Run Code Online (Sandbox Code Playgroud)

这适用于某些expand实现(尽管不是 GNU)。

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

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

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

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

  • @IsmaelMiguel,对于一个字形/图形/图形集群,`u\u308` 是两个 _characters_(至少在 Unix/`wc -m` 意义上),并且已经被提到并包含在这个答案中。 (6认同)
  • 那是不正确的。`ü`字符可以组成为`u`+`¨`,共3个字节。在这个问题的情况下,它被编码为 2 个字符,但并非所有的 `ü` 都是平等创建的。 (2认同)

Wou*_*lst 10

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

Fr?chte und Gem?se   foo
Milchprodukte        bar
12345678901234567890 baz
Run Code Online (Sandbox Code Playgroud)

实际上,不,但是您的终端不会说 latin-1,因此您会得到垃圾而不是变音。

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

printf foo bar | iconv -f ISO8859-1 -t UTF-8
Run Code Online (Sandbox Code Playgroud)

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

  • @WouterVerhelst,是的,虽然这只适用于可以用单字节字符集编码的文本。 (4认同)
  • 我也把这个问题读为“我怎样才能得到正确的输出”而不是“我不介意错误的输出,只要我知道为什么”。 (4认同)
  • 这是一个有用的评论,但没有回答核心问题。 (3认同)

Léa*_*ris 5

${#var} 从 bash3.0+ 开始,字符数是正确的。

尝试(使用任何版本的 bash):

bash -c "a="$'aáíóuúüoözu\u308\u1100'';printf "%s\n" "${a} ${#a}"'
Run Code Online (Sandbox Code Playgroud)

这将给出自 bash 3.0 以来的正确计数。

但是请注意,这$'u\u308'要求 bash 为 4.2+。

这使得计算适当的填充成为可能:

#!/usr/bin/env bash

strings=(
  'Früchte und Gemüse'
  'Milchprodukte'
  '12345678901234567890'
)

# Initialize column width
cw=20

for str in "${strings[@]}"
do
  # Format column1 with computed padding
  printf -v col1string '%s%*s' "$str" $((cw-${#str})) ''

  # Print column1 with computed padding, followed by column2
  printf "%s %s\n" "$col1string" 'col2string'
done
Run Code Online (Sandbox Code Playgroud)

输出:

Früchte und Gemüse   col2string
Milchprodukte        col2string
12345678901234567890 col2string
Run Code Online (Sandbox Code Playgroud)

使用特色对齐功能:

#!/usr/bin/env bash

# Space pad align string to width
# @params
# $1: The alignment width
# $2: The string to align
# @stdout
# aligned string
# @return:
# 1: If a string exceeds alignment width
# 2: If missing arguments
align_left ()
{
  (($#==2)) || return 2
  ((${#2}>$1)) && return 1
  printf '%s%*s' "$2" $(($1-${#2})) ''
}
align_right ()
{
  (($#==2)) || return 2
  ((${#2}>$1)) && return 1
  printf '%*s%s' $(($1-${#2})) '' "$2"
}
align_center ()
{
  (($#==2)) || return 2
  ((${#2}>$1)) && return 1
  l=$((($1-${#2})/2))
  printf '%*s%s%*s' $l '' "$2" $(($1-${#2}-l)) ''
}

strings=(
  'Früchte und Gemüse'
  'Milchprodukte'
  '12345678901234567890'
)

echo 'Left-aligned:'
for str in "${strings[@]}"
do
  printf "| %s |\n" "$(align_left 20 "$str")"
done
echo
echo 'Right-aligned:'
for str in "${strings[@]}"
do
  printf "| %s |\n" "$(align_right 20 "$str")"
done
echo
echo 'Center-aligned:'
for str in "${strings[@]}"
do
  printf "| %s |\n" "$(align_center 20 "$str")"
done
Run Code Online (Sandbox Code Playgroud)

输出:

Left-aligned:
| Früchte und Gemüse   |
| Milchprodukte        |
| 12345678901234567890 |

Right-aligned:
|   Früchte und Gemüse |
|        Milchprodukte |
| 12345678901234567890 |

Center-aligned:
|  Früchte und Gemüse  |
|    Milchprodukte     |
| 12345678901234567890 |
Run Code Online (Sandbox Code Playgroud)

编辑

  1. 添加 ksh-93 | POSIX 实现
  2. 更多 POSIXness with expr,现在还测试了与:
  • 灰(Busybox 1.x)
  • ksh93 A 版 2020.0.0
  • zsh 5.8
  1. 根据Stéphane Chazelas 的建议:替换expr length "$2"expr " $2" : '.*' - 1.
  2. 使用isaac的评论更新了介绍。

    ${#var} 从 bash3.0+ 开始,字符数是正确的。

这似乎也适用于 ksh 或 POSIX 语法:

#!/usr/bin/env sh

# Space pad align or truncate string to width
# @params
# $1: The alignment width
# $2: The string to align
# @stdout
# The aligned string
# @return:
# 1: If the string was truncated alignment width
# 2: If missing arguments
__align_check ()
{
  if [ $# -ne 2 ]; then return 2; fi
  if [ "$(expr " $2" : '.*' - 1)" -gt "$1" ]; then
    printf '%s' "$(expr substr "$2" 1 $1)"
    return 1
  fi
}
align_left ()
{
  __align_check "$@" || return $?
  printf '%s%*s' "$2" $(($1-$(expr " $2" : '.*' - 1))) ''
}
align_right ()
{
  __align_check "$@" || return $?
  printf '%*s%s' $(($1-$(expr " $2" : '.*' - 1))) '' "$2"
}
align_center ()
{
  __align_check "$@" || return $?
  tpl=$(($1-$(expr " $2" : '.*' - 1)))
  lpl=$((tpl/2))
  rpl=$((tpl-lpl))
  printf '%*s%s%*s' $lpl '' "$2" $rpl ''
}

main ()
{
  hr="+----------------------+----------------------+----------------------\
+------+"
  echo "$hr"
  printf '| %s | %s | %s | %s |\n' \
    "$(align_left 20 'Left-aligned')" \
    "$(align_center 20 'Center-aligned')" \
    "$(align_right 20 'Right-aligned')" \
    "$(align_center 4 'RC')"
  echo "$hr"

  for str
  do
    printf '| %s | %s | %s | %s |\n' \
      "$(align_left 20 "$str")" \
      "$(align_center 20 "$str")" \
      "$(align_right 20 "$str")" \
      "$(align_right 4 "$?")"
  done
  echo "$hr"
}

main \
  'Früchte und Gemüse' \
  'Milchprodukte' \
  '12345678901234567890' \
  'This string is much too long'
Run Code Online (Sandbox Code Playgroud)

输出:

+----------------------+----------------------+----------------------+------+
| Left-aligned         |    Center-aligned    |        Right-aligned |  RC  |
+----------------------+----------------------+----------------------+------+
| Früchte und Gemüse   |  Früchte und Gemüse  |   Früchte und Gemüse |    0 |
| Milchprodukte        |    Milchprodukte     |        Milchprodukte |    0 |
| 12345678901234567890 | 12345678901234567890 | 12345678901234567890 |    0 |
| This string is much  | This string is much  | This string is much  |    1 |
+----------------------+----------------------+----------------------+------+
Run Code Online (Sandbox Code Playgroud)