mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-17 16:38:54 +00:00
Lets an agent type-check a DSL expression before spending a full input
pass. `mlr put --explain '...'` (and filter) runs the existing
parse -> ValidateAST -> CST build -> Resolve path, then:
- valid: prints "mlr {put,filter}: DSL expression is valid." and exits 0
- invalid: returns the build error up the normal path, so --errors-json
emits a structured document; exits 1
- -W with fatal warnings: reports and exits 1
The gate lives in the pass-two constructor, before any input file is
opened, so no input stream is read (verified with a nonexistent input
file still validating OK).
Also categorize bare "parse error: ..." messages from the DSL parser as
kind "dsl-parse-error" rather than "generic" (climain/errors_json.go),
so --explain --errors-json gives an agent a useful error kind. The CSV
reader's "parse error on line ..." is stream-time and never reaches this
command-line-parse categorizer.
Tests: dsl-explain/0001-0004 regression cases (valid put/filter, invalid
plain, invalid --errors-json) and categorize unit tests. Regenerated
verb docs, manpage, and the help usage-verbs golden case.
The older -X ("exit after parsing") still exits 0 even on a parse error;
left as-is since --explain is the correct validation path.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
78ed1563db
commit
7f60e7da57
28 changed files with 2335 additions and 1820 deletions
|
|
@ -142,8 +142,13 @@ func categorize(err error) StructuredError {
|
|||
}
|
||||
}
|
||||
|
||||
// DSL parse errors
|
||||
if strings.Contains(msg, "cannot parse DSL") || strings.Contains(msg, "DSL expression") {
|
||||
// DSL parse errors. The "parse error:" prefix comes from the DSL parser
|
||||
// (pkg/parsing/parser); "cannot parse DSL"/"DSL expression" are the wrapper
|
||||
// messages. (The CSV reader's "parse error on line ..." is a stream-time
|
||||
// error and never reaches this command-line-parse categorizer.)
|
||||
if strings.Contains(msg, "cannot parse DSL") ||
|
||||
strings.Contains(msg, "DSL expression") ||
|
||||
strings.Contains(msg, "parse error:") {
|
||||
return StructuredError{
|
||||
Error: msg,
|
||||
Kind: "dsl-parse-error",
|
||||
|
|
|
|||
|
|
@ -175,3 +175,27 @@ func TestCategorizeGenericFallback(t *testing.T) {
|
|||
t.Errorf("kind: got %q, want generic", se.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizeDSLParseError(t *testing.T) {
|
||||
// The DSL parser (pkg/parsing/parser) emits bare "parse error: ..."
|
||||
// messages; these should categorize as dsl-parse-error, e.g. for
|
||||
// `mlr put --explain` with --errors-json.
|
||||
err := fmt.Errorf("parse error: unexpected equals (\"=\")")
|
||||
se := categorize(err)
|
||||
if se.Kind != "dsl-parse-error" {
|
||||
t.Errorf("kind: got %q, want dsl-parse-error", se.Kind)
|
||||
}
|
||||
if se.Hint == "" {
|
||||
t.Error("hint should be non-empty for dsl-parse-error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategorizeDSLParseErrorNotCSV(t *testing.T) {
|
||||
// The CSV reader's "parse error on line ..." is a stream-time error and
|
||||
// should not be mistaken for a DSL parse error by the substring match.
|
||||
err := fmt.Errorf("parse error on line 3, column 5: bare \" in non-quoted-field")
|
||||
se := categorize(err)
|
||||
if se.Kind == "dsl-parse-error" {
|
||||
t.Errorf("kind: got dsl-parse-error, want non-DSL categorization for a CSV parse error")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ var putOptions = []OptionSpec{
|
|||
{Flag: "-E", Type: "bool", Desc: "Echo DSL expression before printing parse-tree."},
|
||||
{Flag: "-v", Type: "bool", Desc: "Same as -E -p."},
|
||||
{Flag: "-X", Type: "bool", Desc: "Exit after parsing but before stream-processing. Useful with -v/-d/-D, if you only want to look at parser information."},
|
||||
{Flag: "--explain", Type: "bool", Desc: "Parse and type-check the DSL expression, report whether it is valid, and exit without reading the input stream. Exit status is 0 if the expression is valid and non-zero otherwise; combine with --errors-json for a machine-readable error."},
|
||||
}
|
||||
|
||||
var PutSetup = TransformerSetup{
|
||||
|
|
@ -60,6 +61,7 @@ var filterOptions = []OptionSpec{
|
|||
{Flag: "-E", Type: "bool", Desc: "Echo DSL expression before printing parse-tree."},
|
||||
{Flag: "-v", Type: "bool", Desc: "Same as -E -p."},
|
||||
{Flag: "-X", Type: "bool", Desc: "Exit after parsing but before stream-processing. Useful with -v/-d/-D, if you only want to look at parser information."},
|
||||
{Flag: "--explain", Type: "bool", Desc: "Parse and type-check the DSL expression, report whether it is valid, and exit without reading the input stream. Exit status is 0 if the expression is valid and non-zero otherwise; combine with --errors-json for a machine-readable error."},
|
||||
}
|
||||
|
||||
var FilterSetup = TransformerSetup{
|
||||
|
|
@ -195,6 +197,7 @@ func transformerPutOrFilterParseCLI(
|
|||
printASTMultiLine := false
|
||||
printASTSingleLine := false
|
||||
exitAfterParse := false
|
||||
doExplain := false
|
||||
doWarnings := false
|
||||
warningsAreFatal := false
|
||||
strictMode := false
|
||||
|
|
@ -301,6 +304,8 @@ func transformerPutOrFilterParseCLI(
|
|||
printASTSingleLine = true
|
||||
case "-X":
|
||||
exitAfterParse = true
|
||||
case "--explain":
|
||||
doExplain = true
|
||||
case "-w":
|
||||
doWarnings = true
|
||||
warningsAreFatal = false
|
||||
|
|
@ -375,6 +380,7 @@ func transformerPutOrFilterParseCLI(
|
|||
printASTMultiLine,
|
||||
printASTSingleLine,
|
||||
exitAfterParse,
|
||||
doExplain,
|
||||
doWarnings,
|
||||
warningsAreFatal,
|
||||
strictMode,
|
||||
|
|
@ -409,6 +415,7 @@ func NewTransformerPut(
|
|||
printASTMultiLine bool,
|
||||
printASTSingleLine bool,
|
||||
exitAfterParse bool,
|
||||
doExplain bool,
|
||||
doWarnings bool,
|
||||
warningsAreFatal bool,
|
||||
strictMode bool,
|
||||
|
|
@ -449,6 +456,26 @@ func NewTransformerPut(
|
|||
},
|
||||
)
|
||||
|
||||
// --explain is a validate/dry-run: report whether the DSL parsed and
|
||||
// type-checked, then exit without reading the input stream. A parse/build
|
||||
// error is returned so it flows through the normal error path (including
|
||||
// --errors-json); a valid expression prints a confirmation and exits 0.
|
||||
if doExplain {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
verbName := "put"
|
||||
if doFilter {
|
||||
verbName = "filter"
|
||||
}
|
||||
if warningsAreFatal && hadWarnings {
|
||||
fmt.Fprintf(os.Stderr, "mlr %s: DSL expression has warnings treated as fatal.\n", verbName)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("mlr %s: DSL expression is valid.\n", verbName)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if warningsAreFatal && hadWarnings {
|
||||
fmt.Printf(
|
||||
"%s: Exiting due to warnings treated as fatal.\n",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue