mirror of
https://github.com/google/go-jsonnet.git
synced 2025-08-07 23:07:14 +02:00
Feature-complete commandline interface (#138)
* Feature-complete commandline interface * Make errors match cpp implementation
This commit is contained in:
parent
b6ee2c2f51
commit
c60056c75f
112
interpreter.go
112
interpreter.go
@ -740,10 +740,55 @@ func (i *interpreter) manifestString(buf *bytes.Buffer, trace *TraceElement, v v
|
||||
buf.WriteString(v.getString())
|
||||
return nil
|
||||
default:
|
||||
return makeRuntimeError(fmt.Sprint("Expected string result, got: " + v.getType().name), i.getCurrentStackTrace(trace))
|
||||
return makeRuntimeError(fmt.Sprintf("Expected string result, got: %s", v.getType().name), i.getCurrentStackTrace(trace))
|
||||
}
|
||||
}
|
||||
|
||||
func (i *interpreter) manifestAndSerializeMulti(trace *TraceElement, v value) (r map[string]string, err error) {
|
||||
r = make(map[string]string)
|
||||
json, err := i.manifestJSON(trace, v)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
switch json := json.(type) {
|
||||
case map[string]interface{}:
|
||||
for filename, fileJson := range json {
|
||||
var buf bytes.Buffer
|
||||
serializeJSON(fileJson, true, "", &buf)
|
||||
buf.WriteString("\n")
|
||||
r[filename] = buf.String()
|
||||
}
|
||||
default:
|
||||
msg := fmt.Sprintf("Multi mode: Top-level object was a %s, "+
|
||||
"should be an object whose keys are filenames and values hold "+
|
||||
"the JSON for that file.", v.getType().name)
|
||||
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *interpreter) manifestAndSerializeYAMLStream(trace *TraceElement, v value) (r []string, err error) {
|
||||
r = make([]string, 0)
|
||||
json, err := i.manifestJSON(trace, v)
|
||||
if err != nil {
|
||||
return r, err
|
||||
}
|
||||
switch json := json.(type) {
|
||||
case []interface{}:
|
||||
for _, doc := range json {
|
||||
var buf bytes.Buffer
|
||||
serializeJSON(doc, true, "", &buf)
|
||||
buf.WriteString("\n")
|
||||
r = append(r, buf.String())
|
||||
}
|
||||
default:
|
||||
msg := fmt.Sprintf("Stream mode: Top-level object was a %s, "+
|
||||
"should be an array whose elements hold "+
|
||||
"the JSON for each document in the stream.", v.getType().name)
|
||||
return r, makeRuntimeError(msg, i.getCurrentStackTrace(trace))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func jsonToValue(e *evaluator, v interface{}) (value, error) {
|
||||
switch v := v.(type) {
|
||||
@ -890,13 +935,7 @@ func makeInitialEnv(filename string, baseStd valueObject) environment {
|
||||
)
|
||||
}
|
||||
|
||||
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
||||
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
||||
maxStack int, importer Importer, stringOutput bool) (string, error) {
|
||||
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
func evaluateAux(i *interpreter, node ast.Node, tla vmExtMap) (value, *TraceElement, error) {
|
||||
evalLoc := ast.MakeLocationRangeMessage("During evaluation")
|
||||
evalTrace := &TraceElement{
|
||||
loc: &evalLoc,
|
||||
@ -904,7 +943,7 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
|
||||
env := makeInitialEnv(node.Loc().FileName, i.baseStd)
|
||||
result, err := i.EvalInCleanEnv(evalTrace, &env, node, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(tla) != 0 {
|
||||
// If it's not a function, ignore TLA
|
||||
@ -914,13 +953,13 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
|
||||
for argName, pv := range toplevelArgMap {
|
||||
args.named = append(args.named, namedCallArgument{name: ast.Identifier(argName), pv: pv})
|
||||
}
|
||||
funcLoc := ast.MakeLocationRangeMessage("Top-level-function")
|
||||
funcLoc := ast.MakeLocationRangeMessage("Top-level function")
|
||||
funcTrace := &TraceElement{
|
||||
loc: &funcLoc,
|
||||
}
|
||||
result, err = f.call(args).getValue(i, funcTrace)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -928,6 +967,23 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
|
||||
manifestationTrace := &TraceElement{
|
||||
loc: &manifestationLoc,
|
||||
}
|
||||
return result, manifestationTrace, nil
|
||||
}
|
||||
|
||||
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
||||
func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
||||
maxStack int, importer Importer, stringOutput bool) (string, error) {
|
||||
|
||||
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result, manifestationTrace, err := evaluateAux(i, node, tla)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if stringOutput {
|
||||
err = i.manifestString(&buf, manifestationTrace, result)
|
||||
@ -940,3 +996,37 @@ func evaluate(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]
|
||||
buf.WriteString("\n")
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
||||
func evaluateMulti(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
||||
maxStack int, importer Importer, stringOutput bool) (map[string]string, error) {
|
||||
|
||||
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, manifestationTrace, err := evaluateAux(i, node, tla)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.manifestAndSerializeMulti(manifestationTrace, result)
|
||||
}
|
||||
|
||||
// TODO(sbarzowski) this function takes far too many arguments - build interpreter in vm instead
|
||||
func evaluateStream(node ast.Node, ext vmExtMap, tla vmExtMap, nativeFuncs map[string]*NativeFunction,
|
||||
maxStack int, importer Importer) ([]string, error) {
|
||||
|
||||
i, err := buildInterpreter(ext, nativeFuncs, maxStack, importer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, manifestationTrace, err := evaluateAux(i, node, tla)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.manifestAndSerializeYAMLStream(manifestationTrace, result)
|
||||
}
|
||||
|
567
jsonnet/cmd.go
567
jsonnet/cmd.go
@ -18,20 +18,145 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-jsonnet"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Println("usage: jsonnet <filename>")
|
||||
func nextArg(i *int, args []string) string {
|
||||
(*i)++
|
||||
if (*i) >= len(args) {
|
||||
fmt.Fprintln(os.Stderr, "Expected another commandline argument.")
|
||||
os.Exit(1)
|
||||
}
|
||||
return args[*i]
|
||||
}
|
||||
|
||||
func getVar(s string) (string, string, error) {
|
||||
// simplifyArgs transforms an array of commandline arguments so that
|
||||
// any -abc arg before the first -- (if any) are expanded into
|
||||
// -a -b -c.
|
||||
func simplifyArgs(args []string) (r []string) {
|
||||
r = make([]string, 0, len(args)*2)
|
||||
for i, arg := range args {
|
||||
if arg == "--" {
|
||||
for j := i; j < len(args); j++ {
|
||||
r = append(r, args[j])
|
||||
}
|
||||
break
|
||||
}
|
||||
if len(arg) > 2 && arg[0] == '-' && arg[1] != '-' {
|
||||
for j := 1; j < len(arg); j++ {
|
||||
r = append(r, "-"+string(arg[j]))
|
||||
}
|
||||
} else {
|
||||
r = append(r, arg)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func version(o io.Writer) {
|
||||
fmt.Fprintf(o, "Jsonnet commandline interpreter %s\n", jsonnet.Version())
|
||||
}
|
||||
|
||||
func usage(o io.Writer) {
|
||||
version(o)
|
||||
fmt.Fprintln(o)
|
||||
fmt.Fprintln(o, "General commandline:")
|
||||
fmt.Fprintln(o, "jsonnet [<cmd>] {<option>} { <filename> }")
|
||||
fmt.Fprintln(o, "Note: <cmd> defaults to \"eval\"")
|
||||
fmt.Fprintln(o)
|
||||
fmt.Fprintln(o, "The eval command:")
|
||||
fmt.Fprintln(o, "jsonnet eval {<option>} <filename>")
|
||||
fmt.Fprintln(o, "Note: Only one filename is supported")
|
||||
fmt.Fprintln(o)
|
||||
fmt.Fprintln(o, "Available eval options:")
|
||||
fmt.Fprintln(o, " -h / --help This message")
|
||||
fmt.Fprintln(o, " -e / --exec Treat filename as code")
|
||||
fmt.Fprintln(o, " -J / --jpath <dir> Specify an additional library search dir")
|
||||
fmt.Fprintln(o, " -o / --output-file <file> Write to the output file rather than stdout")
|
||||
fmt.Fprintln(o, " -m / --multi <dir> Write multiple files to the directory, list files on stdout")
|
||||
fmt.Fprintln(o, " -y / --yaml-stream Write output as a YAML stream of JSON documents")
|
||||
fmt.Fprintln(o, " -S / --string Expect a string, manifest as plain text")
|
||||
fmt.Fprintln(o, " -s / --max-stack <n> Number of allowed stack frames")
|
||||
fmt.Fprintln(o, " -t / --max-trace <n> Max length of stack trace before cropping")
|
||||
fmt.Fprintln(o, " --version Print version")
|
||||
fmt.Fprintln(o, "Available options for specifying values of 'external' variables:")
|
||||
fmt.Fprintln(o, "Provide the value as a string:")
|
||||
fmt.Fprintln(o, " -V / --ext-str <var>[=<val>] If <val> is omitted, get from environment var <var>")
|
||||
fmt.Fprintln(o, " --ext-str-file <var>=<file> Read the string from the file")
|
||||
fmt.Fprintln(o, "Provide a value as Jsonnet code:")
|
||||
fmt.Fprintln(o, " --ext-code <var>[=<code>] If <code> is omitted, get from environment var <var>")
|
||||
fmt.Fprintln(o, " --ext-code-file <var>=<file> Read the code from the file")
|
||||
fmt.Fprintln(o, "Available options for specifying values of 'top-level arguments':")
|
||||
fmt.Fprintln(o, "Provide the value as a string:")
|
||||
fmt.Fprintln(o, " -A / --tla-str <var>[=<val>] If <val> is omitted, get from environment var <var>")
|
||||
fmt.Fprintln(o, " --tla-str-file <var>=<file> Read the string from the file")
|
||||
fmt.Fprintln(o, "Provide a value as Jsonnet code:")
|
||||
fmt.Fprintln(o, " --tla-code <var>[=<code>] If <code> is omitted, get from environment var <var>")
|
||||
fmt.Fprintln(o, " --tla-code-file <var>=<file> Read the code from the file")
|
||||
fmt.Fprintln(o)
|
||||
fmt.Fprintln(o, "The fmt command:")
|
||||
fmt.Fprintln(o, "jsonnet fmt is currently not available in the Go implementation")
|
||||
fmt.Fprintln(o)
|
||||
fmt.Fprintln(o, "In all cases:")
|
||||
fmt.Fprintln(o, "<filename> can be - (stdin)")
|
||||
fmt.Fprintln(o, "Multichar options are expanded e.g. -abc becomes -a -b -c.")
|
||||
fmt.Fprintln(o, "The -- option suppresses option processing for subsequent arguments.")
|
||||
fmt.Fprintln(o, "Note that since filenames and jsonnet programs can begin with -, it is advised to")
|
||||
fmt.Fprintln(o, "use -- if the argument is unknown, e.g. jsonnet -- \"$FILENAME\".")
|
||||
}
|
||||
|
||||
func safeStrToInt(str string) (i int) {
|
||||
i, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "ERROR: Invalid integer \"%s\"", str)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Command int
|
||||
|
||||
const (
|
||||
commandEval = iota
|
||||
commandFmt = iota
|
||||
)
|
||||
|
||||
type config struct {
|
||||
cmd Command
|
||||
inputFiles []string
|
||||
outputFile string
|
||||
filenameIsCode bool
|
||||
|
||||
// commandEval flags
|
||||
evalMulti bool
|
||||
evalStream bool
|
||||
evalMultiOutputDir string
|
||||
evalJpath []string
|
||||
|
||||
// commandFmt flags
|
||||
// commandFmt is currently unsupported.
|
||||
}
|
||||
|
||||
func makeConfig() config {
|
||||
return config{
|
||||
cmd: commandEval,
|
||||
filenameIsCode: false,
|
||||
evalMulti: false,
|
||||
evalStream: false,
|
||||
evalJpath: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
func getVarVal(s string) (string, string, error) {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
name := parts[0]
|
||||
if len(parts) == 1 {
|
||||
@ -45,6 +170,324 @@ func getVar(s string) (string, string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func getVarFile(s string) (string, string, error) {
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
name := parts[0]
|
||||
if len(parts) == 1 {
|
||||
return "", "", fmt.Errorf("ERROR: argument not in form <var>=<file> \"%s\".", s)
|
||||
} else {
|
||||
b, err := ioutil.ReadFile(parts[1])
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return name, string(b), nil
|
||||
}
|
||||
}
|
||||
|
||||
type processArgsStatus = int
|
||||
const (
|
||||
processArgsStatusContinue = iota
|
||||
processArgsStatusSuccessUsage = iota
|
||||
processArgsStatusFailureUsage = iota
|
||||
processArgsStatusSuccess = iota
|
||||
processArgsStatusFailure = iota
|
||||
)
|
||||
|
||||
func processArgs(givenArgs []string, config *config, vm *jsonnet.VM) (processArgsStatus, error) {
|
||||
args := simplifyArgs(givenArgs)
|
||||
remainingArgs := make([]string, 0, 0)
|
||||
i := 0
|
||||
if len(args) > 0 && args[i] == "fmt" {
|
||||
config.cmd = commandFmt
|
||||
i++
|
||||
} else if len(args) > 0 && args[i] == "eval" {
|
||||
config.cmd = commandEval
|
||||
i++
|
||||
}
|
||||
|
||||
for ; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
if arg == "-h" || arg == "--help" {
|
||||
return processArgsStatusSuccessUsage, nil
|
||||
} else if arg == "-v" || arg == "--version" {
|
||||
version(os.Stdout)
|
||||
return processArgsStatusSuccess, nil
|
||||
} else if arg == "-e" || arg == "--exec" {
|
||||
config.filenameIsCode = true
|
||||
} else if arg == "-o" || arg == "--exec" {
|
||||
outputFile := nextArg(&i, args)
|
||||
if len(outputFile) == 0 {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: -o argument was empty string")
|
||||
}
|
||||
config.outputFile = outputFile
|
||||
} else if arg == "--" {
|
||||
// All subsequent args are not options.
|
||||
i++
|
||||
for ; i < len(args); i++ {
|
||||
remainingArgs = append(remainingArgs, args[i])
|
||||
}
|
||||
break
|
||||
} else if config.cmd == commandEval {
|
||||
if arg == "-s" || arg == "--max-stack" {
|
||||
l := safeStrToInt(nextArg(&i, args))
|
||||
if l < 1 {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: Invalid --max-stack value: %d", l)
|
||||
}
|
||||
vm.MaxStack = l
|
||||
} else if arg == "-J" || arg == "--jpath" {
|
||||
dir := nextArg(&i, args)
|
||||
if len(dir) == 0 {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: -J argument was empty string")
|
||||
}
|
||||
if dir[len(dir)-1] != '/' {
|
||||
dir += "/"
|
||||
}
|
||||
config.evalJpath = append(config.evalJpath, dir)
|
||||
} else if arg == "-V" || arg == "--ext-str" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarVal(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.ExtVar(name, content)
|
||||
} else if arg == "--ext-str-file" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarFile(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.ExtVar(name, content)
|
||||
} else if arg == "--ext-code" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarVal(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.ExtCode(name, content)
|
||||
} else if arg == "--ext-code-file" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarFile(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.ExtCode(name, content)
|
||||
} else if arg == "-A" || arg == "--tla-str" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarVal(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.TLAVar(name, content)
|
||||
} else if arg == "--tla-str-file" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarFile(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.TLAVar(name, content)
|
||||
} else if arg == "--tla-code" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarVal(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.TLACode(name, content)
|
||||
} else if arg == "--tla-code-file" {
|
||||
next := nextArg(&i, args)
|
||||
name, content, err := getVarFile(next)
|
||||
if err != nil {
|
||||
return processArgsStatusFailure, err
|
||||
}
|
||||
vm.TLACode(name, content)
|
||||
} else if arg == "-t" || arg == "--max-trace" {
|
||||
l := safeStrToInt(nextArg(&i, args))
|
||||
if l < 0 {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: Invalid --max-trace value: %d", l)
|
||||
}
|
||||
vm.ErrorFormatter.MaxStackTraceSize = l
|
||||
} else if arg == "-m" || arg == "--multi" {
|
||||
config.evalMulti = true
|
||||
outputDir := nextArg(&i, args)
|
||||
if len(outputDir) == 0 {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: -m argument was empty string")
|
||||
}
|
||||
if outputDir[len(outputDir)-1] != '/' {
|
||||
outputDir += "/"
|
||||
}
|
||||
config.evalMultiOutputDir = outputDir
|
||||
} else if arg == "-y" || arg == "--yaml-stream" {
|
||||
config.evalStream = true
|
||||
} else if arg == "-S" || arg == "--string" {
|
||||
vm.StringOutput = true
|
||||
} else if len(arg) > 1 && arg[0] == '-' {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: Unrecognized argument: %s", arg)
|
||||
} else {
|
||||
remainingArgs = append(remainingArgs, arg)
|
||||
}
|
||||
|
||||
} else {
|
||||
return processArgsStatusFailure, fmt.Errorf("The Go implementation currently does not support jsonnet fmt.")
|
||||
}
|
||||
}
|
||||
|
||||
want := "filename"
|
||||
if config.filenameIsCode {
|
||||
want = "code"
|
||||
}
|
||||
if len(remainingArgs) == 0 {
|
||||
return processArgsStatusFailureUsage, fmt.Errorf("ERROR: Must give %s", want)
|
||||
}
|
||||
|
||||
// TODO(dcunnin): Formatter allows multiple files in test and in-place mode.
|
||||
multipleFilesAllowed := false
|
||||
|
||||
if !multipleFilesAllowed {
|
||||
if len(remainingArgs) > 1 {
|
||||
return processArgsStatusFailure, fmt.Errorf("ERROR: Only one %s is allowed", want)
|
||||
}
|
||||
}
|
||||
|
||||
config.inputFiles = remainingArgs
|
||||
return processArgsStatusContinue, nil
|
||||
}
|
||||
|
||||
// readInput gets Jsonnet code from the given place (file, commandline, stdin).
|
||||
// It also updates the given filename to <stdin> or <cmdline> if it wasn't a real filename.
|
||||
func readInput(config config, filename *string) (input string, err error) {
|
||||
if config.filenameIsCode {
|
||||
input, err = *filename, nil
|
||||
*filename = "<cmdline>"
|
||||
} else if *filename == "-" {
|
||||
var bytes []byte
|
||||
bytes, err = ioutil.ReadAll(os.Stdin)
|
||||
input = string(bytes)
|
||||
*filename = "<stdin>"
|
||||
} else {
|
||||
var bytes []byte
|
||||
bytes, err = ioutil.ReadFile(*filename)
|
||||
input = string(bytes)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeMultiOutputFiles(output map[string]string, outputDir, outputFile string) error {
|
||||
// If multiple file output is used, then iterate over each string from
|
||||
// the sequence of strings returned by jsonnet_evaluate_snippet_multi,
|
||||
// construct pairs of filename and content, and write each output file.
|
||||
|
||||
var manifest *os.File
|
||||
|
||||
if outputFile == "" {
|
||||
manifest = os.Stdout
|
||||
} else {
|
||||
var err error
|
||||
manifest, err = os.Create(outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer manifest.Close()
|
||||
}
|
||||
|
||||
// Iterate through the map in order.
|
||||
keys := make([]string, 0, len(output))
|
||||
for k, _ := range output {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
newContent := output[key]
|
||||
filename := outputDir + key
|
||||
|
||||
_, err := manifest.WriteString(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = manifest.WriteString("\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filename); !os.IsNotExist(err) {
|
||||
existingContent, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if string(existingContent) == newContent {
|
||||
// Do not bump the timestamp on the file if its content is
|
||||
// the same. This may trigger other tools (e.g. make) to do
|
||||
// unnecessary work.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(newContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeOutputStream writes the output as a YAML stream.
|
||||
func writeOutputStream(output []string, outputFile string) error {
|
||||
var f *os.File
|
||||
|
||||
if outputFile == "" {
|
||||
f = os.Stdout
|
||||
} else {
|
||||
var err error
|
||||
f, err = os.Create(outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
for _, doc := range output {
|
||||
_, err := f.WriteString("---\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.WriteString(doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(output) > 0 {
|
||||
_, err := f.WriteString("...\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeOutputFile(output string, outputFile string) error {
|
||||
if outputFile == "" {
|
||||
fmt.Print(output)
|
||||
return nil
|
||||
}
|
||||
|
||||
f, err := os.Create(outputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = f.WriteString(output)
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
// https://blog.golang.org/profiling-go-programs
|
||||
var cpuprofile = os.Getenv("JSONNET_CPU_PROFILE")
|
||||
@ -57,62 +500,102 @@ func main() {
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
// TODO(sbarzowski) Be consistent about error codes with C++ maybe
|
||||
var filename string
|
||||
|
||||
vm := jsonnet.MakeVM()
|
||||
for i := 1; i < len(os.Args); i++ {
|
||||
arg := os.Args[i]
|
||||
switch arg {
|
||||
case "--tla-str":
|
||||
i++
|
||||
name, content, err := getVar(os.Args[i])
|
||||
|
||||
config := makeConfig()
|
||||
status, err := processArgs(os.Args[1:], &config, vm)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
}
|
||||
switch status {
|
||||
case processArgsStatusContinue:
|
||||
break
|
||||
case processArgsStatusSuccessUsage:
|
||||
usage(os.Stdout)
|
||||
os.Exit(0)
|
||||
case processArgsStatusFailureUsage:
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "")
|
||||
}
|
||||
usage(os.Stderr)
|
||||
os.Exit(1)
|
||||
case processArgsStatusSuccess:
|
||||
os.Exit(0)
|
||||
case processArgsStatusFailure:
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
vm.Importer(&jsonnet.FileImporter{
|
||||
JPaths: config.evalJpath,
|
||||
})
|
||||
|
||||
if config.cmd == commandEval {
|
||||
if len(config.inputFiles) != 1 {
|
||||
// Should already have been caught by processArgs.
|
||||
panic(fmt.Sprintf("Internal error: Expected a single input file."))
|
||||
}
|
||||
filename := config.inputFiles[0]
|
||||
input, err := readInput(config, &filename)
|
||||
if err != nil {
|
||||
var op string
|
||||
switch typedErr := err.(type) {
|
||||
case *os.PathError:
|
||||
op = typedErr.Op
|
||||
err = typedErr.Err
|
||||
}
|
||||
if op == "open" {
|
||||
fmt.Fprintf(os.Stderr, "Opening input file: %s: %s\n", filename, err.Error())
|
||||
} else if op == "read" {
|
||||
fmt.Fprintf(os.Stderr, "Reading input file: %s: %s\n", filename, err.Error())
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
var output string
|
||||
var outputArray []string
|
||||
var outputDict map[string]string
|
||||
if config.evalMulti {
|
||||
outputDict, err = vm.EvaluateSnippetMulti(filename, input)
|
||||
} else if config.evalStream {
|
||||
outputArray, err = vm.EvaluateSnippetStream(filename, input)
|
||||
} else {
|
||||
output, err = vm.EvaluateSnippet(filename, input)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
vm.TLAVar(name, content)
|
||||
case "--tla-code":
|
||||
i++
|
||||
name, content, err := getVar(os.Args[i])
|
||||
|
||||
// Write output JSON.
|
||||
if config.evalMulti {
|
||||
err := writeMultiOutputFiles(outputDict, config.evalMultiOutputDir, config.outputFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
vm.TLACode(name, content)
|
||||
case "--ext-code":
|
||||
i++
|
||||
name, content, err := getVar(os.Args[i])
|
||||
} else if config.evalStream {
|
||||
err := writeOutputStream(outputArray, config.outputFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
vm.ExtCode(name, content)
|
||||
case "--ext-str":
|
||||
i++
|
||||
name, content, err := getVar(os.Args[i])
|
||||
} else {
|
||||
err := writeOutputFile(output, config.outputFile)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
vm.ExtVar(name, content)
|
||||
default:
|
||||
if filename != "" {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
filename = arg
|
||||
|
||||
} else if config.cmd == commandFmt {
|
||||
// Should already have been caught by processArgs.
|
||||
panic(fmt.Sprintf("Internal error: No jsonnet fmt."))
|
||||
|
||||
} else {
|
||||
panic(fmt.Sprintf("Internal error (please report this): Bad cmd value: %d\n", config.cmd))
|
||||
|
||||
}
|
||||
}
|
||||
snippet, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error reading input file: %v\n", err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
json, err := vm.EvaluateSnippet(filename, string(snippet))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "%v\n", err.Error())
|
||||
os.Exit(2)
|
||||
}
|
||||
fmt.Print(json)
|
||||
|
||||
}
|
||||
|
58
jsonnet/cmd_test.go
Normal file
58
jsonnet/cmd_test.go
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright 2017 Google Inc. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testEq(a, b []string) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func testSimplifyAux(t *testing.T, name string, input, expected []string) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := simplifyArgs(input)
|
||||
if !testEq(got, expected) {
|
||||
t.Fail()
|
||||
t.Errorf("Got %v, expected %v\n", got, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSimplifyArgs(t *testing.T) {
|
||||
testSimplifyAux(t, "empty", []string{}, []string{})
|
||||
testSimplifyAux(t, "-a", []string{"-a"}, []string{"-a"})
|
||||
testSimplifyAux(t, "-a -b", []string{"-a", "-b"}, []string{"-a", "-b"})
|
||||
testSimplifyAux(t, "-a -c -b", []string{"-a", "-c", "-b"}, []string{"-a", "-c", "-b"})
|
||||
testSimplifyAux(t, "-abc", []string{"-abc"}, []string{"-a", "-b", "-c"})
|
||||
testSimplifyAux(t, "-acb", []string{"-acb"}, []string{"-a", "-c", "-b"})
|
||||
}
|
@ -122,12 +122,15 @@ func TestMain(t *testing.T) {
|
||||
}
|
||||
|
||||
input := read(test.input)
|
||||
output, err := vm.evaluateSnippet(test.name, string(input))
|
||||
rawOutput, err := vm.evaluateSnippet(test.name, string(input), evalKindRegular)
|
||||
var output string
|
||||
if err != nil {
|
||||
// TODO(sbarzowski) perhaps somehow mark that we are processing
|
||||
// an error. But for now we can treat them the same.
|
||||
output = errFormatter.format(err)
|
||||
output += "\n"
|
||||
} else {
|
||||
output = rawOutput.(string)
|
||||
}
|
||||
if *update {
|
||||
err := ioutil.WriteFile(test.golden, []byte(output), 0666)
|
||||
@ -169,7 +172,7 @@ type errorFormattingTest struct {
|
||||
func genericTestErrorMessage(t *testing.T, tests []errorFormattingTest, format func(RuntimeError) string) {
|
||||
for _, test := range tests {
|
||||
vm := MakeVM()
|
||||
output, err := vm.evaluateSnippet(test.name, test.input)
|
||||
rawOutput, err := vm.evaluateSnippet(test.name, test.input, evalKindRegular)
|
||||
var errString string
|
||||
if err != nil {
|
||||
switch typedErr := err.(type) {
|
||||
@ -178,8 +181,8 @@ func genericTestErrorMessage(t *testing.T, tests []errorFormattingTest, format f
|
||||
default:
|
||||
t.Errorf("%s: unexpected error: %v", test.name, err)
|
||||
}
|
||||
|
||||
}
|
||||
output := rawOutput.(string)
|
||||
if errString != test.errString {
|
||||
t.Errorf("%s: error result does not match. got\n\t%+#v\nexpected\n\t%+#v",
|
||||
test.name, errString, test.errString)
|
||||
|
55
vm.go
55
vm.go
@ -32,12 +32,11 @@ import (
|
||||
// Jsonnet.
|
||||
type VM struct {
|
||||
MaxStack int
|
||||
MaxTrace int // The number of lines of stack trace to display (0 for all of them).
|
||||
ext vmExtMap
|
||||
tla vmExtMap
|
||||
nativeFuncs map[string]*NativeFunction
|
||||
importer Importer
|
||||
ef ErrorFormatter
|
||||
ErrorFormatter ErrorFormatter
|
||||
StringOutput bool
|
||||
}
|
||||
|
||||
@ -59,7 +58,7 @@ func MakeVM() *VM {
|
||||
ext: make(vmExtMap),
|
||||
tla: make(vmExtMap),
|
||||
nativeFuncs: make(map[string]*NativeFunction),
|
||||
ef: ErrorFormatter{pretty: true, colorful: true, MaxStackTraceSize: 20},
|
||||
ErrorFormatter: ErrorFormatter{pretty: false, colorful: false, MaxStackTraceSize: 20},
|
||||
importer: &FileImporter{},
|
||||
}
|
||||
}
|
||||
@ -89,7 +88,15 @@ func (vm *VM) Importer(i Importer) {
|
||||
vm.importer = i
|
||||
}
|
||||
|
||||
func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, err error) {
|
||||
type evalKind = int
|
||||
|
||||
const (
|
||||
evalKindRegular = iota
|
||||
evalKindMulti = iota
|
||||
evalKindStream = iota
|
||||
)
|
||||
|
||||
func (vm *VM) evaluateSnippet(filename string, snippet string, kind evalKind) (output interface{}, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("(CRASH) %v\n%s", r, debug.Stack())
|
||||
@ -99,7 +106,14 @@ func (vm *VM) evaluateSnippet(filename string, snippet string) (output string, e
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch kind {
|
||||
case evalKindRegular:
|
||||
output, err = evaluate(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer, vm.StringOutput)
|
||||
case evalKindMulti:
|
||||
output, err = evaluateMulti(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer, vm.StringOutput)
|
||||
case evalKindStream:
|
||||
output, err = evaluateStream(node, vm.ext, vm.tla, vm.nativeFuncs, vm.MaxStack, vm.importer)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -116,11 +130,38 @@ func (vm *VM) NativeFunction(f *NativeFunction) {
|
||||
//
|
||||
// The filename parameter is only used for error messages.
|
||||
func (vm *VM) EvaluateSnippet(filename string, snippet string) (json string, formattedErr error) {
|
||||
json, err := vm.evaluateSnippet(filename, snippet)
|
||||
output, err := vm.evaluateSnippet(filename, snippet, evalKindRegular)
|
||||
if err != nil {
|
||||
return "", errors.New(vm.ef.format(err))
|
||||
return "", errors.New(vm.ErrorFormatter.format(err))
|
||||
}
|
||||
return json, nil
|
||||
json = output.(string)
|
||||
return
|
||||
}
|
||||
|
||||
// EvaluateSnippetStream evaluates a string containing Jsonnet code to an array.
|
||||
// The array is returned as an array of JSON strings.
|
||||
//
|
||||
// The filename parameter is only used for error messages.
|
||||
func (vm *VM) EvaluateSnippetStream(filename string, snippet string) (docs []string, formattedErr error) {
|
||||
output, err := vm.evaluateSnippet(filename, snippet, evalKindStream)
|
||||
if err != nil {
|
||||
return nil, errors.New(vm.ErrorFormatter.format(err))
|
||||
}
|
||||
docs = output.([]string)
|
||||
return
|
||||
}
|
||||
|
||||
// EvaluateSnippetStream evaluates a string containing Jsonnet code to an array.
|
||||
// The array is returned as an array of JSON strings.
|
||||
//
|
||||
// The filename parameter is only used for error messages.
|
||||
func (vm *VM) EvaluateSnippetMulti(filename string, snippet string) (files map[string]string, formattedErr error) {
|
||||
output, err := vm.evaluateSnippet(filename, snippet, evalKindMulti)
|
||||
if err != nil {
|
||||
return nil, errors.New(vm.ErrorFormatter.format(err))
|
||||
}
|
||||
files = output.(map[string]string)
|
||||
return
|
||||
}
|
||||
|
||||
func snippetToAST(filename string, snippet string) (ast.Node, error) {
|
||||
|
Loading…
Reference in New Issue
Block a user