Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 39 additions & 26 deletions commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,63 @@ package commands

import (
"fmt"
"os"
"os/exec"
"os/exec" // 'os' import removed as os.Exit is no longer used
"regexp"
"strings"

log "github.com/sirupsen/logrus"
// 'log "github.com/sirupsen/logrus"' import removed as log.Fatal/log.Error is no longer used here
"github.com/spf13/cobra"
)

var checkInstalledVersion = &cobra.Command{
Use: "list",
Short: "list",
Long: `Check installed PHP version`,
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error { // Changed to RunE
if len(args) > 0 {
log.Fatal("subcommand list dont take any argument")
return fmt.Errorf("subcommand list does not take any arguments")
}
err := printInstalledVersion()
if err != nil {
log.Fatal(err)
if err := printInstalledVersion(); err != nil {
return fmt.Errorf("failed to list installed versions: %w", err)
}
return nil
},
}

// parsePHPAlternativesOutput takes the raw string output from update-alternatives
// and returns a slice of strings containing only the lines that match the regex.
func parsePHPAlternativesOutput(output string) []string {
regexPattern := `(?:^Selection\s+Path\s+Priority\s+Status$|^------------------------------------------------------------$|^\s*(\*?\s*\d+)\s+([\w/.-]*php[\w.-]*)\s+(\d+)\s+(auto|manual)\s+mode$)`
re, compErr := regexp.Compile(regexPattern)
if compErr != nil {
return nil
}

lines := strings.Split(output, "
")
var matchedLines []string
for _, line := range lines {
if re.MatchString(line) {
matchedLines = append(matchedLines, line)
}
}
return matchedLines
}

func printInstalledVersion() error {
cmd := exec.Command("update-alternatives", "--config", "php")
output, err := cmd.Output()
rawOutput, err := cmd.Output()
if err != nil {
log.Fatal(err)
os.Exit(1)
return fmt.Errorf("failed to execute update-alternatives: %w", err)
}

/**
Selection Path Priority Status
------------------------------------------------------------
0 /usr/bin/php8.1 81 auto mode
* 1 /usr/bin/php7.2 72 manual mode
2 /usr/bin/php8.1 81 manual mode
**/
lines := strings.Split(string(output), "\n")
for i := 1; i < len(lines)-1; i++ {
if lines[i] == "" {
continue
}
fmt.Println(lines[i])

parsedLines := parsePHPAlternativesOutput(string(rawOutput))
if parsedLines == nil {
// No specific logrus log here anymore, error is propagated.
return fmt.Errorf("internal error: regex compilation failed in parsePHPAlternativesOutput")
}

for _, line := range parsedLines {
fmt.Println(line)
}

return nil
Expand Down
95 changes: 95 additions & 0 deletions commands/list_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package commands

import (
"reflect"
"testing"
)

func TestParsePHPAlternativesOutput(t *testing.T) {
testCases := []struct {
name string
input string
expected []string
}{
{
name: "Typical output",
input: `There are 2 choices for the alternative php (providing /usr/bin/php).

Selection Path Priority Status
------------------------------------------------------------
* 0 /usr/bin/php8.1 81 auto mode
1 /usr/bin/php7.4 74 manual mode
2 /usr/local/bin/php8.0 80 manual mode

Press <enter> to keep the current choice[*], or type selection number:`,
expected: []string{
" Selection Path Priority Status",
"------------------------------------------------------------",
"* 0 /usr/bin/php8.1 81 auto mode",
" 1 /usr/bin/php7.4 74 manual mode",
" 2 /usr/local/bin/php8.0 80 manual mode",
},
},
{
name: "Empty input",
input: "",
// parsePHPAlternativesOutput with "" input returns []string{""} if regex doesn't match empty string,
// or empty slice if it does. Given the regex, it should not match an empty string.
// strings.Split("", "
") is []string{""}. The regex won't match "". So, empty slice.
expected: []string{},
},
{
name: "Only non-matching lines",
input: "This is a junk line.
Another junk line.",
expected: []string{},
},
{
name: "No actual PHP entries, just structure",
input: `There are 0 choices for the alternative php.

Selection Path Priority Status
------------------------------------------------------------
Press <enter> to keep the current choice[*], or type selection number:`,
expected: []string{
" Selection Path Priority Status",
"------------------------------------------------------------",
},
},
{
name: "Input with only a prompt",
input: "Press <enter> to keep the current choice[*], or type selection number:",
expected: []string{},
},
{
name: "Header and Separator only",
input: ` Selection Path Priority Status
------------------------------------------------------------`,
expected: []string{
" Selection Path Priority Status",
"------------------------------------------------------------",
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := parsePHPAlternativesOutput(tc.input)
// It's important to handle nil if expected is also nil,
// but parsePHPAlternativesOutput returns []string{} for no matches, not nil, unless regex fails to compile.
// Regex compilation failure is a panic-worthy scenario for tests with hardcoded regex.
if actual == nil && tc.expected != nil {
t.Errorf("Test Case: '%s': Expected %q, but got nil (regex compilation failure?)", tc.name, tc.expected)
return
}
if !reflect.DeepEqual(actual, tc.expected) {
t.Errorf("Test Case: '%s'
Expected:
%q
Got:
%q", tc.name, tc.expected, actual)
}
})
}
}
26 changes: 12 additions & 14 deletions commands/set.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
package commands

import (
"fmt"
"os"
"os/exec"

log "github.com/sirupsen/logrus"
// 'log "github.com/sirupsen/logrus"' import removed as log.Fatal is no longer used
"github.com/spf13/cobra"
)

var setVersion = &cobra.Command{
Use: "set",
Short: "set",
Long: `Set the version of PHP`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
log.Fatal("subcommand set dont take any argument")
RunE: func(cmd *cobra.Command, args []string) error { // Changed to RunE
// The logic of this argument check will be addressed in Issue 3
if len(args) > 0 {
return fmt.Errorf("subcommand set currently does not take any arguments")
}
err := showAndSetInstalledVersion()
if err != nil {
log.Fatal(err)
if err := showAndSetInstalledVersion(); err != nil {
return fmt.Errorf("failed to set version: %w", err)
}
return nil
},
}

Expand All @@ -28,12 +29,9 @@ func showAndSetInstalledVersion() error {
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
err := cmd.Run()
if err != nil {
log.Fatal(err)
os.Exit(1)
if err := cmd.Run(); err != nil {
// Removed log.Fatal(err) and os.Exit(1)
return fmt.Errorf("update-alternatives --config php failed: %w", err)
}

return nil
}