Repository: puppetlabs/wash
Pull request #333 (2019-06-16 05:52:00)

Optimize the action primary in wash find


Patch:
diff --git a/api/client/client.go b/api/client/client.go
index 623b871..107cd63 100644
--- a/api/client/client.go
+++ b/api/client/client.go
@@ -35,7 +35,8 @@ type Client interface {
 	History(bool) (chan apitypes.Activity, error)
 	ActivityJournal(index int, follow bool) (io.ReadCloser, error)
 	Clear(path string) ([]string, error)
-	Schema(path string) (apitypes.EntrySchema, error)
+	// A "nil" schema means that the schema's unknown.
+	Schema(path string) (*apitypes.EntrySchema, error)
 }
 
 // A domainSocketClient is a wash API client.
@@ -268,8 +269,8 @@ func (c *domainSocketClient) Clear(path string) ([]string, error) {
 }
 
 // Schema returns the entry's schema
-func (c *domainSocketClient) Schema(path string) (apitypes.EntrySchema, error) {
-	var schema apitypes.EntrySchema
+func (c *domainSocketClient) Schema(path string) (*apitypes.EntrySchema, error) {
+	var schema *apitypes.EntrySchema
 	if err := c.getRequest("/fs/schema", url.Values{"path": []string{path}}, &schema); err != nil {
 		return schema, err
 	}
diff --git a/api/fs/dir.go b/api/fs/dir.go
index e07bf6e..47c5c5d 100644
--- a/api/fs/dir.go
+++ b/api/fs/dir.go
@@ -26,7 +26,7 @@ func newDir(ctx context.Context, finfo os.FileInfo, path string) *dir {
 	return d
 }
 
-func (d *dir) ChildSchemas() []plugin.EntrySchema {
+func (d *dir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(dirBase(), fileBase())
 }
 
diff --git a/api/helpers.go b/api/helpers.go
index 3737667..b80bec7 100644
--- a/api/helpers.go
+++ b/api/helpers.go
@@ -17,6 +17,7 @@ import (
 
 func toAPIEntry(e plugin.Entry) apitypes.Entry {
 	return apitypes.Entry{
+		TypeID:     plugin.TypeID(e),
 		Name:       plugin.Name(e),
 		CName:      plugin.CName(e),
 		Actions:    plugin.SupportedActionsOf(e),
diff --git a/api/types/entry.go b/api/types/entry.go
index ea86172..254b480 100644
--- a/api/types/entry.go
+++ b/api/types/entry.go
@@ -6,6 +6,7 @@ import "github.com/puppetlabs/wash/plugin"
 //
 // swagger:response
 type Entry struct {
+	TypeID     string                 `json:"type_id"`
 	Path       string                 `json:"path"`
 	Actions    []string               `json:"actions"`
 	Name       string                 `json:"name"`
diff --git a/cmd/internal/find/clientWrappers.go b/cmd/internal/find/clientWrappers.go
index c23c599..3d98e80 100644
--- a/cmd/internal/find/clientWrappers.go
+++ b/cmd/internal/find/clientWrappers.go
@@ -11,7 +11,7 @@ func info(c client.Client, path string) (types.Entry, error) {
 	if err != nil {
 		return types.Entry{}, err
 	}
-	return types.NewEntryBase(e, path), nil
+	return types.NewEntry(e, path), nil
 }
 
 // list is a wrapper to c.List that handles normalizing the children's
@@ -23,7 +23,7 @@ func list(c client.Client, e types.Entry) ([]types.Entry, error) {
 	}
 	children := make([]types.Entry, len(rawChildren))
 	for i, rawChild := range rawChildren {
-		children[i] = types.NewEntryBase(rawChild, e.NormalizedPath + "/" + rawChild.CName)
+		children[i] = types.NewEntry(rawChild, e.NormalizedPath+"/"+rawChild.CName)
 	}
 	return children, nil
 }
diff --git a/cmd/internal/find/parser/parse.go b/cmd/internal/find/parser/parse.go
index 64c93f0..5b97199 100644
--- a/cmd/internal/find/parser/parse.go
+++ b/cmd/internal/find/parser/parse.go
@@ -9,7 +9,7 @@ import (
 type Result struct {
 	Paths     []string
 	Options   types.Options
-	Predicate types.EntryPredicate
+	Predicate *types.EntryPredicate
 }
 
 /*
diff --git a/cmd/internal/find/parser/parseExpression.go b/cmd/internal/find/parser/parseExpression.go
index f901021..6f44c9a 100644
--- a/cmd/internal/find/parser/parseExpression.go
+++ b/cmd/internal/find/parser/parseExpression.go
@@ -3,24 +3,24 @@ package parser
 import (
 	"fmt"
 
-	"github.com/puppetlabs/wash/cmd/util"
 	"github.com/puppetlabs/wash/cmd/internal/find/parser/expression"
 	"github.com/puppetlabs/wash/cmd/internal/find/primary"
 	"github.com/puppetlabs/wash/cmd/internal/find/types"
+	cmdutil "github.com/puppetlabs/wash/cmd/util"
 )
 
 /*
 See the comments of expression.Parser#Parse for the grammar. Substitute
 Predicate with Primary.
 */
-func parseExpression(tokens []string) (types.EntryPredicate, error) {
+func parseExpression(tokens []string) (*types.EntryPredicate, error) {
 	if len(tokens) == 0 {
 		// tokens is empty, meaning the user did not provide an expression
 		// to `wash find`. Thus, we default to a predicate that always returns
 		// true.
-		return func(e types.Entry) bool {
+		return types.ToEntryP(func(e types.Entry) bool {
 			return true
-		}, nil
+		}), nil
 	}
 	parser := expression.NewParser(primary.Parser)
 	p, tks, err := parser.Parse(tokens)
@@ -35,21 +35,21 @@ func parseExpression(tokens []string) (types.EntryPredicate, error) {
 		msg := fmt.Sprintf("parser.parseExpression(): returned a non-empty set of tokens: %v", tks)
 		panic(msg)
 	}
-	return p.(types.EntryPredicate), nil
+	return p.(*types.EntryPredicate), nil
 }
 
 // OperandsTable returns a table containing all of `wash find`'s available
 // operands
 func OperandsTable() *cmdutil.Table {
 	return cmdutil.NewTable(
-		[]string{"Operands:",    ""},
-		[]string{"  ( e )",      "Parentheses operator. Evaluates to true if e evaluates to true"},
-		[]string{"  !, -not e",  "Logical NOT operator. Evaluates to true if e evaluates to false"},
+		[]string{"Operands:", ""},
+		[]string{"  ( e )", "Parentheses operator. Evaluates to true if e evaluates to true"},
+		[]string{"  !, -not e", "Logical NOT operator. Evaluates to true if e evaluates to false"},
 		[]string{"  e1 -and e2", "Logical AND operator. Evalutes to true if both e1 and e2 are true"},
-		[]string{"  e1 -a e2",   ""},
-		[]string{"  e1 e2",      ""},
-		[]string{"  e1 -or e2",  "Logical OR operator. Evalutes to true if either e1 or e2 are true"},
-		[]string{"  e1 -o e2",   ""},
+		[]string{"  e1 -a e2", ""},
+		[]string{"  e1 e2", ""},
+		[]string{"  e1 -or e2", "Logical OR operator. Evalutes to true if either e1 or e2 are true"},
+		[]string{"  e1 -o e2", ""},
 	)
 }
 
diff --git a/cmd/internal/find/primary/action.go b/cmd/internal/find/primary/action.go
index 60f917b..1e4c7e3 100644
--- a/cmd/internal/find/primary/action.go
+++ b/cmd/internal/find/primary/action.go
@@ -4,8 +4,8 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/puppetlabs/wash/plugin"
 	"github.com/puppetlabs/wash/cmd/internal/find/types"
+	"github.com/puppetlabs/wash/plugin"
 )
 
 // Action is the action primary
@@ -14,9 +14,9 @@ import (
 //nolint
 var Action = Parser.add(&Primary{
 	Description: "Returns true if the entry supports action",
-	name: "action",
-	args: "action",
-	parseFunc: func(tokens []string) (types.EntryPredicate, []string, error) {
+	name:        "action",
+	args:        "action",
+	parseFunc: func(tokens []string) (*types.EntryPredicate, []string, error) {
 		if len(tokens) == 0 {
 			return nil, nil, fmt.Errorf("requires additional arguments")
 		}
@@ -31,8 +31,16 @@ var Action = Parser.add(&Primary{
 			validActionsStr := strings.Join(validActionsArray, ", ")
 			return nil, nil, fmt.Errorf("%v is an invalid action. Valid actions are %v", tokens[0], validActionsStr)
 		}
-		p := func(e types.Entry) bool {
+		p := types.ToEntryP(func(e types.Entry) bool {
 			return e.Supports(action)
+		})
+		p.SchemaP = func(s *types.EntrySchema) bool {
+			for _, a := range s.Actions {
+				if action.Name == a {
+					return true
+				}
+			}
+			return false
 		}
 		return p, tokens[1:], nil
 	},
diff --git a/cmd/internal/find/primary/boolean.go b/cmd/internal/find/primary/boolean.go
index d3dabb6..e1ae649 100644
--- a/cmd/internal/find/primary/boolean.go
+++ b/cmd/internal/find/primary/boolean.go
@@ -10,11 +10,15 @@ import (
 func newBooleanPrimary(val bool) *Primary {
 	return Parser.add(&Primary{
 		Description: fmt.Sprintf("Always returns %v", val),
-		name: fmt.Sprintf("%v", val),
-		parseFunc: func(tokens []string) (types.EntryPredicate, []string, error) {
-			return func(e types.Entry) bool {
+		name:        fmt.Sprintf("%v", val),
+		parseFunc: func(tokens []string) (*types.EntryPredicate, []string, error) {
+			p := types.ToEntryP(func(e types.Entry) bool {
 				return val
-			}, tokens, nil
+			})
+			p.SchemaP = func(s *types.EntrySchema) bool {
+				return val
+			}
+			return p, tokens, nil
 		},
 	})
 }
diff --git a/cmd/internal/find/primary/meta/parse.go b/cmd/internal/find/primary/meta/parse.go
index 9cf19a1..ebfb175 100644
--- a/cmd/internal/find/primary/meta/parse.go
+++ b/cmd/internal/find/primary/meta/parse.go
@@ -8,12 +8,12 @@ import (
 // The functionality here is tested in primary/meta_test.go
 
 // Parse is the meta primary's parse function.
-func Parse(tokens []string) (types.EntryPredicate, []string, error) {
+func Parse(tokens []string) (*types.EntryPredicate, []string, error) {
 	p, tokens, err := parseExpression(tokens)
 	if err != nil {
 		return nil, nil, err
 	}
-	return func(e types.Entry) bool {
+	return types.ToEntryP(func(e types.Entry) bool {
 		return p.IsSatisfiedBy(e.Metadata)
-	}, tokens, nil
+	}), tokens, nil
 }
diff --git a/cmd/internal/find/primary/name.go b/cmd/internal/find/primary/name.go
index 0fcfae2..5f41472 100644
--- a/cmd/internal/find/primary/name.go
+++ b/cmd/internal/find/primary/name.go
@@ -13,9 +13,9 @@ import (
 //nolint
 var Name = Parser.add(&Primary{
 	Description: "Returns true if the entry's cname matches glob",
-	name: "name",
-	args: "glob",
-	parseFunc: func(tokens []string) (types.EntryPredicate, []string, error) {
+	name:        "name",
+	args:        "glob",
+	parseFunc: func(tokens []string) (*types.EntryPredicate, []string, error) {
 		if len(tokens) == 0 {
 			return nil, nil, fmt.Errorf("requires additional arguments")
 		}
@@ -23,8 +23,8 @@ var Name = Parser.add(&Primary{
 		if err != nil {
 			return nil, nil, fmt.Errorf("invalid glob: %v", err)
 		}
-		return func(e types.Entry) bool {
+		return types.ToEntryP(func(e types.Entry) bool {
 			return g.Match(e.CName)
-		}, tokens[1:], nil
+		}), tokens[1:], nil
 	},
 })
diff --git a/cmd/internal/find/primary/path.go b/cmd/internal/find/primary/path.go
index fbe7779..b19ae9e 100644
--- a/cmd/internal/find/primary/path.go
+++ b/cmd/internal/find/primary/path.go
@@ -13,9 +13,9 @@ import (
 //nolint
 var Path = Parser.add(&Primary{
 	Description: "Returns true if the entry's normalized path matches glob",
-	name: "path",
-	args: "glob",
-	parseFunc: func(tokens []string) (types.EntryPredicate, []string, error) {
+	name:        "path",
+	args:        "glob",
+	parseFunc: func(tokens []string) (*types.EntryPredicate, []string, error) {
 		if len(tokens) == 0 {
 			return nil, nil, fmt.Errorf("requires additional arguments")
 		}
@@ -23,8 +23,8 @@ var Path = Parser.add(&Primary{
 		if err != nil {
 			return nil, nil, fmt.Errorf("invalid glob: %v", err)
 		}
-		return func(e types.Entry) bool {
+		return types.ToEntryP(func(e types.Entry) bool {
 			return g.Match(e.NormalizedPath)
-		}, tokens[1:], nil
+		}), tokens[1:], nil
 	},
 })
diff --git a/cmd/internal/find/primary/size.go b/cmd/internal/find/primary/size.go
index fa9830b..d380fa7 100644
--- a/cmd/internal/find/primary/size.go
+++ b/cmd/internal/find/primary/size.go
@@ -14,11 +14,11 @@ import (
 //
 //nolint
 var Size = Parser.add(&Primary{
-	Description: "Returns true if the entry's size attribute satisfies the given size predicate",
+	Description:         "Returns true if the entry's size attribute satisfies the given size predicate",
 	DetailedDescription: sizeDetailedDescription,
-	name: "size",
-	args: "[+|-]n[ckMGTP]",
-	parseFunc: func(tokens []string) (types.EntryPredicate, []string, error) {
+	name:                "size",
+	args:                "[+|-]n[ckMGTP]",
+	parseFunc: func(tokens []string) (*types.EntryPredicate, []string, error) {
 		if len(tokens) == 0 {
 			return nil, nil, fmt.Errorf("requires additional arguments")
 		}
@@ -31,7 +31,7 @@ var Size = Parser.add(&Primary{
 			return nil, nil, fmt.Errorf("%v: illegal size value", tokens[0])
 		}
 
-		p := func(e types.Entry) bool {
+		p := types.ToEntryP(func(e types.Entry) bool {
 			if !e.Attributes.HasSize() {
 				return false
 			}
@@ -41,7 +41,7 @@ var Size = Parser.add(&Primary{
 				size = int64(math.Ceil(float64(size) / 512.0))
 			}
 			return numericP(size)
-		}
+		})
 		return p, tokens[1:], nil
 	},
 })
diff --git a/cmd/internal/find/primary/timeAttr.go b/cmd/internal/find/primary/timeAttr.go
index c2a1f74..c683566 100644
--- a/cmd/internal/find/primary/timeAttr.go
+++ b/cmd/internal/find/primary/timeAttr.go
@@ -30,11 +30,11 @@ func getTimeAttrValue(name string, e types.Entry) (time.Time, bool) {
 // timeAttrPrimary => -<name> (+|-)?(\d+ | (numeric.DurationRegex)+)
 func newTimeAttrPrimary(name string) *Primary {
 	return Parser.add(&Primary{
-		Description: fmt.Sprintf("Returns true if the entry's %v attribute satisfies the given time predicate", name),
+		Description:         fmt.Sprintf("Returns true if the entry's %v attribute satisfies the given time predicate", name),
 		DetailedDescription: timeAttrDetailedDescription(name),
-		name: name,
-		args: "[+|-]n[smhdw]",
-		parseFunc: func(tokens []string) (types.EntryPredicate, []string, error) {
+		name:                name,
+		args:                "[+|-]n[smhdw]",
+		parseFunc: func(tokens []string) (*types.EntryPredicate, []string, error) {
 			if params.ReferenceTime.IsZero() {
 				panic("Attempting to parse a time primary without setting params.ReferenceTime")
 			}
@@ -50,7 +50,7 @@ func newTimeAttrPrimary(name string) *Primary {
 				return nil, nil, fmt.Errorf("%v: illegal time value", tokens[0])
 			}
 
-			p := func(e types.Entry) bool {
+			p := types.ToEntryP(func(e types.Entry) bool {
 				t, ok := getTimeAttrValue(name, e)
 				if !ok {
 					return false
@@ -61,7 +61,7 @@ func newTimeAttrPrimary(name string) *Primary {
 					diff = int64(math.Ceil(float64(diff) / float64(numeric.DurationOf('d'))))
 				}
 				return numericP(diff)
-			}
+			})
 			return p, tokens[1:], nil
 		},
 	})
diff --git a/cmd/internal/find/types/entry.go b/cmd/internal/find/types/entry.go
index 695ffa4..124f21e 100644
--- a/cmd/internal/find/types/entry.go
+++ b/cmd/internal/find/types/entry.go
@@ -1,7 +1,6 @@
 package types
 
 import (
-	"github.com/puppetlabs/wash/cmd/internal/find/parser/predicate"
 	apitypes "github.com/puppetlabs/wash/api/types"
 	"github.com/puppetlabs/wash/plugin"
 )
@@ -11,54 +10,22 @@ type Entry struct {
 	apitypes.Entry
 	NormalizedPath string
 	Metadata       plugin.JSONObject
+	SchemaKnown    bool
+	Schema         *EntrySchema
 }
 
-// NewEntryBase constructs a new `wash find` entry
-func NewEntryBase(e apitypes.Entry, normalizedPath string) Entry {
+// NewEntry constructs a new `wash find` entry
+func NewEntry(e apitypes.Entry, normalizedPath string) Entry {
 	return Entry{
-		Entry: e,
+		Entry:          e,
 		NormalizedPath: normalizedPath,
-		Metadata: e.Attributes.Meta(),
+		Metadata:       e.Attributes.Meta(),
 	}
 }
 
-// EntryPredicate represents a predicate on a Wash entry.
-type EntryPredicate func(Entry) bool
-
-// And returns p1 && p2
-func (p1 EntryPredicate) And(p2 predicate.Predicate) predicate.Predicate {
-	return EntryPredicate(func(e Entry) bool {
-		return p1(e) && (p2.(EntryPredicate))(e)
-	})
-}
-
-// Or returns p1 || p2
-func (p1 EntryPredicate) Or(p2 predicate.Predicate) predicate.Predicate {
-	return EntryPredicate(func(e Entry) bool {
-		return p1(e) || (p2.(EntryPredicate))(e)
-	})
-}
-
-// Negate returns Not(p1)
-func (p1 EntryPredicate) Negate() predicate.Predicate {
-	return EntryPredicate(func(e Entry) bool {
-		return !p1(e)
-	})
-}
-
-// IsSatisfiedBy returns true if v satisfies the predicate, false otherwise
-func (p1 EntryPredicate) IsSatisfiedBy(v interface{}) bool {
-	entry, ok := v.(Entry)
-	if !ok {
-		return false
-	}
-	return p1(entry)
-}
-
-// EntryPredicateParser parses Entry predicates
-type EntryPredicateParser func(tokens []string) (EntryPredicate, []string, error)
-
-// Parse parses an EntryPredicate from the given input.
-func (parser EntryPredicateParser) Parse(tokens []string) (predicate.Predicate, []string, error) {
-	return parser(tokens)
+// SetSchema sets the entry's schema. Note that s == nil
+// means the entry's schema was pruned from the stree.
+func (e *Entry) SetSchema(s *EntrySchema) {
+	e.SchemaKnown = true
+	e.Schema = s
 }
diff --git a/cmd/internal/find/types/entryPredicate.go b/cmd/internal/find/types/entryPredicate.go
new file mode 100644
index 0000000..6a55f18
--- /dev/null
+++ b/cmd/internal/find/types/entryPredicate.go
@@ -0,0 +1,84 @@
+package types
+
+import (
+	"github.com/puppetlabs/wash/cmd/internal/find/parser/predicate"
+)
+
+// EntryPredicate represents a predicate on a Wash entry.
+type EntryPredicate struct {
+	P func(Entry) bool
+	// Maintain a SchemaP object for the walker so that it only
+	// traverses satisfying paths.
+	SchemaP EntrySchemaPredicate
+}
+
+// ToEntryP converts p to an EntryPredicate object
+func ToEntryP(p func(e Entry) bool) *EntryPredicate {
+	return &EntryPredicate{
+		P: p,
+		SchemaP: func(s *EntrySchema) bool {
+			return true
+		},
+	}
+}
+
+// And returns p1 && p2
+func (p1 *EntryPredicate) And(p2 predicate.Predicate) predicate.Predicate {
+	ep2 := p2.(*EntryPredicate)
+	return &EntryPredicate{
+		P: func(e Entry) bool {
+			return p1.P(e) && ep2.P(e)
+		},
+		SchemaP: p1.SchemaP.And(ep2.SchemaP).(EntrySchemaPredicate),
+	}
+}
+
+// Or returns p1 || p2
+func (p1 *EntryPredicate) Or(p2 predicate.Predicate) predicate.Predicate {
+	ep2 := p2.(*EntryPredicate)
+	return &EntryPredicate{
+		P: func(e Entry) bool {
+			return p1.P(e) || ep2.P(e)
+		},
+		SchemaP: p1.SchemaP.Or(ep2.SchemaP).(EntrySchemaPredicate),
+	}
+}
+
+// Negate returns Not(p1)
+func (p1 EntryPredicate) Negate() predicate.Predicate {
+	return &EntryPredicate{
+		P: func(e Entry) bool {
+			return !p1.P(e)
+		},
+		SchemaP: p1.SchemaP.Negate().(EntrySchemaPredicate),
+	}
+}
+
+// IsSatisfiedBy returns true if v satisfies the predicate, false otherwise
+func (p1 *EntryPredicate) IsSatisfiedBy(v interface{}) bool {
+	entry, ok := v.(Entry)
+	if !ok {
+		return false
+	}
+	return p1.P(entry)
+}
+
+// EntryPredicateParser parses Entry predicates
+type EntryPredicateParser func(tokens []string) (*EntryPredicate, []string, error)
+
+// Parse parses an EntryPredicate from the given input.
+func (parser EntryPredicateParser) Parse(tokens []string) (predicate.Predicate, []string, error) {
+	return parser(tokens)
+}
+
+// ToSchemaPParser converts parser to a schema predicate parser. This is mostly
+// used by the tests
+func (parser EntryPredicateParser) ToSchemaPParser() EntrySchemaPredicateParser {
+	return func(tokens []string) (EntrySchemaPredicate, []string, error) {
+		entryP, tokens, err := parser(tokens)
+		if err != nil {
+			return nil, tokens, err
+		}
+		return entryP.SchemaP, tokens, nil
+	}
+}
diff --git a/cmd/internal/find/types/entrySchema.go b/cmd/internal/find/types/entrySchema.go
new file mode 100644
index 0000000..5a72636
--- /dev/null
+++ b/cmd/internal/find/types/entrySchema.go
@@ -0,0 +1,189 @@
+package types
+
+import (
+	"sort"
+
+	apitypes "github.com/puppetlabs/wash/api/types"
+)
+
+// EntrySchema is a wrapper to apitypes.EntrySchema
+type EntrySchema struct {
+	// "nil" means an unknown schema
+	*apitypes.EntrySchema
+	// Use a map for faster lookup
+	Children map[string]*EntrySchema
+}
+
+// NewEntrySchema returns a new EntrySchema object. The returned
+// object is faithful to s' representation as a graph. This means
+// that all duplicate nodes are filtered out and that cycles are
+// collapsed.
+func NewEntrySchema(s *apitypes.EntrySchema) *EntrySchema {
+	nodes := make(map[string]*EntrySchema)
+
+	var gatherNodes func(s *apitypes.EntrySchema)
+	gatherNodes = func(s *apitypes.EntrySchema) {
+		if len(s.TypeID) == 0 {
+			panic("gatherNodes called with an empty type ID!")
+		}
+		node := nodes[s.TypeID]
+		if node == nil {
+			node = &EntrySchema{
+				EntrySchema: s,
+				Children:    make(map[string]*EntrySchema),
+			}
+			nodes[s.TypeID] = node
+		}
+		if len(node.Children) > 0 {
+			// We already gathered everything we need to know about this node
+			return
+		}
+		for _, child := range s.Children {
+			// The node's children will be filled in after all the nodes have been
+			// gathered.
+			node.Children[child.TypeID] = nil
+			gatherNodes(child)
+		}
+	}
+	gatherNodes(s)
+
+	for _, node := range nodes {
+		for childTypeID := range node.Children {
+			node.Children[childTypeID] = nodes[childTypeID]
+		}
+	}
+	return nodes[s.TypeID]
+}
+
+// Prune prunes s to contain only nodes that satisfy p. It modifies s'
+// state so it is not an idempotent operation.
+func Prune(s *EntrySchema, p EntrySchemaPredicate) *EntrySchema {
+	keep := evaluateNodesToKeep(s, p)
+	if !keep[s.TypeID] {
+		return nil
+	}
+
+	visited := make(map[string]bool)
+	var prune func(s *EntrySchema)
+	prune = func(s *EntrySchema) {
+		if visited[s.TypeID] {
+			return
+		}
+		visited[s.TypeID] = true
+		for _, child := range s.Children {
+			if !keep[child.TypeID] {
+				delete(s.Children, child.TypeID)
+			} else {
+				prune(child)
+			}
+		}
+	}
+	prune(s)
+
+	return s
+}
+
+/*
+"result" represents the returned value, which is a map of <node> => <keep?>.
+Thus, result[N] == false means we prune N, result[N] == true means we keep
+it. result[N] == true if:
+	* p(N) returns true
+	* p(N) returns true for at least one of N's children (ignoring self-loops)
+Since entry schemas can contain cycles (e.g. the volume.dir class in the
+volume package), and since they do not strictly adhere to a tree structure
+(e.g. paths A-B-C and A-D-C are possible), we cannot completely calculate
+"result" in a single iteration. For example in the A-B-C, A-D-C case, assume
+that p returns false for A, B, C and D. Then we keep B and D if we keep C,
+and we keep C if we keep any of its children. If C does not contain a cycle,
+then we can recurse back the information to B and D. If C does contain a cycle,
+say back to B, then it is difficult, if not impossible, to coordinate all the
+updates in a single iteration. However if we let "result[N] == p(N) for all N
+such that either p(N) == true or N has no children" be our starting state, then
+we can iteratively evaluate result[V] for all other nodes V using the information
+provided by a previous iteration. The terminating condition is when an iteration
+does not update "result". This is possible if "result" contains all nodes N, or if
+there are some indeterminate nodes V. In the latter case, every node M in V is either
+part of a cycle, or there is more than one way to get to M from the root. In either
+case, we can say that result[M] == false for those nodes.
+
+NOTE: To see why result[M] == false for all M in V, we do a proof by contradiction.
+Assume result[M] == true for some M in V, and that M is part of cycle A-B-...-M...-C-A.
+Here, we see that one iteration would update all ancestors of M. A subsequent iteration
+would update M's descendants starting from C. Thus, all the nodes in the cycle are
+determinant, which contradicts our previous assumption of their indeterminacy (since
+those nodes are also part of V). The proof for the other case is similar.
+
+NOTE: The starting state formalizes the first condition for result[N] (and also notes
+that if N is a leaf, then result[N] == p(N)). Subsequent iterations check the second
+condition.
+*/
+func evaluateNodesToKeep(s *EntrySchema, p EntrySchemaPredicate) map[string]bool {
+	result := make(map[string]bool)
+	visited := make(map[string]bool)
+
+	// Set-up our starting state by evaluating p(N). Note that after this code,
+	// result[N] == p(N) for all nodes N such that p(N) == true or N has no
+	// children.
+	var applyPredicate func(s *EntrySchema)
+	applyPredicate = func(s *EntrySchema) {
+		if _, ok := visited[s.TypeID]; ok {
+			return
+		}
+		result[s.TypeID] = p(s)
+		if !result[s.TypeID] && len(s.Children) > 0 {
+			delete(result, s.TypeID)
+		}
+		visited[s.TypeID] = true
+		for _, child := range s.Children {
+			applyPredicate(child)
+		}
+	}
+	applyPredicate(s)
+
+	// Now we iteratively update "result".
+	var updateResult func(s *EntrySchema)
+	updateResult = func(s *EntrySchema) {
+		if _, ok := visited[s.TypeID]; ok {
+			return
+		}
+		visited[s.TypeID] = true
+		for _, child := range s.Children {
+			updateResult(child)
+			if result[child.TypeID] {
+				result[s.TypeID] = true
+			}
+		}
+	}
+	for {
+		visited = make(map[string]bool)
+		prevNumNodes := len(result)
+		updateResult(s)
+		newNodes := len(result) - prevNumNodes
+		if newNodes <= 0 {
+			// We've reached the terminating condition. Note that result[V] == false
+			// for an indeterminate node V since false is the 0-value for a "bool" type.
+			// Thus, we can just return here.
+			return result
+		}
+	}
+}
+
+// toMap returns a map of <typeID> => <childTypeIDs...> (i.e. its pre-serialized
+// graph representation). It is useful for testing.
+func (s *EntrySchema) toMap() map[string][]string {
+	mp := make(map[string][]string)
+	var visit func(s *EntrySchema)
+	visit = func(s *EntrySchema) {
+		if _, ok := mp[s.TypeID]; ok {
+			return
+		}
+		mp[s.TypeID] = []string{}
+		for _, child := range s.Children {
+			mp[s.TypeID] = append(mp[s.TypeID], child.TypeID)
+			visit(child)
+		}
+		sort.Strings(mp[s.TypeID])
+	}
+	visit(s)
+	return mp
+}
diff --git a/cmd/internal/find/types/entrySchemaPredicate.go b/cmd/internal/find/types/entrySchemaPredicate.go
new file mode 100644
index 0000000..70b0f9c
--- /dev/null
+++ b/cmd/internal/find/types/entrySchemaPredicate.go
@@ -0,0 +1,46 @@
+package types
+
+import (
+	"github.com/puppetlabs/wash/cmd/internal/find/parser/predicate"
+)
+
+// EntrySchemaPredicate represents a predicate on an entry's schema
+type EntrySchemaPredicate func(*EntrySchema) bool
+
+// And returns p1 && p2
+func (p1 EntrySchemaPredicate) And(p2 predicate.Predicate) predicate.Predicate {
+	return EntrySchemaPredicate(func(s *EntrySchema) bool {
+		return p1(s) && (p2.(EntrySchemaPredicate))(s)
+	})
+}
+
+// Or returns p1 || p2
+func (p1 EntrySchemaPredicate) Or(p2 predicate.Predicate) predicate.Predicate {
+	return EntrySchemaPredicate(func(s *EntrySchema) bool {
+		return p1(s) || (p2.(EntrySchemaPredicate))(s)
+	})
+}
+
+// Negate returns Not(p1)
+func (p1 EntrySchemaPredicate) Negate() predicate.Predicate {
+	return EntrySchemaPredicate(func(s *EntrySchema) bool {
+		return !p1(s)
+	})
+}
+
+// IsSatisfiedBy returns true if v satisfies the predicate, false otherwise
+func (p1 EntrySchemaPredicate) IsSatisfiedBy(v interface{}) bool {
+	s, ok := v.(*EntrySchema)
+	if !ok {
+		return false
+	}
+	return p1(s)
+}
+
+// EntrySchemaPredicateParser parses EntrySchema predicates
+type EntrySchemaPredicateParser func(tokens []string) (EntrySchemaPredicate, []string, error)
+
+// Parse parses an EntrySchemaPredicate from the given input.
+func (parser EntrySchemaPredicateParser) Parse(tokens []string) (predicate.Predicate, []string, error) {
+	return parser(tokens)
+}
diff --git a/cmd/internal/find/walker.go b/cmd/internal/find/walker.go
index df2b10d..134c29f 100644
--- a/cmd/internal/find/walker.go
+++ b/cmd/internal/find/walker.go
@@ -16,7 +16,7 @@ type walker interface {
 }
 
 type walkerImpl struct {
-	p    types.EntryPredicate
+	p    *types.EntryPredicate
 	opts types.Options
 	conn client.Client
 }
@@ -36,14 +36,21 @@ func (w *walkerImpl) Walk(path string) bool {
 		cmdutil.ErrPrintf("%v\n", err)
 		return false
 	}
+	s, err := w.conn.Schema(path)
+	if err != nil {
+		cmdutil.ErrPrintf("%v\n", err)
+		return false
+	}
+	if s != nil {
+		schema := types.Prune(types.NewEntrySchema(s), w.p.SchemaP)
+		e.SetSchema(schema)
+	}
 	return w.walk(e, 0)
 }
 
 func (w *walkerImpl) walk(e types.Entry, depth uint) bool {
 	// If the Depth option is set, then we visit e after visiting its children.
 	// Otherwise, we visit e first.
-	//
-	// TODO: Write unit tests for the walker by mocking out the client.
 	successful := true
 	check := func(result bool) {
 		// Use "&&" to short-circuit if successful is false
@@ -54,12 +61,22 @@ func (w *walkerImpl) walk(e types.Entry, depth uint) bool {
 	}
 	childDepth := depth + 1
 	if int(childDepth) <= w.opts.Maxdepth && e.Supports(plugin.ListAction()) {
+		if e.SchemaKnown {
+			if e.Schema == nil || len(e.Schema.Children) == 0 {
+				// We've reached the end of our traversal
+				return successful
+			}
+		}
 		children, err := list(w.conn, e)
 		if err != nil {
 			cmdutil.ErrPrintf("could not get children of %v: %v\n", e.NormalizedPath, err)
 			successful = false
 		} else {
 			for _, child := range children {
+				if e.SchemaKnown {
+					// Note that e.Schema != nil here
+					child.SetSchema(e.Schema.Children[child.TypeID])
+				}
 				check(w.walk(child, childDepth))
 			}
 		}
@@ -74,6 +91,13 @@ func (w *walkerImpl) visit(e types.Entry, depth uint) bool {
 	if depth < w.opts.Mindepth {
 		return true
 	}
+	if e.SchemaKnown {
+		if e.Schema == nil || !w.p.SchemaP(e.Schema) {
+			// This is possible if e's a sibling/ancestor to a satisfying
+			// node
+			return true
+		}
+	}
 	if primary.IsSet(primary.Meta) && w.opts.Fullmeta {
 		// Fetch the entry's full metadata
 		meta, err := w.conn.Metadata(e.Path)
@@ -83,7 +107,7 @@ func (w *walkerImpl) visit(e types.Entry, depth uint) bool {
 		}
 		e.Metadata = meta
 	}
-	if w.p(e) {
+	if w.p.P(e) {
 		cmdutil.Printf("%v\n", e.NormalizedPath)
 	}
 	return true
diff --git a/cmd/stree.go b/cmd/stree.go
index 6111b5e..95c332d 100644
--- a/cmd/stree.go
+++ b/cmd/stree.go
@@ -27,12 +27,20 @@ func streeMain(cmd *cobra.Command, args []string) exitCode {
 		paths = []string{"."}
 	}
 	conn := cmdutil.NewClient()
+	schemas := make(map[string]*apitypes.EntrySchema)
 	for _, path := range paths {
 		schema, err := conn.Schema(path)
 		if err != nil {
 			cmdutil.ErrPrintf("%v\n", err)
 			return exitCode{1}
 		}
+		if schema == nil {
+			cmdutil.ErrPrintf("%v: schema unknown\n", path)
+			continue
+		}
+		schemas[path] = schema
+	}
+	for path, schema := range schemas {
 		stree := treeprint.New()
 		fill(stree, schema)
 		stree.SetValue(path)
@@ -41,7 +49,7 @@ func streeMain(cmd *cobra.Command, args []string) exitCode {
 	return exitCode{0}
 }
 
-func fill(stree treeprint.Tree, schema apitypes.EntrySchema) treeprint.Tree {
+func fill(stree treeprint.Tree, schema *apitypes.EntrySchema) treeprint.Tree {
 	value := schema.Label
 	if !schema.Singleton {
 		value = fmt.Sprintf("[%v]", value)
diff --git a/plugin/aws/ec2Dir.go b/plugin/aws/ec2Dir.go
index 82eb432..241560b 100644
--- a/plugin/aws/ec2Dir.go
+++ b/plugin/aws/ec2Dir.go
@@ -40,7 +40,7 @@ func newEC2Dir(session *session.Session) *ec2Dir {
 	return ec2Dir
 }
 
-func (e *ec2Dir) ChildSchemas() []plugin.EntrySchema {
+func (e *ec2Dir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(ec2InstancesDirBase())
 }
 
diff --git a/plugin/aws/ec2Instance.go b/plugin/aws/ec2Instance.go
index ebe9ab4..783d0e0 100644
--- a/plugin/aws/ec2Instance.go
+++ b/plugin/aws/ec2Instance.go
@@ -107,7 +107,7 @@ func getAttributes(inst *ec2Client.Instance) plugin.EntryAttributes {
 	return attr
 }
 
-func (inst *ec2Instance) ChildSchemas() []plugin.EntrySchema {
+func (inst *ec2Instance) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(
 		ec2InstanceConsoleOutputBase(),
 		ec2InstanceMetadataJSONBase(),
diff --git a/plugin/aws/ec2InstancesDir.go b/plugin/aws/ec2InstancesDir.go
index 2a5792f..b7f762c 100644
--- a/plugin/aws/ec2InstancesDir.go
+++ b/plugin/aws/ec2InstancesDir.go
@@ -36,7 +36,7 @@ func newEC2InstancesDir(session *session.Session, client *ec2Client.EC2) *ec2Ins
 	return ec2InstancesDir
 }
 
-func (is *ec2InstancesDir) ChildSchemas() []plugin.EntrySchema {
+func (is *ec2InstancesDir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(ec2InstanceBase())
 }
 
diff --git a/plugin/aws/profile.go b/plugin/aws/profile.go
index 1dc1e43..0e996cc 100644
--- a/plugin/aws/profile.go
+++ b/plugin/aws/profile.go
@@ -68,7 +68,7 @@ func newProfile(ctx context.Context, name string) (*profile, error) {
 	return profile, nil
 }
 
-func (p *profile) ChildSchemas() []plugin.EntrySchema {
+func (p *profile) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(resourcesDirBase())
 }
 
diff --git a/plugin/aws/resourcesDir.go b/plugin/aws/resourcesDir.go
index 24c5dcd..3f04aef 100644
--- a/plugin/aws/resourcesDir.go
+++ b/plugin/aws/resourcesDir.go
@@ -37,7 +37,7 @@ func newResourcesDir(session *session.Session) *resourcesDir {
 	return resourcesDir
 }
 
-func (r *resourcesDir) ChildSchemas() []plugin.EntrySchema {
+func (r *resourcesDir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(s3DirBase(), ec2DirBase())
 }
 
diff --git a/plugin/aws/root.go b/plugin/aws/root.go
index b63776e..424f7d9 100644
--- a/plugin/aws/root.go
+++ b/plugin/aws/root.go
@@ -87,7 +87,7 @@ func (r *Root) Init(cfg map[string]interface{}) error {
 }
 
 // ChildSchemas returns the root's child schema
-func (r *Root) ChildSchemas() []plugin.EntrySchema {
+func (r *Root) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(profileBase())
 }
 
diff --git a/plugin/aws/s3Bucket.go b/plugin/aws/s3Bucket.go
index 029f0cf..4c0f357 100644
--- a/plugin/aws/s3Bucket.go
+++ b/plugin/aws/s3Bucket.go
@@ -174,7 +174,7 @@ func newS3Bucket(name string, ctime time.Time, session *session.Session) *s3Buck
 	return bucket
 }
 
-func (b *s3Bucket) ChildSchemas() []plugin.EntrySchema {
+func (b *s3Bucket) ChildSchemas() []*plugin.EntrySchema {
 	return s3ObjectPrefixBase().ChildSchemas()
 }
 
diff --git a/plugin/aws/s3Dir.go b/plugin/aws/s3Dir.go
index 3261e6a..9fd1ed7 100644
--- a/plugin/aws/s3Dir.go
+++ b/plugin/aws/s3Dir.go
@@ -33,7 +33,7 @@ func newS3Dir(session *session.Session) *s3Dir {
 	return s3Dir
 }
 
-func (s *s3Dir) ChildSchemas() []plugin.EntrySchema {
+func (s *s3Dir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(s3BucketBase())
 }
 
diff --git a/plugin/aws/s3ObjectPrefix.go b/plugin/aws/s3ObjectPrefix.go
index c2d99a5..7744f43 100644
--- a/plugin/aws/s3ObjectPrefix.go
+++ b/plugin/aws/s3ObjectPrefix.go
@@ -36,7 +36,7 @@ func newS3ObjectPrefix(name string, bucket string, prefix string, client *s3Clie
 	return objPrefix
 }
 
-func (d *s3ObjectPrefix) ChildSchemas() []plugin.EntrySchema {
+func (d *s3ObjectPrefix) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(s3ObjectPrefixBase(), s3ObjectBase())
 }
 
diff --git a/plugin/docker/container.go b/plugin/docker/container.go
index 885f93e..4fe470e 100644
--- a/plugin/docker/container.go
+++ b/plugin/docker/container.go
@@ -66,7 +66,7 @@ func (c *container) Metadata(ctx context.Context) (plugin.JSONObject, error) {
 	return plugin.ToJSONObject(raw), nil
 }
 
-func (c *container) ChildSchemas() []plugin.EntrySchema {
+func (c *container) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(
 		containerLogFileBase(),
 		containerMetadataBase(),
diff --git a/plugin/docker/containersDir.go b/plugin/docker/containersDir.go
index b733ce7..1c75a05 100644
--- a/plugin/docker/containersDir.go
+++ b/plugin/docker/containersDir.go
@@ -28,7 +28,7 @@ func newContainersDir(client *client.Client) *containersDir {
 	return containersDir
 }
 
-func (cs *containersDir) ChildSchemas() []plugin.EntrySchema {
+func (cs *containersDir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(containerBase())
 }
 
diff --git a/plugin/docker/root.go b/plugin/docker/root.go
index 89ec721..15ff82a 100644
--- a/plugin/docker/root.go
+++ b/plugin/docker/root.go
@@ -38,7 +38,7 @@ func (r *Root) Init(map[string]interface{}) error {
 }
 
 // ChildSchemas returns the root's child schema
-func (r *Root) ChildSchemas() []plugin.EntrySchema {
+func (r *Root) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(containersDirBase(), volumesDirBase())
 }
 
diff --git a/plugin/docker/volume.go b/plugin/docker/volume.go
index 30e20a1..7719601 100644
--- a/plugin/docker/volume.go
+++ b/plugin/docker/volume.go
@@ -53,7 +53,7 @@ func newVolume(c *client.Client, v *types.Volume) (*volume, error) {
 	return vol, nil
 }
 
-func (v *volume) ChildSchemas() []plugin.EntrySchema {
+func (v *volume) ChildSchemas() []*plugin.EntrySchema {
 	return vol.ChildSchemas()
 }
 
diff --git a/plugin/docker/volumesDir.go b/plugin/docker/volumesDir.go
index 732cbaf..4ea7eea 100644
--- a/plugin/docker/volumesDir.go
+++ b/plugin/docker/volumesDir.go
@@ -28,7 +28,7 @@ func newVolumesDir(client *client.Client) *volumesDir {
 	return volumesDir
 }
 
-func (vs *volumesDir) ChildSchemas() []plugin.EntrySchema {
+func (vs *volumesDir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(volumeBase())
 }
 
diff --git a/plugin/externalPluginEntry.go b/plugin/externalPluginEntry.go
index 218ca0e..bb31b97 100644
--- a/plugin/externalPluginEntry.go
+++ b/plugin/externalPluginEntry.go
@@ -22,12 +22,12 @@ type decodedCacheTTLs struct {
 
 // decodedExternalPluginEntry describes a decoded serialized entry.
 type decodedExternalPluginEntry struct {
-	Name                 string           `json:"name"`
-	Methods             []string          `json:"methods"`
-	SlashReplacer        string           `json:"slash_replacer"`
-	CacheTTLs            decodedCacheTTLs `json:"cache_ttls"`
-	Attributes           EntryAttributes  `json:"attributes"`
-	State                string           `json:"state"`
+	Name          string           `json:"name"`
+	Methods       []string         `json:"methods"`
+	SlashReplacer string           `json:"slash_replacer"`
+	CacheTTLs     decodedCacheTTLs `json:"cache_ttls"`
+	Attributes    EntryAttributes  `json:"attributes"`
+	State         string           `json:"state"`
 }
 
 func (e decodedExternalPluginEntry) toExternalPluginEntry() (*externalPluginEntry, error) {
@@ -38,9 +38,9 @@ func (e decodedExternalPluginEntry) toExternalPluginEntry() (*externalPluginEntr
 		return nil, fmt.Errorf("the entry's methods must be provided")
 	}
 	entry := &externalPluginEntry{
-		EntryBase:        NewEntryBase(),
-		methods:          e.Methods,
-		state:            e.State,
+		EntryBase: NewEntryBase(),
+		methods:   e.Methods,
+		state:     e.State,
 	}
 	entry.SetName(e.Name)
 	entry.SetAttributes(e.Attributes)
@@ -59,9 +59,9 @@ func (e decodedExternalPluginEntry) toExternalPluginEntry() (*externalPluginEntr
 // externalPluginEntry represents an external plugin entry
 type externalPluginEntry struct {
 	EntryBase
-	script           externalPluginScript
-	methods         []string
-	state            string
+	script  externalPluginScript
+	methods []string
+	state   string
 }
 
 func (e *externalPluginEntry) setCacheTTLs(ttls decodedCacheTTLs) {
@@ -87,9 +87,9 @@ func (e *externalPluginEntry) implements(method string) bool {
 	return false
 }
 
-func (e *externalPluginEntry) ChildSchemas() []EntrySchema {
+func (e *externalPluginEntry) ChildSchemas() []*EntrySchema {
 	// ChildSchema's meant for core plugins.
-	return []EntrySchema{}
+	return []*EntrySchema{}
 }
 
 func (e *externalPluginEntry) List(ctx context.Context) ([]Entry, error) {
@@ -270,7 +270,7 @@ func (e *externalPluginEntry) Exec(ctx context.Context, cmd string, args []strin
 }
 
 type stdoutStreamer struct {
-	cmd    *internal.Command 
+	cmd    *internal.Command
 	stdout io.ReadCloser
 }
 
@@ -291,4 +291,4 @@ func newStdoutDecodeErr(ctx context.Context, decodedThing string, reason error,
 		example,
 	)
 	return fmt.Errorf("could not decode %v from stdout: %v", decodedThing, reason)
-}
\ No newline at end of file
+}
diff --git a/plugin/kubernetes/context.go b/plugin/kubernetes/context.go
index d335f73..54cf9ca 100644
--- a/plugin/kubernetes/context.go
+++ b/plugin/kubernetes/context.go
@@ -34,7 +34,7 @@ func newK8Context(name string, client *k8s.Clientset, config *rest.Config, defau
 	return context
 }
 
-func (c *k8context) ChildSchemas() []plugin.EntrySchema {
+func (c *k8context) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(namespaceBase())
 }
 
diff --git a/plugin/kubernetes/namespace.go b/plugin/kubernetes/namespace.go
index 395239b..9e98dc3 100644
--- a/plugin/kubernetes/namespace.go
+++ b/plugin/kubernetes/namespace.go
@@ -38,7 +38,7 @@ func newNamespace(name string, meta *corev1.Namespace, c *k8s.Clientset, cfg *re
 	return ns
 }
 
-func (n *namespace) ChildSchemas() []plugin.EntrySchema {
+func (n *namespace) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(podsDirBase(), pvcsDirBase())
 }
 
diff --git a/plugin/kubernetes/podsDir.go b/plugin/kubernetes/podsDir.go
index f2211ff..5d8f2c1 100644
--- a/plugin/kubernetes/podsDir.go
+++ b/plugin/kubernetes/podsDir.go
@@ -32,7 +32,7 @@ func newPodsDir(ns *namespace) *podsDir {
 	return pds
 }
 
-func (ps *podsDir) ChildSchemas() []plugin.EntrySchema {
+func (ps *podsDir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(podBase())
 }
 
diff --git a/plugin/kubernetes/pvc.go b/plugin/kubernetes/pvc.go
index 984d7e2..e4d8873 100644
--- a/plugin/kubernetes/pvc.go
+++ b/plugin/kubernetes/pvc.go
@@ -52,7 +52,7 @@ func newPVC(pi typedv1.PersistentVolumeClaimInterface, pd typedv1.PodInterface,
 	return vol
 }
 
-func (v *pvc) ChildSchemas() []plugin.EntrySchema {
+func (v *pvc) ChildSchemas() []*plugin.EntrySchema {
 	return volume.ChildSchemas()
 }
 
diff --git a/plugin/kubernetes/pvcsDir.go b/plugin/kubernetes/pvcsDir.go
index 96e28bb..0098b21 100644
--- a/plugin/kubernetes/pvcsDir.go
+++ b/plugin/kubernetes/pvcsDir.go
@@ -29,7 +29,7 @@ func newPVCSDir(ns *namespace) *pvcsDir {
 	return pv
 }
 
-func (pv *pvcsDir) ChildSchemas() []plugin.EntrySchema {
+func (pv *pvcsDir) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(pvcBase())
 }
 
diff --git a/plugin/kubernetes/root.go b/plugin/kubernetes/root.go
index a0c52df..57b69f2 100644
--- a/plugin/kubernetes/root.go
+++ b/plugin/kubernetes/root.go
@@ -67,7 +67,7 @@ func (r *Root) Init(map[string]interface{}) error {
 }
 
 // ChildSchemas returns the root's child schemas
-func (r *Root) ChildSchemas() []plugin.EntrySchema {
+func (r *Root) ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(k8ContextBase())
 }
 
diff --git a/plugin/registry.go b/plugin/registry.go
index e6bd5f3..f3f5f9e 100644
--- a/plugin/registry.go
+++ b/plugin/registry.go
@@ -59,7 +59,7 @@ func (r *Registry) RegisterPlugin(root Root, config map[string]interface{}) erro
 }
 
 // ChildSchemas returns the child schemas of the plugin registry
-func (r *Registry) ChildSchemas() []EntrySchema {
+func (r *Registry) ChildSchemas() []*EntrySchema {
 	return ChildSchemas(r.pluginRoots...)
 }
 
diff --git a/plugin/schema.go b/plugin/schema.go
index 52c5e4c..ab9a1e5 100644
--- a/plugin/schema.go
+++ b/plugin/schema.go
@@ -8,11 +8,11 @@ import (
 
 // EntrySchema represents an entry's schema.
 type EntrySchema struct {
-	Type      string        `json:"type"`
-	Label     string        `json:"label"`
-	Singleton bool          `json:"singleton"`
-	Actions   []string      `json:"actions"`
-	Children  []EntrySchema `json:"children"`
+	TypeID    string         `json:"type_id"`
+	Label     string         `json:"label"`
+	Singleton bool           `json:"singleton"`
+	Actions   []string       `json:"actions"`
+	Children  []*EntrySchema `json:"children"`
 	entry     Entry
 }
 
@@ -23,45 +23,47 @@ type EntrySchema struct {
 // get a Parent child's child schemas. We could move ChildSchemas over to
 // EntryBase, but doing so removes the existing compile-time check on whether
 // a Parent provided their child schemas.
-func ChildSchemas(childBases ...Entry) []EntrySchema {
-	var schemas []EntrySchema
+func ChildSchemas(childBases ...Entry) []*EntrySchema {
+	var schemas []*EntrySchema
 	for _, childBase := range childBases {
 		schemas = append(schemas, schema(childBase, false))
 	}
 	return schemas
 }
 
+// TypeID returns the entry's type ID
+func TypeID(e Entry) string {
+	// TODO: Handle external plugin type IDs.
+	return strings.TrimPrefix(reflect.TypeOf(e).String(), "*")
+}
+
 // Schema returns the entry's schema. Plugin authors should use plugin.ChildSchemas
 // when implementing Parent#ChildSchemas. Using Schema to do this can cause infinite
 // recursion if e's children have the same type as e, which can happen if e's e.g.
 // a volume directory.
-func Schema(e Entry) EntrySchema {
+func Schema(e Entry) *EntrySchema {
 	return schema(e, true)
 }
 
 // Common helper for Schema and ChildSchema
-func schema(e Entry, includeChildren bool) EntrySchema {
+func schema(e Entry, includeChildren bool) *EntrySchema {
 	// TODO: Handle external plugin schemas
 	switch e.(type) {
 	case *externalPluginRoot:
-		return EntrySchema{
-			Type: "external-plugin-root",
-		}
+		return nil
 	case *externalPluginEntry:
-		return EntrySchema{
-			Type: "external-plugin-entry",
-		}
+		return nil
 	}
 
-	s := EntrySchema{
-		Type:      strings.TrimPrefix(reflect.TypeOf(e).String(), "*"),
+	s := &EntrySchema{
+		TypeID:    TypeID(e),
 		Label:     e.entryBase().label,
 		Singleton: e.entryBase().isSingleton,
 		Actions:   SupportedActionsOf(e),
 		entry:     e,
 	}
 	if len(s.Label) == 0 {
-		msg := fmt.Sprintf("Schema for type %v has an empty label. Use EntryBase#SetLabel to set the label.", s.Type)
+		msg := fmt.Sprintf("Schema for type %v has an empty label. Use EntryBase#SetLabel to set the label.", s.TypeID)
 		panic(msg)
 	}
 	if includeChildren {
@@ -74,20 +76,21 @@ func (s *EntrySchema) fillChildren(visited map[string]bool) {
 	if !ListAction().IsSupportedOn(s.entry) {
 		return
 	}
-	if visited[s.Type] {
+	if visited[s.TypeID] {
 		// This means that s' children can have s' type, which is
 		// true if s is e.g. a volume directory.
 		return
 	}
-	s.Children = s.entry.(Parent).ChildSchemas()
-	visited[s.Type] = true
-	for i, child := range s.Children {
+	children := s.entry.(Parent).ChildSchemas()
+	visited[s.TypeID] = true
+	for _, child := range children {
+		if child == nil {
+			continue
+		}
+		s.Children = append(s.Children, child)
 		child.fillChildren(visited)
-		// Need to re-assign because child is not a pointer,
-		// so s.Children[i] won't be updated.
-		s.Children[i] = child
 	}
 	// Delete "s" from visited so that siblings or ancestors that
 	// also use "s" won't be affected.
-	delete(visited, s.Type)
+	delete(visited, s.TypeID)
 }
diff --git a/plugin/types.go b/plugin/types.go
index d3bfd6b..10ff5b4 100644
--- a/plugin/types.go
+++ b/plugin/types.go
@@ -53,7 +53,12 @@ type Entry interface {
 // filesystem.
 type Parent interface {
 	Entry
-	ChildSchemas() []EntrySchema
+	// TODO: "nil" schemas mean that the schema's unknown. This condition's necessary for
+	// the plugin registry b/c external plugins may not have a schema. However, core plugins
+	// should never return a "nil" schema -- their schema's always known. Since this specification
+	// affects core plugin authors, it should be cleaned up once the metadata schema + external
+	// plugin schema work is finished.
+	ChildSchemas() []*EntrySchema
 	List(context.Context) ([]Entry, error)
 }
 
diff --git a/volume/core.go b/volume/core.go
index 1a7dc64..16d87b0 100644
--- a/volume/core.go
+++ b/volume/core.go
@@ -39,7 +39,7 @@ type Dir = map[string]plugin.EntryAttributes
 type DirMap = map[string]Dir
 
 // ChildSchemas returns a volume's child schema
-func ChildSchemas() []plugin.EntrySchema {
+func ChildSchemas() []*plugin.EntrySchema {
 	return plugin.ChildSchemas(dirBase(), fileBase())
 }
 
diff --git a/volume/dir.go b/volume/dir.go
index d4507c0..fc648b9 100644
--- a/volume/dir.go
+++ b/volume/dir.go
@@ -38,7 +38,7 @@ func newDir(name string, attr plugin.EntryAttributes, impl Interface, key plugin
 	return vd
 }
 
-func (v *dir) ChildSchemas() []plugin.EntrySchema {
+func (v *dir) ChildSchemas() []*plugin.EntrySchema {
 	return ChildSchemas()
 }
 
diff --git a/volume/fs.go b/volume/fs.go
index fde87a2..d85f7d1 100644
--- a/volume/fs.go
+++ b/volume/fs.go
@@ -40,7 +40,7 @@ func NewFS(name string, executor plugin.Execable, maxdepth int) *FS {
 }
 
 // ChildSchemas returns the FS entry's child schema
-func (d *FS) ChildSchemas() []plugin.EntrySchema {
+func (d *FS) ChildSchemas() []*plugin.EntrySchema {
 	return ChildSchemas()
 }
 


Test patch:
diff --git a/api/cache_test.go b/api/cache_test.go
index f8e73bc..b847d9a 100644
--- a/api/cache_test.go
+++ b/api/cache_test.go
@@ -163,6 +163,6 @@ func (p *mockedParent) List(ctx context.Context) ([]plugin.Entry, error) {
 	return args.Get(0).([]plugin.Entry), args.Error(1)
 }
 
-func (p *mockedParent) ChildSchemas() []plugin.EntrySchema {
+func (p *mockedParent) ChildSchemas() []*plugin.EntrySchema {
 	return nil
 }
diff --git a/api/helpers_test.go b/api/helpers_test.go
index 0ae5b36..183a3d5 100644
--- a/api/helpers_test.go
+++ b/api/helpers_test.go
@@ -20,7 +20,7 @@ func (g *mockParent) List(context.Context) ([]plugin.Entry, error) {
 	return g.entries, nil
 }
 
-func (g *mockParent) ChildSchemas() []plugin.EntrySchema {
+func (g *mockParent) ChildSchemas() []*plugin.EntrySchema {
 	return nil
 }
 
@@ -116,10 +116,10 @@ func (m *mockRoot) List(ctx context.Context) ([]plugin.Entry, error) {
 	return args.Get(0).([]plugin.Entry), args.Error(1)
 }
 
-func (m *mockRoot) ChildSchemas() []plugin.EntrySchema {
-	return []plugin.EntrySchema{
-		plugin.EntrySchema{
-			Type: "mockEntry",
+func (m *mockRoot) ChildSchemas() []*plugin.EntrySchema {
+	return []*plugin.EntrySchema{
+		&plugin.EntrySchema{
+			TypeID: "mockEntry",
 		},
 	}
 }
diff --git a/cmd/internal/cmdtest/client.go b/cmd/internal/cmdtest/client.go
index a0f50b0..bc10b6c 100644
--- a/cmd/internal/cmdtest/client.go
+++ b/cmd/internal/cmdtest/client.go
@@ -62,7 +62,7 @@ func (c *MockClient) Clear(path string) ([]string, error) {
 }
 
 // Schema mocks Client#Schema
-func (c *MockClient) Schema(path string) (apitypes.EntrySchema, error) {
+func (c *MockClient) Schema(path string) (*apitypes.EntrySchema, error) {
 	args := c.Called(path)
-	return args.Get(0).(apitypes.EntrySchema), args.Error(1)
+	return args.Get(0).(*apitypes.EntrySchema), args.Error(1)
 }
diff --git a/cmd/internal/find/parser/parseExpression_test.go b/cmd/internal/find/parser/parseExpression_test.go
index 9922f8d..00d42e5 100644
--- a/cmd/internal/find/parser/parseExpression_test.go
+++ b/cmd/internal/find/parser/parseExpression_test.go
@@ -145,7 +145,7 @@ func (s *ParseExpressionTestSuite) TestParseExpressionComplexEval() {
 
 func TestParseExpression(t *testing.T) {
 	s := new(ParseExpressionTestSuite)
-	s.Parser = types.EntryPredicateParser(func(tokens []string) (types.EntryPredicate, []string, error) {
+	s.Parser = types.EntryPredicateParser(func(tokens []string) (*types.EntryPredicate, []string, error) {
 		p, err := parseExpression(tokens)
 		return p, []string{}, err
 	})
diff --git a/cmd/internal/find/parser/parse_test.go b/cmd/internal/find/parser/parse_test.go
index 4204039..e35b5d8 100644
--- a/cmd/internal/find/parser/parse_test.go
+++ b/cmd/internal/find/parser/parse_test.go
@@ -33,7 +33,7 @@ func (suite *ParseTestSuite) TestValidInput() {
 		expectedOpts.MarkAsSet(types.DepthFlag)
 		expectedOpts.Depth = true
 		suite.Equal(expectedOpts, r.Options)
-		suite.Equal(true, r.Predicate(types.Entry{}))
+		suite.Equal(true, r.Predicate.P(types.Entry{}))
 	}
 }
 
diff --git a/cmd/internal/find/primary/action_test.go b/cmd/internal/find/primary/action_test.go
index c526ad6..446f60a 100644
--- a/cmd/internal/find/primary/action_test.go
+++ b/cmd/internal/find/primary/action_test.go
@@ -3,6 +3,7 @@ package primary
 import (
 	"testing"
 
+	"github.com/puppetlabs/wash/api/types"
 	"github.com/puppetlabs/wash/cmd/internal/find/types"
 	"github.com/stretchr/testify/suite"
 )
@@ -16,12 +17,18 @@ func (s *ActionPrimaryTestSuite) TestErrors() {
 	s.RETC("foo", "foo is an invalid action. Valid actions are.*list")
 }
 
-func (s *ActionPrimaryTestSuite) TestValidInput() {
+func (s *ActionPrimaryTestSuite) TestValidInput_EntryP() {
 	s.RTC("list", "", []string{"list"}, []string{"exec"})
 	// Test multiple supported actions
 	s.RTC("list", "", []string{"read", "stream", "list"}, []string{"read", "stream"})
 }
 
+func (s *ActionPrimaryTestSuite) TestValidInput_SchemaP() {
+	// Same test cases as EntryP
+	s.RSTC("list", "", []string{"list"}, []string{"exec"})
+	s.RSTC("list", "", []string{"read", "stream", "list"}, []string{"read", "stream"})
+}
+
 func TestActionPrimary(t *testing.T) {
 	s := new(ActionPrimaryTestSuite)
 	s.Parser = Action
@@ -30,5 +37,12 @@ func TestActionPrimary(t *testing.T) {
 		e.Actions = v.([]string)
 		return e
 	}
+	s.ConstructEntrySchema = func(v interface{}) *types.EntrySchema {
+		s := &types.EntrySchema{
+			EntrySchema: &apitypes.EntrySchema{},
+		}
+		s.Actions = v.([]string)
+		return s
+	}
 	suite.Run(t, s)
 }
diff --git a/cmd/internal/find/primary/primary_test.go b/cmd/internal/find/primary/primary_test.go
index 3ab28fa..a19bbee 100644
--- a/cmd/internal/find/primary/primary_test.go
+++ b/cmd/internal/find/primary/primary_test.go
@@ -9,7 +9,8 @@ import (
 
 type primaryTestSuite struct {
 	parsertest.Suite
-	ConstructEntry func(v interface{}) types.Entry
+	ConstructEntry       func(v interface{}) types.Entry
+	ConstructEntrySchema func(v interface{}) *types.EntrySchema
 }
 
 func (s *primaryTestSuite) RETC(input string, errRegex string) {
@@ -23,6 +24,19 @@ func (s *primaryTestSuite) RTC(input string, remInput string, trueValue interfac
 	}
 }
 
+// RSTC => RunSchemaPTestCase
+func (s *primaryTestSuite) RSTC(input string, remInput string, trueValue interface{}, falseValue ...interface{}) {
+	oldParser := s.Parser.(*Primary)
+	defer func() {
+		s.Parser = oldParser
+	}()
+	s.Parser = types.EntryPredicateParser(oldParser.parseFunc).ToSchemaPParser()
+	s.Suite.RTC(input, remInput, s.ConstructEntrySchema(trueValue))
+	if len(falseValue) > 0 {
+		s.Suite.RNTC(input, remInput, s.ConstructEntrySchema(falseValue[0]))
+	}
+}
+
 func (s *primaryTestSuite) RNTC(input string, remInput string, falseValue interface{}) {
 	s.Suite.RNTC(input, remInput, s.ConstructEntry(falseValue))
 }
diff --git a/cmd/internal/find/types/entrySchema_test.go b/cmd/internal/find/types/entrySchema_test.go
new file mode 100644
index 0000000..b99827f
--- /dev/null
+++ b/cmd/internal/find/types/entrySchema_test.go
@@ -0,0 +1,200 @@
+package types
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"path"
+	"strings"
+	"testing"
+
+	apitypes "github.com/puppetlabs/wash/api/types"
+	"github.com/stretchr/testify/suite"
+)
+
+type EntrySchemaTestSuite struct {
+	suite.Suite
+}
+
+func (suite *EntrySchemaTestSuite) TestNewEntrySchema() {
+	s := suite.readFixture("complex")
+	expected := map[string][]string{
+		"A": []string{"B", "C"},
+		"B": []string{"D", "E"},
+		"C": []string{"C", "F"},
+		"D": []string{},
+		"E": []string{"A", "C"},
+		"F": []string{},
+	}
+	suite.Equal(expected, s.toMap())
+
+	// Ensure that duplicates are properly handled
+	ABEC := suite.findNestedChild(s, "B", "E", "C")
+	AC := suite.findNestedChild(s, "C")
+	suite.True(ABEC == AC, "ABEC != AC")
+	A := suite.findNestedChild(s)
+	ABEA := suite.findNestedChild(A, "B", "E", "A")
+	suite.True(A == ABEA, "A != ABEA")
+}
+
+func (suite *EntrySchemaTestSuite) TestPrune_CanPruneRoot() {
+	s := suite.readFixture("tree")
+	p := suite.makeSchemaP()
+	s = Prune(s, p)
+	suite.Nil(s)
+}
+
+func (suite *EntrySchemaTestSuite) TestPrune_Tree() {
+	s := suite.readFixture("tree")
+	p := suite.makeSchemaP("D")
+	s = Prune(s, p)
+	expected := map[string][]string{
+		"A": []string{"B"},
+		"B": []string{"D"},
+		"D": []string{},
+	}
+	suite.Equal(expected, s.toMap())
+}
+
+func (suite *EntrySchemaTestSuite) TestPrune_Complex() {
+	s := suite.readFixture("complex")
+	p := suite.makeSchemaP("D")
+	s = Prune(s, p)
+	expected := map[string][]string{
+		"A": []string{"B"},
+		"B": []string{"D", "E"},
+		"D": []string{},
+		"E": []string{"A"},
+	}
+	suite.Equal(expected, s.toMap())
+}
+
+func (suite *EntrySchemaTestSuite) TestPrune_RealWorld_AWS() {
+	s := suite.readFixture("real_world_aws")
+	p := func(s *EntrySchema) bool {
+		for _, action := range s.Actions {
+			if action == "exec" {
+				return true
+			}
+		}
+		return false
+	}
+	s = Prune(s, p)
+	expected := map[string][]string{
+		"aws.Root": []string{"aws.profile"},
+		"aws.profile": []string{"aws.resourcesDir"},
+		"aws.resourcesDir":  []string{"aws.ec2Dir"},
+		"aws.ec2Dir": []string{"aws.ec2InstancesDir"},
+		"aws.ec2InstancesDir": []string{"aws.ec2Instance"},
+		"aws.ec2Instance": []string{},
+	}
+	suite.Equal(expected, s.toMap())
+}
+
+func (suite *EntrySchemaTestSuite) TestEvaluateNodesToKeep_Tree() {
+	s := suite.readFixture("tree")
+
+	p := suite.makeSchemaP("A", "B", "C", "D", "E", "F", "G")
+	result := evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "C", "D", "E", "F", "G")
+
+	p = suite.makeSchemaP()
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, false, "A", "B", "C", "D", "E", "F", "G")
+
+	p = suite.makeSchemaP("D")
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "D")
+	suite.assertKeepResult(result, false, "E", "C", "F", "G")
+}
+
+func (suite *EntrySchemaTestSuite) TestEvaluateNodesToKeep_Triangle() {
+	s := suite.readFixture("triangle")
+
+	p := suite.makeSchemaP("A", "B", "C")
+	result := evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "C")
+
+	p = suite.makeSchemaP()
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, false, "A", "B", "C")
+
+	p = suite.makeSchemaP("B")
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "C")
+}
+
+func (suite *EntrySchemaTestSuite) TestEvaluateNodesToKeep_Complex() {
+	s := suite.readFixture("complex")
+
+	p := suite.makeSchemaP("A", "B", "C", "D", "E", "F")
+	result := evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "C", "D", "E", "F")
+
+	p = suite.makeSchemaP()
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, false, "A", "B", "C", "D", "E", "F")
+
+	p = suite.makeSchemaP("F")
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "C", "E", "F")
+	suite.assertKeepResult(result, false, "D")
+
+	p = suite.makeSchemaP("D")
+	result = evaluateNodesToKeep(s, p)
+	suite.assertKeepResult(result, true, "A", "B", "D", "E")
+	suite.assertKeepResult(result, false, "C", "F")
+}
+
+func TestEntrySchema(t *testing.T) {
+	suite.Run(t, new(EntrySchemaTestSuite))
+}
+
+// This should be called after suite.schema.FillChildren() is called
+func (suite *EntrySchemaTestSuite) findNestedChild(s *EntrySchema, segments ...string) *EntrySchema {
+	var visitedSegments []string
+	child := s
+	for _, segment := range segments {
+		visitedSegments = append(visitedSegments, segment)
+		child = child.Children[segment]
+		if child == nil {
+			suite.T().Fatal(fmt.Sprintf("Child %v does not exist", strings.Join(visitedSegments, "/")))
+		}
+	}
+	return child
+}
+
+func (suite *EntrySchemaTestSuite) readFixture(name string) *EntrySchema {
+	filePath := path.Join("testdata", name+".json")
+	rawSchema, err := ioutil.ReadFile(filePath)
+	if err != nil {
+		suite.T().Fatal(fmt.Sprintf("Failed to read %v", filePath))
+	}
+	var s *apitypes.EntrySchema
+	if err := json.Unmarshal(rawSchema, &s); err != nil {
+		suite.T().Fatal(fmt.Sprintf("Failed to unmarshal %v: %v", filePath, err))
+	}
+	return NewEntrySchema(s)
+}
+
+func (suite *EntrySchemaTestSuite) makeSchemaP(trueValues ...string) EntrySchemaPredicate {
+	return func(s *EntrySchema) bool {
+		for _, typeID := range trueValues {
+			if s.TypeID == typeID {
+				return true
+			}
+		}
+		return false
+	}
+}
+
+func (suite *EntrySchemaTestSuite) assertKeepResult(result map[string]bool, expected bool, values ...string) {
+	for _, v := range values {
+		msg := fmt.Sprintf("Expected result[%v] == %v", v, expected)
+		if expected {
+			suite.True(result[v], msg)
+		} else {
+			suite.False(result[v], msg)
+		}
+	}
+}
\ No newline at end of file
diff --git a/cmd/internal/find/types/testdata/complex.json b/cmd/internal/find/types/testdata/complex.json
new file mode 100644
index 0000000..16c0522
--- /dev/null
+++ b/cmd/internal/find/types/testdata/complex.json
@@ -0,0 +1,35 @@
+{
+    "type_id": "A",
+    "children": [
+        {
+            "type_id": "B",
+            "children": [
+                {
+                    "type_id": "D"
+                },
+                {
+                    "type_id": "E",
+                    "children": [
+                        {
+                            "type_id": "A"
+                        },
+                        {
+                            "type_id": "C",
+                            "children": [
+                                {
+                                    "type_id": "C"
+                                },
+                                {
+                                    "type_id": "F"
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        },
+        {
+            "type_id": "C"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/cmd/internal/find/types/testdata/real_world_aws.json b/cmd/internal/find/types/testdata/real_world_aws.json
new file mode 100644
index 0000000..b35bf2c
--- /dev/null
+++ b/cmd/internal/find/types/testdata/real_world_aws.json
@@ -0,0 +1,187 @@
+{
+    "type_id": "aws.Root",
+    "label": "aws",
+    "singleton": true,
+    "actions": [
+        "list"
+    ],
+    "children": [
+        {
+            "type_id": "aws.profile",
+            "label": "profile",
+            "singleton": false,
+            "actions": [
+                "list"
+            ],
+            "children": [
+                {
+                    "type_id": "aws.resourcesDir",
+                    "label": "resources",
+                    "singleton": true,
+                    "actions": [
+                        "list"
+                    ],
+                    "children": [
+                        {
+                            "type_id": "aws.s3Dir",
+                            "label": "s3",
+                            "singleton": true,
+                            "actions": [
+                                "list"
+                            ],
+                            "children": [
+                                {
+                                    "type_id": "aws.s3Bucket",
+                                    "label": "bucket",
+                                    "singleton": false,
+                                    "actions": [
+                                        "list"
+                                    ],
+                                    "children": [
+                                        {
+                                            "type_id": "aws.s3ObjectPrefix",
+                                            "label": "prefix",
+                                            "singleton": false,
+                                            "actions": [
+                                                "list"
+                                            ],
+                                            "children": [
+                                                {
+                                                    "type_id": "aws.s3ObjectPrefix",
+                                                    "label": "prefix",
+                                                    "singleton": false,
+                                                    "actions": [
+                                                        "list"
+                                                    ],
+                                                    "children": null
+                                                },
+                                                {
+                                                    "type_id": "aws.s3Object",
+                                                    "label": "object",
+                                                    "singleton": false,
+                                                    "actions": [
+                                                        "read",
+                                                        "stream"
+                                                    ],
+                                                    "children": null
+                                                }
+                                            ]
+                                        },
+                                        {
+                                            "type_id": "aws.s3Object",
+                                            "label": "object",
+                                            "singleton": false,
+                                            "actions": [
+                                                "read",
+                                                "stream"
+                                            ],
+                                            "children": null
+                                        }
+                                    ]
+                                }
+                            ]
+                        },
+                        {
+                            "type_id": "aws.ec2Dir",
+                            "label": "ec2",
+                            "singleton": true,
+                            "actions": [
+                                "list"
+                            ],
+                            "children": [
+                                {
+                                    "type_id": "aws.ec2InstancesDir",
+                                    "label": "instances",
+                                    "singleton": true,
+                                    "actions": [
+                                        "list"
+                                    ],
+                                    "children": [
+                                        {
+                                            "type_id": "aws.ec2Instance",
+                                            "label": "instance",
+                                            "singleton": false,
+                                            "actions": [
+                                                "list",
+                                                "exec"
+                                            ],
+                                            "children": [
+                                                {
+                                                    "type_id": "aws.ec2InstanceConsoleOutput",
+                                                    "label": "console.out",
+                                                    "singleton": false,
+                                                    "actions": [
+                                                        "read"
+                                                    ],
+                                                    "children": null
+                                                },
+                                                {
+                                                    "type_id": "aws.ec2InstanceMetadataJSON",
+                                                    "label": "metadata.json",
+                                                    "singleton": true,
+                                                    "actions": [
+                                                        "read"
+                                                    ],
+                                                    "children": null
+                                                },
+                                                {
+                                                    "type_id": "volume.FS",
+                                                    "label": "fs",
+                                                    "singleton": true,
+                                                    "actions": [
+                                                        "list"
+                                                    ],
+                                                    "children": [
+                                                        {
+                                                            "type_id": "volume.dir",
+                                                            "label": "dir",
+                                                            "singleton": false,
+                                                            "actions": [
+                                                                "list"
+                                                            ],
+                                                            "children": [
+                                                                {
+                                                                    "type_id": "volume.dir",
+                                                                    "label": "dir",
+                                                                    "singleton": false,
+                                                                    "actions": [
+                                                                        "list"
+                                                                    ],
+                                                                    "children": null
+                                                                },
+                                                                {
+                                                                    "type_id": "volume.file",
+                                                                    "label": "file",
+                                                                    "singleton": false,
+                                                                    "actions": [
+                                                                        "read",
+                                                                        "stream"
+                                                                    ],
+                                                                    "children": null
+                                                                }
+                                                            ]
+                                                        },
+                                                        {
+                                                            "type_id": "volume.file",
+                                                            "label": "file",
+                                                            "singleton": false,
+                                                            "actions": [
+                                                                "read",
+                                                                "stream"
+                                                            ],
+                                                            "children": null
+                                                        }
+                                                    ]
+                                                }
+                                            ]
+                                        }
+                                    ]
+                                }
+                            ]
+                        }
+                    ]
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/cmd/internal/find/types/testdata/tree.json b/cmd/internal/find/types/testdata/tree.json
new file mode 100644
index 0000000..16dde92
--- /dev/null
+++ b/cmd/internal/find/types/testdata/tree.json
@@ -0,0 +1,27 @@
+{
+    "type_id": "A",
+    "children": [
+        {
+            "type_id": "B",
+            "children": [
+                {
+                    "type_id": "D"
+                },
+                {
+                    "type_id": "E"
+                }
+            ]
+        },
+        {
+            "type_id": "C",
+            "children": [
+                {
+                    "type_id": "F"
+                },
+                {
+                    "type_id": "G"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/cmd/internal/find/types/testdata/triangle.json b/cmd/internal/find/types/testdata/triangle.json
new file mode 100644
index 0000000..46d4f3e
--- /dev/null
+++ b/cmd/internal/find/types/testdata/triangle.json
@@ -0,0 +1,18 @@
+{
+    "type_id": "A",
+    "children": [
+        {
+            "type_id": "B",
+            "children": [
+                {
+                    "type_id": "C",
+                    "children": [
+                        {
+                            "type_id": "A"
+                        }
+                    ]
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/cmd/internal/find/walker_test.go b/cmd/internal/find/walker_test.go
index 54729bf..5ba29ec 100644
--- a/cmd/internal/find/walker_test.go
+++ b/cmd/internal/find/walker_test.go
@@ -6,14 +6,14 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/stretchr/testify/suite"
-	"github.com/stretchr/testify/mock"
-	"github.com/puppetlabs/wash/api/types"
+	apitypes "github.com/puppetlabs/wash/api/types"
 	"github.com/puppetlabs/wash/cmd/internal/cmdtest"
 	"github.com/puppetlabs/wash/cmd/internal/find/parser"
 	"github.com/puppetlabs/wash/cmd/internal/find/primary"
 	"github.com/puppetlabs/wash/cmd/internal/find/types"
 	"github.com/puppetlabs/wash/plugin"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/suite"
 )
 
 type WalkerTestSuite struct {
@@ -26,9 +26,9 @@ func (s *WalkerTestSuite) SetupTest() {
 	s.walker = newWalker(
 		parser.Result{
 			Options: types.NewOptions(),
-			Predicate: func(e types.Entry) bool {
+			Predicate: types.ToEntryP(func(e types.Entry) bool {
 				return true
-			},
+			}),
 		},
 		s.Suite.Client,
 	).(*walkerImpl)
@@ -40,15 +40,23 @@ func (s *WalkerTestSuite) TearDownTest() {
 	primary.Parser.SetPrimaries = make(map[*primary.Primary]bool)
 }
 
-func (s *WalkerTestSuite) TestWalk_InfoErrors()() {
+func (s *WalkerTestSuite) TestWalk_InfoErrors() {
 	err := fmt.Errorf("failed to get the info")
 	s.Client.On("Info", ".").Return(apitypes.Entry{}, err)
 	s.False(s.walker.Walk("."))
 	s.Regexp(err.Error(), s.Stderr())
 }
 
+func (s *WalkerTestSuite) TestWalk_SchemaErrors() {
+	s.Client.On("Info", ".").Return(apitypes.Entry{}, nil)
+	err := fmt.Errorf("failed to get the schema")
+	s.Client.On("Schema", ".").Return((*apitypes.EntrySchema)(nil), err)
+	s.False(s.walker.Walk("."))
+	s.Regexp(err.Error(), s.Stderr())
+}
+
 func (s *WalkerTestSuite) TestWalk_HappyCase() {
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	s.True(s.walker.Walk("."))
 	s.assertPrintedTree(
 		".",
@@ -60,8 +68,60 @@ func (s *WalkerTestSuite) TestWalk_HappyCase() {
 	)
 }
 
+func (s *WalkerTestSuite) TestWalk_WithSchema_HappyCase() {
+	// Set-up the mocks
+	fileSchema := func(typeID string) *apitypes.EntrySchema {
+		return &apitypes.EntrySchema{
+			TypeID: typeID,
+		}
+	}
+	dirSchema := func(typeID string, children ...*apitypes.EntrySchema) *apitypes.EntrySchema {
+		return &apitypes.EntrySchema{
+			TypeID: typeID,
+			Children: children,
+		}
+	}
+	schema := dirSchema(
+		".",
+		dirSchema("foo", fileSchema("file")),
+		dirSchema("bar", fileSchema("no_file")),
+		dirSchema("baz", fileSchema("file")),
+	)
+	s.setupMocksForWalk(schema, map[string][]apitypes.Entry{
+		".": []apitypes.Entry{
+			s.toEntry("./foo", true, "foo"),
+			s.toEntry("./bar", true, "bar"),
+			s.toEntry("./baz", true, "baz"),
+		},
+		"./foo": []apitypes.Entry{
+			s.toEntry("./foo/1", true, "file"),
+		},
+		"./bar": []apitypes.Entry{
+			s.toEntry("./bar/1", false, "no_file"),
+			s.toEntry("./bar/2", false, "no_file"),
+		},
+		"./baz": []apitypes.Entry{
+			s.toEntry("./baz/1", false, "file"),
+			s.toEntry("./baz/2", false, "file"),
+		},
+	})
+
+	s.walker.p.SchemaP = func(s *types.EntrySchema) bool {
+		// Print only "foo" or "file"s. Ignore everything else.
+		return s.TypeID == "foo" || s.TypeID == "file"
+	}
+
+	s.True(s.walker.Walk("."))
+	s.assertPrintedTree(
+		"./foo",
+		"./foo/1",
+		"./baz/1",
+		"./baz/2",
+	)
+}
+
 func (s *WalkerTestSuite) TestWalk_MaxdepthSet() {
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	s.walker.opts.Maxdepth = 2
 	s.walker.opts.MarkAsSet(types.MaxdepthFlag)
 	s.True(s.walker.Walk("."))
@@ -74,7 +134,7 @@ func (s *WalkerTestSuite) TestWalk_MaxdepthSet() {
 }
 
 func (s *WalkerTestSuite) TestWalk_MaxdepthAndMindepthSet() {
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	s.walker.opts.Mindepth = 1
 	s.walker.opts.Maxdepth = 2
 	s.True(s.walker.Walk("."))
@@ -86,7 +146,7 @@ func (s *WalkerTestSuite) TestWalk_MaxdepthAndMindepthSet() {
 }
 
 func (s *WalkerTestSuite) TestWalk_DepthSet() {
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	s.walker.opts.Depth = true
 	s.True(s.walker.Walk("."))
 	s.assertPrintedTree(
@@ -100,7 +160,7 @@ func (s *WalkerTestSuite) TestWalk_DepthSet() {
 }
 
 func (s *WalkerTestSuite) TestWalk_ListErrors() {
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	err := fmt.Errorf("failed to list")
 	s.mockList("./foo", true, nil, err)
 	s.False(s.walker.Walk("."))
@@ -108,11 +168,11 @@ func (s *WalkerTestSuite) TestWalk_ListErrors() {
 		".",
 		"./foo",
 	)
-	s.Regexp("children.*./foo.*" + err.Error(), s.Stderr())
+	s.Regexp("children.*./foo.*"+err.Error(), s.Stderr())
 }
 
 func (s *WalkerTestSuite) TestWalk_VisitErrors() {
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	s.walker.opts.Fullmeta = true
 	primary.Parser.SetPrimaries[primary.Meta] = true
 
@@ -121,11 +181,11 @@ func (s *WalkerTestSuite) TestWalk_VisitErrors() {
 
 	s.False(s.walker.Walk("."))
 	s.assertPrintedTree()
-	
+
 	// Also test the behavior when depth is set since visit is called
 	// on a different code-path
 	s.walker.opts.Depth = true
-	s.setupMocksForWalk()
+	s.setupDefaultMocksForWalk()
 	s.False(s.walker.Walk("."))
 	s.assertPrintedTree()
 }
@@ -137,6 +197,23 @@ func (s *WalkerTestSuite) TestVisit_MindepthSet() {
 	s.assertNotPrintedEntry(e)
 }
 
+func (s *WalkerTestSuite) TestVisit_NilSchema_DoesNotVisit() {
+	e := newMockEntryForVisit()
+	e.SetSchema(nil)
+	s.walker.visit(e, 0)
+	s.assertNotPrintedEntry(e)
+}
+
+func (s *WalkerTestSuite) TestVisit_UnsatisfyingSchema_DoesNotVisit() {
+	e := newMockEntryForVisit()
+	e.SetSchema(&types.EntrySchema{})
+	s.walker.p.SchemaP = func(_ *types.EntrySchema) bool {
+		return false
+	}
+	s.walker.visit(e, 0)
+	s.assertNotPrintedEntry(e)
+}
+
 func (s *WalkerTestSuite) TestVisit_FullmetaSet_MetaPrimaryUnset_DoesNotFetchFullMetadata() {
 	s.walker.opts.Fullmeta = true
 	e := newMockEntryForVisit()
@@ -166,9 +243,9 @@ func (s *WalkerTestSuite) TestVisit_FullmetaSet_MetaPrimarySet_FetchesFullMetada
 	primary.Parser.SetPrimaries[primary.Meta] = true
 
 	fullMeta := plugin.JSONObject{"foo": "bar"}
-	s.walker.p = func(entry types.Entry) bool {
+	s.walker.p = types.ToEntryP(func(entry types.Entry) bool {
 		return s.Equal(fullMeta, entry.Metadata)
-	}
+	})
 
 	e := newMockEntryForVisit()
 	s.Client.On("Metadata", e.Path).Return(fullMeta, nil).Once()
@@ -180,6 +257,16 @@ func (s *WalkerTestSuite) TestVisit_FullmetaSet_MetaPrimarySet_FetchesFullMetada
 	s.assertPrintedEntry(e)
 }
 
+func (s *WalkerTestSuite) TestVisit_FullmetaSet_MetaPrimarySet_UnsatisfyingSchema_DoesNotFetchFullMetadata() {
+	// This test is a sanity check since O(N) metadata requests can be expensive
+	s.walker.opts.Fullmeta = true
+	primary.Parser.SetPrimaries[primary.Meta] = true
+	e := newMockEntryForVisit()
+	e.SetSchema(nil)
+	s.walker.visit(e, 0)
+	s.Client.AssertNotCalled(s.T(), "Metadata")
+}
+
 func (s *WalkerTestSuite) TestVisit_PrintsSatisfyingEntry() {
 	e := newMockEntryForVisit()
 	s.True(s.walker.visit(e, 0))
@@ -187,41 +274,33 @@ func (s *WalkerTestSuite) TestVisit_PrintsSatisfyingEntry() {
 }
 
 func (s *WalkerTestSuite) TestVisit_DoesNotPrintUnsatisfyingEntry() {
-	s.walker.p = func(e types.Entry) bool {
+	s.walker.p = types.ToEntryP(func(e types.Entry) bool {
 		return false
-	}
+	})
 	e := newMockEntryForVisit()
 	s.True(s.walker.visit(e, 0))
 	s.assertNotPrintedEntry(e)
 }
 
-func (s *WalkerTestSuite) setupMocksForWalk() {
-	toEntry := func(path string, isParent bool) apitypes.Entry {
-		e := apitypes.Entry{
-			Path: s.toAbsPath(path),
-			CName: filepath.Base(path),
-		}
-		if isParent {
-			e.Actions = []string{"list"}
-		}
-		return e
-	}
-
-	// Mock-out "Info" for the root
-	s.Client.On("Info", ".").Return(toEntry(".", true), nil).Once()
-
-	// Mock out "List" for each entry in the tree
-	tree := map[string][]apitypes.Entry{
-		".": []apitypes.Entry{toEntry("./foo", true)},
+func (s *WalkerTestSuite) setupDefaultMocksForWalk() {
+	s.setupMocksForWalk(nil, map[string][]apitypes.Entry{
+		".": []apitypes.Entry{s.toEntry("./foo", true, "")},
 		"./foo": []apitypes.Entry{
-			toEntry("./foo/bar", true),
-			toEntry("./foo/baz", false),
+			s.toEntry("./foo/bar", true, ""),
+			s.toEntry("./foo/baz", false, ""),
 		},
 		"./foo/bar": []apitypes.Entry{
-			toEntry("./foo/bar/1", false),
-			toEntry("./foo/bar/2", false),
+			s.toEntry("./foo/bar/1", false, ""),
+			s.toEntry("./foo/bar/2", false, ""),
 		},
-	}
+	})
+}
+
+func (s *WalkerTestSuite) setupMocksForWalk(schema *apitypes.EntrySchema, tree map[string][]apitypes.Entry) {
+	// Mock-out "Info" + "Schema" for the root
+	s.Client.On("Info", ".").Return(s.toEntry(".", true, "."), nil).Once()
+	s.Client.On("Schema", ".").Return(schema, nil).Once()
+	// Mock out "List" for each entry in the tree
 	for dir, children := range tree {
 		s.mockList(dir, false, children, nil)
 	}
@@ -245,6 +324,18 @@ func (s *WalkerTestSuite) toAbsPath(path string) string {
 	return strings.TrimPrefix(path, ".")
 }
 
+func (s *WalkerTestSuite) toEntry(path string, isParent bool, typeID string) apitypes.Entry {
+	e := apitypes.Entry{
+		Path:  s.toAbsPath(path),
+		CName: filepath.Base(path),
+		TypeID: typeID,
+	}
+	if isParent {
+		e.Actions = []string{"list"}
+	}
+	return e
+}
+
 func (s *WalkerTestSuite) assertPrintedEntry(e types.Entry) {
 	s.Regexp(e.NormalizedPath, s.Stdout())
 }
@@ -273,4 +364,3 @@ func TestWalker(t *testing.T) {
 	s.Suite = new(cmdtest.Suite)
 	suite.Run(t, s)
 }
-
diff --git a/plugin/cache_test.go b/plugin/cache_test.go
index 0d3ace5..9d7a0e5 100644
--- a/plugin/cache_test.go
+++ b/plugin/cache_test.go
@@ -134,7 +134,7 @@ func (e *cacheTestsMockEntry) List(ctx context.Context) ([]Entry, error) {
 	return args.Get(0).([]Entry), args.Error(1)
 }
 
-func (e *cacheTestsMockEntry) ChildSchemas() []EntrySchema {
+func (e *cacheTestsMockEntry) ChildSchemas() []*EntrySchema {
 	return nil
 }
 
diff --git a/plugin/registry_test.go b/plugin/registry_test.go
index b2e8507..e1fcad6 100644
--- a/plugin/registry_test.go
+++ b/plugin/registry_test.go
@@ -33,10 +33,10 @@ func (m *mockRoot) List(ctx context.Context) ([]Entry, error) {
 	return args.Get(0).([]Entry), args.Error(1)
 }
 
-func (m *mockRoot) ChildSchemas() []EntrySchema {
-	return []EntrySchema{
-		EntrySchema{
-			Type: "entry",
+func (m *mockRoot) ChildSchemas() []*EntrySchema {
+	return []*EntrySchema{
+		&EntrySchema{
+			TypeID: "entry",
 		},
 	}
 }