如果我执行以下简单脚本:
#!/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 |
编辑:
- 添加ksh-93 | POSIX 实施
- 更多 POSIXness
expr
,现在还测试了使用:
- 灰烬(Busybox 1.x)
- ksh93 版本 A 2020.0.0
- zsh 5.8
这似乎适用于 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 -