simplify.go

  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}