-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathshellparse.go
More file actions
98 lines (92 loc) · 2.74 KB
/
shellparse.go
File metadata and controls
98 lines (92 loc) · 2.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Package shellparse extracts command steps from shell scripts.
package shellparse
import (
"strings"
"mvdan.cc/sh/v3/syntax"
)
// Parse returns one slice per simple command in src, in source order.
// Each is [program] or [program, arg], where arg is the first non-flag
// positional argument.
//
// Some malformed inputs (e.g. trailing unterminated tokens after valid
// semicolon-separated commands) yield partial results alongside a
// non-nil error. Callers that show parsed output to users should treat
// a non-nil err as a signal to fall back to the raw input rather than
// display the partial.
func Parse(src string) ([][]string, error) {
if src == "" {
return nil, nil
}
f, err := syntax.NewParser().Parse(strings.NewReader(src), "")
if f == nil {
return nil, err
}
var out [][]string
syntax.Walk(f, func(node syntax.Node) bool {
call, ok := node.(*syntax.CallExpr)
if !ok || len(call.Args) == 0 {
return true
}
prog := wordLiteral(call.Args[0])
if prog == "" {
return true
}
step := []string{prog}
if arg := firstNonFlagLiteral(call.Args[1:]); arg != "" {
step = append(step, arg)
}
out = append(out, step)
return true
})
return out, err
}
// wordLiteral returns the literal content of w by concatenating the
// literal pieces of its parts. Bare literals, single-quoted strings,
// and double-quoted strings (when the inner parts are all literals)
// contribute their text. Any part involving variable expansion,
// command substitution, or arithmetic returns "" for the whole word
// because we cannot resolve those without executing the shell.
func wordLiteral(w *syntax.Word) string {
if w == nil {
return ""
}
var sb strings.Builder
for _, part := range w.Parts {
switch p := part.(type) {
case *syntax.Lit:
_, _ = sb.WriteString(p.Value)
case *syntax.SglQuoted:
_, _ = sb.WriteString(p.Value)
case *syntax.DblQuoted:
for _, inner := range p.Parts {
lit, ok := inner.(*syntax.Lit)
if !ok {
return ""
}
_, _ = sb.WriteString(lit.Value)
}
default:
return ""
}
}
return sb.String()
}
// firstNonFlagLiteral returns the literal value of the first word in
// ws that does not start with "-", or "" if none qualifies.
//
// Known limitation: no flag-arity knowledge. For programs whose global
// flags take a separate-word value ("git -C path verb", "kubectl -n ns
// verb", "docker --context X verb"), this returns the flag's value as
// the first positional, not the actual verb. Consumers that need the
// verb in those cases need per-program awareness; this function does
// not provide it.
func firstNonFlagLiteral(ws []*syntax.Word) string {
for _, w := range ws {
lit := wordLiteral(w)
if lit == "" || strings.HasPrefix(lit, "-") {
continue
}
return lit
}
return ""
}