获取字符串的显示宽度

获取字符串的显示宽度

从 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>colprintf '++%s\b\b--\n' <character> | col -bperl

这或多或少是后续另一个问题这是关于在屏幕右侧显示文本,在显示文本之前您需要获得该信息。

答案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 调用:

这些或多或少都是直接的: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 上的colfrom 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"
}

使用ksh93printf '%Ls'

charwidth() {
  set "$(printf '.%2Ls.' "$1")"
  echo "$((5 - ${#1}))"
}

stringwidth() {
  set "$(printf '.%*Ls.' "$((2*${#1}))" "$1")" "$1"
  echo "$((2 + 3 * ${#2} - ${#1}))"
}

使用perlText::CharWidth

stringwidth() {
  perl -MText::CharWidth=mbswidth -le 'print mbswidth shift' -- "$@"
}

相关内容