Internal improvements to linter and util packages

* linter.Result now includes a map field, storing pointers to all schemas
  that were introspected during linting. The schemas are keyed by dir path.

* util.ShellOut now includes a Dir field, allowing callers to specify the
  initial working directory of the process.

* util.NewShellOut() has been removed, as it was unused and would not support
  adding additional fields to the ShellOut struct. Callers should create
  &ShellOut{} values directly with the desired fields.
This commit is contained in:
Evan Elias 2019-04-17 17:18:32 -04:00
Родитель 5ed5daf409
Коммит 705fb734c9
4 изменённых файлов: 65 добавлений и 42 удалений

Просмотреть файл

@ -40,6 +40,7 @@ type Result struct {
FormatNotices []*Annotation
DebugLogs []string
Exceptions []error
Schemas map[string]*tengo.Schema // Keyed by dir path and optionally schema name
}
// Merge combines other into r's value in-place.
@ -52,6 +53,12 @@ func (r *Result) Merge(other *Result) {
r.FormatNotices = append(r.FormatNotices, other.FormatNotices...)
r.DebugLogs = append(r.DebugLogs, other.DebugLogs...)
r.Exceptions = append(r.Exceptions, other.Exceptions...)
if r.Schemas == nil {
r.Schemas = make(map[string]*tengo.Schema)
}
for key, value := range other.Schemas {
r.Schemas[key] = value
}
}
// BadConfigResult returns a *Result containing a single ConfigError in the
@ -91,7 +98,14 @@ func LintDir(dir *fs.Dir, wsOpts workspace.Options) *Result {
return result
}
}
_, res := ExecLogicalSchema(logicalSchema, wsOpts, opts)
schema, res := ExecLogicalSchema(logicalSchema, wsOpts, opts)
if schema != nil {
schemaKey := dir.Path
if logicalSchema.Name != "" {
schemaKey = fmt.Sprintf("%s:%s", schemaKey, logicalSchema.Name)
}
res.Schemas = map[string]*tengo.Schema{schemaKey: schema}
}
result.Merge(res)
}
@ -112,7 +126,8 @@ func LintDir(dir *fs.Dir, wsOpts workspace.Options) *Result {
// ExecLogicalSchema is a wrapper around workspace.ExecLogicalSchema. After the
// tengo.Schema is obtained and introspected, it is also linted. Any errors
// are captured as part of the *Result.
// are captured as part of the *Result. However, the schema itself is not yet
// placed into the *Result; this is the caller's responsibility.
func ExecLogicalSchema(logicalSchema *fs.LogicalSchema, wsOpts workspace.Options, opts Options) (*tengo.Schema, *Result) {
result := &Result{}

Просмотреть файл

@ -118,6 +118,17 @@ func TestLintDir(t *testing.T) {
if len(result.DebugLogs) != 1 {
t.Errorf("Expected 1 debug log, instead found %d", len(result.DebugLogs))
}
// One *tengo.Schema expected in map, with key corresponding to dir
if len(result.Schemas) != 1 {
t.Errorf("Expected 1 schema in Schemas map, instead found %d", len(result.Schemas))
} else {
for key := range result.Schemas {
if key != dir.Path {
t.Errorf("Expected schema to have key %s, instead found %s", dir.Path, key)
}
}
}
}
func TestLintDirIgnoreSchema(t *testing.T) {

Просмотреть файл

@ -9,17 +9,11 @@ import (
"strings"
)
// varPlaceholder is a regexp for detecting placeholders in format "{VARNAME}"
var varPlaceholder = regexp.MustCompile(`{([^}]*)}`)
// noQuotesNeeded is a regexp for detecting which variable values do not require
// escaping and quote-wrapping
var noQuotesNeeded = regexp.MustCompile(`^[\w/@%=:.,+-]*$`)
// ShellOut represents a command-line for an external command, executed via sh -c
type ShellOut struct {
Command string
PrintableCommand string // Same as Command, but used in String() if non-empty; useful for hiding passwords in output
PrintableCommand string // Used in String() if non-empty; useful for hiding passwords in output
Dir string // Initial working dir for the command if non-empty
}
func (s *ShellOut) String() string {
@ -37,6 +31,7 @@ func (s *ShellOut) Run() error {
return errors.New("Attempted to shell out to an empty command string")
}
cmd := exec.Command("/bin/sh", "-c", s.Command)
cmd.Dir = s.Dir
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@ -51,6 +46,7 @@ func (s *ShellOut) RunCapture() (string, error) {
return "", errors.New("Attempted to shell out to an empty command string")
}
cmd := exec.Command("/bin/sh", "-c", s.Command)
cmd.Dir = s.Dir
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
out, err := cmd.Output()
@ -91,14 +87,9 @@ func (s *ShellOut) RunCaptureSplit() ([]string, error) {
return result, err
}
// NewShellOut takes a shell command-line string and returns a ShellOut, without
// performing any variable interpolation.
func NewShellOut(command, printableCommand string) *ShellOut {
return &ShellOut{
Command: command,
PrintableCommand: printableCommand,
}
}
// varPlaceholder is a regexp for detecting placeholders of format "{VARNAME}"
// in NewInterpolatedShellOut()
var varPlaceholder = regexp.MustCompile(`{([^}]*)}`)
// NewInterpolatedShellOut takes a shell command-line containing variables of
// format {VARNAME}, and performs substitution on them based on the supplied
@ -115,33 +106,37 @@ func NewShellOut(command, printableCommand string) *ShellOut {
// string contains "{PASSWORDX}" and variables has a key "PASSWORD", it will be
// replaced in a manner that obfuscates the actual password in PrintableCommand.
func NewInterpolatedShellOut(command string, variables map[string]string) (*ShellOut, error) {
var err error
var forDisplay bool
var forDisplay bool // affects behavior of replacer closure
var err error // may be mutated by replacer closure
replacer := func(input string) string {
input = strings.ToUpper(input[1 : len(input)-1])
value, ok := variables[input]
if !ok && input[len(input)-1] == 'X' {
value, ok = variables[input[:len(input)-1]]
varName := strings.ToUpper(input[1 : len(input)-1])
value, ok := variables[varName]
if !ok && varName[len(varName)-1] == 'X' {
value, ok = variables[varName[:len(varName)-1]]
if ok && forDisplay {
return "XXXXX"
}
}
if ok {
return escapeVarValue(value)
if !ok {
err = fmt.Errorf("Unknown variable %s", input)
return input
}
err = fmt.Errorf("Unknown variable {%s}", input)
return fmt.Sprintf("{%s}", input)
return escapeVarValue(value)
}
result := varPlaceholder.ReplaceAllStringFunc(command, replacer)
s := &ShellOut{}
s.Command = varPlaceholder.ReplaceAllStringFunc(command, replacer)
if strings.Contains(strings.ToUpper(command), "X}") {
forDisplay = true
resultForDisplay := varPlaceholder.ReplaceAllStringFunc(command, replacer)
return NewShellOut(result, resultForDisplay), err
s.PrintableCommand = varPlaceholder.ReplaceAllStringFunc(command, replacer)
}
return NewShellOut(result, ""), err
return s, err
}
// noQuotesNeeded is a regexp for detecting which variable values do not require
// escaping and quote-wrapping in escapeVarValue()
var noQuotesNeeded = regexp.MustCompile(`^[\w/@%=:.,+-]*$`)
// escapeVarValue takes a string, and wraps it in single-quotes so that it will
// be interpretted as a single arg in a shell-out command line. If the value
// already contained any single-quotes, they will be escaped in a way that will

Просмотреть файл

@ -6,24 +6,26 @@ import (
)
func TestShellOutRun(t *testing.T) {
assertResult := func(command string, expectSuccess bool) {
assertResult := func(command, dirPath string, expectSuccess bool) {
t.Helper()
s := NewShellOut(command, "")
s := &ShellOut{Command: command, Dir: dirPath}
if err := s.Run(); expectSuccess && err != nil {
t.Errorf("Expected command `%s` to return no error, but it returned error %s", command, err)
} else if !expectSuccess && err == nil {
t.Errorf("Expected command `%s` to return an error, but it did not", command)
}
}
assertResult("", false)
assertResult("false", false)
assertResult("/does/not/exist", false)
assertResult("true", true)
assertResult("", "", false)
assertResult("false", "", false)
assertResult("/does/not/exist", "", false)
assertResult("true", "", true)
assertResult("true", "..", true)
assertResult("true", "/invalid/dir", false)
}
func TestRunCaptureSplit(t *testing.T) {
assertResult := func(command string, expectedTokens ...string) {
s := NewShellOut(command, "")
s := &ShellOut{Command: command}
result, err := s.RunCaptureSplit()
if err != nil {
t.Logf("Unexpected error return from %#v: %s", s, err)
@ -42,11 +44,11 @@ func TestRunCaptureSplit(t *testing.T) {
assertResult(`/usr/bin/printf 'intentionally "no support" for quotes'`, "intentionally", `"no`, `support"`, "for", "quotes")
// Test error responses
s := NewShellOut("", "")
s := &ShellOut{}
if _, err := s.RunCaptureSplit(); err == nil {
t.Error("Expected empty shellout to error, but it did not")
}
s = NewShellOut("false", "")
s = &ShellOut{Command: "false"}
if _, err := s.RunCaptureSplit(); err == nil {
t.Error("Expected non-zero exit code from shellout to error, but it did not")
}
@ -95,7 +97,7 @@ func TestNewInterpolatedShellOut(t *testing.T) {
t.Errorf("Unexpected result from NewInterpolatedShellOut when an invalid variable was present: %+v", s)
}
}
assertShellOutError("/bin/echo {HOST} {iNvAlId} {SCHEMA}", "/bin/echo ahost {INVALID} aschema")
assertShellOutError("/bin/echo {HOST} {iNvAlId} {SCHEMA}", "/bin/echo ahost {iNvAlId} aschema")
assertShellOutError("/bin/echo {HOST} {INVALIDX} {SCHEMA}", "/bin/echo ahost {INVALIDX} aschema")
assertShellOutError("/bin/echo {HOST} {X} {SCHEMA}", "/bin/echo ahost {X} aschema")
}