Login Register






Thread Rating:
  • 0 Vote(s) - 0 Average


Tutorial Command line progress bars filter_list
Author
Message
Command line progress bars #1
So last night while browsing the discord, one of the members was talking about some program he had written, and posted the following screenshot
[Image: U2u7n0n.png]
So, i had a look at it, it seems like its some kind of console based music downloading program, which is neat, since it looks like it handles multiple
sources just fine. The one thing I hated about it was that it didn't give any indication on progress, it just told you what it was downloading and it
was up to you to tell if it had stalled or if it was still going. So, I posted some code from slbot in
there to show him how to do a simple progress bar. Of course this didn't fit the scenario all that well because that progress bar was time based, and
this program was for downloads. So, it needed a little modification, that's what this tutorial is for.

We'll start with building the time based one.
Here's what my criteria for writing this was:
  1. The progress bar needs to be portable
  2. It needs to be the same width for every application
  3. Must have the ability to update faster than one segment of the bar
  4. Have a spinner to indicate when progress is actually happening

So, I came up with the following design:
Use two functions:
One that draws a progress bar if it's given the number of segments to place on the bar and some indicator of data moving.
And one that does the actual work, which will call the drawing function when it has any info.

For the first function, the two inputs will be:
  1. A number (0 - 100)
  2. A number (0 - 3) that will increment and loop as progress happens

For the second function, I require at least
  1. How often to update the progress bar (in milliseconds)
  2. A pointer to the drawing (callback) function

Ok, so let's to the easy part, writing the second function. This one is easy because it's just a simple for-sleep loop.
Code:
void waitLoop(
             unsigned ms, /* the total time to run the loop for */
             unsigned interval, /* how often to update the progress bar (in ms) */
             progressCallback callback) /* a pointer to the drawing function */
{
    unsigned i, j, s, count = ms/interval; // here we define some loop variables, and count is the number of intervals in our wait loop
    for (i = 0, s = 0; i < ms; i += interval, ++s) // loop while i (our time elapsed variable) is less than what we want to wait for, increment s
(our step) each time
    {
         (*callback)( /* call our callback with */
                     s % 4, /* our step, fixed to the range 0-3 */
                     (i * 100) / ms); /* the integer percentage of our total */
         usleep(interval * 1000); // sleep for <interval> milliseconds (usleep works in microseconds)
    }
}

Ok cool, I hope I explained that well enough. As we move on, I won't be keeping the formatting and commenting like that, it takes up too much space. Now, moving on to the progress drawing function.
This one is actually pretty simple, we follow this process
  1. Clear the line
  2. Move to the beginning of the line
  3. Print our opening bracket ( [ )
  4. Print <step> segments ( # )
  5. Print 100 - <step> spaces
  6. Print our closing bracket ( ] )
  7. Display the spinner
That one is pretty easy once it's broken down, so here's the code
Code:
void progress(unsigned spin, unsigned step)
{
       int i;
       const char *pgstep = "|\\-/";  // this holds the characters for our spinner, the order is important because step is modulo 4
       printf("\r"); for (i = 0; i < 109; ++i); printf(" "); printf("\r ["); // clear the line
       for (i = 0; i < step; ++i) printf("#"); // print bar
       for (; i < 100; ++i) printf(" "); // print blank space
       printf("] %c\b", step > 99 ? ' ' : pgstep[spin]);
       fflush(stdout);
}

Perfect, now let's write a simple main function and give it a test
Code:
int main()
{
       waitLoop(15 * 1000, 75, &progress); // wait 15 seconds, update every 75 ms
       return 0;
}
[Image: FSL4VPt.png]
Cool, it works! Now let's make this exact same progress bar show the progress of a download, like say an arch linux ISO. To do this, we need to do a little trickery, see cURL has a feature that lets you spy on the progress of an operation, but it wouldn't allow us to pass in data like our callback, interval, step, etc. So to do this, we use a nice little hack in the way the C language works, the ability to cast to anything with a void *.
See, the function signature that lets us do that looks like this:
Code:
static int xferinfo(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
That p variable is set by cURL, but since it's a structure we can use it to our advantage, see cURL will be using it's own structure, but if we make one of our own that contains all of the same data as the one cURLdoes, then we can add whatever we want to it and cURL won't touch it, but will pass it through to us unchanged. Here's our structure:
Code:
struct cURLProgress
{
       double lastrun;
       CURL *curl;
       progressCallback callback;
       unsigned interval;
       unsigned char step;
};
Now, we could have done this with a couple global variables, but that would mean that we could only have one download at a time running, or else they would interfere with each other, this way is just cleaner. You want to avoid using global variables whenever possible, these variables are only valid within a specific context, so let's keep them grouped in with those other context variables.
Ok, so now that we have all that defined, let's write a pretty basic xferinfo function:
Code:
static int downloadProgress(void *p, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow)
{
       /* define some local variables */
       struct cURLProgress *info;
       CURL *curl;
       double curtime; // we'll use this to check how long it's been running for (to determine the interval)
       info = p;
       curl = info->curl; // and again, set our local curl pointer
       curtime = 0; // initialize this to 0, just in case something goes wrong, we will have a safe value
       curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &curtime); // retrieve from CURL the time that the download has been running
       if ((curtime - info->lastrun) > ((double)info->interval / 1000.0) && dltotal > 0) // if we last got data > interval ms ago, and we've downloaded at least 1 byte, print progress
       {
               info->lastrun = curtime;                        // we set this so that we know the last time we got data was now
               (*info->callback)(info->step++ % 4, (dlnow * 100) / dltotal); // go ahead and call our callback, notice how we increment step inline
       }
       return 0; // we MUST return 0, if we don't, the download will stop
}

Ok, so that was pretty easy, now let's write some code that makes it actually download the file. To do this, we need to
  1. initialize and set up cURL
  2. build our custom context structure
  3. do error checking
  4. download the file
  5. clean up
It's actually a pretty basic function, of course, this needs to keep the same minimum arguments as our first progress method, the interval and the callback pointer.
Code:
int downloadFile(const char *URL, unsigned interval, progressCallback callback)
{
       CURL *curl;
       CURLcode res;
       struct cURLProgress progressInfo;
       res = CURLE_OK;
       if ((curl = curl_easy_init()) != NULL)
       {
               /* set up our context */
               progressInfo.lastrun = 0;
               progressInfo.curl = curl;
               progressInfo.callback = callback;
               progressInfo.interval = interval;
               progressInfo.step = 0;

               /* set up cURL */
               curl_easy_setopt(curl, CURLOPT_URL, URL);
               curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, downloadProgress);
               curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progressInfo);
               curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);

               /* do the operation, check for errors */
               if ((res = curl_easy_perform(curl)) == CURLE_OK)
                       (*callback)(0, 100);
               curl_easy_cleanup(curl); // clean up
       }
       printf("\n"); // we have to print a newline so that the progress bar stays on screen
       return res;
}

Ok, perfect, the only problem is that this will put all of the file contents on screen, we don't want that. Rather than force the user to use bash's output forwarding to a file, we're just going to allow the user to supply a file, and we'll make sure it gets written.

Good news! cURL has that functionality built in, we just need to tell it how to write, and where to write. Let's go ahead and write the function that tells it how to write:
Code:
size_t cURLWriteData(void *ptr, size_t size, size_t nmemb, FILE *stream)
{
    return fwrite(ptr, size, nmemb, stream);
}
this is a pretty simple function. It doesn't do any sort of error checking (though it should), but it will do the job for now.
Now, it's as simple as modifying our downloadFile function to take advantage of this. We only need a couple more lines of code, and an extra parameter. here's the new function:
Code:
int downloadFile(const char *URL, const char *path, unsigned interval, progressCallback callback)
{
       CURL *curl;
       CURLcode res;
       FILE *fp; /* NEW */
       struct cURLProgress progressInfo;
       res = CURLE_OK;
       if ((curl = curl_easy_init()) != NULL)
       {
               progressInfo.lastrun = 0;
               progressInfo.curl = curl;
               progressInfo.callback = callback;
               progressInfo.interval = interval;
               progressInfo.step = 0;
               fp = fopen(path, "wb"); /* NEW (we need to open before we set it up, or it will fail */
               curl_easy_setopt(curl, CURLOPT_URL, URL);
               curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, downloadProgress);
               curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progressInfo);
               curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
               curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cURLWriteData); /* NEW */
               curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); /* NEW */
               if ((res = curl_easy_perform(curl)) == CURLE_OK)
                       (*callback)(0, 100);
               curl_easy_cleanup(curl);
               fclose(fp);
       }
       printf("\n");
       return res;
}
I marked the new lines with comments so you can find them easily.

Alright, now it should work. Let's go ahead and modify our main so that it downloads our ISO and displays progress every say 300ms. I also wrote some code so that the timed progress bar happens at random.
Code:
int main()
{
       int retVal;
       srand(time(NULL));
       waitLoop((rand() % 15 + 1) * 1000, 75, &progress);
       retVal = downloadFile("http://mirror.digitalnova.at/archlinux/iso/2018.03.01/archlinux-2018.03.01-x86_64.iso",
                             "archlinux.iso", 300, &progress);
       if (retVal != 0) fprintf(stderr, "There was an error downloading your file\n");
       return 0;
}

[Image: x2a7Q8j.png]

And it works! It downloads to a file named 'archlinux.iso'.

I hope you enjoyed this tutorial, feel free to grab the full code and play around with it or suit it to your own needs:
https://pastebin.com/YNr9P6jS

Now I won't have to see any more of those crappy "please wait while we download something" lines in a text mode program again....

[+] 2 users Like phyrrus9's post
Reply

RE: Command line progress bars #2
Nice tutorial and actually a really good recommendation. I'd still prefer to just show the current percentage and file size like "Download in progress - 50% [150KB/300KB]" instead of a progress bar on a console application. But that's just my personal preference and the progress bar is a really good way as well.

Reply

RE: Command line progress bars #3
(03-08-2018, 08:59 PM)chunky Wrote: Nice tutorial and actually a really good recommendation. I'd still prefer to just show the current percentage and file size like "Download in progress - 50% [150KB/300KB]" instead of a progress bar on a console application. But that's just my personal preference and the progress bar is a really good way as well.

You can do that with this code, you would just change the progress callback function to one that did that. The progress bars are useful if you have things like timers (use a reverse progress bar) that delay how long things update (which is what I was using this code for), or if your program downloads a bunch of files. It makes it easy to see all of the completed bars (indicating how many are done), as well as the progress of the current one.

Reply







Users browsing this thread: 1 Guest(s)