从 shell 脚本中获取一串字符的显示宽度(至少在终端上(在当前区域设置中以正确的宽度显示字符))最接近的便携式方法是什么。
我主要对非控制字符的宽度感兴趣,但也欢迎考虑退格、回车、水平制表等控制字符的解决方案。
换句话说,我正在寻找一个壳围绕 POSIX 函数的 API wcswidth()
。
该命令应该返回:
$ that-command 'unix' # 4 fullwidth characters
8
$ that-command 'Stéphane' # 9 characters, one of which zero-width
8
$ that-command 'もで 諤奯ゞ' # 5 double-width Japanese characters and a space
11
人们可以使用考虑到填充到列的字符宽度的ksh93
's ,或者使用命令(例如)来尝试导出,至少有一个 Text::CharWidth 模块,但是是否有更直接或更可移植的方法。printf '%<n>Ls'
<n>
col
printf '++%s\b\b--\n' <character> | col -b
perl
这或多或少是后续另一个问题这是关于在屏幕右侧显示文本,在显示文本之前您需要获得该信息。
答案1
对于单行字符串,GNU 实现wc
有一个-L
(又名--max-line-length
)选项,它可以完全满足您的需求(控制字符除外)。
答案2
在终端仿真器中,可以使用光标位置报告来获取之前/之后的位置,例如,
...record position
printf '%s' $string
...record position
并找出终端上打印的字符有多宽。由于这是一个 ECMA-48(以及 VT100)控制序列,几乎所有您可能使用的终端都支持它,因此它相当便携。
以供参考
CSI Ps n 设备状态报告 (DSR)。 ... Ps = 6 -> 报告光标位置 (CPR) [行;列]。 结果是 CSI r ;受体
最终,终端模拟器决定可打印宽度,因为以下因素:
- 区域设置会影响字符串的格式化方式,但发送到终端的一系列字节是根据终端的配置方式进行解释的(请注意,有些人会认为它必须是 UTF-8,而另一方面可移植性是问题中请求的功能)。
wcswidth
单独并不能说明如何处理组合字符; POSIX 在该函数的描述中没有提到这方面。- 一些人们可能认为理所当然的单宽度字符(例如线条画)是(在 Unicode 中)“模糊宽度”,破坏了
wcswidth
单独使用的应用程序的可移植性(参见例如第 2 章设置 Cygwin)。xterm
例如,可以为需要的配置选择双角字符。 - 要处理除可打印字符之外的任何内容,您必须依赖终端仿真器(除非您想模拟它)。
wcswidth
不同程度支持Shell API 调用:
- Text::CharWidth - 获取终端上字符串占用的列数
该模块提供功能相似的如C语言中的wcwidth(3)和wcswidth(3)。
- 讨论对于红宝石
- Python API
这些或多或少都是直接的:wcswidth
在 Perl 的情况下进行模拟,从 Ruby 和 Python 调用 C 运行时。你甚至可以使用curses,例如Python(它可以处理组合字符):
- 使用初始化终端设置项(没有文字写入屏幕)
- 使用
filter
函数(对于单行) - 在行的开头绘制文本
addstr
,检查错误(如果太长),然后检查结束位置 - 如果有空间,调整起始位置。
- 称呼
endwin
(这不应该做refresh
) - 将有关起始位置的结果信息写入标准输出
使用诅咒输出(而不是将信息反馈给脚本或直接调用tput
)将清除整行(filter
确实将其限制为一行)。
答案3
在 my 中.profile
,我调用脚本来确定终端上字符串的宽度。当我在不信任 system-set 的计算机控制台上登录时LC_CTYPE
,或者当我远程登录且无法信任LC_CTYPE
与远程端匹配时,我会使用此选项。我的脚本查询终端,而不是调用任何库,因为这就是我的用例的重点:确定终端的编码。
这在几个方面是脆弱的:
- 它会修改显示,因此用户体验不是很好;
- 如果另一个程序在错误的时间显示某些内容,则存在竞争条件;
- 如果终端没有响应,它就会锁定。 (几年前我询问如何改进这一点,但在实践中这并不是什么大问题,所以我从来没有抽出时间切换到该解决方案。我遇到的终端不响应的唯一情况是 Windows Emacs 使用该
plink
方法从 Linux 计算机访问远程文件,我通过以下方式解决了它使用该plinkx
方法代替.)
这可能符合也可能不符合您的用例。
#! /bin/sh
if [ z"$ZSH_VERSION" = z ]; then :; else
emulate sh 2>/dev/null
fi
set -e
help_and_exit () {
cat <<EOF
Usage: $0 {-NUMBER|TEXT}
Find out the width of TEXT on the terminal.
LIMITATION: this program has been designed to work in an xterm. Only
xterm and sufficiently compatible terminals will work. If you think
this program may be blocked waiting for input from the the terminal,
try entering the characters "0n0n" (digit 0, lowercase letter n,
repeat).
Display TEXT and erase it. Find out the position of the cursor before
and after displaying TEXT so as to compute the width of TEXT. The width
is returned as the exit code of the program. A value of 100 is returned if
the text is wider than 100 columns.
TEXT may contain backslash-escapes: \\0DDD represents the byte whose numeric
value is DDD in octal. Use '\\\\' to include a single backslash character.
You may use -NUMBER instead of TEXT (if TEXT begins with a dash, use
"-- TEXT"). This selects one of the built-in texts that are designed
to discriminate between common encodings. The following table lists
supported values of NUMBER (leftmost column) and the widths of the
sample text in several encodings.
1 ASCII=0 UTF-8=2 latinN=3 8bits=4
EOF
exit
}
builtin_text () {
case $1 in
-*[!0-9]*)
echo 1>&2 "$0: bad number: $1"
exit 119;;
-1) # UTF8: {\'E\'e}; latin1: {\~A\~A\copyright}; ASCII: {}
text='\0303\0211\0303\0251';;
*)
echo 1>&2 "$0: there is no text number $1. Stop."
exit 118;;
esac
}
text=
if [ $# -eq 0 ]; then
help_and_exit 1>&2
fi
case "$1" in
--) shift;;
-h|--help) help_and_exit;;
-[0-9]) builtin_text "$1";;
-*)
echo 1>&2 "$0: unknown option: $1"
exit 119
esac
if [ z"$text" = z ]; then
text="$1"
fi
printf "" # test that it is there (abort on very old systems)
csi='\033['
dsr_cpr="${csi}6n" # Device Status Report --- Report Cursor Position
dsr_ok="${csi}5n" # Device Status Report --- Status Report
stty_save=`stty -g`
if [ z"$stty_save" = z ]; then
echo 1>&2 "$0: \`stty -g' failed ($?)."
exit 3
fi
initial_x=
final_x=
delta_x=
cleanup () {
set +e
# Restore terminal settings
stty "$stty_save"
# Restore cursor position (unless something unexpected happened)
if [ z"$2" = z ]; then
if [ z"$initial_report" = z ]; then :; else
x=`expr "${initial_report}" : "\\(.*\\)0"`
printf "%b" "${csi}${x}H"
fi
fi
if [ z"$1" = z ]; then
# cleanup was called explicitly, so don't exit.
# We use `trap : 0' rather than `trap - 0' because the latter doesn't
# work in older Bourne shells.
trap : 0
return
fi
exit $1
}
trap 'cleanup 120 no' 0
trap 'cleanup 129' 1
trap 'cleanup 130' 2
trap 'cleanup 131' 3
trap 'cleanup 143' 15
stty eol 0 eof n -echo
printf "%b" "$dsr_cpr$dsr_ok"
initial_report=`tr -dc \;0123456789`
# Get the initial cursor position. Time out if the terminal does not reply
# within 1 second. The trick of calling tr and sleep in a pipeline to put
# them in a process group, and using "kill 0" to kill the whole process
# group, was suggested by Stephane Gimenez at
# https://unix.stackexchange.com/questions/10698/timing-out-in-a-shell-script
#trap : 14
#set +e
#initial_report=`sh -c 'ps -t $(tty) -o pid,ppid,pgid,command >/tmp/p;
# { tr -dc \;0123456789 >&3; kill -14 0; } |
# { sleep 1; kill -14 0; }' 3>&1`
#set -e
#initial_report=`{ sleep 1; kill 0; } |
# { tr -dc \;0123456789 </dev/tty; kill 0; }`
if [ z"$initial_report" = z"" ]; then
# We couldn't read the initial cursor position, so abort.
cleanup 120
fi
# Write some text and get the final cursor position.
printf "%b%b" "$text" "$dsr_cpr$dsr_ok"
final_report=`tr -dc \;0123456789`
initial_x=`expr "$initial_report" : "[0-9][0-9]*;\\([0-9][0-9]*\\)0" || test $? -eq 1`
final_x=`expr "$final_report" : "[0-9][0-9]*;\\([0-9][0-9]*\\)0" || test $? -eq 1`
delta_x=`expr "$final_x" - "$initial_x" || test $? -eq 1`
cleanup
# Zsh has function-local EXIT traps, even in sh emulation mode. This
# is a long-standing bug.
trap : 0
if [ $delta_x -gt 100 ]; then
delta_x=100
fi
exit $delta_x
该脚本在其返回状态中返回宽度,剪裁为 100。示例用法:
widthof -1
case $? in
0) export LC_CTYPE=C;; # 7-bit charset
2) locale_search .utf8 .UTF-8;; # utf8
3) locale_search .iso88591 .ISO8859-1 .latin1 '';; # 8-bit with nonprintable 128-159, we assume latin1
4) locale_search .iso88591 .ISO8859-1 .latin1 '';; # some full 8-bit charset, we assume latin1
*) export LC_CTYPE=C;; # weird charset
esac
答案4
要在我的问题中使用col
和来扩展可能的解决方案的提示:ksh93
使用Debian 上的col
from bsdmainutils
(可能不适用于其他col
实现)来获取单个非控制字符的宽度:
charwidth() {
set "$(printf '...%s\b\b...\n' "$1" | col -b)"
echo "$((${#1} - 4))"
}
例子:
$ charwidth x
1
$ charwidth $'\u301'
0
$ charwidth $'\u94f6'
2
扩展为字符串:
stringwidth() {
awk '
BEGIN{
s = ARGV[1]
l = length(s)
for (i=0; i<l; i++) {
s1 = s1 ".."
s2 = s2 "\b\b"
}
print s1 s s2 s1
exit
}' "$1" | col -b | awk '
{print length - 2 * length(ARGV[2]); exit}' - "$1"
}
使用ksh93
的printf '%Ls'
:
charwidth() {
set "$(printf '.%2Ls.' "$1")"
echo "$((5 - ${#1}))"
}
stringwidth() {
set "$(printf '.%*Ls.' "$((2*${#1}))" "$1")" "$1"
echo "$((2 + 3 * ${#2} - ${#1}))"
}
使用perl
的Text::CharWidth
:
stringwidth() {
perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' -- "$@"
}