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

为什么 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"

它打印:

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

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

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

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

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

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

答案1

POSIX需要 printf%-20s计算这 20 个字节不是人物尽管这和printf打印没什么意义文本,格式化(参见讨论在奥斯汀集团(POSIX)和bash邮件列表)。

内置printf的POSIX shellbash和大多数其他 POSIX shell 都遵循这一点。

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作为 GNUwc扩展,用于报告输入中最宽行的显示宽度),但编码为 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),尽管 asksh93认为所有控制字符的宽度为-1

其他选项

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

$ () { printf '%s|\n' "${(ml[3])@}"; } u ü $'u\u308' $'\u1100'
  u|
  ü|
  ü|
 ᄀ|

expand

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

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

在 GNU 系统上,您可以使用以字符计数awk的GNU printf(不是字节,不是显示宽度,因此对于 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

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

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

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

从 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

输出:

Früchte und Gemüse   col2string
Milchprodukte        col2string
12345678901234567890 col2string

使用特色对齐功能:

#!/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

输出:

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 |

编辑

  1. 添加ksh-93 | POSIX 实施
  2. 更多 POSIXness expr,现在还测试了使用:
  • 灰烬(Busybox 1.x)
  • ksh93 版本 A 2020.0.0
  • zsh 5.8
  1. 经建议斯蒂芬·查泽拉斯expr length "$2"取而代之expr " $2" : '.*' - 1
  2. 更新了介绍以撒的评论。

    ${#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'

输出:

+----------------------+----------------------+----------------------+------+
| 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 |
+----------------------+----------------------+----------------------+------+

答案3

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

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

实际上,不,但是您的终端不支持 latin-1,因此您得到的是垃圾信息而不是元音变音符号。

您可以使用 iconv 修复此问题:

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

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

答案4

我很高兴找到这个答案:

您可以通过告诉终端将光标移动到所需位置来解决这个问题,而不是计算printf字符数:

$ printf "%s\033[10G-\n" "abc" "├─cd" "└──ef"
abc      -
├─cd     -
└──ef    -

信用:https://unix.stackexchange.com/a/407135

相关内容