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/tty
和stty
.使用功能这里并将代码片段修改为:
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}"
}