140

I want to implement a progress bar showing elapsed seconds in bash. For this, I need to erase the last line shown on the screen (command "clear" erases all the screen, but I need to erase only the line of the progress bar and replace it with the new information).

Final result should look like:

$ Elapsed time 5 seconds 

Then after 10 seconds i want to replace this sentence (in the same position in the screen) by:

$ Elapsed time 15 seconds 

10 Answers 10

230

The carriage return by itself only moves the cursor to the beginning of the line. That's OK if each new line of output is at least as long as the previous one, but if the new line is shorter, the previous line will not be completely overwritten, e.g.:

$ echo -e "abcdefghijklmnopqrstuvwxyz\r0123456789" 0123456789klmnopqrstuvwxyz 

To actually clear the line for the new text, you can add \033[K after the \r:

$ echo -e "abcdefghijklmnopqrstuvwxyz\r\033[K0123456789" 0123456789 

http://en.wikipedia.org/wiki/ANSI_escape_code

Sign up to request clarification or add additional context in comments.

6 Comments

This works really well in my environment. Any knowledge of compatibility?
In Bash at least that can be shortened to \e[K instead of \033[K.
@The Pixel Developer: Thanks for trying to improve it, but your edit was incorrect. The return must happen first to move the cursor to the beginning of the line, then the kill clears everything from that cursor location to the end, leaving the whole line blank, which is the intent. You put those at the beginning of each new line, similarly to Ken's answer, so that it is written on an empty line.
@DerekVeit I was using zshell which might have different behaviour. I'll have to test my hypothesis.
Just for the record, one can also do \033[G i.o. \r before \033[K though obviously \r is much simpler. Also invisible-island.net/xterm/ctlseqs/ctlseqs.html gives more details than Wikipedia and is from xterm developer.
|
137

echo a carriage return with \r

seq 1 1000000 | while read i; do echo -en "\r$i"; done 

from man echo:

-n do not output the trailing newline -e enable interpretation of backslash escapes \r carriage return 

5 Comments

for i in {1..100000}; do echo -en "\r$i"; done to avoid the seq call :-)
When you use things like "for i in $(...)" or "for i in {1..N}" you are generating all elements before iterating, that's very inefficient for large inputs. You can either take advantage of pipes: "seq 1 100000 | while read i; do ..." or use the bash c-style for loop: "for ((i=0;;i++)); do ..."
Thanks Douglas and tokland - athough the sequence production wasn't directly part of the question I've changed to tokland's more efficient pipe
My matrix printer is completely messing up my paper. It keeps jamming dots on the same piece of paper which is no longer there, how long does this program run?
how to do the same with php and stdout?
36

Derek Veit's answer works well as long as the line length never exceeds the terminal width. If this is not the case, the following code will prevent junk output:

before the line is written for the first time, do

tput sc 

which saves the current cursor position. Now whenever you want to print your line, use

tput rc tput ed echo "your stuff here" 

to first return to the saved cursor position, then clear the screen from cursor to bottom, and finally write the output.

7 Comments

Wierd, this does nothing in terminator. Do you know if there are compatibility limitations?
Note for Cygwin: You need to installed package "ncurses" to use "tput".
FYI, Here is the list of tput commands Colours and Cursor Movement With tput
It looks like this saves and restores the “graphical” position of the cursor, not its “logical” position. If the cursor initially lies on the last graphical row of the terminal window, and a line of text is written which spans on two graphical rows, then the display moves up by one row while the recorded graphical position stays the same (i.e. on the last row of the display). So we’ll end up jumping one row below where we should (observed with urxvt). I hope I’m clear. Fascinating how hard getting some piece of TUI correct is hard…
Also, it may be worth mentioning that tput ed is a more costly operation than tput el and hence (at least in my terminal, urxvt) it causes the text to blink annoyingly.
|
23

The \033 method didn't work for me. The \r method works but it doesn't actually erase anything, just puts the cursor at the beginning of the line. So if the new string is shorter than the old one you can see the leftover text at the end of the line. In the end tput was the best way to go. It has other uses besides the cursor stuff plus it comes pre-installed in many Linux & BSD distros so it should be available for most bash users.

#/bin/bash tput sc # save cursor printf "Something that I made up for this string" sleep 1 tput rc;tput el # rc = restore cursor, el = erase to end of line printf "Another message for testing" sleep 1 tput rc;tput el printf "Yet another one" sleep 1 tput rc;tput el 

Here's a little countdown script to play with:

#!/bin/bash timeout () { tput sc time=$1; while [ $time -ge 0 ]; do tput rc; tput el printf "$2" $time ((time--)) sleep 1 done tput rc; tput ed; } timeout 10 "Self-destructing in %s" 

1 Comment

That clears the whole line indeed, problem it twinkles too much for me :'(
17

In case the progress output is multi line, or the script would have already printed the new line character, you can jump lines up with something like:

printf "\033[5A"

which will make the cursor to jump 5 lines up. Then you can overwrite whatever you need.

If that wouldn't work you could try printf "\e[5A" or echo -e "\033[5A", which should have the same effect.

Basically, with escape sequences you can control almost everything in the screen.

2 Comments

The portable equivalent of this is tput cuu 5, where 5 is the number of rows (cuu to move up, cud to move down).
@Maëlan Thanks! Would you happen to know how to clear ("reset") line after running tput cuu 5?
7

If you just want to clear the previous line, the following might do the trick.

printf '\033[1A\033[K' 

For multiple lines, use it in a loop:

for i in {1..10}; do printf '\033[1A\033[K' done 

This will clear the last 10 lines.

1 Comment

One thing I'd like to add is that you can combine the pattern into one; depending on your needs. For example, if you want to delete 2 lines, you can avoid using it in a loop with this statement - printf '\033[1A\033[K\033[1A\033[K'. Use echo -e '\e[1A\e[K\e[1A\e[K' if the echo supports -e flag and cleaner to you. BTW, echo is a builtin function in most shells, so depending on the shell you're using, the echo might or might not support the -e flag.
7

You can achieve it by placing carriage return \r.

In a single line of code with printf

for i in {10..1}; do printf "Counting down: $i\r" && sleep 1; done 

or with echo -ne

for i in {10..1}; do echo -ne "Counting down: $i\r" && sleep 1; done 

Comments

5

Use the carriage return character:

echo -e "Foo\rBar" # Will print "Bar" 

2 Comments

This answer is the simplest way. You can even achieve the same effect using: printf "Foo\rBar"
But echo -e "Football\rBar" will print Bartball which is what the other answers are trying to avoid. That's IF echo -e does what you want, which it might not, hence it's best avoided in favor of printf.
0

Multiline solution


In comment referencing @Um's answer, @Maëlan correctly identified that tput sc saves only the graphical position, not logical. You can clearly see it when you run the script, while being at the last line of a terminal. The printed text behaves like it is not being cleared at all.


To solve it, as well as make the solution more optimised, I've made the following clearLastLines function:

function clearLastLines() { local linesToClear=$1 for (( i=0; i<linesToClear; i++ )); do tput cuu 1 tput el done } 

Passed as a parameter the number of lines to clear.

Example usage:

echo 'Text to be deleted 1.' echo 'Text to be deleted 2.' clearLastLines 2 echo 'New text.' 

Notes:

  • It works only when the number of lines to delete is known (shouldn't be hard to implement it when it is unknown).
  • Would be great if anyone could actually verify whether the solution is actually better for terminals like urxvt, etc. (as mentioned by @Maëlan).

Comments

-1

Instead of using backslash in your echo's, you can use tput. To simplify this you can create a function:

#!/bin/bash function consoleoneline { # Start process and save all outputs in a temporary directory tput sc # save current cursor in console local PIPE_DIRECTORY=$(mktemp -d) trap "rm -rf '$PIPE_DIRECTORY'" EXIT mkfifo "$PIPE_DIRECTORY/stdout" mkfifo "$PIPE_DIRECTORY/stderr" "$@" >"$PIPE_DIRECTORY/stdout" 2>"$PIPE_DIRECTORY/stderr" & local CHILD_PID=$! # Replace all outputs with a leading "›" # `tput` allows to reset the cursor to the previous line sed "s/^/`tput rc;tput el`› /" "$PIPE_DIRECTORY/stdout" & sed "s/^/`tput rc;tput el`› /" "$PIPE_DIRECTORY/stderr" >&2 & # Wait command has exited, remove temporary directory and reset cursor wait "$CHILD_PID" rm -rf "$PIPE_DIRECTORY" tput ed } consoleoneline bash -c 'echo 1 && sleep 1 && echo 2 && sleep 1 && echo 3' 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.