1// Copyright (c) 2017, Daniel MartΓ <mvdan@mvdan.cc>
2// See LICENSE for licensing information
3
4package syntax
5
6import "strings"
7
8// Simplify modifies a node to remove redundant pieces of syntax, and returns
9// whether any changes were made.
10//
11// The changes currently applied are:
12//
13// Remove clearly useless parentheses $(( (expr) ))
14// Remove dollars from vars in exprs (($var))
15// Remove duplicate subshells $( (stmts) )
16// Remove redundant quotes [[ "$var" == str ]]
17// Merge negations with unary operators [[ ! -n $var ]]
18// Use single quotes to shorten literals "\$foo"
19func Simplify(n Node) bool {
20 s := simplifier{}
21 Walk(n, s.visit)
22 return s.modified
23}
24
25type simplifier struct {
26 modified bool
27}
28
29func (s *simplifier) visit(node Node) bool {
30 switch node := node.(type) {
31 case *Assign:
32 node.Index = s.removeParensArithm(node.Index)
33 // Don't inline params, as x[i] and x[$i] mean
34 // different things when x is an associative
35 // array; the first means "i", the second "$i".
36 case *ParamExp:
37 node.Index = s.removeParensArithm(node.Index)
38 // don't inline params - same as above.
39
40 if node.Slice == nil {
41 break
42 }
43 node.Slice.Offset = s.removeParensArithm(node.Slice.Offset)
44 node.Slice.Offset = s.inlineSimpleParams(node.Slice.Offset)
45 node.Slice.Length = s.removeParensArithm(node.Slice.Length)
46 node.Slice.Length = s.inlineSimpleParams(node.Slice.Length)
47 case *ArithmExp:
48 node.X = s.removeParensArithm(node.X)
49 node.X = s.inlineSimpleParams(node.X)
50 case *ArithmCmd:
51 node.X = s.removeParensArithm(node.X)
52 node.X = s.inlineSimpleParams(node.X)
53 case *ParenArithm:
54 node.X = s.removeParensArithm(node.X)
55 node.X = s.inlineSimpleParams(node.X)
56 case *BinaryArithm:
57 node.X = s.inlineSimpleParams(node.X)
58 node.Y = s.inlineSimpleParams(node.Y)
59 case *CmdSubst:
60 node.Stmts = s.inlineSubshell(node.Stmts)
61 case *Subshell:
62 node.Stmts = s.inlineSubshell(node.Stmts)
63 case *Word:
64 node.Parts = s.simplifyWord(node.Parts)
65 case *TestClause:
66 node.X = s.removeParensTest(node.X)
67 node.X = s.removeNegateTest(node.X)
68 case *ParenTest:
69 node.X = s.removeParensTest(node.X)
70 node.X = s.removeNegateTest(node.X)
71 case *BinaryTest:
72 node.X = s.unquoteParams(node.X)
73 node.X = s.removeNegateTest(node.X)
74 if node.Op == TsMatchShort {
75 s.modified = true
76 node.Op = TsMatch
77 }
78 switch node.Op {
79 case TsMatch, TsNoMatch:
80 // unquoting enables globbing
81 default:
82 node.Y = s.unquoteParams(node.Y)
83 }
84 node.Y = s.removeNegateTest(node.Y)
85 case *UnaryTest:
86 node.X = s.unquoteParams(node.X)
87 }
88 return true
89}
90
91func (s *simplifier) simplifyWord(wps []WordPart) []WordPart {
92parts:
93 for i, wp := range wps {
94 dq, _ := wp.(*DblQuoted)
95 if dq == nil || len(dq.Parts) != 1 {
96 break
97 }
98 lit, _ := dq.Parts[0].(*Lit)
99 if lit == nil {
100 break
101 }
102 var sb strings.Builder
103 escaped := false
104 for _, r := range lit.Value {
105 switch r {
106 case '\\':
107 escaped = !escaped
108 if escaped {
109 continue
110 }
111 case '\'':
112 continue parts
113 case '$', '"', '`':
114 escaped = false
115 default:
116 if escaped {
117 continue parts
118 }
119 escaped = false
120 }
121 sb.WriteRune(r)
122 }
123 newVal := sb.String()
124 if newVal == lit.Value {
125 break
126 }
127 s.modified = true
128 wps[i] = &SglQuoted{
129 Left: dq.Pos(),
130 Right: dq.End(),
131 Dollar: dq.Dollar,
132 Value: newVal,
133 }
134 }
135 return wps
136}
137
138func (s *simplifier) removeParensArithm(x ArithmExpr) ArithmExpr {
139 for {
140 par, _ := x.(*ParenArithm)
141 if par == nil {
142 return x
143 }
144 s.modified = true
145 x = par.X
146 }
147}
148
149func (s *simplifier) inlineSimpleParams(x ArithmExpr) ArithmExpr {
150 w, _ := x.(*Word)
151 if w == nil || len(w.Parts) != 1 {
152 return x
153 }
154 pe, _ := w.Parts[0].(*ParamExp)
155 if pe == nil || !ValidName(pe.Param.Value) {
156 // Not a parameter expansion, or not a valid name, like $3.
157 return x
158 }
159 if pe.Excl || pe.Length || pe.Width || pe.Slice != nil ||
160 pe.Repl != nil || pe.Exp != nil || pe.Index != nil {
161 // A complex parameter expansion can't be simplified.
162 //
163 // Note that index expressions can't generally be simplified
164 // either. It's fine to turn ${a[0]} into a[0], but others like
165 // a[*] are invalid in many shells including Bash.
166 return x
167 }
168 s.modified = true
169 return &Word{Parts: []WordPart{pe.Param}}
170}
171
172func (s *simplifier) inlineSubshell(stmts []*Stmt) []*Stmt {
173 for len(stmts) == 1 {
174 st := stmts[0]
175 if st.Negated || st.Background || st.Coprocess ||
176 len(st.Redirs) > 0 {
177 break
178 }
179 sub, _ := st.Cmd.(*Subshell)
180 if sub == nil {
181 break
182 }
183 s.modified = true
184 stmts = sub.Stmts
185 }
186 return stmts
187}
188
189func (s *simplifier) unquoteParams(x TestExpr) TestExpr {
190 w, _ := x.(*Word)
191 if w == nil || len(w.Parts) != 1 {
192 return x
193 }
194 dq, _ := w.Parts[0].(*DblQuoted)
195 if dq == nil || len(dq.Parts) != 1 {
196 return x
197 }
198 if _, ok := dq.Parts[0].(*ParamExp); !ok {
199 return x
200 }
201 s.modified = true
202 w.Parts = dq.Parts
203 return w
204}
205
206func (s *simplifier) removeParensTest(x TestExpr) TestExpr {
207 for {
208 par, _ := x.(*ParenTest)
209 if par == nil {
210 return x
211 }
212 s.modified = true
213 x = par.X
214 }
215}
216
217func (s *simplifier) removeNegateTest(x TestExpr) TestExpr {
218 u, _ := x.(*UnaryTest)
219 if u == nil || u.Op != TsNot {
220 return x
221 }
222 switch y := u.X.(type) {
223 case *UnaryTest:
224 switch y.Op {
225 case TsEmpStr:
226 y.Op = TsNempStr
227 s.modified = true
228 return y
229 case TsNempStr:
230 y.Op = TsEmpStr
231 s.modified = true
232 return y
233 case TsNot:
234 s.modified = true
235 return y.X
236 }
237 case *BinaryTest:
238 switch y.Op {
239 case TsMatch:
240 y.Op = TsNoMatch
241 s.modified = true
242 return y
243 case TsNoMatch:
244 y.Op = TsMatch
245 s.modified = true
246 return y
247 }
248 }
249 return x
250}