Bash · lesson 17 of 22
Also part of: Linux
Bash System Information
Inspect a Linux system from Bash with uname, df, du, free, ps, pgrep, ss, lsof and systemctl, and parse their output reliably in scripts.
By SkillByExample · Updated
The outputs below were captured in an Ubuntu 24.04 container running on a WSL2 kernel, so the sizes, PIDs and kernel version will differ on your machine. The shape of the output, and the parsing traps it shows, are the same on any Linux host.
uname and os-release: What Am I Running On?
Scripts that download binaries need the OS and CPU architecture in the vendor’s naming scheme:
echo "kernel: $(uname -s) $(uname -r)"
echo "arch: $(uname -m)"
case "$(uname -m)" in
x86_64) arch=amd64 ;;
aarch64|arm64) arch=arm64 ;;
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;
esac
os=$(uname -s | tr '[:upper:]' '[:lower:]')
echo "download: https://releases.example.com/app-${os}-${arch}.tar.gz"
. /etc/os-release && echo "distro: $PRETTY_NAME"
$ bash uname.sh
kernel: Linux 5.15.153.1-microsoft-standard-WSL2
arch: x86_64
download: https://releases.example.com/app-linux-amd64.tar.gz
distro: Ubuntu 24.04.5 LTS
uname -m says x86_64, but most release pages say amd64, so the case translates
between the two. Linux reports aarch64 and macOS reports arm64 for the same kind of
chip. /etc/os-release is a file of shell variable assignments, so sourcing it gives you
the distribution name and version.
df and du: Disk Space
df shows free space per filesystem. du shows what is using it.
df -h / | cut -c1-60
echo "---"
pct=$(df --output=pcent / | tail -1 | tr -dc '0-9')
echo "root filesystem is ${pct}% full"
(( pct >= 80 )) && echo "ALERT" || echo "OK (threshold 80%)"
echo "---"
mkdir -p logs && head -c 3M /dev/zero > logs/big.log && head -c 200K /dev/zero > logs/small.log && echo hi > logs/tiny.log
du -sh logs/* | sort -rh
$ bash disk.sh
Filesystem Size Used Avail Use% Mounted on
overlay 251G 14G 225G 6% /
---
root filesystem is 6% full
OK (threshold 80%)
---
3.0M logs/big.log
200K logs/small.log
4.0K logs/tiny.log
df --output=pcent asks for just the one column, which is sturdier than picking field 5
out of the full output. On some systems a long device name makes df put the numbers on a
second line, and field 5 is no longer the percentage. tr -dc '0-9' strips the % and the
padding. du reports blocks in use, so a 3-byte file still takes 4.0K. sort -rh
understands K/M/G suffixes. When the disk is full but du finds nothing large, check
inodes with df -i.
free: Memory
free -m
avail=$(free -m | awk '/^Mem:/ {print $7}')
echo "available: ${avail} MB"
(( avail < 500 )) && echo "WARN: low memory" || echo "OK"
$ bash memory.sh
total used free shared buff/cache available
Mem: 7795 1339 5393 49 1405 6455
Swap: 2048 0 2048
available: 6454 MB
OK
Alert on available, not free. Linux uses idle RAM as cache (buff/cache) and gives it
back when programs need it, so a low free figure is normal. The two numbers differ by 1 MB
because free ran twice, and memory changed between the calls. Matching the row by
/^Mem:/ is safer than using a line number.
ps and pgrep: Processes
sleep 30 & sleep 31 &
echo "-- ps -C: chosen columns for one command name:"
ps -o pid,ppid,user,stat,etime,cmd -C sleep
echo "-- pgrep -a sleep:"
pgrep -a sleep
kill %1 %2
$ bash ps.sh
-- ps -C: chosen columns for one command name:
PID PPID USER STAT ELAPSED CMD
9 8 alice S 00:00 sleep 30
10 8 alice S 00:00 sleep 31
-- pgrep -a sleep:
9 sleep 30
10 sleep 31
-o picks the columns and -C filters by command name, so there’s no ps aux | grep
pipeline. That pipeline has a well-known flaw: grep sometimes matches its own command
line, depending on timing. pgrep avoids that, and pgrep -f matches against the full
command line. For interactive use, ps aux (BSD style, with CPU and memory columns) and
ps -ef (with the parent PID) are the usual full listings, and --sort=-%cpu puts the
busiest processes first.
Don’t Parse top by Position
top -bn1 prints one snapshot in batch mode. Many scripts read the CPU idle figure out of
field 8:
top -bn1 | grep '^%Cpu' > cpu.txt
echo '%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st' >> cpu.txt
cat cpu.txt
echo "-- field 8 of each line:"
awk '{print "[" $8 "]"}' cpu.txt
echo "-- idle found by its label:"
awk -F',' '{ for (i = 1; i <= NF; i++) if ($i ~ / id$/) { gsub(/[^0-9.]/, "", $i); print $i "%" } }' cpu.txt
$ bash top-cpu.sh
%Cpu(s): 13.6 us, 5.3 sy, 0.0 ni, 77.3 id, 0.0 wa, 0.0 hi, 3.8 si, 0.0 st
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
-- field 8 of each line:
[77.3]
[id,]
-- idle found by its label:
77.3%
100.0%
The first line is a real snapshot. The second is the same format on a fully idle machine,
exactly as top prints it. top pads each number to five characters, and 100.0 fills
the whole space, so it joins onto the comma before it. Every field then shifts by one, and
“field 8” becomes the text id,, which breaks any arithmetic done on it. So the script
breaks exactly when the machine is at its quietest. Finding the value by its id label
works for both lines.
ss and lsof: Who Owns That Port?
nc -l 8080 >/dev/null &
sleep 0.5
echo "-- ss -tlnp:"
ss -tlnp | awk 'NR==1 || /:8080/'
echo "-- lsof -i :8080:"
lsof -nP -i :8080
echo "-- PID only:"
lsof -ti :8080
is_listening() { ss -tln | awk -v p=":$1" '$4 ~ p"$" {found=1} END {exit !found}'; }
is_listening 8080 && echo "8080: listening"
is_listening 80 || echo "80: nothing listening"
kill %1
$ bash ports.sh
-- ss -tlnp:
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0 1 0.0.0.0:8080 0.0.0.0:* users:(("nc",pid=9,fd=3))
-- lsof -i :8080:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nc 9 alice 3u IPv4 274181 0t0 TCP *:8080 (LISTEN)
-- PID only:
9
8080: listening
80: nothing listening
-t means TCP, -l listening sockets, -n numeric ports, and -p the owning process.
is_listening matches the port at the end of the local-address column. A plain
grep :80 would also match :8080. Processes owned by other users only appear with
sudo.
systemd: systemctl and journalctl
systemd doesn’t run inside this lesson’s container, so these commands are shown for reference only, with no captured output. On a real server they look like this:
systemctl is-active --quiet nginx && echo "nginx up" # exit status only — ideal in scripts
systemctl is-enabled nginx # enabled / disabled
sudo systemctl restart nginx
systemctl list-units --type=service --state=failed
journalctl -u nginx --since "1 hour ago" -p err # this unit's errors, last hour
journalctl -u nginx -f # follow, like tail -f
For scripts, prefer the --quiet and is-* forms. They report through their exit status,
so there’s no text to parse.
Practice
-
Print a one-line health summary with the 1-minute load, the memory in use as a percentage (total minus available), and the root disk usage. Set
status=ALERTif memory is over 90% or disk over 85%.Solution
read -r load1 _ < /proc/loadavg mem_pct=$(free -m | awk '/^Mem:/ { printf "%d", ($2 - $7) * 100 / $2 }') disk_pct=$(df --output=pcent / | tail -1 | tr -dc '0-9') printf 'host=%s load1=%s mem=%s%% disk=%s%%\n' "$(hostname)" "$load1" "$mem_pct" "$disk_pct" status=OK (( mem_pct > 90 || disk_pct > 85 )) && status=ALERT echo "status=$status"$ bash p1.sh host=devbox load1=1.35 mem=18% disk=6% status=OK/proc/loadavgis designed to be parsed, which makes it more reliable than scraping the output ofuptime. -
Free port 9090 by killing whatever is listening on it, then confirm the port is free.
Solution
nc -l 9090 >/dev/null & sleep 0.5 pids=$(lsof -ti :9090) echo "port 9090 held by PID(s): $pids" kill $pids sleep 0.2 lsof -ti :9090 || echo "port 9090 is free"$ bash p2.sh port 9090 held by PID(s): 9 port 9090 is free$pidsis deliberately unquoted so that several PIDs become separate arguments tokill. -
Wrap the OS and architecture detection in a
platformfunction that returns strings likelinux-amd64, and use it to build a release-download URL.Solution
platform() { local os arch os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$(uname -m)" in x86_64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) return 1 ;; esac echo "${os}-${arch}" } echo "https://github.com/example/tool/releases/download/v1.2.0/tool-$(platform).tar.gz"$ bash p3.sh https://github.com/example/tool/releases/download/v1.2.0/tool-linux-amd64.tar.gz