当文本比终端宽时(例如换行多行),准确覆盖 bash 终端输出的前几行?

当文本比终端宽时(例如换行多行),准确覆盖 bash 终端输出的前几行?

TL;DR - 我知道如何正常覆盖输出行,但是printf '\e[1A\e[2K'当被覆盖的行长于终端宽度时(例如换行),我以前使用过的方法(例如)或在网上找到的方法似乎都不起作用被触发)。禁用换行会有效地截断显示的文本。是否还有其他技巧或工具或某种方法来处理我所缺少的这种情况?

我有一个 bash 脚本,可以在桌面(Fedora)和手机(Android 上的 Termux)之间共享。就功能而言,我没有任何问题,一切都按预期进行。但脚本相当冗长,而且终端输出一团糟。最近,我了解到我可以使用ANSI 转义码从 bash 覆盖前一行输出并显着清理脚本中的输出,同时仍然了解其进度并在遇到错误时查看任何错误。我对这些的理解仍然非常基本,但从我的测试来看,它似乎printf识别\e为 ASCII 转义的开始,然后基于ESC[#A“将光标向上移动 # 行”和ESC[2K“删除整行”。

不管怎样,我遇到的一个问题是,我期望除最后一行之外的所有行都被覆盖,而其他多行仍在显示。最初,我认为这是由于一些 Termux 错误造成的,但后来我确认这是由于终端的大小(宽度)造成的(我可以通过gnome-terminal调整窗口大小或增加文本长度来重新创建问题) )。基本上,我看到的是,如果我希望覆盖的输出行比终端的宽度长,那么该行似乎将剩余文本“包装”到新行上,并且覆盖仅替换包装的文本文本的一部分。

这是一个片段,重现了我在脚本中遇到的问题:

# create an array with variable-length texts to simulate status messages
arrStatusTexts=(  );
for i in {10..200..10}; do
    arrStatusTexts+=("$(printf '%*s\n' $i ' ' | tr ' ' X)");
done
 
# print out status at each step, overwriting output of each previous step as we go
echo "";
echo "--------------------";
echo "Steps of process"
echo "--------------------";
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    [[ 0 != "$i" ]] && printf '\e[1A\e[2K';
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done

在运行上述命令之前,从我的桌面终端窗口中,我得到:

$ stty size
34 135

编辑:从我的桌面和 termux,我的 TERM 变量显示为:

$ echo "$TERM"
xterm-256color

运行上面的 for 循环后我希望看到的最终输出是这样的:

Step 20 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

for 循环完成后我实际看到的是:

Step 13 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 14 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 15 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 16 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 17 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 18 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 19 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Step 20 of 20: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

基本上它正确地覆盖了步骤 1-12,因为这些步骤小于我的终端窗口的宽度。但对于步骤 13 以后的步骤,每条线的长度都会“缠绕”,并且仅清除缠绕的部分。


2022 年 9 月 1 日编辑:发现更多奇怪之处。基于答案这里这里,使用该\e[6n序列,我试图获取每个printf语句之前的光标位置,以查看这是否有用。

将上面的内容修改为:

# from: https://unix.stackexchange.com/a/183121/379297
function pos () {
    local CURPOS
    read -sdR -p $'\E[6n' CURPOS
    CURPOS=${CURPOS#*[} # Strip decoration characters <ESC>[
    echo "${CURPOS}"    # Return position in "row;col" format
}
export -f pos;
arrCursorPos=(  );
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    # get cursor position
    arrCursorPos+=("$(pos)");
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done
for ((i=0; i < ${#arrCursorPos[@]}; i++)); do
    echo "cursor pos $(($i + 1)): '${arrCursorPos[$i]}'";
done

我得到的状态消息的输出与上面相同,但这是保存光标位置的第二个数组的输出:

cursor pos 1: '19;1'
cursor pos 2: '20;1'
cursor pos 3: '21;1'
cursor pos 4: '22;1'
cursor pos 5: '23;1'
cursor pos 6: '24;1'
cursor pos 7: '25;1'
cursor pos 8: '26;1'
cursor pos 9: '27;1'
cursor pos 10: '28;1'
cursor pos 11: '29;1'
cursor pos 12: '30;1'
cursor pos 13: '31;1'
cursor pos 14: '33;1'
cursor pos 15: '34;1'
cursor pos 16: '34;1'
cursor pos 17: '34;1'
cursor pos 18: '34;1'
cursor pos 19: '34;1'
cursor pos 20: '34;1'

起初,我认为这不能正常工作,因为索引 15-20 的位置相同。清除屏幕 ( Ctrl+L) 并重新运行几次后,我看到不同的输出

cursor pos 1: '11;1'
cursor pos 2: '12;1'
cursor pos 3: '13;1'
cursor pos 4: '14;1'
cursor pos 5: '15;1'
cursor pos 6: '16;1'
cursor pos 7: '17;1'
cursor pos 8: '18;1'
cursor pos 9: '19;1'
cursor pos 10: '20;1'
cursor pos 11: '21;1'
cursor pos 12: '22;1'
cursor pos 13: '23;1'
cursor pos 14: '25;1'
cursor pos 15: '27;1'
cursor pos 16: '29;1'
cursor pos 17: '31;1'
cursor pos 18: '33;1'
cursor pos 19: '34;1'
cursor pos 20: '34;1'

并意识到发生了什么事。对于最后几个数组元素,它到达最后一行(在我的例子中为 34 - 正如 col1 by 中报告的那样stty size)。此时,输出的任何新行都会导致显示的文本滚动,但我仍然会停留在最后一行 (34)。所以这个方法看起来这可能是跟踪初始光标位置的可靠方法。

我还尝试了另一种方法(建议 这里,这里, 和这里涉及exec < /dev/ttystty.使用功能这里并将代码片段修改为:

function extract_current_cursor_position () {
    export $1
    exec < /dev/tty
    oldstty=$(stty -g)
    stty raw -echo min 0
    echo -en "\033[6n" > /dev/tty
    IFS=';' read -r -d R -a pos
    stty $oldstty
    eval "$1[0]=$((${pos[0]:2} - 2))"
    eval "$1[1]=$((${pos[1]} - 1))"
}
export -f extract_current_cursor_position;
arrCursorPos=(  );
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    # get cursor position
    extract_current_cursor_position pos1
    arrCursorPos+=("${pos1[0]} ${pos1[1]}");
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done
for ((i=0; i < ${#arrCursorPos[@]}; i++)); do
    echo "cursor pos $(($i + 1)): '${arrCursorPos[$i]}'";
done

清屏后重新运行此命令,我得到:

cursor pos 1: '10 0'
cursor pos 2: '11 0'
cursor pos 3: '12 0'
cursor pos 4: '13 0'
cursor pos 5: '14 0'
cursor pos 6: '15 0'
cursor pos 7: '16 0'
cursor pos 8: '17 0'
cursor pos 9: '18 0'
cursor pos 10: '19 0'
cursor pos 11: '20 0'
cursor pos 12: '21 0'
cursor pos 13: '22 0'
cursor pos 14: '24 0'
cursor pos 15: '26 0'
cursor pos 16: '28 0'
cursor pos 17: '30 0'
cursor pos 18: '32 0'
cursor pos 19: '32 0'
cursor pos 20: '32 0'

这个似乎也可以。虽然我不确定为什么该extract_current_cursor_position函数从 y 值中减去 2,从 x 值中减去 1。我可能需要调查一下或删除它的减法

我仍然需要研究 ncurses 选项(例如tput)。我已经检查过 ncurses 包至少在 Termux 上可用,但我会在测试更多内容时回来填写更多信息。


我可以做出的明显改变是:

1. 不要改变我的脚本,就像我一直在做的那样,忍受多行混乱。但我强烈希望修复我的输出,除非它变得太多工作。

2. 缩短所有状态文本并减少输出中的打印变量,直到所有内容都小于最小屏幕宽度(stty size 在我的手机报告中17 48,例如将所有内容限制为每行 48 个字符)。虽然我不介意进行一些重构并付出更多的努力,但进行数百个文本更改的想法似乎非常乏味,并且没有告诉我实际发生的情况。如果没有更好的方法,我宁愿将此保留为最后的手段。那个和这个可能会丢失有意义的信息,或者需要我对事物的显示方式做出其他妥协。

3. 与 2 相同,但用于print -v msg将输出放入变量中,然后打印截断的文本,例如printf '%s\n' "${msg:0:48}"。与 2 相同的问题,但我也可能会丢失一些有意义的信息。

4. 与上面类似,但不是截断,只需跟踪前一条消息的长度并除以终端宽度以确定printf '\e[1A\e[2K';我应该使用的语句数。仍然有一些工作,但比我必须编辑每条消息要少,并且应该给出更好的最终结果。

很好奇是否有一种更简单的方法可以解决我所缺少的问题。也许某种方式可以“grep”已经打印并清除到当前位置的特定偏移量的文本(在每个状态行的开头添加 unicode 字符或星号或其他内容将非常容易搜索/替换)。或者我不知道的一些命令或内置命令?除了我上面列出的之外,我的谷歌没有找到任何明显的解决方案。

我不需要完全符合 POSIX 标准的解决方案;只是一个可以在 bash 中使用标准工具(python/perl/awk一句台词都是公平的游戏......但将我的整个 bash 脚本重写为其中之一则不是)。考虑到这个域通常涉及桌面问题,我并不期望熟悉 Termux,但如果解决方案需要图形会话(例如无法在 ssh 上工作)或仅在 x86_64 架构上可用的工具(Termux 使用 ARM 版本),那么它可能无法工作。最常见的 bash/linux 工具似乎在那里工作得很好。我的桌面目前有 bash 5.1.8(我很快就会升级到 Fedora 36),Termux 有 bash 5.1.16 的 ARM 版本。

答案1

大约在为您的问题编写解决方案的过程中,我意识到这就是为什么 (n)curses 终端虚拟化如此容易的原因:它隐藏了所有令人讨厌的终端相关细节(其中大多数,以及其中一些)。直接使用 terminfo 很痛苦 - 但比原始转义序列更好。

这是bash代码。重写应该不难sh

# Output a printf style format string and arguments and return the cursor
# to the beginning of the line. DO NOT use newline `\n`.
#
lineOut() {
    local rows cols len lines

    # Number of rows/columns on the terminal device
    rows=$(tput lines)
    cols=$(tput cols)

    # Output
    printf "$@"

    # How many lines we wrote
    len=$(printf "$@" | unexpand -a | wc -m)
    lines=$(( len / cols ))

    if tput am
    then
        # Cursor does not wrap when writing to the last column
        len=$(( len - (cols * lines) ))
        [[ $len -eq 0 ]] && (( lines-- ))
    fi

    # Move up the necessary number of lines to column 1
    printf '\r'
    for (( ; lines > 0; lines-- )); do tput cuu1; done
}


# Populate the arrStatusTexts (demo only) to simulate status messages
arrStatusTexts=()
for i in {10..200..10}; do
    arrStatusTexts+=("$(printf '%*s\n' "$i" ' ' | tr ' ' X)")
done

# Output
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do
    lineOut 'Step %s of %s: %s' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
    sleep 1
done
printf '.\n'

使用的 terminfo 代码tput记录在man 5 terminfo。使用没有必要的控制代码集(export TERM=dumb例如 try )的终端类型运行时,解决方案会完全降级。


在研究解决方案时,我发现了u7( user7),这恰好触发了“你的光标位置在哪里?” 终端问题:

# Magic to read the current cursor position (origin 1,1)
tput u7; read -t1 -srd'[' _; IFS=';' read -t1 -srd'R' y x

它不再与此处提出的解决方案相关,但它可能作为奖励有用。

答案2

一种方法是记录光标所在位置,然后根据需要跳回该点。这可以通过转义序列争论手动完成,但这需要更多工作。尽管 Curses 确实有一个scrollok调用来影响在窗口末尾打印某些内容时发生的情况,但此方法可能无法很好地处理滚动文本。可能取决于您想在回滚窗口中看到什么,或者只是在非滚动窗口中显示文本;也许可以尝试使用备用屏幕来显示一件事,然后再显示另一件事?

#!/usr/bin/env perl
use strict;
use warnings;
# or you could do it the hard way with XTerm Control Sequences: ask the
# terminal where the cursor is, manually parse the result, etc
use Curses;
initscr;

my ( $y, $x );    # where we started printing from to jump back to

move int rand(4), 0;    # put the cursor somewhere (start point)
getyx $y, $x;           # record this

my $i = 0;
while ( ++$i < 10 ) {
    clrtobot;       # maybe subsequent messages are shorter? clear all below
    addstring $i x ( $COLS + int rand 8 );    # noise that wraps a line
    refresh;
    sleep 1;
    move $y, $x;                              # back to where we started from
}

endwin;

答案3

一个老问题但是......如果将已经提到的 \r 与 printf 的 %s 精度选项结合起来,就会给你一个空格填充的固定宽度字段,你可以根据需要覆盖它

这是不是我会如何做,但你特别想要一些不需要对代码进行大量更新的东西。实际上,我通常会执行一个“msg()”函数,将修剪后的输出写入屏幕,并将未修剪的输出写入日志..但为了讨论:

cols=$( tput cols )
eol=$'\r'
printf() {
    command printf "%-${cols}.${cols}s${eol:-\n}" "$( command printf "${1%\\n}" "${@:2}" )"
}

# print out status at each step, overwriting output of each previous step as we go
echo "";
echo "--------------------";
echo "Steps of process"
echo "--------------------";
echo
for ((i=0; i < ${#arrStatusTexts[@]}; i++)); do

    eol=$'\r'
    (( i % 5 )) || eol=$'\n'
    printf 'Step %s of %s: %s\n' "$(($i + 1))" "${#arrStatusTexts[@]}" "${arrStatusTexts[$i]}";
done
echo

鉴于您可以轻松获得屏幕行和列,您也可以打印到 $(( $rows - 2 )) 或其他...

#print_at [col] [row] [normal printf params...]
printf_at() {
    printf "\E[%d;%dH" "$1" "$2"
    shift 2
    eval "printf \"$@\""
}

作为更安全的版本,需要预先格式化字符串

# printf_at [col] [row] "$( printf fmt vars.... )"
printf_at() {
    printf "\E[%d;%dH%b" "$1" "$2" "${@:2}"
}

相关内容