zsh 字符串的长度,可能包含 unicode 和转义字符

zsh length of a string with possibly unicode and escape characters

上下文:我想右对齐部分提示。在这样做时,我目前的做法是计算它的左右部分的长度,并用空格填充中间部分。

问题:应对 %G(参见 prompt expansion) when the string possibly contains unicode (for example git status). Possibly the actual problem is that I don't grasp it correctly. The use of %G was suggested in another thread answer about ,这可能是我困惑的根源。以下片段说明了问题:

strlen() {
    FOO=
    local invisible='%([BSUbfksu]|([FB]|){*})' # (1)
    LEN=${#${(S%%)FOO//$~invisible/}}
    echo $LEN
}

local blob="%{↓%G%}"
echo $blob $(strlen $blob) # (2) Unexpectedly gives 0

local blob="↓"
echo $blob $(strlen $blob) # (3) Gives the wanted output of 1 
                           # but then this result would tell us to not use %G for unicode

strlen函数来自this tentative explanation of counting user-visible string。不幸的是,invisible 部分没有明确完整的解释 (1) 任何额外的 references/explanation 也将受到欢迎。

问题:什么时候才真正使用%G?或者我应该按照上面的代码片段的建议放弃它吗?

简答:

使用 Unicode 字符而不是纯 ASCII 时,您无需采取任何额外步骤。当前版本的 zsh 完全支持 Unicode 字符并且可以正确处理它们。所以即使一个字符被多个字节编码,zsh仍然会知道它只是一个字符。


何时使用 %{...%}%G

%{...%}用来向zsh表明里面的字符串不改变光标位置。例如,如果您想添加用于设置颜色的转义序列,这很有用:

print -P '%{\e[31m%}terminal red%{\e[0m%}'
print -P '%{\e[38;2;0;127;255m%}#007FFF%{\e[0m%}'

如果没有 %{...%}zsh 将不得不假设转义序列的每个字符都将光标向右移动一个位置。

%{...%}(或%1{...%})中使用%G告诉zsh假定将输出单个字符。这仅用于计数目的,它不会自行移动光标。

根据 ZSH Manual:

This is useful when outputting characters that otherwise cannot be correctly handled by the shell, such as the alternate character set on some terminals.

由于zsh可以处理Unicode字符,所以这里没有必要(虽然不一定是错的)。


strlen "%{↓%G%}" 出现意外结果的原因:

这是因为 strlen 实际上只是尝试删除任何空长度提示序列(如 %B%F{red}),而不是实际测量打印的长度结果字符串(无论如何这可能是不可能的)。在许多情况下,这已经足够好了,但是在 "%{↓%G%}" 的情况下它会失败,这实际上等同于 zsh 提示上下文中的 "↓"

解释:

为了找到这些空长度提示序列,strlen 将其输入匹配到此模式

invisible=%([BSUbfksu]|([FB]|){*})'

这还包含子模式 %{*},它将匹配 %{…%}。然后

LEN=${#${(S%%)FOO//$~invisible/}}

只是在计算字符数之前从 FOO 中删除任何匹配的子字符串。

最重要的是,它实际上并不以任何方式处理 %G,只是将其与周围的 %{...%}.

一起删除

由于整个字符串 "%{↓%G%}" 与模式匹配,它将被完全删除,导致意外的字符数 0


顺便说一句:这并不意味着你不应该使用 strlen(我在我的提示中使用了从它派生的东西已经有一段时间了)。但您应该注意一些限制:

  • 它不适用于 %G(很明显)。
  • 它无法处理 %{...%} 的数字参数,例如 %3{...%}
  • 它也不识别 % 后的数字参数,如 %1F(而不是 %F{1}%F{red}
  • 它无法处理嵌套的 %{...%},或者 %{...%} 中的任何 }。 (例如,当打算使用 %D{string} 进行日期格式化时,这很重要,因为格式字符串 string 的长度必须与结果日期的长度相匹配,而无需使用 `%{...% } 周围。)

最后,原来的定义有一个错误,应该是:

local invisible='%([BSUbfksu]|([FK]|){*})'

第二个 B 应该是 K,因为它旨在匹配背景颜色的提示转义。 (%B 启动粗体模式)

以下函数计算字符串长度的方法与提示符展开时的方法相同。与其他解决方案不同,它可以正确处理所有输入。

# Usage: prompt-length TEXT [COLUMNS]
#
# If you run `print -P TEXT`, how many characters will be printed
# on the last line?
#
# Or, equivalently, if you set PROMPT=TEXT with prompt_subst
# option unset, on which column will the cursor be?
#
# The second argument specifies terminal width. Defaults to the
# real terminal width.
#
# Assumes that `%{%}` and `%G` don't lie.
#
# Examples:
#
#   prompt-length ''            => 0
#   prompt-length 'abc'         => 3
#   prompt-length $'abc\nxy'    => 2
#   prompt-length '❎'          => 2
#   prompt-length $'\t'         => 8
#   prompt-length $'\u274E'     => 2
#   prompt-length '%F{red}abc'  => 3
#   prompt-length $'%{a\b%Gb%}' => 1
#   prompt-length '%D'          => 8
#   prompt-length '%1(l..ab)'   => 2
#   prompt-length '%(!.a.)'     => 1 if root, 0 if not
function prompt-length() {
  emulate -L zsh
  local COLUMNS=${2:-$COLUMNS}
  local -i x y=${#1} m
  if (( y )); then
    while (( ${${(%):-%$y(l.1.0)}[-1]} )); do
      x=y
      (( y *= 2 ))
    done
    while (( y > x + 1 )); do
      (( m = x + (y - x) / 2 ))
      (( ${${(%):-%$m(l.x.y)}[-1]} = m ))
    done
  fi
  echo $x
}

此函数来自Powerlevel10k ZSH theme where it's used to implement multi-line right prompt and responsive current directory truncation (demo). More info: Multi-line prompt: The missing ingredient.