test.go

  1// Copyright (c) 2017, Daniel MartΓ­ <mvdan@mvdan.cc>
  2// See LICENSE for licensing information
  3
  4package interp
  5
  6import (
  7	"context"
  8	"fmt"
  9	"os"
 10	"regexp"
 11
 12	"golang.org/x/term"
 13
 14	"mvdan.cc/sh/v3/expand"
 15	"mvdan.cc/sh/v3/syntax"
 16)
 17
 18// non-empty string is true, empty string is false
 19func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string {
 20	switch x := expr.(type) {
 21	case *syntax.Word:
 22		if classic {
 23			// In the classic "test" mode, we already expanded and
 24			// split the list of words, so don't redo that work.
 25			return r.document(x)
 26		}
 27		return r.literal(x)
 28	case *syntax.ParenTest:
 29		return r.bashTest(ctx, x.X, classic)
 30	case *syntax.BinaryTest:
 31		switch x.Op {
 32		case syntax.TsMatchShort, syntax.TsMatch, syntax.TsNoMatch:
 33			str := r.literal(x.X.(*syntax.Word))
 34			yw := x.Y.(*syntax.Word)
 35			if classic { // test, [
 36				lit := r.literal(yw)
 37				if (str == lit) == (x.Op != syntax.TsNoMatch) {
 38					return "1"
 39				}
 40			} else { // [[
 41				pattern := r.pattern(yw)
 42				if match(pattern, str) == (x.Op != syntax.TsNoMatch) {
 43					return "1"
 44				}
 45			}
 46			return ""
 47		}
 48		if r.binTest(ctx, x.Op, r.bashTest(ctx, x.X, classic), r.bashTest(ctx, x.Y, classic)) {
 49			return "1"
 50		}
 51		return ""
 52	case *syntax.UnaryTest:
 53		if r.unTest(ctx, x.Op, r.bashTest(ctx, x.X, classic)) {
 54			return "1"
 55		}
 56		return ""
 57	}
 58	return ""
 59}
 60
 61func (r *Runner) binTest(ctx context.Context, op syntax.BinTestOperator, x, y string) bool {
 62	switch op {
 63	case syntax.TsReMatch:
 64		re, err := regexp.Compile(y)
 65		if err != nil {
 66			r.exit = 2
 67			return false
 68		}
 69		return re.MatchString(x)
 70	case syntax.TsNewer:
 71		info1, err1 := r.stat(ctx, x)
 72		info2, err2 := r.stat(ctx, y)
 73		if err1 != nil || err2 != nil {
 74			return false
 75		}
 76		return info1.ModTime().After(info2.ModTime())
 77	case syntax.TsOlder:
 78		info1, err1 := r.stat(ctx, x)
 79		info2, err2 := r.stat(ctx, y)
 80		if err1 != nil || err2 != nil {
 81			return false
 82		}
 83		return info1.ModTime().Before(info2.ModTime())
 84	case syntax.TsDevIno:
 85		info1, err1 := r.stat(ctx, x)
 86		info2, err2 := r.stat(ctx, y)
 87		if err1 != nil || err2 != nil {
 88			return false
 89		}
 90		return os.SameFile(info1, info2)
 91	case syntax.TsEql:
 92		return atoi(x) == atoi(y)
 93	case syntax.TsNeq:
 94		return atoi(x) != atoi(y)
 95	case syntax.TsLeq:
 96		return atoi(x) <= atoi(y)
 97	case syntax.TsGeq:
 98		return atoi(x) >= atoi(y)
 99	case syntax.TsLss:
100		return atoi(x) < atoi(y)
101	case syntax.TsGtr:
102		return atoi(x) > atoi(y)
103	case syntax.AndTest:
104		return x != "" && y != ""
105	case syntax.OrTest:
106		return x != "" || y != ""
107	case syntax.TsBefore:
108		return x < y
109	default: // syntax.TsAfter
110		return x > y
111	}
112}
113
114func (r *Runner) statMode(ctx context.Context, name string, mode os.FileMode) bool {
115	info, err := r.stat(ctx, name)
116	return err == nil && info.Mode()&mode != 0
117}
118
119// These are copied from x/sys/unix as we can't import it here.
120const (
121	access_R_OK = 0x4
122	access_W_OK = 0x2
123	access_X_OK = 0x1
124)
125
126func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) bool {
127	switch op {
128	case syntax.TsExists:
129		_, err := r.stat(ctx, x)
130		return err == nil
131	case syntax.TsRegFile:
132		info, err := r.stat(ctx, x)
133		return err == nil && info.Mode().IsRegular()
134	case syntax.TsDirect:
135		return r.statMode(ctx, x, os.ModeDir)
136	case syntax.TsCharSp:
137		return r.statMode(ctx, x, os.ModeCharDevice)
138	case syntax.TsBlckSp:
139		info, err := r.stat(ctx, x)
140		return err == nil && info.Mode()&os.ModeDevice != 0 &&
141			info.Mode()&os.ModeCharDevice == 0
142	case syntax.TsNmPipe:
143		return r.statMode(ctx, x, os.ModeNamedPipe)
144	case syntax.TsSocket:
145		return r.statMode(ctx, x, os.ModeSocket)
146	case syntax.TsSmbLink:
147		info, err := r.lstat(ctx, x)
148		return err == nil && info.Mode()&os.ModeSymlink != 0
149	case syntax.TsSticky:
150		return r.statMode(ctx, x, os.ModeSticky)
151	case syntax.TsUIDSet:
152		return r.statMode(ctx, x, os.ModeSetuid)
153	case syntax.TsGIDSet:
154		return r.statMode(ctx, x, os.ModeSetgid)
155	// case syntax.TsGrpOwn:
156	// case syntax.TsUsrOwn:
157	// case syntax.TsModif:
158	case syntax.TsRead:
159		return r.access(ctx, r.absPath(x), access_R_OK) == nil
160	case syntax.TsWrite:
161		return r.access(ctx, r.absPath(x), access_W_OK) == nil
162	case syntax.TsExec:
163		return r.access(ctx, r.absPath(x), access_X_OK) == nil
164	case syntax.TsNoEmpty:
165		info, err := r.stat(ctx, x)
166		return err == nil && info.Size() > 0
167	case syntax.TsFdTerm:
168		fd := atoi(x)
169		var f any
170		switch fd {
171		case 0:
172			f = r.stdin
173		case 1:
174			f = r.stdout
175		case 2:
176			f = r.stderr
177		}
178		if f, ok := f.(interface{ Fd() uintptr }); ok {
179			// Support [os.File.Fd] methods such as the one on [*os.File].
180			return term.IsTerminal(int(f.Fd()))
181		}
182		// TODO: allow term.IsTerminal here too if running in the
183		// "single process" mode.
184		return false
185	case syntax.TsEmpStr:
186		return x == ""
187	case syntax.TsNempStr:
188		return x != ""
189	case syntax.TsOptSet:
190		if _, opt := r.optByName(x, false); opt != nil {
191			return *opt
192		}
193		return false
194	case syntax.TsVarSet:
195		return r.lookupVar(x).IsSet()
196	case syntax.TsRefVar:
197		return r.lookupVar(x).Kind == expand.NameRef
198	case syntax.TsNot:
199		return x == ""
200	case syntax.TsUsrOwn, syntax.TsGrpOwn:
201		return r.unTestOwnOrGrp(ctx, op, x)
202	default:
203		panic(fmt.Sprintf("unhandled unary test op: %v", op))
204	}
205}