blob: df37ab8f9347282adfc57cdb1cb04a3d91c575b2 [file] [log] [blame]
Josh Bleecher Snyder495c1fa2025-05-29 00:37:22 +00001package bashkit
2
3import (
4 "fmt"
5 "strings"
6
Josh Bleecher Snyder67b4c772025-05-30 08:46:27 -07007 "mvdan.cc/sh/v3/interp"
Josh Bleecher Snyder495c1fa2025-05-29 00:37:22 +00008 "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)
28func 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 Snyder67b4c772025-05-30 08:46:27 -070056 if interp.IsBuiltin(cmdName) {
Josh Bleecher Snyder495c1fa2025-05-29 00:37:22 +000057 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}