
I finally got into learning Bash.
Not just the “copy this command from a forum and hope nothing catches fire” kind of Bash.
Actual Bash.
The kind where I look at a script, understand what each part does, and slowly realize that the terminal is not magic. It is just a bunch of small commands doing exactly what they were told to do.
Which is comforting.
And also slightly terrifying.
For a long time, I treated Bash as something that came with Linux. I knew it was there. I used it. I typed commands into it. I installed packages, restarted services, checked logs, and occasionally used sudo with the confidence of someone who was about 70% sure the command was harmless.
But using Bash is not the same as learning Bash.
That is the part I finally decided to fix.
So What Is Bash?
Bash stands for Bourne Again Shell.
It is one of the most common command-line shells used in Linux and Unix-like systems. When you open a terminal in Ubuntu, Debian, Linux Mint, or many Linux servers, there is a good chance you are using Bash or something very close to it.
At its simplest, Bash lets you type commands and interact with the operating system.
At its more useful level, Bash lets you automate work.
That is where it becomes interesting.
Instead of manually checking uptime, memory, disk space, failed services, and updates one command at a time, you can put all of that into a script and run it whenever you need it.
That was my starting point.
A simple Linux health check script.
Then, because apparently I cannot leave things alone, I added update checking.
Then update installation.
Then logging.
Then cleanup.
Then a reboot check.
Suddenly, my beginner script became something that actually does useful sysadmin work.
Starting the Script
The script starts with this line:
#!/bin/bash
That line is called a shebang.
It tells the system which interpreter should run the script. In this case, it says, “Run this using Bash.”
Without it, the script might still run depending on how you launch it, but adding it makes the script clearer and more predictable.
Right after that, I added a basic comment block:
# Simple Linux Health Check + Update Script
# Purpose:
# 1. Show basic system health information
# 2. Check for updates
# 3. Download and install updates if available
# 4. Save results to a log file
This part does not affect the script. Bash ignores lines that start with #, except for the shebang at the top.
But comments matter.
Especially when you come back to your own script later and wonder what version of yourself wrote it and why that person made certain choices.
Creating a Log File
One of the first useful parts of the script is this:
LOG_FILE="$HOME/health-update-report.txt"
This creates a variable called LOG_FILE.
Instead of typing the full path to the log file over and over again, the script stores it in one place.
$HOME points to the current user’s home directory. So if my username is teo, the file would be saved somewhere like:
/home/teo/health-update-report.txt
That is one thing I like about scripting. You start removing repetition.
Instead of hardcoding the same file path twenty times, you define it once and reuse it.
Writing to the Screen and the Log
The next part prints the report header:
echo "====================================" | tee "$LOG_FILE"
echo " Linux Health Check + Update Report" | tee -a "$LOG_FILE"
echo " Date: $(date)" | tee -a "$LOG_FILE"
echo " Hostname: $(hostname)" | tee -a "$LOG_FILE"
echo "====================================" | tee -a "$LOG_FILE"
There are a few things happening here.
echo prints text.
tee sends the output to two places: the screen and a file.
This line creates or overwrites the log file:
echo "====================================" | tee "$LOG_FILE"
Then the rest use tee -a:
tee -a "$LOG_FILE"
The -a means append.
That means the script keeps adding to the log file instead of replacing it each time.
This is a nice little Bash pattern. Show the user what is happening on screen, but also save the results to a report.
The script also uses command substitution here:
echo " Date: $(date)" | tee -a "$LOG_FILE"
echo " Hostname: $(hostname)" | tee -a "$LOG_FILE"
The $(date) part runs the date command and places the result inside the output.
Same thing with $(hostname).
So instead of just printing the words “Date” and “Hostname,” the script prints the actual date and the actual computer name.
Basic System Information
The script then starts checking the machine.
First, it checks the logged-in user:
echo "Logged-in user:" | tee -a "$LOG_FILE"
whoami | tee -a "$LOG_FILE"
whoami prints the username running the script.
Simple, but useful.
Especially if you are using sudo, switching accounts, or working on a machine where you want to confirm who is actually running the command.
Next is uptime:
echo "Uptime:" | tee -a "$LOG_FILE"
uptime | tee -a "$LOG_FILE"
uptime shows how long the system has been running, how many users are logged in, and the load average.
That gives you a quick feel for the machine.
Has it been up for five minutes?
Five days?
Five hundred days and now everyone is afraid to reboot it?
The script also checks the IP address:
echo "IP Address:" | tee -a "$LOG_FILE"
hostname -I | tee -a "$LOG_FILE"
hostname -I prints the IP addresses assigned to the system.
For a Linux machine, especially one you are connecting to over the network, that is handy.
Memory and Disk Usage
The script checks memory with:
echo "Memory Usage:" | tee -a "$LOG_FILE"
free -h | tee -a "$LOG_FILE"
free shows memory usage.
The -h makes the output human-readable, so instead of showing raw numbers that make your eyes glaze over, it shows values in familiar units like MiB or GiB.
Then it checks disk usage:
echo "Disk Usage:" | tee -a "$LOG_FILE"
df -h | tee -a "$LOG_FILE"
df shows filesystem disk space.
Again, -h makes it human-readable.
This is one of those boring checks that becomes very important when something breaks.
A full disk can cause all kinds of problems. Services fail. Logs stop writing. Databases get angry. Users get confused. IT gets blamed.
So yes, checking disk space is boring.
It is also responsible adult behavior.
Finding Large Files
This is probably one of the more Bash-looking parts of the script:
find "$HOME" -type f -exec du -h {} + 2>/dev/null | sort -rh | head -n 5 | tee -a "$LOG_FILE"
At first glance, that line looks like someone spilled punctuation into the terminal.
But once you break it down, it makes sense.
This part searches the home folder for files:
find "$HOME" -type f
find searches through directories.
"$HOME" tells it to search the current user’s home directory.
-type f means only look for regular files.
Then this part checks the size of those files:
-exec du -h {} +
du -h shows file size in a human-readable format.
The {} represents each file found by find.
Then there is this:
2>/dev/null
This hides error messages.
For example, if the script tries to read a file it does not have permission to access, it may throw an error. Sending those errors to /dev/null basically says, “Throw that noise away.”
Then this part sorts the results:
sort -rh
sort sorts the output.
-r means reverse order.
-h means sort human-readable sizes properly.
Then this part keeps only the top five:
head -n 5
Finally, the output is saved to the log file:
tee -a "$LOG_FILE"
So the full command finds the five largest files in the user’s home folder and includes them in the report.
That is very Bash.
Take one command. Pipe it into another. Then another. Then another.
Each tool does one small job.
Together, they do something useful.
Checking Failed Services
The script checks failed systemd services with:
echo "Failed systemd services:" | tee -a "$LOG_FILE"
systemctl --failed | tee -a "$LOG_FILE"
systemctl --failed shows services that failed to start or failed while running.
This is useful because a machine can look fine at first glance while still having broken services in the background.
This command gives you a quick list of things that may need attention.
For a health check script, this belongs in there.
Checking for Updates
After the health check, the script moves into updates:
echo "Updating package list..." | tee -a "$LOG_FILE"
sudo apt update | tee -a "$LOG_FILE"
sudo apt update refreshes the local package list.
This does not install updates yet.
That part is important.
apt update tells the system, “Go check the repositories and see what newer packages are available.”
After that, the script counts how many updates exist:
UPDATE_COUNT=$(apt list --upgradable 2>/dev/null | tail -n +2 | wc -l)
This is another line that looks worse than it really is.
Here is the breakdown.
This part lists packages that can be upgraded:
apt list --upgradable
This hides unnecessary warning output:
2>/dev/null
This removes the first line of output:
tail -n +2
That first line is usually just a header, so we do not want to count it as an update.
Then this counts the remaining lines:
wc -l
Finally, the result gets stored in the variable:
UPDATE_COUNT=$(...)
So if there are 12 available updates, UPDATE_COUNT becomes 12.
That number matters because the script uses it to decide what to do next.
Making a Decision
This is where the script starts behaving like an actual script instead of just a list of commands:
if [ "$UPDATE_COUNT" -gt 0 ]; then
echo "$UPDATE_COUNT update(s) available." | tee -a "$LOG_FILE"
echo "Downloading and installing updates..." | tee -a "$LOG_FILE"
sudo apt upgrade -y | tee -a "$LOG_FILE"
else
echo "No updates available. System is already up to date." | tee -a "$LOG_FILE"
fi
This is an if statement.
It checks whether UPDATE_COUNT is greater than zero.
The -gt means greater than.
So this part:
if [ "$UPDATE_COUNT" -gt 0 ]; then
means:
“If the number of available updates is greater than zero, do the next thing.”
And the next thing is:
sudo apt upgrade -y
That downloads and installs available package updates.
The -y automatically answers yes to prompts.
That is convenient on a personal Linux machine.
But on a production server, I would be more careful. I would want to review what is being upgraded first, especially if the server runs something important.
Automation is good.
Blind automation on production systems is how you create new hobbies you did not ask for.
Cleaning Up Packages
After updates are installed, the script cleans up:
sudo apt autoremove -y | tee -a "$LOG_FILE"
sudo apt autoclean | tee -a "$LOG_FILE"
This removes packages that were installed as dependencies but are no longer needed:
sudo apt autoremove -y
Then this cleans old package files from the local package cache:
sudo apt autoclean
This helps keep the system tidy and can free up disk space.
It is not exciting.
But again, this is the kind of maintenance work that adds up.
Checking if a Reboot Is Required
The script does not reboot the machine automatically.
It only checks whether a reboot is needed:
if [ -f /var/run/reboot-required ]; then
echo "A reboot is required." | tee -a "$LOG_FILE"
echo "Run this when ready:" | tee -a "$LOG_FILE"
echo "sudo reboot" | tee -a "$LOG_FILE"
else
echo "No reboot required." | tee -a "$LOG_FILE"
fi
This part checks for a file:
[ -f /var/run/reboot-required ]
On Ubuntu and Debian-based systems, that file usually appears when updates require a reboot.
If the file exists, the script tells you a reboot is needed.
If not, it says no reboot is required.
I like that the script stops there.
It does not assume it is safe to reboot.
It does not just restart the machine like an overconfident intern.
It gives the information and lets the admin decide.
That is the better behavior.
Ending the Report
The script ends with:
echo "Health check and update process complete." | tee -a "$LOG_FILE"
echo "Report saved to: $LOG_FILE"
Simple closing message.
The first line is written to the screen and log file.
The second line tells you where the report was saved.
That final message is useful because once a script gets longer, it is nice to know where the output went.
What This Taught Me About Bash
This little script taught me more than I expected.
It uses variables:
LOG_FILE="$HOME/health-update-report.txt"
It uses command substitution:
$(date)
$(hostname)
It uses pipes:
df -h | tee -a "$LOG_FILE"
It uses redirects:
2>/dev/null
It uses conditionals:
if [ "$UPDATE_COUNT" -gt 0 ]; then
It uses common Linux admin commands:
uptime
free -h
df -h
systemctl --failed
sudo apt update
sudo apt upgrade -y
That is why this was a good beginner project.
It was not just a toy script.
It performed real tasks using commands that Linux admins actually use.
Bash vs. PowerShell
Since I already use PowerShell, I kept comparing the two.
They both automate tasks.
They both run commands.
They both can be used seriously by sysadmins.
But they feel very different.
Bash mostly works with text.
PowerShell works with objects.
That is the big difference.
In Bash, you often take text output and pass it through tools like grep, awk, sed, cut, sort, head, and wc.
In PowerShell, commands usually return structured objects with properties.
For example, in PowerShell, this makes sense:
Get-Process | Where-Object CPU -gt 100
PowerShell understands that CPU is a property of the process object.
In Bash, you are more likely to work with plain-text output and shape it using other tools.
That can feel messier.
But it is also extremely flexible.
Where PowerShell Makes More Sense
PowerShell is still the better tool for many Microsoft administration tasks.
If I am working with Windows Server, Active Directory, Microsoft 365, Exchange, Azure, Entra ID, Intune, Defender, or other Microsoft systems, PowerShell makes sense.
That is its home field.
It is structured. It is readable. It handles objects and APIs well.
PowerShell commands also follow a pattern that becomes easier to read over time:
Get-Service
Get-Process
New-Item
Remove-Item
Set-ExecutionPolicy
For Microsoft-heavy environments, PowerShell is not going away for me.
It is too useful.
Where Bash Makes More Sense
Bash makes the most sense when I am working directly in Linux.
Especially over SSH.
Bash is usually already installed. It is already expected. Most Linux documentation assumes you understand it or at least know how to survive in it.
For Linux system administration, Bash is effective for:
Linux health checks.
Package updates.
Log review.
File cleanup.
Cron jobs.
Service checks.
Disk usage checks.
Small automation tasks.
Quick troubleshooting.
PowerShell can run on Linux now, and that is great.
But Bash still feels native there.
It is close to the operating system. It works naturally with Linux commands, files, permissions, logs, services, and text streams.
That is why it is worth learning.