blob: 2b17ae3209e4f18e7b76a5666508e347e365edfb [file] [log] [blame]
package bashkit
import (
"fmt"
"strings"
"mvdan.cc/sh/v3/syntax"
)
// ExtractCommands parses a bash command and extracts individual command names that are
// candidates for auto-installation.
//
// Returns only simple command names (no paths, no builtins, no variable assignments)
// that could potentially be missing tools that need installation.
//
// Filtering logic:
// - Excludes commands with paths (./script.sh, /usr/bin/tool, ../build.sh)
// - Excludes shell builtins (echo, cd, test, [, etc.)
// - Excludes variable assignments (FOO=bar)
// - Deduplicates repeated command names
//
// Examples:
//
// "ls -la && echo done" → ["ls"] (echo filtered as builtin)
// "./deploy.sh && curl api.com" → ["curl"] (./deploy.sh filtered as path)
// "yamllint config.yaml" → ["yamllint"] (candidate for installation)
func ExtractCommands(command string) ([]string, error) {
r := strings.NewReader(command)
parser := syntax.NewParser()
file, err := parser.Parse(r, "")
if err != nil {
return nil, fmt.Errorf("failed to parse bash command: %w", err)
}
var commands []string
seen := make(map[string]bool)
syntax.Walk(file, func(node syntax.Node) bool {
callExpr, ok := node.(*syntax.CallExpr)
if !ok || len(callExpr.Args) == 0 {
return true
}
cmdName := callExpr.Args[0].Lit()
if cmdName == "" {
return true
}
if strings.Contains(cmdName, "=") {
// variable assignment
return true
}
if strings.Contains(cmdName, "/") {
// commands with slashes are user-specified executables/scripts
return true
}
if isBuiltin(cmdName) {
return true
}
if !seen[cmdName] {
seen[cmdName] = true
commands = append(commands, cmdName)
}
return true
})
return commands, nil
}
// isBuiltin checks if a command is a shell built-in using the same logic as mvdan.cc/sh/v3/interp
// This is copied from mvdan.cc/sh/v3/interp.isBuiltin since it's not exported
// See https://github.com/mvdan/sh/issues/1164
func isBuiltin(name string) bool {
switch name {
case "true", ":", "false", "exit", "set", "shift", "unset",
"echo", "printf", "break", "continue", "pwd", "cd",
"wait", "builtin", "trap", "type", "source", ".", "command",
"dirs", "pushd", "popd", "umask", "alias", "unalias",
"fg", "bg", "getopts", "eval", "test", "[", "exec",
"return", "read", "mapfile", "readarray", "shopt":
return true
}
return false
}