Repository: mvdan/sh
Pull request #647 (2020-12-27 11:25:46)

interp: implement `type -t`

zimbatm: That's really up to you if you want to squash, rebase or merge. The self-contained commits that I created are there to make the review easier.


Patch:
diff --git a/interp/builtin.go b/interp/builtin.go
index d9fc59cc..cbd3dc09 100644
--- a/interp/builtin.go
+++ b/interp/builtin.go
@@ -218,7 +218,10 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
 		fp := flagParser{remaining: args}
 		for fp.more() {
 			switch flag := fp.flag(); flag {
-			case "-p":
+			case "-a", "-f", "-P", "--help":
+				r.errf("command: NOT IMPLEMENTED\n")
+				return 3
+			case "-p", "-t":
 				mode = flag
 			default:
 				r.errf("command: invalid option %q\n", flag)
@@ -235,6 +238,14 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
 				}
 				continue
 			}
+			if syntax.IsKeyword(arg) {
+				if mode == "-t" {
+					r.out("keyword\n")
+				} else {
+					r.outf("%s is a shell keyword\n", arg)
+				}
+				continue
+			}
 			if als, ok := r.alias[arg]; ok && r.opts[optExpandAliases] {
 				var buf bytes.Buffer
 				if len(als.args) > 0 {
@@ -246,22 +257,40 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
 				if als.blank {
 					buf.WriteByte(' ')
 				}
-				r.outf("%s is aliased to `%s'\n", arg, &buf)
+				if mode == "-t" {
+					r.out("alias\n")
+				} else {
+					r.outf("%s is aliased to `%s'\n", arg, &buf)
+				}
 				continue
 			}
 			if _, ok := r.Funcs[arg]; ok {
-				r.outf("%s is a function\n", arg)
+				if mode == "-t" {
+					r.out("function\n")
+				} else {
+					r.outf("%s is a function\n", arg)
+				}
 				continue
 			}
 			if isBuiltin(arg) {
-				r.outf("%s is a shell builtin\n", arg)
+				if mode == "-t" {
+					r.out("builtin\n")
+				} else {
+					r.outf("%s is a shell builtin\n", arg)
+				}
 				continue
 			}
 			if path, err := LookPathDir(r.Dir, expandEnv{r}, arg); err == nil {
-				r.outf("%s is %s\n", arg, path)
+				if mode == "-t" {
+					r.out("file\n")
+				} else {
+					r.outf("%s is %s\n", arg, path)
+				}
 				continue
 			}
-			r.errf("type: %s: not found\n", arg)
+			if mode != "-t" {
+				r.errf("type: %s: not found\n", arg)
+			}
 			anyNotFound = true
 		}
 		if anyNotFound {
diff --git a/syntax/parser.go b/syntax/parser.go
index e0e50605..f02cf421 100644
--- a/syntax/parser.go
+++ b/syntax/parser.go
@@ -753,6 +753,37 @@ func IsIncomplete(err error) bool {
 	return ok && perr.Incomplete
 }
 
+// IsKeyword returns true if the given word is part of the language keywords.
+func IsKeyword(word string) bool {
+	// This list has been copied from the bash 5.1 source code, file y.tab.c +4460
+	switch word {
+	case
+		"!",
+		"[[", // only if COND_COMMAND is defined
+		"]]", // only if COND_COMMAND is defined
+		"case",
+		"coproc", // only if COPROCESS_SUPPORT is defined
+		"do",
+		"done",
+		"else",
+		"esac",
+		"fi",
+		"for",
+		"function",
+		"if",
+		"in",
+		"select", // only if SELECT_COMMAND is defined
+		"then",
+		"time", // only if COMMAND_TIMING is defined
+		"until",
+		"while",
+		"{",
+		"}":
+		return true
+	}
+	return false
+}
+
 // ParseError represents an error found when parsing a source file, from which
 // the parser cannot recover.
 type ParseError struct {


Test patch:
diff --git a/interp/interp_test.go b/interp/interp_test.go
index 28ca5490..ca66be3d 100644
--- a/interp/interp_test.go
+++ b/interp/interp_test.go
@@ -1909,6 +1909,7 @@ set +o pipefail
 
 	// type
 	{"type", ""},
+	{"type for", "for is a shell keyword\n"},
 	{"type echo", "echo is a shell builtin\n"},
 	{"echo() { :; }; type echo | grep 'is a function'", "echo is a function\n"},
 	{"type $PATH_PROG | grep -q -E ' is (/|[A-Z]:)'", ""},
@@ -1918,6 +1919,12 @@ set +o pipefail
 	{"alias foo='bar baz'\ntype foo", "type: foo: not found\nexit status 1 #JUSTERR"},
 	{"type -p $PATH_PROG | grep -q -E '^(/|[A-Z]:)'", ""},
 	{"PATH=/; type -p $PATH_PROG", "exit status 1"},
+	{"shopt -s expand_aliases; alias foo='bar'; type -t foo", "alias\n"},
+	{"type -t case", "keyword\n"},
+	{"foo(){ :; }; type -t foo", "function\n"},
+	{"type -t type", "builtin\n"},
+	{"type -t $PATH_PROG", "file\n"},
+	{"type -t inexisting_dfgsdgfds", "exit status 1"},
 
 	// eval
 	{"eval", ""},