确定标签 '\t' 在一行上的长度

αғs*_*нιη 12 text-processing control-characters

在文本处理字段中,有没有办法知道制表符的长度是 8 个字符(默认长度)还是更少?

例如,如果我有一个带有制表符分隔符的示例文件,并且一个字段的内容适合少于一个制表符 (?7),并且如果我之后有一个制表符,那么该制表符将只是“制表符大小 – 字段大小” '的长度。

有没有办法获得一行上标签的总长度?我不是在寻找标签的数量(即 10 个标签不应该返回 10),而是这些标签的字符长度。

对于以下输入数据(字段之间的制表符分隔且只有一个制表符):

field0  field00 field000        last-field
fld1    fld11   fld001  last-fld
fd2     fld3    last-fld
Run Code Online (Sandbox Code Playgroud)

我希望计算每行中标签的长度,所以

11
9
9
Run Code Online (Sandbox Code Playgroud)

Sté*_*las 22

TAB字符是一个控制字符,当发送到终端¹时,它会使终端的光标移动到下一个制表位。默认情况下,在大多数终端中,制表位相隔 8 列,但这是可配置的。

您还可以不规则间隔设置制表位:

$ tabs 3 9 11; printf '\tx\ty\tz\n'
  x     y z
Run Code Online (Sandbox Code Playgroud)

只有终端知道 TAB 将光标向右移动多少列。

您可以通过在发送选项卡之前和之后从终端查询光标位置来获取该信息。

如果您想为给定的行手动进行计算并假设该行打印在屏幕的第一列,您需要:

  • 知道制表位在哪里²
  • 知道每个字符的显示宽度
  • 知道屏幕的宽度
  • 决定是否要处理其他控制字符\r(将光标移至第一列)或\b将光标移回...)

如果您假设制表位是每 8 列,并且该行适合屏幕并且没有其他控制字符或终端无法正确显示的字符(或非字符),则可以简化它。

使用 GNU wc,如果该行存储在$line

width=$(printf %s "$line" | wc -L)
width_without_tabs=$(printf %s "$line" | tr -d '\t' | wc -L)
width_of_tabs=$((width - width_without_tabs))
Run Code Online (Sandbox Code Playgroud)

wc -L给出输入中最宽线的宽度。它通过使用wcwidth(3)来确定字符的宽度并假设制表位是每 8 列来实现的。

对于非 GNU 系统,并具有相同的假设,请参阅@Kusalananda 的方法。它甚至更好,因为它允许您指定制表位,但不幸的是expand,当输入包含多字节字符或 0 宽度(如组合字符)或双宽度字符时,目前不适用于 GNU (至少)。


¹ 请注意,如果您这样做stty tab3,tty 设备行规则将接管制表符处理(根据它自己对光标在发送到终端之前可能所在位置的想法将制表符转换为空格)并每 8 列实现制表符停止。在 Linux 上测试,它似乎可以正确处理 CR、LF 和 BS 字符以及多字节 UTF-8 字符(前提iutf8是也打开),但仅此而已。它假设所有其他非控制字符(包括零宽度、双宽度字符)的宽度为 1,它(显然)不处理转义序列,不正确包装......这可能是用于终端的不能做标签处理。

在任何情况下,tty 行规则确实需要知道光标在哪里并使用上面的那些启发式方法,因为当使用icanon行编辑器时(比如当你为这样的应用程序输入文本时cat,没有实现他们自己的行编辑器),当你按TabBackspace,线路规则需要知道发送多少个 BS 字符以擦除该 Tab 字符以进行显示。如果您更改制表符停止的位置(如使用tabs 12),您会注意到制表符未正确擦除。如果在按 之前输入全角字符,则相同TabBackspace


² 为此,您可以发送制表符并在每个制表符之后查询光标位置。就像是:

tabs=$(
  saved_settings=$(stty -g)
  stty -icanon min 1 time 0 -echo
  gawk -vRS=R -F';' -vORS= < /dev/tty '
    function out(s) {print s > "/dev/tty"; fflush("/dev/tty")}
    BEGIN{out("\r\t\33[6n")}
    $NF <= prev {out("\r"); exit}
    {print sep ($NF - 1); sep=","; prev = $NF; out("\t\33[6n")}'
  stty "$saved_settings"
)
Run Code Online (Sandbox Code Playgroud)

然后,您可以将其用作expand -t "$tabs"@Kusalananda 的解决方案。


Kus*_*nda 8

$ expand file | awk '{ print gsub(/ /, " ") }'
11
9
9
Run Code Online (Sandbox Code Playgroud)

POSIXexpand实用程序将制表符扩展为空格。该awk脚本计算并输出替换每行上所有空格所需的替换次数。

为避免计算输入文件中任何预先存在的空格:

$ tr ' ' '@' <file | expand | awk '{ print gsub(/ /, " ") }'
Run Code Online (Sandbox Code Playgroud)

where@是保证输入数据中不存在的字符。

如果你想要每个标签 10 个空格而不是普通的 8 个:

$ tr ' ' '@' <file | expand -t 10 | awk '{ print gsub(/ /, " ") }'
9 
15
13
Run Code Online (Sandbox Code Playgroud)

  • 在调用 `expand` 之前,你想用其他一些单宽字符(如 `x`)替换空格,否则,你还要计算输入中最初的空格。 (3认同)