Skip to content

Instantly share code, notes, and snippets.

@jwodder
Last active July 1, 2026 15:04
Show Gist options
  • Select an option

  • Save jwodder/19317d3e4b9a58f2355e7643040d483a to your computer and use it in GitHub Desktop.

Select an option

Save jwodder/19317d3e4b9a58f2355e7643040d483a to your computer and use it in GitHub Desktop.
Display a table of available APT package updates
#!/usr/bin/python3
"""
This script lists all APT package updates currently available for your system
along with the version numbers of the old & new packages. It is derived from
``/usr/lib/update-notifier/apt-check`` in the ``update-notifier-common``
package on Ubuntu 14.04 (Trusty Tahr) and is made available under the same
license (the GNU GPL v2).
This script is know to work on Ubuntu Trusty and Xenial, and it should work on
any recent version of Ubuntu with the ``python3-apt`` package installed.
Options:
- ``--csv`` - output table as CSV
- ``--plain`` - output a plain table (default)
- ``--pretty`` - output an ASCII table
Output columns:
- package name
- installed version
- update candidate version
- 'S' if the update is a security update, otherwise '-' (or empty in CSV)
- 'P' if the update is a `phased update
<https://wiki.ubuntu.com/PhasedUpdates>`_, otherwise '-' (or empty in CSV)
"""
from __future__ import annotations
import argparse
from collections.abc import Iterator
import csv
from dataclasses import dataclass, field
from itertools import zip_longest
from operator import attrgetter
import re
import sys
import apt
import apt_pkg
__author__ = "John Thorvald Wodder II"
__author_email__ = "apt-check@varonathe.org"
@dataclass
class Upgrade:
package: str
installed: str | None
candidate: str
security: bool
phased: bool
@property
def flags(self) -> str:
return ("S" if self.security else "-") + ("P" if self.phased else "-")
class AptChecker:
def __init__(self) -> None:
self.distro = None
with open("/etc/os-release") as fp:
for line in fp:
if m := re.fullmatch(r"VERSION_CODENAME=(\w+)", line.strip()):
self.distro = m[1]
if self.distro is None:
raise RuntimeError("Could not determine version codename")
self.security_pockets = [
("Debian", self.distro + "-security"), # TODO: Is this accurate?
("Ubuntu", self.distro + "-security"),
("UbuntuESM", self.distro + "-infra-security"),
("UbuntuESMApps", self.distro + "-apps-security"),
("gNewSense", self.distro + "-security"),
("Debian", self.distro + "-updates"),
]
def isSecurityUpgrade(self, ver) -> bool:
"""Check if the given version is a security update (or masks one)"""
return any(
(f.origin, f.archive) in self.security_pockets for f, _ in ver.file_list
)
def apt_check(self) -> Iterator[Upgrade]:
apt_pkg.init()
cache = apt_pkg.Cache(apt.progress.base.OpProgress())
depcache = apt_pkg.DepCache(cache)
if depcache.broken_count > 0:
raise SystemExit("Error: BrokenCount > 0")
try:
from UpdateManager.Core.UpdateList import UpdateList
ul = UpdateList(None)
except ImportError:
ul = None
# This mimics an upgrade but will never remove anything
depcache.upgrade(True)
if depcache.del_count > 0:
# Unmark (clean) all changes from the given depcache
depcache.init()
depcache.upgrade()
with apt.Cache() as aptcache:
for pkg in cache.packages:
if not depcache.marked_install(pkg) and not depcache.marked_upgrade(
pkg
):
continue
inst_ver = pkg.current_ver
cand_ver = depcache.get_candidate_ver(pkg)
if cand_ver == inst_ver:
continue
security = False
phased = False
if self.isSecurityUpgrade(cand_ver):
security = True
elif inst_ver:
# Check for security updates that are masked by a candidate
# version from another repo (-proposed or -updates)
for ver in pkg.version_list:
if apt_pkg.version_compare(
ver.ver_str, inst_ver.ver_str
) > 0 and self.isSecurityUpgrade(ver):
security = True
break
if ul is not None and ul._is_ignored_phased_update(aptcache[pkg.name]):
phased = True
yield Upgrade(
package=pkg.name,
installed=inst_ver.ver_str if inst_ver else None,
candidate=cand_ver.ver_str,
security=security,
phased=phased,
)
@dataclass
class Table:
headers: list[str]
rows: list[list[str]] = field(init=False, default_factory=list)
colwidths: list[int] = field(init=False)
def __post_init__(self) -> None:
self.colwidths = [len(s) for s in self.headers]
def add_row(self, row: list[str]) -> None:
for i, cell in enumerate(row):
if i < len(self.colwidths):
self.colwidths[i] = max(self.colwidths[i], len(cell))
else:
self.colwidths.append(len(cell))
self.rows.append(row)
def render(self) -> str:
hrule = "+-" + "-+-".join("-" * w for w in self.colwidths) + "-+\n"
s = hrule
s += (
"| "
+ " | ".join(
h.ljust(w)
for (h, w) in zip_longest(self.headers, self.colwidths, fillvalue="")
)
+ " |\n"
)
s += hrule
for row in self.rows:
s += (
"| "
+ " | ".join(
h.ljust(w)
for (h, w) in zip_longest(row, self.colwidths, fillvalue="")
)
+ " |\n"
)
s += hrule
return s
def main() -> None:
parser = argparse.ArgumentParser(description="List available APT package updates")
parser.add_argument(
"--csv",
action="store_const",
dest="format",
const="csv",
help="Output table as comma-separated values",
)
parser.add_argument(
"--plain",
action="store_const",
dest="format",
const="plain",
help="Output a plain table (default)",
)
parser.add_argument(
"--pretty",
action="store_const",
dest="format",
const="pretty",
help="Output a pretty table",
)
args = parser.parse_args()
updates = AptChecker().apt_check()
if args.format is None or args.format == "plain":
for upd in updates:
print(
f"{upd.package:20} {upd.installed!s:20} {upd.candidate:20} {upd.flags}"
)
elif args.format == "pretty":
tbl = Table(["Package", "Installed", "Candidate", "SP"])
for upd in sorted(updates, key=attrgetter("package")):
tbl.add_row([upd.package, upd.installed or "", upd.candidate, upd.flags])
print(tbl.render())
elif args.format == "csv":
out = csv.writer(sys.stdout)
out.writerow(["package", "installed", "candidate", "security", "phased"])
for upd in updates:
out.writerow(
[
upd.package,
upd.installed,
upd.candidate,
"S" if upd.security else "",
"P" if upd.phased else "",
]
)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment