7

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
what the
  • 398
  • 3
  • 10

2 Answers2

2

Something tricky

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 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

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

More complex sample

  • Using trap WINCH for querying terminal size only when window is resized
  • Addind newlines for scrolling up before cuu
  • Reducing forks to tput

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'}"
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • Can you explain what the stty commands do? Also what is this escape: "\E[6n" – what the May 27 '20 at 10:55
  • 1
    `stty` will change terminal behaviours (noecho), see `man stty`, then `tput u7` is not well documented, but found by searching for *How to get the cursor position in bash?*. See linked answer. – F. Hauri - Give Up GitHub May 27 '20 at 12:03
-1

Yes, but it is not easy.

Your first and best options is ANSI Escape sequences and you can control cursor by this codes.

For compatibility of your script with everyone else's Terminal you should calculate the width / height of that Terminal.
Here using Xwininfow you can check yours

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

and the output for me will be:

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

Then in your script based on width / height you should update the cursor position or clear unused text on Terminal.

As user1934428 commented we have a better option by enabling two global variables using shopt (= shell options)

>>> shopt | grep win
checkwinsize    on

it is on so we can use them

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

NOTE that such a task by python is much easier to do than using bash

oguz ismail
  • 1
  • 16
  • 47
  • 69
Shakiba Moshiri
  • 21,040
  • 2
  • 34
  • 44
  • @KamilCuk for the beauty of orange color, the same theme with stack-over-flow && I love SVG. && if you want to remove it, remove it – Shakiba Moshiri May 27 '20 at 09:02
  • 2
    With the beauty of orange color - for 100% it looks cool. I do not want it removed, no no, I just wanted to let you know that something like `---` exists. – KamilCuk May 27 '20 at 09:02
  • From the looks of it ANSI Escape Sequences don't really do anything that tput can't do. Can you clarify how knowing the height/width of the terminal will help clear text when the terminal gets scrolled up? – what the May 27 '20 at 09:14
  • @whatthe **tput** is good but I am not much familiar with it. Please take look at this q [color in terminal](https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux) also there is good answer for **tput** | As I said it requires width / height calculation .... and most moderns CLI use ** python** – Shakiba Moshiri May 27 '20 at 09:21
  • 1
    @ShakibaMoshiri : For calculating the Windows size, do a `shopt -s checkwinsize` and retrieve the size from `$LINES` and `$COLUMNS`. – user1934428 May 27 '20 at 09:23
  • 1
    @ShakibaMoshiri : And, doesn't xwininfo require that X is running (which we don't know; the OP does not mention it). Also, _Width_ and _Height_ is the size in pixel, which we don't need, unless the OP is using a proportional font - in which case we can't use a simple bash approach anyway. – user1934428 May 27 '20 at 09:26
  • @user1934428, Yes, you are right , my answer is just a guide and not a complete solution. `shopt` also was very good, I did not know it , thanks – Shakiba Moshiri May 27 '20 at 09:30
  • @KamilCuk of course I know about --- but I did not link it. I created that SVG specifically for stack-over-flow . [badge-for-git](https://github.com/k-five/s/) and of course you can use it :) – Shakiba Moshiri May 27 '20 at 09:35
  • Ok to clarify, using this method we first calculate the length of the string and compare it to the column length to check if it'll wrap around. After outputting the string, move the cursor accordingly to reach the beginning of the string using tput or ANSI, then clear it using tput ed. Is that what you are suggesting? Though of course this assumes that the cursor is currently at the leftmost position. – what the May 27 '20 at 09:41
  • @whatthe ... yes . As I said it seems easy but it is NOT. because we have to be very accurate about cursor's positions. In fact for any good CLI running on a terminal we should know : Up / Down / Right / Left / Clear code for the cursor either we using **tput** or **ANSI**. I used to play with them much , but nowadays not too much. – Shakiba Moshiri May 27 '20 at 09:48