Major refactor, add updates command with json output flag

This commit is contained in:
ferrreo 2023-06-05 19:37:41 +01:00
parent 123386c27b
commit 98f41ec031
8 changed files with 306 additions and 160 deletions

View File

@ -4,7 +4,7 @@
pikman [global options] command [command options] [arguments...] pikman [global options] command [command options] [arguments...]
## VERSION: ## VERSION:
v1.23.2.17.0 v1.23.6.5.1
## COMMANDS: ## COMMANDS:
@ -24,6 +24,7 @@ v1.23.2.17.0
- **unexport** - Unexport/Remove a program's desktop entry - **unexport** - Unexport/Remove a program's desktop entry
- **update** - Update the list of available packages - **update** - Update the list of available packages
- **upgrade** - Upgrade the system by installing/upgrading available packages - **upgrade** - Upgrade the system by installing/upgrading available packages
- **upgrades** - List the available upgrades
- **help, h** - Shows a list of commands or help for one command - **help, h** - Shows a list of commands or help for one command
## GLOBAL OPTIONS: ## GLOBAL OPTIONS:

112
command/command.go Normal file
View File

@ -0,0 +1,112 @@
package command
import (
"fmt"
"os"
"os/exec"
"strings"
"pikman/alpine"
"pikman/arch"
"pikman/fedora"
"pikman/flatpak"
"pikman/types"
"pikman/ubuntu"
"pikman/updates"
"github.com/urfave/cli/v2"
)
var commandsMap = map[types.OSType]map[string]string{
types.Ubuntu: ubuntu.Commands,
types.Arch: arch.Commands,
types.Fedora: fedora.Commands,
types.Alpine: alpine.Commands,
types.Flatpak: flatpak.Commands,
}
var packageManagerMap = map[types.OSType]string{
types.Ubuntu: ubuntu.PackageManager,
types.Arch: arch.PackageManager,
types.Fedora: fedora.PackageManager,
types.Alpine: alpine.PackageManager,
types.Flatpak: flatpak.PackageManager,
}
type Command struct {
Command string
OsType types.OSType
ContainerName string
PackageName []string
IsUpgradable bool
IsInstalled bool
IsJSON bool
}
func (c *Command) Process(cCtx *cli.Context) error {
c.Command = cCtx.Command.FullName()
c.PackageName = cCtx.Args().Slice()
return c.processCommand()
}
func (c *Command) processCommand() error {
if (c.OsType == types.Ubuntu || c.OsType == types.Flatpak) && c.Command == "upgrades" {
return c.runUpgrades()
}
var err error
if c.OsType != types.Ubuntu && c.OsType != types.Flatpak && c.ContainerName != "" {
c.PackageName = append([]string{"--name " + c.ContainerName}, c.PackageName...)
}
if c.OsType == types.Ubuntu && c.IsUpgradable {
c.PackageName = append([]string{"--upgradable"}, c.PackageName...)
}
if c.OsType == types.Ubuntu && c.IsInstalled {
c.PackageName = append([]string{"--installed"}, c.PackageName...)
}
commandToExecute, err := c.getCommand()
if err != nil {
return err
}
cmd := exec.Command("/bin/sh", "-c", commandToExecute)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
if err != nil {
return err
}
return nil
}
func (c *Command) getCommand() (string, error) {
commandMap := commandsMap[c.OsType]
cmd, ok := commandMap[c.Command]
if ok {
return fmt.Sprintf("%s %s %s", packageManagerMap[c.OsType], cmd, strings.Join(c.PackageName, " ")), nil
}
return "", fmt.Errorf("%s: is not a valid command for this distro", c.Command)
}
func (c *Command) runUpgrades() error {
var err error
if c.OsType == types.Ubuntu {
err = updates.GetUbuntuUpdates(c.IsJSON)
if err != nil {
return err
}
}
err = updates.GetFlatpakUpdates(c.IsJSON)
return err
}

View File

@ -1,8 +1,9 @@
package loader package command
import ( import (
"pikman/types"
"testing" "testing"
"pikman/types"
) )
func Test_getCommand(t *testing.T) { func Test_getCommand(t *testing.T) {
@ -24,7 +25,7 @@ func Test_getCommand(t *testing.T) {
osType: types.Ubuntu, osType: types.Ubuntu,
packageName: []string{"testPackage"}, packageName: []string{"testPackage"},
}, },
want: "sudo -S nala install testPackage", want: "sudo -S apt install testPackage",
wantErr: false, wantErr: false,
}, },
{ {
@ -60,7 +61,12 @@ func Test_getCommand(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := getCommand(tt.args.command, tt.args.osType, tt.args.packageName) command := &Command{
Command: tt.args.command,
OsType: tt.args.osType,
PackageName: tt.args.packageName,
}
got, err := command.getCommand()
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("getCommand() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("getCommand() error = %v, wantErr %v", err, tt.wantErr)
return return

7
debian/changelog vendored
View File

@ -1,3 +1,10 @@
pikman (1.23.6.5.1-99pika2.lunar) lunar; urgency=low
* Add upgrades with --json flag
* Big refactor, code is nicer now
-- Ward Nakchbandi <hotrod.master@hotmail.com> Sat, 10 Dec 2022 13:48:00 +0300
pikman (1.23.2.17.1-99pika2.lunar) lunar; urgency=low pikman (1.23.2.17.1-99pika2.lunar) lunar; urgency=low
* Update. * Update.

View File

@ -1,69 +0,0 @@
package loader
import (
"fmt"
"os"
"os/exec"
"pikman/alpine"
"pikman/arch"
"pikman/fedora"
"pikman/flatpak"
"pikman/types"
"pikman/ubuntu"
"strings"
)
var commandsMap = map[types.OSType]map[string]string{
types.Ubuntu: ubuntu.Commands,
types.Arch: arch.Commands,
types.Fedora: fedora.Commands,
types.Alpine: alpine.Commands,
types.Flatpak: flatpak.Commands,
}
var packageManagerMap = map[types.OSType]string{
types.Ubuntu: ubuntu.PackageManager,
types.Arch: arch.PackageManager,
types.Fedora: fedora.PackageManager,
types.Alpine: alpine.PackageManager,
types.Flatpak: flatpak.PackageManager,
}
func ProcessCommand(command string, osType types.OSType, containerName string, packageName []string, upgradableFlag bool, installedFlag bool) error {
var err error
if osType != types.Ubuntu && osType != types.Flatpak && containerName != "" {
packageName = append([]string{"--name " + containerName}, packageName...)
}
if osType == types.Ubuntu && upgradableFlag {
packageName = append([]string{"--upgradable"}, packageName...)
}
if osType == types.Ubuntu && installedFlag {
packageName = append([]string{"--installed"}, packageName...)
}
commandToExecute, err := getCommand(command, osType, packageName)
cmd := exec.Command("/bin/sh", "-c", commandToExecute)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
if err != nil {
return err
}
return nil
}
func getCommand(command string, osType types.OSType, packageName []string) (string, error) {
commandMap := commandsMap[osType]
cmd, ok := commandMap[command]
if ok {
return fmt.Sprintf("%s %s %s", packageManagerMap[osType], cmd, strings.Join(packageName, " ")), nil
}
return "", fmt.Errorf("%s: is not a valid command for this distro", command)
}

152
main.go
View File

@ -3,22 +3,27 @@ package main
import ( import (
"log" "log"
"os" "os"
"pikman/loader"
"pikman/command"
"pikman/types" "pikman/types"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
func main() { func main() {
if os.Getuid() == 0 { if os.Getuid() == 0 {
log.Fatalf("Error: Do not run pikman as root") log.Fatalf("Error: Do not run pikman as root")
} }
osType := types.Ubuntu cmd := &command.Command{
containerName := "" OsType: types.Ubuntu,
upgradableFlag := false ContainerName: "",
installedFlag := false IsUpgradable: false,
IsJSON: false,
IsInstalled: false,
PackageName: make([]string, 0),
}
cli.VersionFlag = &cli.BoolFlag{ cli.VersionFlag = &cli.BoolFlag{
Name: "version", Name: "version",
Aliases: []string{"v"}, Aliases: []string{"v"},
@ -28,7 +33,7 @@ func main() {
app := &cli.App{ app := &cli.App{
Name: "pikman", Name: "pikman",
Usage: "One package manager to rule them all", Usage: "One package manager to rule them all",
Version: "v1.23.2.17.0", Version: "v1.23.6.5.1",
EnableBashCompletion: true, EnableBashCompletion: true,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
@ -37,7 +42,7 @@ func main() {
Usage: "Install Arch packages (including from the AUR)", Usage: "Install Arch packages (including from the AUR)",
Action: func(cCtx *cli.Context, b bool) error { Action: func(cCtx *cli.Context, b bool) error {
if b { if b {
osType = types.Arch cmd.OsType = types.Arch
} }
return nil return nil
}, },
@ -48,7 +53,7 @@ func main() {
Usage: "Install Fedora packages", Usage: "Install Fedora packages",
Action: func(cCtx *cli.Context, b bool) error { Action: func(cCtx *cli.Context, b bool) error {
if b { if b {
osType = types.Fedora cmd.OsType = types.Fedora
} }
return nil return nil
}, },
@ -59,7 +64,7 @@ func main() {
Usage: "Install Alpine packages", Usage: "Install Alpine packages",
Action: func(cCtx *cli.Context, b bool) error { Action: func(cCtx *cli.Context, b bool) error {
if b { if b {
osType = types.Alpine cmd.OsType = types.Alpine
} }
return nil return nil
}, },
@ -70,7 +75,7 @@ func main() {
Usage: "Install Flatpak packages", Usage: "Install Flatpak packages",
Action: func(cCtx *cli.Context, b bool) error { Action: func(cCtx *cli.Context, b bool) error {
if b { if b {
osType = types.Flatpak cmd.OsType = types.Flatpak
} }
return nil return nil
}, },
@ -78,53 +83,41 @@ func main() {
&cli.StringFlag{ &cli.StringFlag{
Name: "name", Name: "name",
Usage: "Name of the managed container", Usage: "Name of the managed container",
Destination: &containerName, Destination: &cmd.ContainerName,
}, },
}, },
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "autoremove", Name: "autoremove",
Usage: "Remove all unused packages", Usage: "Remove all unused packages",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "clean", Name: "clean",
Aliases: []string{"cl"}, Aliases: []string{"cl"},
Usage: "Clean the package manager cache", Usage: "Clean the package manager cache",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "enter", Name: "enter",
Usage: "Enter the container instance for select package manager", Usage: "Enter the container instance for select package manager",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "export", Name: "export",
Usage: "Export/Recreate a program's desktop entry from the container", Usage: "Export/Recreate a program's desktop entry from the container",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "init", Name: "init",
Usage: "Initialize a managed container", Usage: "Initialize a managed container",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "install", Name: "install",
Aliases: []string{"i"}, Aliases: []string{"i"},
Usage: "Install the specified package(s)", Usage: "Install the specified package(s)",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "list", Name: "list",
@ -134,83 +127,74 @@ func main() {
&cli.BoolFlag{ &cli.BoolFlag{
Name: "upgradable", Name: "upgradable",
Usage: "Used by list to check upgradable packages", Usage: "Used by list to check upgradable packages",
Destination: &upgradableFlag, Destination: &cmd.IsUpgradable,
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "installed", Name: "installed",
Usage: "Used by list to check installed packages", Usage: "Used by list to check installed packages",
Destination: &installedFlag, Destination: &cmd.IsInstalled,
}, },
}, },
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), upgradableFlag, installedFlag)
},
}, },
{ {
Name: "log", Name: "log",
Usage: "Show package manager logs", Usage: "Show package manager logs",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "purge", Name: "purge",
Usage: "Fully purge a package", Usage: "Fully purge a package",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "run", Name: "run",
Usage: "Run a command inside a managed container", Usage: "Run a command inside a managed container",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "remove", Name: "remove",
Aliases: []string{"r"}, Aliases: []string{"r"},
Usage: "Remove an installed package", Usage: "Remove an installed package",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "search", Name: "search",
Aliases: []string{"s"}, Aliases: []string{"s"},
Usage: "Search for a package", Usage: "Search for a package",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "show", Name: "show",
Usage: "Show details for a package", Usage: "Show details for a package",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "unexport", Name: "unexport",
Usage: "Unexport/Remove a program's desktop entry", Usage: "Unexport/Remove a program's desktop entry",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "update", Name: "update",
Usage: "Update the list of available packages", Usage: "Update the list of available packages",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false)
},
}, },
{ {
Name: "upgrade", Name: "upgrade",
Usage: "Upgrade the system by installing/upgrading available packages", Usage: "Upgrade the system by installing/upgrading available packages",
Action: func(cCtx *cli.Context) error { Action: cmd.Process,
cCtx.Args().Tail() },
return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) {
Name: "upgrades",
Usage: "List the available upgrades",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "json",
Usage: "Output in JSON",
Destination: &cmd.IsJSON,
},
}, },
Action: cmd.Process,
}, },
}, },
} }

View File

@ -1,6 +1,6 @@
package ubuntu package ubuntu
var PackageManager = "sudo -S nala" var PackageManager = "sudo -S apt"
var Commands = map[string]string{ var Commands = map[string]string{
"autoremove": "autoremove", "autoremove": "autoremove",
@ -12,5 +12,5 @@ var Commands = map[string]string{
"search": "search", "search": "search",
"show": "show", "show": "show",
"update": "update", "update": "update",
"upgrade": "upgrade", "upgrade": "update && sudo -S apt upgrade",
} }

105
updates/updates.go Normal file
View File

@ -0,0 +1,105 @@
package updates
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os/exec"
"regexp"
"strings"
)
func GetUbuntuUpdates(isJSON bool) error {
cmd := exec.Command("apt", "list", "--upgradable")
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("running apt list: %s", err)
}
re := regexp.MustCompile(`^([^ ]+) ([^ ]+) ([^ ]+)( \[upgradable from: [^\[\]]*\])?`)
scanner := bufio.NewScanner(bytes.NewReader(out))
output := make([]aptPackage, 0)
for scanner.Scan() {
matches := re.FindAllStringSubmatch(scanner.Text(), -1)
if len(matches) == 0 {
continue
}
name := strings.Split(matches[0][1], "/")[0]
if isJSON {
pack := &aptPackage{
Name: name,
Version: matches[0][2],
Architecture: matches[0][3],
}
output = append(output, *pack)
} else {
println(name)
}
}
if isJSON {
jsonout, err := json.Marshal(output)
if err != nil {
return fmt.Errorf("formatting apt list output: %s", err)
}
println(string(jsonout))
}
return nil
}
func GetFlatpakUpdates(isJSON bool) error {
cmd := exec.Command("flatpak", "remote-ls", "--updates")
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("running flatpak list: %s", err)
}
re := regexp.MustCompile(`(?-s)(\S+)`)
scanner := bufio.NewScanner(bytes.NewReader(out))
output := make([]flatPackage, 0)
for scanner.Scan() {
matches := re.FindAllString(scanner.Text(), -1)
if len(matches) == 0 {
continue
}
if isJSON {
pack := &flatPackage{
Name: matches[0],
ApplicationID: matches[1],
Branch: matches[2],
Arch: matches[3],
}
output = append(output, *pack)
} else {
println(matches[0])
}
}
if isJSON {
jsonout, err := json.Marshal(output)
if err != nil {
return fmt.Errorf("formatting apt list output: %s", err)
}
println(string(jsonout))
}
return nil
}
type aptPackage struct {
Name string
Version string
Architecture string
}
type flatPackage struct {
Name string
ApplicationID string
Branch string
Arch string
}