1. Introduction
In this tutorial, we’ll learn how to use spinners on our terminal during the execution of long-running commands.
More specifically, we’ll talk about running tasks in the background and implementing our spinner in a Bash script while also looking at existing tools and libraries.
2. Running Tasks in Background
Running long tasks in the background on a terminal is a common practice for several reasons. First and foremost, it ensures that the terminal remains available for concurrent use. Linux terminals are powerful tools for interacting with the operating system, issuing commands, and managing multiple tasks simultaneously. When resource-intensive or time-consuming processes are run in the foreground, they tie up the terminal, rendering it unresponsive.
Moreover, background execution allows for increased efficiency and productivity. Users can initiate a lengthy operation and continue with other tasks, leveraging their time more effectively. That’s particularly valuable for system administrators and power users who often have multiple tasks to manage. In cases where opening another terminal isn’t straightforward (e.g., nested SSH logins on virtual machines), running tasks in the background is crucial.
Background tasks are also less prone to interruption. If a user logs out or the terminal session disconnects, foreground tasks are usually terminated, potentially resulting in data loss or incomplete operations. In contrast, background processes persist. Furthermore, background tasks can be managed, monitored, and automated more effectively. With background tasks, we can check their status, prioritize them, and apply resource constraints.
3. Implementing a Spinner
Let’s build a Βash script that adds a spinner on a long-running command. We’ll see the whole script and then go through it:
#! /bin/bash
sleep 5 & pid=$!
i=1
sp="\|/-"
while ps -p $pid > /dev/null
do
printf "\b%c" "${sp:i++%4:1}"
sleep 0.1
done
printf "\nDone!\n"
And here’s the code in action:
Now, let’s break it down and understand how it builds a spinner. There are two functionalities in the script: printing the spinner and putting the task to sleep.
3.1. Long-running Command
Let’s look at the first couple of lines of the script:
#!/bin/bash
sleep 5 & pid=$!
The first thing we do is declare the script as a bash script with #!/bin/bash. Then, we use sleep 5 to emulate a long-running command. The actual long command we need to run will be placed here instead of sleep. The & makes the sleep 5 command run in the background.
The PID=#! is used to capture the previous command’s PID (Process ID). This will be used later on to check whether the command ended or if it’s still running.
3.2. Printing the Spinner
In the above script, we create and show a spinner on the terminal. First, we create the graphics for the spinner:
sp="\|/-"
That’s simply what the terminal shows. Each element in this array is displayed after the other, creating a spin-like effect. Next, we have a while loop:
while ps -p $pid > /dev/null
Here, we look for the PID of the long-running task in the snapshot of currently running processes. If the process is still running, the PID exists and the loop condition is true. If the task is over, then the PID won’t be found, and the spinner stops. In addition, we pipe the error output to /dev/null so the error where ps can’t find the process doesn’t show on the terminal.
The loop starts with a printf statement:
printf "\b%c" "${sp:i++%4:1}"
The \b%c part is the format string. \b is for printing a backspace character, moving the cursor back one position on the terminal. This deletes the previously shown spinner array element to make room for the new one. Next, the %c is the format specifier for one character. printf prints here the next character from the array.
The ${sp:i++%4:1} part is the printf argument. This part extracts one character from the sp array at a time. In Bash scripting, we use this syntax for array slicing. The syntax is arrayName:index:length, where:
- arrayName is the name of the variable holding the elements,
- index is the position inside the string where we want to start, and
- length is the number of elements we want to extract
In our case, arrayName is equal to sp, index is equal to i++%4, and length is equal to 1. The counter i starts from 0, and by getting its modulo 4 we keep going around the elements of the spinner array.
After the loop ends, we call printf “\nDone!\n” which prints “*Done!*” on a new line and moves the cursor to the line below.
3.3. Putting the Task to Sleep
The other thing inside the loop is the sleeping functionality. After calling printf, we immediately call the sleep function:
do
printf "\b%c" "${sp:i++%4:1}"
sleep 0.1
done
The sleeper instruction sleep 0.1 is very important for two reasons. First, it makes the spinner’s speed appropriate to the human eye. We can adjust that value accordingly based on personal preference. Second, and more importantly, it puts the spinner task to sleep. This allows other processes to grab CPU time and avoids creating busy wait loops, as we’ll see next.
We should be very careful not to create a busy wait loop inside the spinner program. This is the most important part of the spinner, as if the spinner process is always active, it consumes much-needed space in the CPU. In case the system only has one thread, it may even be possible that only the spinner runs instead of the original long-running task. That wouldn’t be a very nice spinner indeed.
To avoid coming into this situation, we could use the sleep function, as above. By making the process sleep, the OS scheduler can then schedule other processes to run, and we avoid the busy wait loop problem from the start.
4. Conclusion
In this article, we discussed spinners for long-running commands on Linux. We saw why running long commands in the background on Linux terminals is important, as it frees the terminal and enhances efficiency and productivity.
Showing a spinner on the terminal lets us know that a command is still running and hasn’t hanged. We showcased a spinner implementation in Bash and went through its code.
Finally, we focused on putting the spinner task to sleep, as if we don’t we will create a busy loop that hinders system performance.