diff --git a/commands/list.go b/commands/list.go index 337df6a..8ef61e3 100644 --- a/commands/list.go +++ b/commands/list.go @@ -2,11 +2,10 @@ 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" ) @@ -14,38 +13,52 @@ 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 diff --git a/commands/list_test.go b/commands/list_test.go new file mode 100644 index 0000000..cb2cbfc --- /dev/null +++ b/commands/list_test.go @@ -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 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 to keep the current choice[*], or type selection number:`, + expected: []string{ + " Selection Path Priority Status", + "------------------------------------------------------------", + }, + }, + { + name: "Input with only a prompt", + input: "Press 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) + } + }) + } +} diff --git a/commands/set.go b/commands/set.go index 440620b..af2e612 100644 --- a/commands/set.go +++ b/commands/set.go @@ -1,10 +1,10 @@ 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" ) @@ -12,14 +12,15 @@ 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 }, } @@ -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 } -