printf:多字节字符

printf:多字节字符

当尝试格式化printf涉及包含多字节字符的字符串的输出时,很明显,它printf不计算文字字符,而是计算字节数,如果混合使用单字节和多字节字符,这会使格式化文本变得困难。例如:

$ cat script
#!/bin/bash
declare -a a b
a+=("0")
a+=("00")
a+=("000")
a+=("0000")
a+=("00000")
b+=("0")
b+=("├─00")
b+=("├─000")
b+=("├─0000")
b+=("└─00000")
printf "%-15s|\n" "${a[@]}" "${b[@]}"

$ ./script
0              |
00             |
000            |
0000           |
00000          |
0              |
├─00       |
├─000      |
├─0000     |
└─00000    |

我找到了各种建议的解决方法(主要是使用另一种语言或实用程序来打印文本的包装器)。有没有原生的 bash 解决方案?没有任何记录printf 格式化字符串似乎有帮助。在这种情况下,设置是否locale相关,例如使用 UTF-32 等固定宽度字符编码?

答案1

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

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

好吧,假设您要打印到终端,那就是......

控制序列在那里<ESC>[nnGnn是要移动到的列,以十进制表示。

当然,如果第一列比分配的空间长,结果就不太好:

$ printf "%s\033[10G-\n" "abcdefghijkl"
abcdefghi-kl

要解决此问题,您可以<ESC>[K在打印以下列之前显式清除该行的其余部分 ( )。

$ printf "%s\033[10G\033[K-\n" "abcdefghijkl"
abcdefghi-

另一种方法是手动进行填充,假设我们有可以确定字符串字符长度的东西。这似乎在 Bash 中适用于简单字符,但当然有点难看。零宽度和双宽度字符可能会破坏它,而且我也没有测试组合字符。

#!/bin/bash
pad() { 
    # parameters:
    #  1: name of variable to pad
    #  2: length to pad to
    local string=${!1}
    local len=${#string}
    printf -v "$1" "%s%$(($2 - len))s" "$string" ""
}
echo "1234567890"
for x in "abc" "├─cd" "└──ef" ; do
    pad x 9
    printf "%s-\n" "$x"
done

输出是:

1234567890
abc      -
├─cd     -
└──ef    -

答案2

这是一个使用的解决方案wc -L

for i in "${a[@]}" "${b[@]}"
do printf "%s%*s|\n" "$i" "$[15 - $(wc -L <<< "$i")]" ""
done

0              |
00             |
000            |
0000           |
00000          |
0              |
├─00           |
├─000          |
├─0000         |
└─00000        |

wc -L打印输入的显示宽度,因此它也适用于双宽字符等

答案3

我做了一些网络搜索,但我无法在纯 Bash 中找到你的问题的解决方案,我认为可能没有。我发现了以下 StackOverflow 帖子:

得票最高的答案那里(由用户发布基督) 包括以下内容:

printf是的,这是我所知道的所有版本的问题。我简要讨论了这个问题这个答案并且也在这个

我还在 Unix StackExchange 上发现了以下帖子:

接受的解决方案其中包括以下解释:

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

看来您想做的事情可能无法实现,printf并且您必须推出自己的解决方案。

我能够使用 Python 脚本生成所需的输出。也许你会发现它很有用:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""script.py"""

# Set the default character encoding to UTF-8
import sys
reload(sys)
sys.setdefaultencoding("utf-8")

# Array of ASCII characters
a=[("0")]
a+=[("00")]
a+=[("000")]
a+=[("0000")]
a+=[("00000")]

# Array of UTF-8 Characters
b=[("0")]
b+=[("├─00")]
b+=[("├─000")]
b+=[("├─0000")]
b+=[("└─00000")]

# Print the elements from both arrays
for x in a + b:
    print (u"%-15s|" % x).encode('utf-8')

这是我运行脚本时得到的结果:

user@host:~$ python script.py

0              |
00             |
000            |
0000           |
00000          |
0              |
├─00           |
├─000          |
├─0000         |
└─00000        |

答案4

为什么 printf 会“缩小”变音符号?有一些适当的解决方案,通过调用适当的工具来做到这一点,因为bash内部缺少该功能或通过切换到不同的 shell,但如果您真的想bash仅使用内置命令来实现它,那么有一些方法可以实现单宽度(可能是多宽度)字节)字符。

$string在 bash 中,就像在所有 POSIX shell 中一样,您可以使用 来获取 a 的字符宽度${#string}${#string}但在 C 语言环境中可以获取以字节为单位的宽度。

因此,您可以用以下方法来解释差异:

clength() { clength=${#1}; }
blength() { local LC_ALL=C; blength=${#1}; }
align() {
  local format="$1" width="$2" arg blength clength
  shift 2
  for arg do
    clength "$arg"; blength "$arg"
    printf "$format" "$((width + blength - clength))" "$arg"
  done
}

a=(0 00 000 0000 00000)
b=(0 ├─00 ├─000 ├─0000 └─00000)
align '%-*s|\n' 12 "${a[@]}" "${b[@]}"

为了解决零宽度(如组合标记)或双宽度字符,除非bash您准备好在脚本中对此类字符的列表进行硬编码(或使用终端转义序列告诉终端对齐文本(最后一个例子那里, 或者那里)并对所有支持的终端硬编码转义序列,因为bash也没有 terminfo/termcap 的内置接口)。据我所知,zsh 和 ksh93 是唯一内置支持对齐可变显示宽度字符的 shell(示例也在链接的问答)。

相关内容