Detect the OS and Package Manager
Problem statement
Read /etc/os-release to find which Linux a machine runs and which package manager it uses, so one script can install a tool on Ubuntu, Rocky, Amazon Linux or Alpine. Setup scripts, Dockerfiles and CI images all need this, because apt, dnf and apk are not interchangeable.
The script saves four sample os-release files, one per system:
ubuntu.txt
NAME="Ubuntu"VERSION_ID="24.04"ID=ubuntuID_LIKE=debianrocky.txt
NAME="Rocky Linux"VERSION_ID="9.4"ID="rocky"ID_LIKE="rhel centos fedora"amazon.txt
NAME="Amazon Linux"VERSION_ID="2023"ID="amzn"ID_LIKE="fedora"alpine.txt
NAME="Alpine Linux"ID=alpineVERSION_ID=3.20.3For each file, print the OS name and version, and the command that would install curl there. Finally print the kernel type of the machine running the script.
Expected output:
== Ubuntu 24.04 ==install: apt-get install -y curl== Rocky Linux 9.4 ==install: dnf install -y curl== Amazon Linux 2023 ==install: dnf install -y curl== Alpine Linux 3.20.3 ==install: apk add curl== this machine ==kernel: LinuxHints
os-release is written as shell variables, so . file (source) loads NAME, ID and the others into your shell. Do it inside ( ) so one file's values do not leak into the next.Approach
Optimal: Source os-release, then case
Covers: /etc/os-release, sourcing a file with ., subshells ( ), case with patterns, ID vs ID_LIKE, uname -s, command -v.
Every modern Linux has /etc/os-release. It is a small text file of KEY=value lines describing the system. Because it is written in shell syntax, the shell can load it directly. The keys that matter most:
| Key | Example | Use it for |
|---|---|---|
NAME |
Rocky Linux |
showing to people |
VERSION_ID |
9.4 |
version checks |
ID |
rocky |
the exact distribution |
ID_LIKE |
rhel centos fedora |
the family it belongs to |
Why ID_LIKE matters. There are dozens of distributions, but only a few families. Rocky, AlmaLinux and Amazon Linux are not rhel, but their ID_LIKE says they behave like it. Matching the family covers systems you have never heard of:
apt-get"]:::blue F --> R["contains rhel, fedora or centos
dnf"]:::red F --> A["contains alpine
apk"]:::green classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px
Loading the file safely. . file (also written source file) runs the file's lines in the current shell, so $NAME and $ID become available. Doing it inside ( ) runs a subshell, a copy of the shell, so the variables disappear at the ) and the next file starts clean. On a real server you would source /etc/os-release once, without the loop.
case picks the first matching pattern. case "$ID $ID_LIKE" in joins both values into one string, then each pattern is tried in order. *debian*|*ubuntu*) means "contains debian or ubuntu". *) at the end catches everything else. Each branch ends with ;;.
Walking through the code. The # Setup: lines only write the four samples, so skip past them.
- The
forloop visits each sample file. - Inside
( ),. "$f"loads its variables,echoprints name and version, andcasepicks the package manager. uname -sprints the kernel name of the machine itself:Linuxhere,Darwinon a Mac.
Edge cases. Very old systems may lack os-release; fall back to /etc/redhat-release or /etc/debian_version. Containers built FROM scratch have no os-release at all. Before installing, it is often simpler to check whether the tool already exists: command -v curl > /dev/null || install it.
# Setup: four sample os-release files in a fresh temporary folder
cd "$(mktemp -d)"
printf 'NAME="Ubuntu"\nVERSION_ID="24.04"\nID=ubuntu\nID_LIKE=debian\n' > ubuntu.txt
printf 'NAME="Rocky Linux"\nVERSION_ID="9.4"\nID="rocky"\nID_LIKE="rhel centos fedora"\n' > rocky.txt
printf 'NAME="Amazon Linux"\nVERSION_ID="2023"\nID="amzn"\nID_LIKE="fedora"\n' > amazon.txt
printf 'NAME="Alpine Linux"\nID=alpine\nVERSION_ID=3.20.3\n' > alpine.txt
for f in ubuntu.txt rocky.txt amazon.txt alpine.txt; do
(
. "./$f" # load NAME, ID, ID_LIKE, VERSION_ID
echo "== $NAME $VERSION_ID =="
case "$ID $ID_LIKE" in
*debian*|*ubuntu*) echo "install: apt-get install -y curl" ;;
*rhel*|*fedora*|*centos*) echo "install: dnf install -y curl" ;;
*alpine*) echo "install: apk add curl" ;;
*) echo "install: unknown package manager" ;;
esac
)
done
echo "== this machine =="
echo "kernel: $(uname -s)"Interview follow-ups
Print the install command for nginx, whose package name differs by family.
Keep the family detection, and add a variable per branch: in the debian branch set
pkg=nginxandmgr="apt-get install -y"; in the rhel branch the package is alsonginx, but for Apache it would behttpdinstead ofapache2. Then run$mgr $pkgonce at the end. Keeping a small table of names per family in one place is cleaner than manyifchecks spread through the script.
Frequently asked questions
Check for the tool you need when you can: command -v apt-get tells you apt exists, and command -v curl tells you curl is already installed. That keeps working on systems your case never listed. Check the OS when behaviour really differs by family, like package names (httpd vs apache2) or service names (ssh vs sshd). Many scripts do both: detect the family, then confirm the command exists.
dnf replaced yum on Fedora, RHEL 8 and later, Rocky, AlmaLinux and Amazon Linux 2023. On those systems yum is kept as a link to dnf, so old scripts still work. Older systems like CentOS 7 and Amazon Linux 2 have only yum. A safe pattern is if command -v dnf > /dev/null; then dnf ...; else yum ...; fi.
Sourcing runs every line as shell code, so a file could run any command. /etc/os-release is owned by root and written by the OS, so it is trusted on that machine. Never source a file that users or downloads can change. To read untrusted KEY=value files, parse them instead, for example with grep '^ID=' file | cut -d= -f2 | tr -d '"'.