Issue
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
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 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
- Using
trap WINCH
for querying terminal size only when window is resized - Addind
newlines
for scrolling up beforecuu
- 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'}"
Answered By - F. Hauri - Give Up GitHub Answer Checked By - Mildred Charles (WPSolving Admin)