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
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1035,6 +1035,10 @@ Options:
|
|||
-v Same as -E -p.
|
||||
-X Exit after parsing but before stream-processing. Useful with
|
||||
-v/-d/-D, if you only want to look at parser information.
|
||||
--explain 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.
|
||||
-h|--help Show this message.
|
||||
|
||||
If you mix -e and -f then the expressions are evaluated in the order encountered.
|
||||
|
|
@ -2436,6 +2440,10 @@ Options:
|
|||
-v Same as -E -p.
|
||||
-X Exit after parsing but before stream-processing. Useful with
|
||||
-v/-d/-D, if you only want to look at parser information.
|
||||
--explain 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.
|
||||
-h|--help Show this message.
|
||||
|
||||
If you mix -e and -f then the expressions are evaluated in the order encountered.
|
||||
|
|
|
|||
823
man/manpage.txt
823
man/manpage.txt
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ per the issue.)
|
|||
|
||||
---
|
||||
|
||||
## PR 5 — DSL `--explain` / validate dry-run
|
||||
## PR 5 — DSL `--explain` / validate dry-run *(landed)*
|
||||
|
||||
**Goal.** Validate/type-check a DSL expression *before* spending a full input
|
||||
pass (a big context saver for agents).
|
||||
|
|
@ -236,6 +236,19 @@ pass (a big context saver for agents).
|
|||
**without consuming the full input stream**.
|
||||
- Reuse the existing DSL parse/CST build path; gate it before the record loop.
|
||||
|
||||
**Landed.** `--explain` added to put/filter (`put_or_filter.go`): after the
|
||||
existing `cstRootNode.Build` (which already does parse → ValidateAST → CST build
|
||||
→ Resolve), a valid expression prints `mlr {put,filter}: DSL expression is
|
||||
valid.` and exits 0; an invalid one returns the build error up the normal path,
|
||||
so `--errors-json` yields a structured document. The gate sits in the pass-two
|
||||
constructor, before any input file is opened, so no input is read. DSL parser
|
||||
messages (`parse error: ...`) now categorize as `dsl-parse-error` rather than
|
||||
`generic` (`climain/errors_json.go`). Tests: `dsl-explain/0001-0004` regression
|
||||
cases (valid put/filter, invalid plain, invalid `--errors-json`) plus categorize
|
||||
unit tests. Note: the older `-X` ("exit after parsing") still exits 0 even on a
|
||||
parse error — a pre-existing quirk left as-is since `--explain` is the correct
|
||||
validation path.
|
||||
|
||||
---
|
||||
|
||||
## PR 6 — `mlr describe` schema/shape introspection
|
||||
|
|
|
|||
|
|
@ -231,6 +231,10 @@ Options:
|
|||
-v Same as -E -p.
|
||||
-X Exit after parsing but before stream-processing. Useful with
|
||||
-v/-d/-D, if you only want to look at parser information.
|
||||
--explain 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.
|
||||
-h|--help Show this message.
|
||||
|
||||
If you mix -e and -f then the expressions are evaluated in the order encountered.
|
||||
|
|
@ -759,6 +763,10 @@ Options:
|
|||
-v Same as -E -p.
|
||||
-X Exit after parsing but before stream-processing. Useful with
|
||||
-v/-d/-D, if you only want to look at parser information.
|
||||
--explain 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.
|
||||
-h|--help Show this message.
|
||||
|
||||
If you mix -e and -f then the expressions are evaluated in the order encountered.
|
||||
|
|
|
|||
1
test/cases/dsl-explain/0001/cmd
Normal file
1
test/cases/dsl-explain/0001/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr -n put --explain -f ${CASEDIR}/mlr
|
||||
0
test/cases/dsl-explain/0001/experr
Normal file
0
test/cases/dsl-explain/0001/experr
Normal file
1
test/cases/dsl-explain/0001/expout
Normal file
1
test/cases/dsl-explain/0001/expout
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr put: DSL expression is valid.
|
||||
1
test/cases/dsl-explain/0001/mlr
Normal file
1
test/cases/dsl-explain/0001/mlr
Normal file
|
|
@ -0,0 +1 @@
|
|||
$y = $x + 1
|
||||
1
test/cases/dsl-explain/0002/cmd
Normal file
1
test/cases/dsl-explain/0002/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr -n filter --explain -f ${CASEDIR}/mlr
|
||||
0
test/cases/dsl-explain/0002/experr
Normal file
0
test/cases/dsl-explain/0002/experr
Normal file
1
test/cases/dsl-explain/0002/expout
Normal file
1
test/cases/dsl-explain/0002/expout
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr filter: DSL expression is valid.
|
||||
1
test/cases/dsl-explain/0002/mlr
Normal file
1
test/cases/dsl-explain/0002/mlr
Normal file
|
|
@ -0,0 +1 @@
|
|||
$x > 0
|
||||
1
test/cases/dsl-explain/0003/cmd
Normal file
1
test/cases/dsl-explain/0003/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr -n put --explain -f ${CASEDIR}/mlr
|
||||
2
test/cases/dsl-explain/0003/experr
Normal file
2
test/cases/dsl-explain/0003/experr
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
mlr: cannot parse DSL expression.
|
||||
mlr: parse error: unexpected equals ("=")
|
||||
0
test/cases/dsl-explain/0003/expout
Normal file
0
test/cases/dsl-explain/0003/expout
Normal file
1
test/cases/dsl-explain/0003/mlr
Normal file
1
test/cases/dsl-explain/0003/mlr
Normal file
|
|
@ -0,0 +1 @@
|
|||
$y = = $x
|
||||
0
test/cases/dsl-explain/0003/should-fail
Normal file
0
test/cases/dsl-explain/0003/should-fail
Normal file
1
test/cases/dsl-explain/0004/cmd
Normal file
1
test/cases/dsl-explain/0004/cmd
Normal file
|
|
@ -0,0 +1 @@
|
|||
mlr --errors-json -n put --explain -f ${CASEDIR}/mlr
|
||||
6
test/cases/dsl-explain/0004/experr
Normal file
6
test/cases/dsl-explain/0004/experr
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
mlr: cannot parse DSL expression.
|
||||
{
|
||||
"error": "parse error: unexpected equals (\"=\")",
|
||||
"kind": "dsl-parse-error",
|
||||
"hint": "Run 'mlr put --help' for DSL syntax reference."
|
||||
}
|
||||
0
test/cases/dsl-explain/0004/expout
Normal file
0
test/cases/dsl-explain/0004/expout
Normal file
1
test/cases/dsl-explain/0004/mlr
Normal file
1
test/cases/dsl-explain/0004/mlr
Normal file
|
|
@ -0,0 +1 @@
|
|||
$y = = $x
|
||||
0
test/cases/dsl-explain/0004/should-fail
Normal file
0
test/cases/dsl-explain/0004/should-fail
Normal file
Loading…
Add table
Add a link
Reference in a new issue