From 98f41ec0313770b52410749a2f3b7154223d22f5 Mon Sep 17 00:00:00 2001 From: ferrreo Date: Mon, 5 Jun 2023 19:37:41 +0100 Subject: [PATCH] Major refactor, add updates command with json output flag --- README.md | 3 +- command/command.go | 112 +++++++++++++ .../loader_test.go => command/command_test.go | 14 +- debian/changelog | 7 + loader/loader.go | 69 -------- main.go | 152 ++++++++---------- ubuntu/commands.go | 4 +- updates/updates.go | 105 ++++++++++++ 8 files changed, 306 insertions(+), 160 deletions(-) create mode 100644 command/command.go rename loader/loader_test.go => command/command_test.go (86%) delete mode 100644 loader/loader.go create mode 100644 updates/updates.go diff --git a/README.md b/README.md index 7b041b4..ed63ad7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ pikman [global options] command [command options] [arguments...] ## VERSION: -v1.23.2.17.0 +v1.23.6.5.1 ## COMMANDS: @@ -24,6 +24,7 @@ v1.23.2.17.0 - **unexport** - Unexport/Remove a program's desktop entry - **update** - Update the list of 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 ## GLOBAL OPTIONS: diff --git a/command/command.go b/command/command.go new file mode 100644 index 0000000..86108dd --- /dev/null +++ b/command/command.go @@ -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 +} diff --git a/loader/loader_test.go b/command/command_test.go similarity index 86% rename from loader/loader_test.go rename to command/command_test.go index ebee546..23e3ae6 100644 --- a/loader/loader_test.go +++ b/command/command_test.go @@ -1,8 +1,9 @@ -package loader +package command import ( - "pikman/types" "testing" + + "pikman/types" ) func Test_getCommand(t *testing.T) { @@ -24,7 +25,7 @@ func Test_getCommand(t *testing.T) { osType: types.Ubuntu, packageName: []string{"testPackage"}, }, - want: "sudo -S nala install testPackage", + want: "sudo -S apt install testPackage", wantErr: false, }, { @@ -60,7 +61,12 @@ func Test_getCommand(t *testing.T) { } for _, tt := range tests { 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 { t.Errorf("getCommand() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/debian/changelog b/debian/changelog index e255d1f..1d11a24 100644 --- a/debian/changelog +++ b/debian/changelog @@ -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 Sat, 10 Dec 2022 13:48:00 +0300 + pikman (1.23.2.17.1-99pika2.lunar) lunar; urgency=low * Update. diff --git a/loader/loader.go b/loader/loader.go deleted file mode 100644 index d2a6470..0000000 --- a/loader/loader.go +++ /dev/null @@ -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) -} diff --git a/main.go b/main.go index b0ba989..69162e3 100644 --- a/main.go +++ b/main.go @@ -3,22 +3,27 @@ package main import ( "log" "os" - "pikman/loader" + + "pikman/command" "pikman/types" "github.com/urfave/cli/v2" ) func main() { - if os.Getuid() == 0 { log.Fatalf("Error: Do not run pikman as root") } - osType := types.Ubuntu - containerName := "" - upgradableFlag := false - installedFlag := false + cmd := &command.Command{ + OsType: types.Ubuntu, + ContainerName: "", + IsUpgradable: false, + IsJSON: false, + IsInstalled: false, + PackageName: make([]string, 0), + } + cli.VersionFlag = &cli.BoolFlag{ Name: "version", Aliases: []string{"v"}, @@ -28,7 +33,7 @@ func main() { app := &cli.App{ Name: "pikman", Usage: "One package manager to rule them all", - Version: "v1.23.2.17.0", + Version: "v1.23.6.5.1", EnableBashCompletion: true, Flags: []cli.Flag{ &cli.BoolFlag{ @@ -37,7 +42,7 @@ func main() { Usage: "Install Arch packages (including from the AUR)", Action: func(cCtx *cli.Context, b bool) error { if b { - osType = types.Arch + cmd.OsType = types.Arch } return nil }, @@ -48,7 +53,7 @@ func main() { Usage: "Install Fedora packages", Action: func(cCtx *cli.Context, b bool) error { if b { - osType = types.Fedora + cmd.OsType = types.Fedora } return nil }, @@ -59,7 +64,7 @@ func main() { Usage: "Install Alpine packages", Action: func(cCtx *cli.Context, b bool) error { if b { - osType = types.Alpine + cmd.OsType = types.Alpine } return nil }, @@ -70,7 +75,7 @@ func main() { Usage: "Install Flatpak packages", Action: func(cCtx *cli.Context, b bool) error { if b { - osType = types.Flatpak + cmd.OsType = types.Flatpak } return nil }, @@ -78,53 +83,41 @@ func main() { &cli.StringFlag{ Name: "name", Usage: "Name of the managed container", - Destination: &containerName, + Destination: &cmd.ContainerName, }, }, Commands: []*cli.Command{ { - Name: "autoremove", - Usage: "Remove all unused packages", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "autoremove", + Usage: "Remove all unused packages", + Action: cmd.Process, }, { Name: "clean", Aliases: []string{"cl"}, Usage: "Clean the package manager cache", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Action: cmd.Process, }, { - Name: "enter", - Usage: "Enter the container instance for select package manager", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "enter", + Usage: "Enter the container instance for select package manager", + Action: cmd.Process, }, { - Name: "export", - Usage: "Export/Recreate a program's desktop entry from the container", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "export", + Usage: "Export/Recreate a program's desktop entry from the container", + Action: cmd.Process, }, { - Name: "init", - Usage: "Initialize a managed container", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "init", + Usage: "Initialize a managed container", + Action: cmd.Process, }, { Name: "install", Aliases: []string{"i"}, Usage: "Install the specified package(s)", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Action: cmd.Process, }, { Name: "list", @@ -134,83 +127,74 @@ func main() { &cli.BoolFlag{ Name: "upgradable", Usage: "Used by list to check upgradable packages", - Destination: &upgradableFlag, + Destination: &cmd.IsUpgradable, }, &cli.BoolFlag{ Name: "installed", Usage: "Used by list to check installed packages", - Destination: &installedFlag, + Destination: &cmd.IsInstalled, }, }, - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), upgradableFlag, installedFlag) - }, + Action: cmd.Process, }, { - Name: "log", - Usage: "Show package manager logs", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "log", + Usage: "Show package manager logs", + Action: cmd.Process, }, { - Name: "purge", - Usage: "Fully purge a package", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "purge", + Usage: "Fully purge a package", + Action: cmd.Process, }, { - Name: "run", - Usage: "Run a command inside a managed container", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "run", + Usage: "Run a command inside a managed container", + Action: cmd.Process, }, { Name: "remove", Aliases: []string{"r"}, Usage: "Remove an installed package", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Action: cmd.Process, }, { Name: "search", Aliases: []string{"s"}, Usage: "Search for a package", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Action: cmd.Process, }, { - Name: "show", - Usage: "Show details for a package", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "show", + Usage: "Show details for a package", + Action: cmd.Process, }, { - Name: "unexport", - Usage: "Unexport/Remove a program's desktop entry", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "unexport", + Usage: "Unexport/Remove a program's desktop entry", + Action: cmd.Process, }, { - Name: "update", - Usage: "Update the list of available packages", - Action: func(cCtx *cli.Context) error { - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) - }, + Name: "update", + Usage: "Update the list of available packages", + Action: cmd.Process, }, { - Name: "upgrade", - Usage: "Upgrade the system by installing/upgrading available packages", - Action: func(cCtx *cli.Context) error { - cCtx.Args().Tail() - return loader.ProcessCommand(cCtx.Command.FullName(), osType, containerName, cCtx.Args().Slice(), false, false) + Name: "upgrade", + Usage: "Upgrade the system by installing/upgrading available packages", + Action: cmd.Process, + }, + { + Name: "upgrades", + Usage: "List the available upgrades", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "json", + Usage: "Output in JSON", + Destination: &cmd.IsJSON, + }, }, + Action: cmd.Process, }, }, } diff --git a/ubuntu/commands.go b/ubuntu/commands.go index c0279fc..68accff 100644 --- a/ubuntu/commands.go +++ b/ubuntu/commands.go @@ -1,6 +1,6 @@ package ubuntu -var PackageManager = "sudo -S nala" +var PackageManager = "sudo -S apt" var Commands = map[string]string{ "autoremove": "autoremove", @@ -12,5 +12,5 @@ var Commands = map[string]string{ "search": "search", "show": "show", "update": "update", - "upgrade": "upgrade", + "upgrade": "update && sudo -S apt upgrade", } diff --git a/updates/updates.go b/updates/updates.go new file mode 100644 index 0000000..bc267df --- /dev/null +++ b/updates/updates.go @@ -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 +}