Ubuntu already includes Software Updater, so I was not trying to replace something that was missing. The graphical application works well when someone wants to check for updates, click a button, and let Ubuntu handle the rest.
I wanted to create a terminal-based clone because the terminal gives me more control over what happens before, during, and after an update.
The terminal approach is useful when:
- The computer does not have a graphical desktop
- I am connected to a computer through SSH
- I need a detailed record of every update
- I want to see the exact package versions that changed
- I need to review previous update dates
- I want APT, Snap, and Flatpak updates in one workflow
- I want to prepare the project for future network automation
It was also a good way to learn more Bash. The project required functions, menus, background processes, text files, terminal control, package comparisons, logging, and error handling.
The final result is an 80-column terminal application that copies the basic idea of Ubuntu’s Software Updater while adding detailed logs, update history, package tracking, and guarded rollback options.
The application was developed and tested on Ubuntu. Other Linux variants may require changes because their package commands, databases, and rollback methods can be different. Anyone adapting the script should review the official documentation for that Linux distribution and package manager.
What the Application Does
The terminal update manager can:
- Refresh Ubuntu package information
- Install available APT upgrades
- Update Snap applications when Snap is installed
- Update Flatpak applications when Flatpak is installed
- Display a colored progress bar
- Save complete package-manager output
- Record update dates and times
- List installed, updated, and removed packages
- Display previous update sessions
- Open full update logs
- Attempt a guarded package rollback
- Save separate rollback logs
The main menu uses an 80-column ASCII layout:
+------------------------------------------------------------------------------+
| System Update Manager |
+------------------------------------------------------------------------------+
| 1. Run system update |
| 2. Display system update dates and times |
| 3. View what was updated |
| 4. View a complete update log |
| 5. Roll back packages from the most recent update |
| 6. Exit |
+------------------------------------------------------------------------------+
The menu looks simple, but each option has several functions working behind it.
Building the Terminal Interface
The first part of the project was creating a screen that looked like an application instead of a list of commands.
The interface uses regular ASCII characters rather than Unicode borders. That makes the layout more dependable in different terminal programs.
BOX_WIDTH=80
INNER_WIDTH=78
The full interface is 80 columns wide. The inside is 78 columns because the left and right border characters each use one column.
The border function creates a line with two plus signs and enough spaces to fill the middle:
print_border() {
local border
printf -v border '+%*s+' "$INNER_WIDTH" ''
border=${border// /-}
printf '%s\n' "$border"
}
This command:
printf -v border '+%*s+' "$INNER_WIDTH" ''
saves the formatted line in the border variable instead of printing it immediately.
The next line replaces every space with a dash:
border=${border// /-}
The result is:
+------------------------------------------------------------------------------+
Text inside the border is handled by another function:
box_line() {
local text="${1//$'\t'/ }"
if (( ${#text} > INNER_WIDTH )); then
text="${text:0:$((INNER_WIDTH - 3))}..."
fi
printf '|%-*s|\n' "$INNER_WIDTH" "$text"
}
At first, long file paths and package names pushed the right border out of place. The function now measures the text before printing it. When the text is too long, it shortens the line and adds three dots.
text="${text:0:$((INNER_WIDTH - 3))}..."
That small check keeps the interface from falling apart when the program displays real data.
Bash’s built-in printf command supports formatted output and can save formatted text directly into a variable, which makes it useful for fixed-width terminal layouts.
Creating the Menu
The program uses Bash’s read command to collect a menu selection:
read -r choice
The -r option prevents backslashes from being treated as special escape characters.
A case statement sends the user to the selected function:
case "$choice" in
1) perform_update ;;
2) view_update_dates ;;
3) view_package_changes ;;
4) view_full_log ;;
5) perform_rollback ;;
6) exit 0 ;;
esac
Each part of the application has its own function. The update code stays inside perform_update, while log viewing and rollback use different functions.
This is easier to maintain than putting the entire application inside one large block of code. The Bash manual documents functions, read, case, background processes, wait, and signal handling.
Creating Update Sessions
Every update needs its own identity so its files do not get mixed with earlier sessions.
The application creates a session ID from the current date and time:
session_id=$(date '+%Y%m%d_%H%M%S')
A session ID may look like this:
20260803_195814
That represents an update that started on August 3, 2026, at 7:58:14 PM.
The ID becomes part of each file name:
update_20260803_195814.log
update_20260803_195814_before.tsv
update_20260803_195814_after.tsv
update_20260803_195814_changes.tsv
Using the same session ID makes it easy to connect the full log, the package inventories, and the final change report.
Choosing the Log Location
The application stores its files under:
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/system-update-manager"
When XDG_STATE_HOME is not set, the application uses:
~/.local/state/system-update-manager
The XDG Base Directory Specification defines XDG_STATE_HOME as the location for user-specific state that should remain available between program runs. Its normal default is $HOME/.local/state.
The folder may look like this:
system-update-manager/
├── update-history.tsv
├── rollback-history.tsv
└── sessions/
├── update_20260803_195814.log
├── update_20260803_195814_before.tsv
├── update_20260803_195814_after.tsv
├── update_20260803_195814_changes.tsv
└── rollback_20260803_203000.log
Keeping these files together gives the application one known location for update history, package reports, complete logs, and rollback records.
Recording Packages Before and After an Update
A message saying “update completed” is useful, but I also wanted the application to show exactly what changed.
Before installing anything, the script records every installed Ubuntu package and its exact version:
dpkg-query -W -f='${Package}\t${Version}\n'
The result looks like this:
bash 5.2.21-2ubuntu4
curl 8.5.0-2ubuntu10
openssh-client 1:9.6p1-3ubuntu13
After the update finishes, the same command runs again.
That creates two package inventories:
before.tsv
after.tsv
dpkg-query reads package information from the dpkg database, and its formatting options allow the script to print the package name and installed version in a predictable format.
Comparing the Package Inventories
The application uses AWK to compare the package lists.
awk -F '\t' '
FNR == NR {
before[$1] = $2
next
}
{
seen[$1] = 1
if (!($1 in before)) {
print "INSTALLED\t" $1 "\t-\t" $2
} else if (before[$1] != $2) {
print "UPDATED\t" $1 "\t" before[$1] "\t" $2
}
}
END {
for (package in before) {
if (!(package in seen)) {
print "REMOVED\t" package "\t" before[package] "\t-"
}
}
}
' "$before_file" "$after_file"
During the first pass, AWK stores the original package versions:
before[$1] = $2
The package name becomes the array index. The installed version becomes the value.
During the second pass, the script checks whether a package is new:
if (!($1 in before))
It checks whether an existing package has a different version:
else if (before[$1] != $2)
At the end, it checks whether anything from the original inventory is missing from the final inventory.
The change report can look like this:
UPDATED curl 8.5.0-2ubuntu10 8.5.0-2ubuntu10.2
INSTALLED new-package - 1.0.0
REMOVED old-package 2.1.0 -
The earlier build only saved the package-manager output, which was hard to review after a large update. Comparing two inventories created a much cleaner summary.
AWK uses associative arrays, which means text such as a package name can be used as an array index. That makes it well suited for matching each package with its earlier version.
Handling Quiet Update Commands
During testing, the APT update finished, but the application appeared to stop when it reached:
sudo snap refresh
Snap sometimes spends several minutes working without printing regular output. It may be connecting to the Snap Store, downloading a package, or completing an update task.
The first improvement displayed an elapsed-time message:
STATUS: Still working... elapsed 00:10
STATUS: Still working... elapsed 00:20
STATUS: Still working... elapsed 00:30
This confirmed that the application was still alive, but a quiet process could still wait for too long. A time limit was added:
SNAP_TIMEOUT_SECONDS=900
That gives the Snap refresh 15 minutes.
When the timeout command is available, the refresh runs like this:
timeout --foreground "${SNAP_TIMEOUT_SECONDS}s" \
sudo snap refresh
If Snap exceeds the limit, the application records a warning and continues to the final report instead of waiting forever.
The timeout utility runs a command with a time limit and returns a special status when that limit is reached. (Snapcraft)
The elapsed-time display solved the “frozen” appearance, but it still did not tell the user how far the entire update had progressed. That led to the percentage bar.
Adding a Percentage Progress Bar
The status message was replaced with a visual progress bar:
Progress: [####################--------------------] 50%
The filled area uses # characters, and the remaining area uses dashes.
filled=$((percent * bar_width / 100))
empty=$((bar_width - filled))
printf -v filled_bar '%*s' "$filled" ''
filled_bar=${filled_bar// /#}
printf -v empty_bar '%*s' "$empty" ''
empty_bar=${empty_bar// /-}
The program first calculates the number of completed and empty characters. It then creates strings of spaces and replaces them with # and -.
The percentage is an estimate of the complete workflow. It is not an exact measurement of downloaded bytes.
APT, Snap, and Flatpak do not all report progress in the same format. APT may print download information, Snap may remain quiet, and Flatpak has its own output style.
The application therefore divides the update into stages:
2% Save the original package inventory
5% Begin the Ubuntu update
20% Refresh package information
70% Install available upgrades
82% Update Flatpak applications
95% Update Snap applications
97% Save the final package inventory
99% Compare package versions
100% Complete the session
During a long-running command, the progress slowly moves through that command’s assigned range:
projected_percent=$((
start_percent +
((progress_range - 1) * elapsed / estimated_seconds)
))
The bar stops just before the end of the range until the command truly finishes. That prevents the display from claiming a task is complete when it is still running.
Saving Full Output Without Filling the Screen
Showing all APT, Snap, and Flatpak output would destroy the clean interface. Hundreds of package messages would push the progress bar down the screen.
The application creates a temporary file for command output:
output_file=$(mktemp "$SESSION_DIR/.command-output.XXXXXX")
The package command runs in the background:
"$@" > "$output_file" 2>&1 &
command_pid=$!
Both normal output and error output are redirected to the temporary file.
While the command is running, the progress bar continues to refresh:
while kill -0 "$command_pid" 2>/dev/null; do
sleep "$PROGRESS_REFRESH_SECONDS"
render_progress "$projected_percent" "$description"
done
When the process finishes, wait collects its real exit status:
wait "$command_pid"
command_status=$?
The saved output is then copied into the full session log:
cat "$output_file" >> "$CURRENT_LOG"
rm -f "$output_file"
This gives the user a clean terminal display without losing any update details. If something fails, the complete package-manager output is still available in the session log.
Adding Color
The plain interface worked, but success messages, warnings, prompts, and errors looked too similar.
ANSI terminal colors were added:
C_RESET=$'\033[0m'
C_BORDER=$'\033[1;36m'
C_TITLE=$'\033[1;96m'
C_SUCCESS=$'\033[1;32m'
C_WARNING=$'\033[1;33m'
C_ERROR=$'\033[1;31m'
The colors are used for different purposes:
- Cyan for borders and headings
- Green for successful actions
- Yellow for warnings and prompts
- Red for failures
- White for normal information
- Gray for secondary details
The script checks whether it is connected to a real terminal before enabling color:
if [[ -t 1 ]] &&
[[ "${TERM:-}" != "dumb" ]] &&
[[ -z "${NO_COLOR:-}" ]]; then
COLOR_ENABLED=1
fi
Color can be disabled when needed:
NO_COLOR=1 ./update-manager.sh
Adding color created another layout problem. ANSI codes are invisible on the screen, but they still exist inside the printed string. If those hidden characters are counted as visible text, an 80-column border can become misaligned.
The printing functions were changed so visible text is measured separately from color codes. The log files also remain plain text, making them easier to search or open in a regular editor.
Solving the Repeating Progress Bar
The progress bar became the most confusing part of the project.
Instead of updating one row, the program printed several bars:
Progress: [##--------------------------------------] 5%
Progress: [########--------------------------------] 20%
Progress: [############################------------] 70%
Progress: [######################################--] 95%
The first guess was that the terminal was wrapping the final character onto a new row. An 80-character line inside an 80-column terminal can sometimes move the cursor to the next line.
The application first tried temporarily disabling terminal wrapping. The bars still appeared one after another.
The next attempt checked the actual terminal width:
terminal_columns=$(tput cols)
The progress row was shortened, and the final terminal column was left unused. That helped prevent real wrapping, but the repeated bars remained. The tput cols command reports the terminal width through the terminal capability database.
The real cause was eventually found in the update workflow. The terminal was not wrapping one progress bar. The code was printing a new progress block after every stage.
The final design uses one reusable terminal line:
printf '\r\033[2K'
The carriage return, \r, moves the cursor to the beginning of the current row.
The escape sequence \033[2K clears that row.
The next progress update is printed without a newline:
render_progress() {
local percent="$1"
local description="$2"
printf '\r\033[2K Progress: [%s%s] %3d%% %s' \
"$filled_bar" \
"$empty_bar" \
"$percent" \
"$description"
}
The same line now changes in place:
Progress: [####################--------] 72% Installing Ubuntu updates
The wrap-control and terminal-width changes were still useful, but they were not fixing the main problem. The main fix was changing how the program created the progress display.
Recording Update History
Each completed update is added to a tab-separated history file:
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$session_id" \
"$start_time" \
"$end_time" \
"$manager" \
"$status" \
"$change_count" \
"$log_file" \
"$changes_file" >> "$HISTORY_FILE"
Each row records:
- Session ID
- Start time
- Completion time
- Package manager
- Final status
- Number of package changes
- Full log location
- Change-report location
The update-history menu reads this file and displays the dates without requiring the user to search through folders manually.
Adding Package Rollback
Rollback became the largest addition to the project.
Before an update starts, the application saves:
- Installed APT package versions
- Snap revision numbers
- Flatpak active commits
This information gives the program a possible path back to the earlier package state.
APT rollback
For an APT package, a specific version can be requested in this format:
package-name=old-version
The program does not immediately run the downgrade. It first asks APT to simulate it:
apt-get -s --allow-downgrades install "${apt_specs[@]}"
The -s option performs a simulation without changing the system.
The simulation is searched for serious problems:
if grep -Eiq \
'essential packages will be removed|held broken packages|unmet dependencies' \
"$simulation_output"; then
rollback_safe=0
fi
The application also blocks automatic removal of:
- Essential packages
- The currently running kernel
- The Snap service package
APT supports simulations, specific package-version selection, and downgrades, but downgrades must be handled carefully because package dependencies may no longer match.
Before the rollback starts, the user must enter an exact confirmation:
ROLLBACK 20260803_195814
Typing only yes is not enough.
Snap rollback
Snap applications use revision numbers.
The application records those revisions before the update:
snap list |
awk 'NR > 1 {
print $1 "\t" $3
}'
Snap supports reverting to a previous revision or requesting a specific revision when it is still available. Snap documentation also warns that shared user data may not always return to its earlier state.
Flatpak rollback
Flatpak applications use commits.
The application records the active commit:
flatpak list --columns=ref,active,installation
Flatpak repositories keep application versions as commits. The flatpak update command can target an earlier commit when it remains available.
What rollback cannot do
This feature is a package rollback, not a full system backup.
It may restore:
- An older APT package
- An older Snap revision
- An older Flatpak commit
It does not guarantee restoration of:
- Personal documents
- Database records
- Every configuration file
- Files created after the update
- Files deleted after the update
- All application data
A complete recovery still requires a proper backup, filesystem snapshot, or system image.
Dependencies and Their Purpose
Most of the required commands are already available on a normal Ubuntu installation.
Required dependencies
| Dependency | Purpose |
|---|---|
| Bash 4 or newer | Runs the script, functions, arrays, loops, menu, and signal handling. |
| sudo | Runs update and rollback commands with administrator privileges. |
| apt-get | Refreshes Ubuntu package information and installs available upgrades. |
| dpkg-query | Reads installed package names and exact version numbers. |
| awk | Compares package inventories from before and after an update. |
| sort | Organizes the package-change report. |
| grep | Searches logs and rollback simulations for warnings and errors. |
| date | Creates timestamps and update-session IDs. |
| mktemp | Creates temporary files for package-manager output. |
| hostname | Records the computer name in the session log. |
| uname | Records system information and identifies the running kernel. |
| wc | Counts package changes and report lines. |
| mkdir | Creates the state, session, and log directories. |
| cat | Adds temporary command output to the complete session log. |
| rm | Removes temporary files. |
| sleep | Controls how often the progress display refreshes. |
Optional dependencies
| Dependency | Purpose |
|---|---|
| tput | Detects the terminal width. |
| timeout | Stops a command after a configured time limit. |
| less | Displays long logs one page at a time. |
| snap | Updates Snap applications and records revision numbers. |
| flatpak | Updates Flatpak applications and records active commits. |
| OpenSSH client | Will connect to remote Linux computers in a future release. |
| OpenSSH server | Will allow remote computers to accept update connections. |
Snap and Flatpak are optional. The script checks whether they are installed before attempting those stages.
Ubuntu Compatibility
The application was developed and tested on Ubuntu.
The code includes basic package-manager detection, but other Linux systems may use different:
- Update commands
- Package databases
- Output formats
- Version rules
- Dependency behavior
- Rollback methods
Basic detection does not mean every function has been fully tested on every Linux distribution.
Before using or modifying the program for another distribution, its official operating-system and package-manager documentation should be reviewed. Testing should begin on a virtual machine or non-production computer.
Handling Ctrl+C Safely
The progress display may temporarily change the cursor or terminal line.
If the user presses Ctrl+C, the application needs to restore the terminal before exiting:
handle_interrupt() {
restore_terminal
printf '\n'
exit 130
}
trap handle_interrupt INT TERM
The trap command tells Bash to run handle_interrupt when the program receives an interrupt or termination signal.
Without this cleanup, the terminal cursor could remain hidden, or the next shell prompt could appear in the wrong position.
Future Remote Updates Through SSH
The next step is to expand the application from one Ubuntu computer to other approved Linux systems on the network.
Instead of opening a separate terminal on each computer, the update manager could use SSH to:
- Test remote connections
- Identify remote computers
- Start update sessions
- Save a separate log for each computer
- Report success, warnings, or failure
- Report when a restart is required
- Attempt rollback when the needed package history exists
A basic connection test could look like this:
check_remote_host() {
local remote_host="$1"
ssh \
-o BatchMode=yes \
-o ConnectTimeout=5 \
"$remote_host" \
'printf "Connected to: "; hostname'
}
BatchMode=yes prevents SSH from stopping for an interactive password or passphrase prompt.
ConnectTimeout=5 limits the time used to establish the connection and complete the first SSH handshake.
Ubuntu provides separate OpenSSH client and server packages. The client starts remote connections, while the server accepts them. OpenSSH also supports key-based authentication, which will be important for controlled automation.
A future remote menu may look like this:
+------------------------------------------------------------------------------+
| Remote System Update Manager |
+------------------------------------------------------------------------------+
| 1. View approved computers |
| 2. Test SSH connections |
| 3. Update one remote computer |
| 4. Update a selected group |
| 5. View remote update history |
| 6. Collect remote logs |
| 7. Return to the main menu |
+------------------------------------------------------------------------------+
Each computer should receive its own log folder:
sessions/
├── workstation01/
│ └── update_20260803_210000.log
├── workstation02/
│ └── update_20260803_211500.log
└── linux-server01/
└── update_20260803_213000.log
Remote updates should be released in stages rather than starting on every computer at once.
A safer process would be:
- Update one test computer.
- Review the update log.
- Confirm that important applications still work.
- Update a small group.
- Update the remaining approved computers.
This lowers the chance of one bad update affecting every Linux computer on the network.
Closing
Ubuntu’s graphical Software Updater is still the easier option for normal desktop use. This project was not built because Ubuntu needed another update button.
The value of the terminal application is the control around the update process.
It records exact package changes, saves complete logs, displays update history, handles quiet commands, keeps the terminal interface organized, and performs safety checks before attempting a rollback.
The actual update commands were only part of the project. Much of the work involved progress tracking, background processes, package comparison, terminal control, file handling, interruption cleanup, and rollback planning.
The next stage is SSH support. Once remote updates are added, the project will move beyond being a terminal clone of Software Updater and become a small Linux administration tool for managing approved computers across a network.
References
Canonical Ltd. (n.d.). Manage updates. Snap documentation. Retrieved August 3, 2026, from https://snapcraft.io/docs/how-to-guides/manage-snaps/manage-updates/
Canonical Ltd. (2026). OpenSSH server. Ubuntu Server documentation. https://documentation.ubuntu.com/server/how-to/security/openssh-server/
Debian Project. (n.d.). apt-get(8): APT package handling utility—Command-line interface. Debian Manpages. Retrieved August 3, 2026, from https://manpages.debian.org/unstable/apt/apt-get.8.en.html
Debian Project. (n.d.). dpkg-query(1): A tool to query the dpkg database. Debian Manpages. Retrieved August 3, 2026, from https://manpages.debian.org/unstable/dpkg/dpkg-query.1.en.html
Flatpak contributors. (n.d.). Flatpak command reference. Flatpak documentation. Retrieved August 3, 2026, from https://docs.flatpak.org/en/latest/flatpak-command-reference.html
Flatpak contributors. (n.d.). Repositories. Flatpak documentation. Retrieved August 3, 2026, from https://docs.flatpak.org/en/latest/repositories.html
Free Software Foundation. (n.d.). Bash reference manual. GNU Project. Retrieved August 3, 2026, from https://www.gnu.org/software/bash/manual/bash.html
Free Software Foundation. (n.d.). GNU Coreutils manual. GNU Project. Retrieved August 3, 2026, from https://www.gnu.org/software/coreutils/manual/coreutils.html
Free Software Foundation. (n.d.). The GNU Awk user’s guide. GNU Project. Retrieved August 3, 2026, from https://www.gnu.org/software/gawk/manual/gawk.html
Freedesktop.org. (2021). XDG Base Directory Specification. https://specifications.freedesktop.org/basedir/
Invisible Island. (n.d.). tput—Initialize a terminal or query the terminfo database. Retrieved August 3, 2026, from https://invisible-island.net/ncurses/man/tput.1.html
OpenBSD Project. (n.d.). ssh_config(5): OpenSSH client configuration file. OpenBSD manual pages. Retrieved August 3, 2026, from https://man.openbsd.org/ssh_config