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 个字节。
在zsh
or 中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系统中,你可以使用GNUawk
其printf
计数字符(不是字节,而不是显示宽度,所以仍然为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)
Wou*_*lst 10
如果我将其编码更改为 latin-1,则对齐是正确的,但变音符号呈现错误:
Run Code Online (Sandbox Code Playgroud)Fr?chte und Gem?se foo Milchprodukte bar 12345678901234567890 baz
实际上,不,但是您的终端不会说 latin-1,因此您会得到垃圾而不是变音。
您可以使用 iconv 解决此问题:
printf foo bar | iconv -f ISO8859-1 -t UTF-8
Run Code Online (Sandbox Code Playgroud)
(或者只是运行通过管道传输到 iconv 的整个 shell 脚本)
${#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)
编辑:
expr
,现在还测试了与:expr length "$2"
为expr " $2" : '.*' - 1
.
${#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)