bashtput

Bash - Clearing the last output correctly


I'm trying to create an update-able progress status. In order to do that, I need to be able to clear the last output in its entirety so that I can update it. Carriage returns can work, but when the output is longer than the terminal width and wraps around, it will fail to clear the last output. So I'm using 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

But this will fail if the output is long enough that it forces the terminal to scroll up. When that happens, the saved cursor position is no longer correct and it will fail to clear the last output correctly.

For example, if the cursor is currently at the bottom of the terminal and the output is longer than the terminal width, it will force the terminal to scroll up, invalidating the previously saved cursor position.

So are there any ways to ensure that the cursor will never the end of the terminal in Bash? Or maybe some other alternative methods to prevent this problem?

EDIT: I made my own version based on F. Hauri's answer, simplified for my use case

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

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

sleep 3s

tput ed

Solution

  • Short minimal function

    promptMsg() {
        local str="$*"
        local lines=$(( ${#str} / $(tput cols) ))
        printf '%s' "$str"
        sleep ${delay:-3}
        if ((lines)); then
            tput cuu $lines cr ed
        else
            tput cr ed
        fi
    }
    

    Then

    promptMsg 'Hello world, this is a string... '{,,}{,,} 
    

    Will show

    Hello world, this is a string...  Hello world, this is a string...  Hello world,
     this is a string...  Hello world, this is a string...  Hello world, this is a s
    tring...  Hello world, this is a string...  Hello world, this is a string...  He
    llo world, this is a string...  Hello world, this is a string... 
    

    for 3 seconds, then drop everything and show next prompt immediately under last command:

    user@host:~$ promptMsg 'Hello world, this is a string... '{,,}{,,} 
    user@host:~$ 
    

    Note: you could alter delay by prepend your command by delay=ARG:

    delay=.25 promptMsg 'Hello world, this is a string... '{,,}{,,} 
    

    Something further

    Inspired by 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 terminal settings
        stty raw -echo min 0     # Suppress echo on terminal
        # echo -en "\E[6n"         # Inquire for cursor position or
        tput u7                  # Inquire for cursor position
        read -sdR CURPOS         # Read cursor position
        stty $oldstty            # Restore terminal 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
    

    As tput cols and tput lines is initiated at each loop, you could resize window while running, cuu argument will be re-computed.

    Command stty will change terminal behaviours (noecho), see man stty.

    The command tput u7 is not well documented... I've found them by searching for How to get the cursor position in bash?.

    More complex sample

    There:

    #!/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'}"