Bash - 正确清除最后一次输出

Bash - Clearing the last output correctly

我正在尝试创建可更新的进度状态。为了做到这一点,我需要能够完全清除最后的输出,以便我可以更新它。 Carriage returns 可以工作,但是当输出长于终端宽度并环绕时,将无法清除最后一个输出。 所以我正在使用 tput:

n=0
while [[ $n -ne 100 ]]; do
    n=$((n+1))
    tput ed #clear
    tput sc #save cursor
    echo -n "Progress: ${n}%"
    tput rc #restore cursor
    sleep 1s
done
echo

但是如果输出足够长以至于强制终端向上滚动,这将失败。发生这种情况时,保存的光标位置不再正确,将无法正确清除最后的输出。

例如,如果光标当前在终端的底部并且输出比终端宽度长,它会强制终端向上滚动,使之前保存的光标位置无效。

那么在Bash中有什么方法可以保证光标永远不会结束终端呢?或者可能有其他替代方法来防止这个问题?

编辑:我根据 F. Hauri 的 制作了自己的版本,针对我的用例进行了简化

#!/bin/bash
str=$(head -c 338 < /dev/zero | tr '[=12=]' '1')
len="${#str}"
col=$(tput cols)
lines=$(( ((len + col - 1) / col) - 1 ))

echo -ne "${str}\r"
(( len > col )) && tput cuu "$lines"

sleep 3s

tput ed

是的,但这并不容易。

你的第一个也是最好的选择是ANSI Escape sequences,你可以通过这个代码控制光标

为了使您的脚本与其他人的终端兼容,您应该计算该终端的宽度/高度。
在这里使用 Xwininfow 你可以检查你的

wininfo | egrep -e Wid+ -e H+ -e A+

我的输出将是:

  Absolute upper-left X:  0
  Absolute upper-left Y:  45
  Width: 1600
  Height: 855

然后在基于宽度/高度的脚本中,您应该更新光标位置或清除终端上未使用的文本。

正如 user1934428 评论的那样,我们有一个更好的选择,即使用 shopt(= shell 选项)启用两个全局变量

>>> shopt | grep win
checkwinsize    on

它已打开,所以我们可以使用它们

>> echo $LINES
56
>> echo $COLUMNS
228

注意 python 比使用 bash

更容易完成这样的任务

有点棘手

灵感来自 How to get the cursor position in bash?

#!/bin/bash

lineformat="This is a very long line with a lot of stuff so they will take " 
lineformat+="more than standard terminal width (80) columns... Progress %3d%%" 

n=0
while [[ $n -ne 100 ]]; do
    n=$((n+1))
    printf -v outputstring "$lineformat" $n
    twidth=$(tput cols)      # Get terminal width
    theight=$(tput lines)    # Get terminal height
    oldstty=$(stty -g)       # Save stty settings
    stty raw -echo min 0     # Suppres echo on terminal
    # echo -en "\E[6n"
    tput u7                  # Inquire for cursor position
    read -sdR CURPOS         # Read cursor position
    stty $oldstty            # Restore stty settings
    IFS=\; read cv ch <<<"${CURPOS#$'\e['}" # split $CURPOS
    uplines=$(((${#outputstring}/twidth)+cv-theight))
    ((uplines>0)) &&
        tput cuu $uplines    # cursor up one or more lines
    tput ed                  # clear to end of screen
    tput sc                  # save cursor position
    echo -n "$outputstring"
    tput rc                  # restore cursor
    sleep .0331s
done
echo

由于 tput colstput lines 在每个循环中启动,您可以调整 window 的大小,同时 运行、cuu 参数将被重新计算。

更复杂的样本

  • 仅在 window 调整大小时使用 trap WINCH 查询终端大小
  • 添加 newlines 用于在 cuu
  • 之前向上滚动
  • 减少 forkstput

那里:

#!/bin/bash

lineformat="This is a very long line with a lot of stuff so they will take " 
lineformat+="more than standard terminal width (80) columns... Progress %3d%%" 

getWinSize() {
    {
        read twidth
        read theight
    } < <(
        tput -S - <<<$'cols\nlines'
    )
}
trap getWinSize WINCH
getWinSize

getCpos=$(tput u7)
getCurPos() {
    stty raw -echo min 0
    echo -en "$getCpos"
    read -sdR CURPOS
    stty $oldstty
    IFS=\; read curv curh <<<"${CURPOS#$'\e['}"
}
oldstty=$(stty -g)

before=$(tput -S - <<<$'ed\nsc')
after=$(tput rc)
n=0
while [[ $n -ne 100 ]]; do
    n=$((n+1))
    printf -v outputstring "$lineformat" $n
    getCurPos
    uplines=$(((${#outputstring}/twidth)+curv-theight))
    if ((uplines>0)) ;then
        printf -v movedown "%${uplines}s" ''
        echo -en "${movedown// /\n}"
        tput cuu $uplines
    fi
    printf "%s%s%s" "$before" "$outputstring" "$after"
    sleep .05
done

downlines=$((${#outputstring}/twidth))
printf -v movedown "%${downlines}s" ''
echo "${movedown// /$'\n'}"