190 lines
4.5 KiB
Python
190 lines
4.5 KiB
Python
|
#!/usr/bin/env python3
|
||
|
|
||
|
import sys
|
||
|
import os
|
||
|
import subprocess
|
||
|
from glob import glob
|
||
|
from functools import partial as p
|
||
|
import re
|
||
|
|
||
|
|
||
|
def first(iterable, default=None):
|
||
|
it = iter(iterable)
|
||
|
try:
|
||
|
return next(it)
|
||
|
except StopIteration:
|
||
|
return default
|
||
|
|
||
|
|
||
|
# Config reading
|
||
|
|
||
|
def read_ini(contents, collector=None):
|
||
|
section = None
|
||
|
|
||
|
if collector is None:
|
||
|
collector = {}
|
||
|
|
||
|
for line in map(str.strip, contents):
|
||
|
line = re.sub(r"\s*#.*$", "", line)
|
||
|
|
||
|
m = re.fullmatch(r"\[(.+)\]", line)
|
||
|
if m:
|
||
|
section = m.group(1)
|
||
|
if section not in collector:
|
||
|
collector[section] = {}
|
||
|
continue
|
||
|
|
||
|
m = re.fullmatch(r"([^=]+?)\s*=\s*(.*?)", line)
|
||
|
if m:
|
||
|
collector[section][m.group(1)] = m.group(2)
|
||
|
continue
|
||
|
|
||
|
return collector
|
||
|
|
||
|
|
||
|
def read_config(path):
|
||
|
collector = {}
|
||
|
# In Gentoo config files can be directories instead of files. The files in the directory are then
|
||
|
# in evaluated alphabetical order.
|
||
|
for repo_conf in sorted(glob(os.path.join(path, "*"))):
|
||
|
with open(repo_conf) as fh:
|
||
|
collector = read_ini(fh, collector)
|
||
|
|
||
|
return collector
|
||
|
|
||
|
|
||
|
def repo_locations():
|
||
|
result = []
|
||
|
for name, defs in read_config("/etc/portage/repos.conf").items():
|
||
|
if "location" in defs:
|
||
|
result.append((name, defs["location"]))
|
||
|
return result
|
||
|
|
||
|
|
||
|
def default_repo():
|
||
|
return read_config("/etc/portage/repos.conf").get("DEFAULT", {}).get("main-repo", None)
|
||
|
|
||
|
|
||
|
# Searching
|
||
|
|
||
|
def list_from_print0(data):
|
||
|
data = data.rstrip(b"\x00")
|
||
|
|
||
|
if not data:
|
||
|
return []
|
||
|
|
||
|
return [
|
||
|
x.decode()
|
||
|
for x in data.split(b"\x00")
|
||
|
]
|
||
|
|
||
|
|
||
|
def find(query, paths, depth=None):
|
||
|
depth_args = ["-mindepth", f"{depth}", "-maxdepth", f"{depth}"] if depth is not None else []
|
||
|
|
||
|
proc = subprocess.run(
|
||
|
["find", *paths, *depth_args, "-iname", f"*query*", "-print0"],
|
||
|
check=True, stdout=subprocess.PIPE
|
||
|
)
|
||
|
return list_from_print0(proc.stdout)
|
||
|
|
||
|
|
||
|
def fd(query, paths, depth):
|
||
|
depth_args = [f"--exact-depth={depth}"] if depth is not None else []
|
||
|
|
||
|
proc = subprocess.run(
|
||
|
["fd", *depth_args, "--print0", "--extension=ebuild", "--", query, *paths],
|
||
|
check=True, stdout=subprocess.PIPE
|
||
|
)
|
||
|
return list_from_print0(proc.stdout)
|
||
|
|
||
|
|
||
|
# search_backend = find
|
||
|
search_backend = fd
|
||
|
|
||
|
|
||
|
# Ebuild reading
|
||
|
|
||
|
def description_from_ebuild(path):
|
||
|
with open(path) as fh:
|
||
|
for line in map(str.strip, fh):
|
||
|
m = re.fullmatch(r'DESCRIPTION=(?:"(.*)"|[^"](.*))', line)
|
||
|
if m:
|
||
|
return m.group(1).replace(r'\"', '"') if m.group(1) else m.group(2)
|
||
|
return None
|
||
|
|
||
|
|
||
|
def ebuild_info(path, repos):
|
||
|
m = re.fullmatch(r"(.+)/([^/]+)/([^/]+)/\3-([^/]+)\.ebuild", path)
|
||
|
if not m:
|
||
|
print(path)
|
||
|
PN = f"{m.group(2)}/{m.group(3)}"
|
||
|
PV = f"{m.group(4)}"
|
||
|
|
||
|
return {
|
||
|
"P": f"{PN}-{PV}",
|
||
|
"PN": PN,
|
||
|
"PV": PV,
|
||
|
"category": m.group(2),
|
||
|
"name": m.group(3),
|
||
|
"repo": first((name for name, path in repos if path == m.group(1))),
|
||
|
"repo_path": m.group(1),
|
||
|
"ebuild_path": path,
|
||
|
"description": description_from_ebuild(path),
|
||
|
"installed": os.path.exists(f"/var/db/pkg/{PN}-{PV}"),
|
||
|
} if m else None
|
||
|
|
||
|
|
||
|
# Formatting
|
||
|
|
||
|
def longest_category_and_nameversion(infos, default_repo=None):
|
||
|
return (
|
||
|
max(len(x["category"]) for x in infos),
|
||
|
max(len(f"{x['name']}-{x['PV']}" + (f"::{x['repo']}" if x["repo"] != default_repo else "")) for x in infos)
|
||
|
)
|
||
|
|
||
|
|
||
|
def format_item(info, longest_cat=0, longest_namever=0, default_repo=None):
|
||
|
name_version = f"{info['name']}-{info['PV']}"
|
||
|
category_colour = "\x1b[0m" if info["installed"] else "\x1b[0;90m"
|
||
|
name_colour = "\x1b[92m" if info["installed"] else "\x1b[36m"
|
||
|
|
||
|
repo = f"::{info['repo']}" if info["repo"] != default_repo else ""
|
||
|
name = f"{name_version}{category_colour}{repo}"
|
||
|
filler = ("\x1b[90m " + ("╴" * (longest_namever - len(name_version) - len(repo)))) if info["description"] else ""
|
||
|
|
||
|
return f"{category_colour}{info['category']:>{longest_cat}}/{name_colour}{name}{filler}" + (f"╴{category_colour}─ \x1b[0m{info['description']}" if info["description"] else "")
|
||
|
|
||
|
|
||
|
|
||
|
def main(argv):
|
||
|
if len(argv) != 2 or not argv[1]:
|
||
|
print("Need exactly 1 argument: search query", file=sys.stderr)
|
||
|
return 1
|
||
|
|
||
|
repos = repo_locations()
|
||
|
default = default_repo()
|
||
|
|
||
|
if not repos:
|
||
|
raise Exception("Could not find any repositories in /etc/portage/repos.conf")
|
||
|
|
||
|
data = search_backend(argv[1], (r[1] for r in repos), depth=3)
|
||
|
if not data:
|
||
|
print("No results")
|
||
|
return 0
|
||
|
|
||
|
data = [ebuild_info(x, repos=repos) for x in data]
|
||
|
data.sort(key=lambda x: x["P"])
|
||
|
|
||
|
longest_cat, longest_namever = longest_category_and_nameversion(data, default)
|
||
|
|
||
|
for x in data:
|
||
|
print(format_item(
|
||
|
x, longest_cat=longest_cat, longest_namever=longest_namever, default_repo=default
|
||
|
))
|
||
|
|
||
|
return 0
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
sys.exit(main(sys.argv))
|