Files
jcoffey-dev 466aa69533 docs: bring the README in line with what the scripts now do
The old README was a flat list of one-line descriptions, several of
which no longer described the script: log_rotate gained flags and no
longer purges by default, restore and process_monitor now confirm before
acting, rsync_magic changed a default, and disk_cleanup grew guards.

Rewritten around the conventions that are now consistent across the
collection -- --dry-run, a confirmation before anything destructive, an
unattended escape hatch, and a refusal rather than a guess when there is
no tty to ask on. That last one is the part worth knowing before putting
any of these in cron.

Groups the scripts by what they are for rather than listing them
alphabetically, and states the things a reader would otherwise have to
discover by reading source: that security_audit as an ordinary user
proves very little, that log_rotate is not a logrotate replacement and
why, that update_system is unattended on every branch, and that
restore's tar options matter because an archive picks its own ownership
and modes.

Also updated three script headers that had gained ASSUME_YES/FORCE
escapes without documenting them, so the headers and the README agree.

Every flag, environment variable and behaviour claimed here was checked
against the scripts rather than written from memory.
2026-08-22 22:27:40 -07:00

5.0 KiB

Linux System Administration Scripts

A collection of Bash scripts for routine Linux system administration: backups, log handling, monitoring, user and service management, and package updates.

Each script is distribution-agnostic, using only base utilities (tar, rsync, find, awk, grep, ss/netstat, systemctl), carries its usage in its header, and is released under the GNU GPL v3.0.

Before you run these

Most of these run as root and several change or delete things. They are written to be careful about it, but read the header of any script before you point it at a system you care about.

The conventions are consistent across the collection:

--dry-run show what would happen, change nothing
confirmation prompts anything destructive asks first
--yes / ASSUME_YES=1 / FORCE=1 skip the prompt for unattended use
no tty, no override the script refuses rather than proceeding unprompted

That last row is the important one for cron: a destructive script with no terminal to ask on will stop rather than guess.

All fifteen run under set -euo pipefail, and the repository is linted by ShellCheck on every push.

The scripts

Backup and restore

  • backup.sh <source_dir> <dest_dir> — archives a directory to name-backup-YYYYmmdd-HHMMSS.tar.gz. Refuses to overwrite an existing archive, and writes to a .partial name renamed only on success, so an interrupted run cannot leave a truncated file that looks like a backup.
  • restore.sh <archive.tar.gz> [target_dir] — lists the archive contents, confirms, then extracts with --no-same-owner and --no-same-permissions. An archive chooses its own paths, ownership and modes, so as root those defaults matter.
  • rsync_magic.sh [--dry-run] [--inplace] [--yes] <source> <dest> — rsync wrapper with checksums, ACLs, xattrs and a timestamped backup directory for anything --delete removes. Confirms before syncing, because reversing the two arguments erases the backup.
  • zimbra_backup.sh / zimbra_restore.sh — per-mailbox Zimbra backup and restore, prompting for address and directory. The backup verifies its own output (size and gzip integrity) rather than trusting a zero exit status, and keeps a failed attempt as .suspect for inspection.

Logs

  • log_inspect.sh [search <pattern> | tail <logfile>] — searches /var/log or tails a log. Distinguishes "no matches" from "could not read everything", so an unprivileged search does not look like a clean one.
  • log_rotate.sh [--days N] [--purge-days N] [--dry-run] [--yes] — compresses old .log files. Skips files a running process still holds open, which is why it is not a logrotate replacement: it cannot signal daemons or truncate in place, so it leaves those for the real thing. Deleting old archives is opt-in via --purge-days.

Monitoring and inspection

  • sys_monitor.sh — uptime, load, memory, disk, top processes.
  • network_info.sh — interfaces, routes, listening ports, firewall rules (iptables, falling back to nft).
  • security_audit.sh — world-writable files and directories, SUID/SGID binaries, listening ports. Run as root: find cannot descend where it may not read, so a clean report as an ordinary user means little.
  • disk_cleanup.sh [--clean] [--dry-run] [--age N] [--dirs A,B] [--yes] — disk usage overview, and optional cleanup of package caches and old temp files. --dirs refuses protected system directories and confirms for anything outside /tmp and /var/tmp.

System management

  • process_monitor.sh [kill <name|PID>] — top consumers; the kill path prints what it matched and asks first, since pkill by name can match more than one process.
  • service_manager.sh <action> <service> — start, stop, restart, status, enable, disable, list. status and list work as any user; the rest require root.
  • user_manage.sh <subcommand> … — add/remove users and groups, group membership, lock/unlock. listusers and listgroups are open to anyone; everything else needs root. deluser removes the home directory, so it shows what will go and confirms.
  • update_system.sh — detects the package manager (apt, dnf, yum, zypper, pacman) and applies updates unattended. Every branch passes -y or --noconfirm; on Arch in particular, -Syuu --noconfirm answers away the prompts that would otherwise warn about a partial upgrade.

Contributing

ShellCheck runs on every push and pull request at warning severity, against a clean tree — anything it reports is new. Run it locally before opening a PR:

shellcheck *.sh

Keep the existing conventions: set -euo pipefail, a root check on anything that changes system state, --dry-run where it makes sense, and a confirmation plus an unattended escape hatch for anything destructive.

License

GNU General Public License v3.0 — see LICENSE.