确定制表符'\ t'在一行上有多长时间


10

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

例如,如果我有一个带有制表符分隔符的示例文件,并且字段内容适合少于一个制表符(≤7),并且如果我之后有一个制表符,则该制表符将仅为'制表符大小–字段大小长度。

有没有一种方法可以获取一行中制表符的总长度?我不是在寻找选项卡的数量(即10个选项卡不应返回10),而是这些选项卡的字符长度。

对于以下输入数据(在字段之间分隔的选项卡和仅一个选项卡):

field0  field00 field000        last-field
fld1    fld11   fld001  last-fld
fd2     fld3    last-fld

我希望计算每行中制表符的长度,因此

11
9
9

Answers:


22

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

您还可以不定期地设置制表位:

$ tabs 3 9 11; printf '\tx\ty\tz\n'
  x     y z

只有终端知道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))

wc -L给出输入中最宽线的宽度。它通过wcwidth(3)确定字符的宽度并假定制表位是每8列来做到这一点。

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


¹请注意,尽管这样做stty tab3,tty设备行规程将接管制表符处理(根据发送光标到终端之前基于游标可能在何处的想法将TAB转换为空格),并实现制表符每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"
)

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


7
$ expand file | awk '{ print gsub(/ /, " ") }'
11
9
9

POSIX expand实用程序将选项卡扩展到空格。该awk脚本计算并输出替换每行所有空格所需的替换数量。

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

$ tr ' ' '@' <file | expand | awk '{ print gsub(/ /, " ") }'

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

如果您希望每个选项卡有10个空格而不是普通的8个空格:

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

3
您需要x在调用expand否则之前用其他一个全角字符(例如)替换空格,同时还要计算最初在输入中的空格。
斯特凡Chazelas

1
expand还假设每8列使用制表位(尽管您可以使用选项进行更改)。请注意,GNU实现不支持多字节字符(更不用说0宽度或全角字符了)。用IIRC的FreeBSD可以。
斯特凡Chazelas

@StéphaneChazelas当然,除非这当然是计划将0x09的宽度与0x20一起计算的一部分;-)
can-ned_food

2

perl

perl -F/\\t/ -lpe '$c = 0; $F[-1] eq "" or pop @F; $_ = (map { $c += 8 - (length) % 8 } @F)[-1]' file

或者:

perl -MList::Util=reduce -lpe \
    '@F = split /\t/, $_, -1; pop @F if $F[-1] ne ""; $_ = reduce { $a + $b } map { 8 - (length) % 8 } @F' file

如果您希望TAB的长度不同,则可以将上面的8更改为其他值。


2

也使用expand,但通过bash参数操作来计算空格数:

$ line=$'field0\tfield00\tfield000\tlast-field'
$ tabs2spaces=$(expand <<<"$line")
$ only_spaces=${tabs2spaces//[^ ]/}    # remove all non-space characters
$ echo "${#only_spaces}"
11
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.