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
1011
docs/src/manpage.md
1011
docs/src/manpage.md
File diff suppressed because it is too large
Load diff
1011
docs/src/manpage.txt
1011
docs/src/manpage.txt
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.
|
||||
|
|
|
|||
1011
man/manpage.txt
1011
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