| Josh Bleecher Snyder | 495c1fa | 2025-05-29 00:37:22 +0000 | [diff] [blame] | 1 | package bashkit |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "strings" |
| 6 | |
| Josh Bleecher Snyder | 67b4c77 | 2025-05-30 08:46:27 -0700 | [diff] [blame] | 7 | "mvdan.cc/sh/v3/interp" |
| Josh Bleecher Snyder | 495c1fa | 2025-05-29 00:37:22 +0000 | [diff] [blame] | 8 | "mvdan.cc/sh/v3/syntax" |
| 9 | ) |
| 10 | |
| 11 | // ExtractCommands parses a bash command and extracts individual command names that are |
| 12 | // candidates for auto-installation. |
| 13 | // |
| 14 | // Returns only simple command names (no paths, no builtins, no variable assignments) |
| 15 | // that could potentially be missing tools that need installation. |
| 16 | // |
| 17 | // Filtering logic: |
| 18 | // - Excludes commands with paths (./script.sh, /usr/bin/tool, ../build.sh) |
| 19 | // - Excludes shell builtins (echo, cd, test, [, etc.) |
| 20 | // - Excludes variable assignments (FOO=bar) |
| 21 | // - Deduplicates repeated command names |
| 22 | // |
| 23 | // Examples: |
| 24 | // |
| 25 | // "ls -la && echo done" → ["ls"] (echo filtered as builtin) |
| 26 | // "./deploy.sh && curl api.com" → ["curl"] (./deploy.sh filtered as path) |
| 27 | // "yamllint config.yaml" → ["yamllint"] (candidate for installation) |
| 28 | func ExtractCommands(command string) ([]string, error) { |
| 29 | r := strings.NewReader(command) |
| 30 | parser := syntax.NewParser() |
| 31 | file, err := parser.Parse(r, "") |
| 32 | if err != nil { |
| 33 | return nil, fmt.Errorf("failed to parse bash command: %w", err) |
| 34 | } |
| 35 | |
| 36 | var commands []string |
| 37 | seen := make(map[string]bool) |
| 38 | |
| 39 | syntax.Walk(file, func(node syntax.Node) bool { |
| 40 | callExpr, ok := node.(*syntax.CallExpr) |
| 41 | if !ok || len(callExpr.Args) == 0 { |
| 42 | return true |
| 43 | } |
| 44 | cmdName := callExpr.Args[0].Lit() |
| 45 | if cmdName == "" { |
| 46 | return true |
| 47 | } |
| 48 | if strings.Contains(cmdName, "=") { |
| 49 | // variable assignment |
| 50 | return true |
| 51 | } |
| 52 | if strings.Contains(cmdName, "/") { |
| 53 | // commands with slashes are user-specified executables/scripts |
| 54 | return true |
| 55 | } |
| Josh Bleecher Snyder | 67b4c77 | 2025-05-30 08:46:27 -0700 | [diff] [blame] | 56 | if interp.IsBuiltin(cmdName) { |
| Josh Bleecher Snyder | 495c1fa | 2025-05-29 00:37:22 +0000 | [diff] [blame] | 57 | return true |
| 58 | } |
| 59 | if !seen[cmdName] { |
| 60 | seen[cmdName] = true |
| 61 | commands = append(commands, cmdName) |
| 62 | } |
| 63 | return true |
| 64 | }) |
| 65 | |
| 66 | return commands, nil |
| 67 | } |