Creating a progress bar in bash without external libraries requires calculating the total work and updating the display dynamically. By using carriage returns to overwrite lines and arithmetic expansion for percentages, you can build a reliable visual indicator. This approach leverages standard tools like pv or manual loop counting to determine task length and provide real-time feedback to the user.
The Core Concept of Terminal Progress
To implement a progress bar in pure bash, you must understand how terminal output works. Standard print commands move the cursor to a new line after every execution. To create an animated effect, such as a filling bar, you need to return the cursor to the beginning of the same line without moving down. This is achieved using the carriage return character, represented as\r. When printed at the end of a string, it forces the next output to overwrite the current line.
You also need a mechanism to track progress. This typically involves knowing the total number of items to process before starting, or estimating the total duration if dealing with file transfers. The percentage complete is calculated by dividing the current step by the total steps and multiplying by 100. This value determines the width of the filled portion of your bar relative to its empty portion.
Using pv for Automatic Tracking
The most efficient way to handle progress bars for data streams is using thepv (pipe viewer) command. Although often considered an external tool, it is a standard utility in most Linux distributions and fits the "no new libraries" constraint because it does not require installing Python or Perl modules. It automatically calculates throughput and progress based on the data flowing through the pipe.
When copying files or transferring data, you can insert pv between two commands. For example, when archiving a directory, you might use:
tar -czf - /path/to/source | pv | gzip > archive.tar.gz
In this scenario, pv monitors the byte count. It displays a progress bar, the current speed, and the estimated time remaining. If you know the exact size of the input, you can pre-set the total size using the -s flag, which ensures the progress bar reaches 100% accurately even if the stream size is ambiguous. This method is superior for large file operations because it handles buffering and timing automatically, avoiding the overhead of shell loops.
For more details on handling output streams effectively, see our guide on Bash: Achieving Side Output - An Easy Way?.
Manual Loop-Based Progress Bars
If you are processing discrete items rather than a continuous byte stream, you must calculate progress manually. This involves iterating through a known list or range and updating the display at each step. The key challenge here is performance; calling echo repeatedly in a tight loop can be slow. Therefore, it is best practice to construct the entire bar string in memory and print it once per iteration. Here is a robust template for a manual progress bar:total=100
current=0
for ((i=1; i<=total; i++)); do
# Simulate work
sleep 0.05
current=$i
percent=$((current * 100 / total))
# Calculate filled and empty segments
filled=$((percent / 2))
empty=$((50 - filled))
# Construct the bar string
bar=$(printf "%${filled}s" "" | tr ' ' '#')
space=$(printf "%${empty}s" "" | tr ' ' '.')
# Print the updated line
printf "\r[%-50s] %d%%" "$bar$space" "$percent"
done
echo "" # Move to next line after completion
This script uses printf to format the string efficiently. The \r ensures the cursor stays at the start of the line. The bar width is fixed at 50 characters for consistency. You can adjust the sleep duration to match your actual processing time. Note that for very large loops, consider reducing the update frequency to avoid excessive I/O operations, which can degrade system performance.
Handling Interruptions and Edge Cases
Scripts that modify terminal output can leave the screen in a messy state if interrupted abruptly. If a user presses Ctrl+C, the script may exit while the cursor is still on the progress line, leaving residual text. To prevent this, you should trap the SIGINT signal and perform cleanup. A simple trap can reset the terminal state:trap 'echo ""; exit' INT
This ensures that when the script is terminated, a newline is printed, returning the prompt to a clean state. Additionally, always ensure that your final output includes a newline character so the shell prompt appears on a fresh line. Without this, the command line interface may appear broken until the next clear operation.
For users concerned about script stability during interruptions, reviewing strategies for handling signals is crucial. See Troubleshooting SIGINT in Bash Scripts for deeper insights into signal handling.
Frequently Asked Questions
Why is my progress bar flickering or lagging?
Flickering usually occurs because the terminal is redrawing too frequently. Reduce the update frequency by only refreshing the bar every N iterations instead of every single step. Ensure you are using printf with \r rather than multiple echo statements, which add unnecessary overhead.
Can I use this method for network downloads?
While possible, manual loops are inefficient for network tasks due to unpredictable speeds. Use curl or wget with their built-in progress features, or pipe the download through pv as shown earlier. This provides accurate byte-based tracking without complex shell logic.
How do I make the progress bar colorful?
You can embed ANSI escape codes within your bar string. For example, use \033[32m for green and \033[0m to reset colors. Place these codes inside the printf string where you define the filled segment to highlight completed portions differently from the empty background.
What if I don't know the total number of items?
If the total is unknown, you cannot show a percentage. Instead, display a spinner or a simple counter. You can alternate characters like /, -, \, and | at the start of the line to indicate activity without implying a specific completion percentage.
Is pv available on macOS?
Yes, pv is available via Homebrew (brew install pv). It functions identically to the Linux version. However, native macOS bash scripts often lack some GNU coreutils features, so test your specific syntax in a local environment before deploying to production.