DEV Community

Cover image for Let's progress together with the Linux Shell
Amin
Amin

Posted on

Let's progress together with the Linux Shell

#!/usr/bin/sh

INDEX=0
TOTAL=12

for operation in $(seq $INDEX $(( TOTAL - 1 )))
do
    PERCENTAGE=$(echo "$INDEX / $TOTAL * 100" | bc -l)

    printf "Backing up computer (%.2f%% done).\r" "$PERCENTAGE"
    sleep 1

    INDEX=$(( INDEX + 1 ))
done

echo "Finished. $TOTAL folders saved.         "

exit 0
Enter fullscreen mode Exit fullscreen mode

If you too you would like to add a nice & simple loader to display the current progression of your shell script for your users, or are curious to what this code does then you are in the right place.

What is going on?

Let's move step by step.

INDEX=0
Enter fullscreen mode Exit fullscreen mode

If you have one folder to backup, you may want to compress it before sending it to a cloud storage provider for instance. If you have 10 folders, then you will have to repeat the process 10 times. This variable is here to keep a count on how many folders we have already backed up.

TOTAL=12
Enter fullscreen mode Exit fullscreen mode

If we have 10 folders to backup, then this variable will take the value 10 instead of 12. Simple enough. This is the total count of operation to do.

$(seq $INDEX $(( TOTAL - 1 )))
Enter fullscreen mode Exit fullscreen mode

In Linux, seq is a small utility program that is used to generate sequences of numbers in a range. This is similar to what we are used to in Python with the range function. Except that seq will separate the generated numbers with a line-return symbol.

$ seq 1 10
1
2
3
4
5
6
7
8
9
10
Enter fullscreen mode Exit fullscreen mode

As you can see, seq can be used as a standalone program as well as in our Shell script.

for operation in $(seq $INDEX $(( TOTAL - 1 )))
do
    # ...
done
Enter fullscreen mode Exit fullscreen mode

This for loop is kind of special since in Shell, the argument passed to the for...in loop can be separated with line-return. This is often used to iterate through all lines of a file. Combined with the seq program, it can be really powerful to loop through a range of numbers.

$ for i in $(seq 1 10); do echo "Index => $i"; done
Index => 1
Index => 2
Index => 3
Index => 4
Index => 5
Index => 6
Index => 7
Index => 8
Index => 9
Index => 10
Enter fullscreen mode Exit fullscreen mode

See how easy it is to make a range in the terminal using Shell? Sometimes, I play with it by attacking my server with hundreds of HTTP requests in parallel using curl just to see how it handles the load. Quick load testing in a single line, not bad huh?

andy dwyer surprised

PERCENTAGE=$(echo "$INDEX / $TOTAL * 100" | bc -l)
Enter fullscreen mode Exit fullscreen mode

This maybe the hardest line in the code so let's get to it. In Shell, one strange thing if you are used to some other programming language is that division always return an integer. So divide one by two won't get the expected result in Shell.

$ echo $(( 1 / 2 ))
0
Enter fullscreen mode Exit fullscreen mode

Hmm... Weird. How to do that then? Well, we can use a small utility called bc which will help us get the decimal part that we need.

$ echo "1 / 2" | bc -l
.50000000000000000000
Enter fullscreen mode Exit fullscreen mode

See all those decimals after the dot? You'll see after how we can make it so that this number will be formated.

printf "Backing up computer (%.2f%% done).\r" "$PERCENTAGE"
Enter fullscreen mode Exit fullscreen mode

Here is the moment when we deal with all those decimal number crazyness. But first, let's try to understand what is printf.

printf is a Shell utility program but also a function that is available in the standard C library. If you have never done C before, this looks pretty much like this.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    printf("Hello world!\n");

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

We use this function to print characters on the console just like you would with Python and its print function. But it gets a little weird when it comes to displaying variables since it is based on some special format strings that can be used to format the desired output based on the variable type. Let's just take an example.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    float discount = 50.75;

    printf("You have a %f%% discount on this product!\n", discount);

    // You have a 50.750000% discount on this product!

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

Here, the %f is a special format string that tells the function to print a float value. There are others special format strings that can be used but you can read the manual if you are on a UNIX system to get more information.

$ man 3 printf
Enter fullscreen mode Exit fullscreen mode

This special format string can also be used to truncate the decimal part.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    float discount = 50.75123456789;

    printf("You have a %.2f%% discount on this product!\n", discount);

    // You have a 50.75% discount on this product!

    return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are left with only 2 decimals. This is done thanks to the %.2f format.

Also, you may have noticed that we repeated twice the % symbol. This is because it is already used for formatted strings so this is a way of escaping this percent sign.

Well, all of what we just learned is useful since the printf program in bash operate just like in C. Congratulations, you are now a C developer!

ron swanson saying yes with the fist gif

One final thing we didn't talk about is the \r escape sequence. As you can see, we already use an escape sequence which is the line-return \n. It is used to go on the next line before printing the next character.

Well, this carriage return \r is used to go on the... Same line but at the beginning. This may seem odd at first, but if you go back to the preview of the program at the top of this article, you'll understand that this is what we used to simulate a progression updated over time.

$ printf "This line wont't show\r" && printf "This line will show correctly\n"
This line will show correctly
Enter fullscreen mode Exit fullscreen mode

Weird, huh? This is because we wrote the second set of characters "on top of" the first set. And since the first call to printf had no line return, it looks like we just updated the percent value.

We did the hardest part, now for an easy one.

sleep 1
Enter fullscreen mode Exit fullscreen mode

The sleep utility is used to mark a pause in our script. You won't use this program too often and it is used here for the only purpose of simulating a high-load process. Simple enough.

INDEX=$(( INDEX + 1 ))
Enter fullscreen mode Exit fullscreen mode

This line is using a special syntax that helps us do calculus easier in Shell. It will simply increase whatever value was previously affected to the INDEX variable.

$ INDEX=1 && echo $INDEX && INDEX=$(( INDEX + 1 )) && echo $INDEX
1
2
Enter fullscreen mode Exit fullscreen mode

This is just a little demonstration to show you how easy it was to increment and print variable on the terminal.

echo "Finished. $TOTAL folders saved.         "
Enter fullscreen mode Exit fullscreen mode

Well, echo is yet another program that we will use often as Linux scripters. It is a more friendly version of what we used with printf and again I invite you to read the documentation since echo has many capabilities that makes it a really useful utility program.

$ echo "Hello world!"
Hello world!
Enter fullscreen mode Exit fullscreen mode

Finishing line

And that's it! I hope that you enjoyed learning things with me. I now use this little piece of code in all my scripts that have high-load processing. This removes the need for ugly displays like this one.

$ sh backup.sh
Backing up folder 1/12.
Backing up folder 2/12.
Backing up folder 3/12.
Backing up folder 4/12.
Enter fullscreen mode Exit fullscreen mode

Now you know how to create a clean, simple a fun progress loader using the Linux Shell.

There are already a plethora of GitHub open-source projects that have written the code for you, with many loading styles! If you have some time, you should check them out and share yours on GitHub too. This is also a fun and educative exercise to try and reproduce some loaders you may find on your day-to-day life as a developer.

Don't hesitate to ask questions on parts that are still blurry for you and I'll try to add as much details as necessary.

Now I'll have to go quick but I'll see you around. Bye!

godspeed

Top comments (5)

Collapse
 
moopet profile image
Ben Sinclair

echo isn't as portable as printf, and I think it's a bit odd to use two different methods of outputting text in the same script. I've switched to using printf for everything these days.

Collapse
 
aminnairi profile image
Amin

Hi Ben and thanks for your answer!

You are right. echo isn't as portable as printf and you are right to point it out.

I'm using it since I have echo available on my operating system and it is a little bit easier to use than printf. And for most GNU/Linux distributions out there, this is also the case. This article is also an opportunity for people not confortable with Shell scripts to discover some of its utility programs so I figured why not. Two birds one stone, right?

If someone is using a distribution that does not include echo, I'll assume this person is also aware of how to edit this script and make it portable again so I didn't worry about it too much when writing this article.

But again you are totally right about its portability. Especially when it comes to using echo with ZSH or in Bash.

Collapse
 
moopet profile image
Ben Sinclair

I'm more on about things like echo -n not being portable than echo itself not existing. It does different things depending on the shell, and if you try to stick to POSIX you're not going to use those options anyway.

Collapse
 
vlasales profile image
Vlastimil Pospichal
PERCENTAGE=$(($INDEX * 100 / $TOTAL))
Collapse
 
aminnairi profile image
Amin

Hi Vlastimil and thanks for you comment.

Yes indeed, it is possible to do more complex operations inside these double parenthesis construct, which is really cool!

Unfortunately, when dealing with numbers, this construct will not return decimal numbers as we might expect.

#!/usr/bin/sh

INDEX=0
TOTAL=12

for operation in $(seq $INDEX $(( TOTAL - 1 )))
do
    PERCENTAGE=$(( INDEX * 100 / TOTAL ))

    echo "Backing up folders (${PERCENTAGE}% done)"

    (( INDEX++ ))
done

echo "Backup done."

This means running this script will give us this output.

Backing up folders (0% done)
Backing up folders (8% done)
Backing up folders (16% done)
Backing up folders (25% done)
Backing up folders (33% done)
Backing up folders (41% done)
Backing up folders (50% done)
Backing up folders (58% done)
Backing up folders (66% done)
Backing up folders (75% done)
Backing up folders (83% done)
Backing up folders (91% done)
Backup done.

In that case this works, but I wanted to have some decimal places for my percentage so it is not very useful here. But in any other cases where the decimal places are not mandatory I would use your version anytime since it does not rely on any other external program like bc. We could even have used python instead of bc if you are more confortable with Python (but it requires it to be installed, which is often the case on most installations)!

#!/usr/bin/sh

INDEX=0
TOTAL=12

for operation in $(seq $INDEX $(( TOTAL - 1 )))
do
    PERCENTAGE=$(python3 -c "print(round($INDEX / $TOTAL * 100, 2))")

    echo "Backing up folders (${PERCENTAGE}% done)"

    (( INDEX++ ))
done

echo "Backup done."

# Backing up folders (0.0% done)
# Backing up folders (8.33% done)
# Backing up folders (16.67% done)
# Backing up folders (25.0% done)
# Backing up folders (33.33% done)
# Backing up folders (41.67% done)
# Backing up folders (50.0% done)
# Backing up folders (58.33% done)
# Backing up folders (66.67% done)
# Backing up folders (75.0% done)
# Backing up folders (83.33% done)
# Backing up folders (91.67% done)
# Backup done.