4

A popular method for getting progress bars on the command line is to use the carriage return character, which overwrites the previous line, as seen in this script:

#!/bin/bash
echo -ne '[#   ] 25%\r'
sleep 1
echo -ne '[##  ] 50%\r'
sleep 1
echo -ne '[### ] 75%\r'
sleep 1
echo -ne '[####] 100%\r'

This is OK... if you only want print a progress bar. But a more sophisticated script may want to print a progress bar WHILE also outputting diagnostic output. You'd like the buffer of your command line application to evolve as follows (separated with ----):

[#   ] 25%
----
msg1
[#   ] 25%
----
msg1
msg2
[#   ] 25%
----
msg1
msg2
[##  ] 50%

etc. And now, the popular approach fails:

#!/bin/bash
echo -ne '[#   ] 25%\r'
echo "msg 1"
sleep 1
echo "msg 2"
echo -ne '[##  ] 50%\r'
echo "msg 3"
sleep 1
echo "msg 4"
echo -ne '[### ] 75%\r'
sleep 1
echo -ne '[####] 100%\r'

outputs

ezyang@sabre:~$ ./test.sh 
msg 1] 25%
msg 2
msg 3] 50%
msg 4

It would seem that to implement this, any other time you want to print a message, you need to:

  1. Check if it's a progress bar,
  2. If it is, clear the line, print your output plus a newline, and then reprint the progress bar

(Alternately, you could use ncurses). Unfortunately, this requires every possible thing which could give output to play ball.

Is there any modular method for making this kind of progress bar work, which doesn't require instrumenting all other methods of outputting to the terminal?

Edward Z. Yang
  • 26,325
  • 16
  • 80
  • 110

2 Answers2

5

Update: You can do this with ANSI/VT100 escape codes/sequences.

Before I add a working example, you need to be aware of the following:

  • You need to make sure you're outputting to a TTY/console.

  • You need to be aware of the difference between IO (stdout/stderr) lines, and TTY lines. \r will get you to the beginning of an IO line, even if the line spans more than one TTY line.

  • Those codes/sequences are not supported natively in the Windows console. But you can use them in ConEmu (recommended), or with ansicon.


Here is a working example. The code should be self-explanatory:

 #!/bin/bash

 ERASE_SCREEN_AFTER="\033[0J"
 ERASE_LINE_BEFORE="\033[1K"
 ERASE_LINE_AFTER="\033[0K"
 UP="\033[1A"

 up_count=2
 messages="\n"
 progress=""

 echo_repeat() {
   c="$2"
   while (( c )); do
     echo -en "$1"
     (( c-- ))
   done
 }

 update_status() {
   echo_repeat "$UP" "$up_count"
   echo -en "$ERASE_LINE_BEFORE" "$ERASE_SCREEN_AFTER" "\r"
   echo -en "$messages"
   echo "$progress"
 }

 add_msg() {
   messages+="$@\n"
   update_status
   (( up_count++ ))
 }

 set_progress() {
   progress="$@"
   update_status
 }

 echo_repeat "\n" $up_count

 set_progress '[#   ] 25%'
 add_msg "msg 1"
 sleep 1

 add_msg "msg 2"
 set_progress '[##  ] 50%'
 add_msg "msg 3"
 sleep 1

 add_msg "msg 4"
 set_progress '[### ] 75%'
 sleep 1

 set_progress '[####] 100%'

You can do much more with those codes/sequences alone, without the use of (n)curses. That includes setting foreground/background colors, using bold fonts, among other functionalities.

saldl is an example of a CLI program where those codes/sequences are extensively used.


Old Answer (obsolete): If I understand your question correctly, all you need to do is add an \n before the msg line.

Not Important
  • 287
  • 1
  • 8
0

I have combined the previous answer with another one, from another thread related to progress bar in bash:

#! /usr/bin/env bash

function ProgressBar {
    # Set escape codes
    local ERASE_SCREEN_AFTER="\033[0J"
    local ERASE_LINE_BEFORE="\033[1K"
    # Set progress bar width
    local bar_width=40
    # Proccess data
    local progress=$(( ($1*100*100/$2)/100 ))
    local done=$(( (progress*bar_width/10)/10 ))
    local left=$(( bar_width-done ))
    # Build progressbar string lengths
    local fill empty
    fill=$(printf "%${done}s")
    empty=$(printf "%${left}s")
    # Output message and progress bar
    echo -en "$ERASE_LINE_BEFORE" "$ERASE_SCREEN_AFTER" "\r"
    printf "\r${3}\nProgress : [${fill// /\#}${empty// /-}] ${progress}%%"
}

# Proof of concept
start=1
end=100

for i in $(seq $start $end); do
    sleep 0.1
    ProgressBar "$i" $end "Message $i"
done
printf '\nFinished!\n'

This code gave a result that I'm happy with:

$ ./progress.sh
Message 1
Message 2
...
...
Message 99
Message 100
Progress : [########################################] 100%
awerebea
  • 56
  • 5