mirror of
https://github.com/gogrlx/snack.git
synced 2026-04-02 05:08:42 -07:00
- Add parseVersionLockDNF5 for dnf5's 'Package name: <name>' format - Wire v5 flag through listHeld - Relax apt ListRepos (DEB822 format may yield empty) - Relax snap Info version assertion (uninstalled snaps) - Add unit test for parseVersionLockDNF5
253 lines
6.3 KiB
Go
253 lines
6.3 KiB
Go
package dnf
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/gogrlx/snack"
|
|
)
|
|
|
|
// stripPreamble removes dnf5 repository loading output that appears on stdout.
|
|
// It strips everything from "Updating and loading repositories:" through
|
|
// "Repositories loaded." inclusive.
|
|
func stripPreamble(output string) string {
|
|
lines := strings.Split(output, "\n")
|
|
var result []string
|
|
inPreamble := false
|
|
for _, line := range lines {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "Updating and loading repositories:") {
|
|
inPreamble = true
|
|
continue
|
|
}
|
|
if inPreamble {
|
|
if strings.HasPrefix(trimmed, "Repositories loaded.") {
|
|
inPreamble = false
|
|
continue
|
|
}
|
|
continue
|
|
}
|
|
result = append(result, line)
|
|
}
|
|
return strings.Join(result, "\n")
|
|
}
|
|
|
|
// parseListDNF5 parses `dnf5 list --installed` / `dnf5 list --upgrades` output.
|
|
// Format:
|
|
//
|
|
// Installed packages
|
|
// name.arch version-release repo-hash
|
|
func parseListDNF5(output string) []snack.Package {
|
|
output = stripPreamble(output)
|
|
var pkgs []snack.Package
|
|
for _, line := range strings.Split(output, "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
// Skip section headers
|
|
lower := strings.ToLower(trimmed)
|
|
if lower == "installed packages" || lower == "available packages" ||
|
|
lower == "upgraded packages" || lower == "available upgrades" {
|
|
continue
|
|
}
|
|
parts := strings.Fields(trimmed)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
nameArch := parts[0]
|
|
ver := parts[1]
|
|
repo := ""
|
|
if len(parts) >= 3 {
|
|
repo = parts[2]
|
|
}
|
|
name, arch := parseArch(nameArch)
|
|
pkgs = append(pkgs, snack.Package{
|
|
Name: name,
|
|
Version: ver,
|
|
Arch: arch,
|
|
Repository: repo,
|
|
Installed: true,
|
|
})
|
|
}
|
|
return pkgs
|
|
}
|
|
|
|
// parseSearchDNF5 parses `dnf5 search` output.
|
|
// Format:
|
|
//
|
|
// Matched fields: name
|
|
// name.arch Description text
|
|
// Matched fields: summary
|
|
// name.arch Description text
|
|
func parseSearchDNF5(output string) []snack.Package {
|
|
output = stripPreamble(output)
|
|
var pkgs []snack.Package
|
|
for _, line := range strings.Split(output, "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" || strings.HasPrefix(trimmed, "Matched fields:") {
|
|
continue
|
|
}
|
|
// Lines are: "name.arch Description"
|
|
// Split on first double-space or use Fields
|
|
parts := strings.Fields(trimmed)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
nameArch := parts[0]
|
|
if !strings.Contains(nameArch, ".") {
|
|
continue
|
|
}
|
|
desc := strings.Join(parts[1:], " ")
|
|
name, arch := parseArch(nameArch)
|
|
pkgs = append(pkgs, snack.Package{
|
|
Name: name,
|
|
Arch: arch,
|
|
Description: desc,
|
|
})
|
|
}
|
|
return pkgs
|
|
}
|
|
|
|
// parseInfoDNF5 parses `dnf5 info` output.
|
|
// Format: "Key : Value" lines with possible section headers like "Available packages".
|
|
func parseInfoDNF5(output string) *snack.Package {
|
|
output = stripPreamble(output)
|
|
pkg := &snack.Package{}
|
|
for _, line := range strings.Split(output, "\n") {
|
|
idx := strings.Index(line, " : ")
|
|
if idx < 0 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(line[:idx])
|
|
val := strings.TrimSpace(line[idx+3:])
|
|
switch key {
|
|
case "Name":
|
|
pkg.Name = val
|
|
case "Version":
|
|
pkg.Version = val
|
|
case "Release":
|
|
if pkg.Version != "" {
|
|
pkg.Version = pkg.Version + "-" + val
|
|
}
|
|
case "Architecture", "Arch":
|
|
pkg.Arch = val
|
|
case "Summary":
|
|
pkg.Description = val
|
|
case "Repository", "From repo":
|
|
pkg.Repository = val
|
|
}
|
|
}
|
|
if pkg.Name == "" {
|
|
return nil
|
|
}
|
|
return pkg
|
|
}
|
|
|
|
// parseRepoListDNF5 parses `dnf5 repolist --all` output.
|
|
// Same tabular format as dnf4, reuses the same logic.
|
|
func parseRepoListDNF5(output string) []snack.Repository {
|
|
output = stripPreamble(output)
|
|
return parseRepoList(output)
|
|
}
|
|
|
|
// parseGroupListDNF5 parses `dnf5 group list` tabular output.
|
|
// Format:
|
|
//
|
|
// ID Name Installed
|
|
// neuron-modelling-simulators Neuron Modelling Simulators no
|
|
func parseGroupListDNF5(output string) []string {
|
|
output = stripPreamble(output)
|
|
var groups []string
|
|
inBody := false
|
|
for _, line := range strings.Split(output, "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
if !inBody {
|
|
if strings.HasPrefix(trimmed, "ID") && strings.Contains(trimmed, "Name") {
|
|
inBody = true
|
|
continue
|
|
}
|
|
continue
|
|
}
|
|
// Last field is "yes"/"no", second-to-last through first space group is name.
|
|
// Parse: first token is ID, last token is yes/no, middle is name.
|
|
parts := strings.Fields(trimmed)
|
|
if len(parts) < 3 {
|
|
continue
|
|
}
|
|
// Last field is installed status
|
|
status := parts[len(parts)-1]
|
|
if status != "yes" && status != "no" {
|
|
continue
|
|
}
|
|
name := strings.Join(parts[1:len(parts)-1], " ")
|
|
groups = append(groups, name)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
// parseVersionLockDNF5 parses `dnf5 versionlock list` output.
|
|
// Format:
|
|
//
|
|
// # Added by 'versionlock add' command on 2026-02-26 03:14:29
|
|
// Package name: tree
|
|
// evr = 2.2.1-2.fc43
|
|
func parseVersionLockDNF5(output string) []snack.Package {
|
|
output = stripPreamble(output)
|
|
var pkgs []snack.Package
|
|
for _, line := range strings.Split(output, "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if strings.HasPrefix(trimmed, "Package name:") {
|
|
name := strings.TrimSpace(strings.TrimPrefix(trimmed, "Package name:"))
|
|
if name != "" {
|
|
pkgs = append(pkgs, snack.Package{Name: name, Installed: true})
|
|
}
|
|
}
|
|
}
|
|
return pkgs
|
|
}
|
|
|
|
// parseGroupInfoDNF5 parses `dnf5 group info` output.
|
|
// Format:
|
|
//
|
|
// Id : kde-desktop
|
|
// Mandatory packages : plasma-desktop
|
|
// : plasma-workspace
|
|
// Default packages : NetworkManager-config-connectivity-fedora
|
|
func parseGroupInfoDNF5(output string) []snack.Package {
|
|
output = stripPreamble(output)
|
|
var pkgs []snack.Package
|
|
inPkgSection := false
|
|
for _, line := range strings.Split(output, "\n") {
|
|
idx := strings.Index(line, " : ")
|
|
if idx < 0 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(line[:idx])
|
|
val := strings.TrimSpace(line[idx+3:])
|
|
|
|
if key == "Mandatory packages" || key == "Default packages" ||
|
|
key == "Optional packages" || key == "Conditional packages" {
|
|
inPkgSection = true
|
|
if val != "" {
|
|
pkgs = append(pkgs, snack.Package{Name: val})
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Continuation line: key is empty
|
|
if key == "" && inPkgSection && val != "" {
|
|
pkgs = append(pkgs, snack.Package{Name: val})
|
|
continue
|
|
}
|
|
|
|
// Any other key ends the package section
|
|
if key != "" {
|
|
inPkgSection = false
|
|
}
|
|
}
|
|
return pkgs
|
|
}
|