From c93bb8f171361cbd348ce8fab5782c6b46f2cc84 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Sat, 14 Feb 2026 19:42:44 -0500 Subject: [PATCH 1/3] initial attempt --- pkg/transformers/nest.go | 186 +++++++++++++++++++++++++++------------ 1 file changed, 132 insertions(+), 54 deletions(-) diff --git a/pkg/transformers/nest.go b/pkg/transformers/nest.go index 2cefda50d..fc7530eec 100644 --- a/pkg/transformers/nest.go +++ b/pkg/transformers/nest.go @@ -37,6 +37,8 @@ func transformerNestUsage( fmt.Fprintf(o, " --values,--pairs One is required.\n") fmt.Fprintf(o, " --across-records,--across-fields One is required.\n") fmt.Fprintf(o, " -f {field name} Required.\n") + fmt.Fprintf(o, " -r Treat -f as regular expression. Match all field names\n") + fmt.Fprintf(o, " and operate on each in record order. Example: -f '^[xy]$' -r\n") fmt.Fprintf(o, " --nested-fs {string} Defaults to \";\". Field separator for nested values.\n") fmt.Fprintf(o, " --nested-ps {string} Defaults to \":\". Pair separator for nested key-value pairs.\n") fmt.Fprintf(o, " --evar {string} Shorthand for --explode --values --across-records --nested-fs {string}\n") @@ -102,6 +104,7 @@ func transformerNestParseCLI( // Parse local flags fieldName := "" + doRegexes := false nestedFS := ";" nestedPS := ":" doExplode := true @@ -130,6 +133,9 @@ func transformerNestParseCLI( } else if opt == "-f" { fieldName = cli.VerbGetStringArgOrDie(verb, opt, args, &argi, argc) + } else if opt == "-r" { + doRegexes = true + } else if opt == "--explode" || opt == "-e" { doExplode = true doExplodeSpecified = true @@ -213,6 +219,10 @@ func transformerNestParseCLI( transformerNestUsage(os.Stderr) os.Exit(1) } + if doRegexes && !doExplode { + fmt.Fprintf(os.Stderr, "mlr nest: -r is only supported with --explode, not --implode.\n") + os.Exit(1) + } *pargi = argi if !doConstruct { // All transformers must do this for main command-line parsing @@ -221,6 +231,7 @@ func transformerNestParseCLI( transformer, err := NewTransformerNest( fieldName, + doRegexes, nestedFS, nestedPS, doExplode, @@ -241,7 +252,10 @@ type TransformerNest struct { nestedFS string nestedPS string - // For implode across fields + doRegexes bool + fieldRegex *regexp.Regexp // when doRegexes, for matching field names + + // For implode across fields (when !doRegexes) regex *regexp.Regexp // For implode across records @@ -253,6 +267,7 @@ type TransformerNest struct { // ---------------------------------------------------------------- func NewTransformerNest( fieldName string, + doRegexes bool, nestedFS string, nestedPS string, doExplode bool, @@ -262,22 +277,38 @@ func NewTransformerNest( tr := &TransformerNest{ fieldName: fieldName, + doRegexes: doRegexes, nestedFS: cli.SeparatorFromArg(nestedFS), // "pipe" -> "|", etc nestedPS: cli.SeparatorFromArg(nestedPS), } - // For implode across fields - regexString := "^" + fieldName + "_[0-9]+$" - regex, err := lib.CompileMillerRegex(regexString) - if err != nil { - fmt.Fprintf( - os.Stderr, - "%s %s: cannot compile regex [%s]\n", - "mlr", verbNameNest, regexString, - ) - os.Exit(1) + // For implode across fields: regex to match exploded form (e.g. x_1, x_2) + if doRegexes { + fieldRegex, err := lib.CompileMillerRegex(fieldName) + if err != nil { + fmt.Fprintf( + os.Stderr, + "%s %s: cannot compile regex [%s]\n", + "mlr", verbNameNest, fieldName, + ) + os.Exit(1) + } + tr.fieldRegex = fieldRegex + // implode uses fieldRegex directly when doRegexes + tr.regex = fieldRegex + } else { + regexString := "^" + fieldName + "_[0-9]+$" + regex, err := lib.CompileMillerRegex(regexString) + if err != nil { + fmt.Fprintf( + os.Stderr, + "%s %s: cannot compile regex [%s]\n", + "mlr", verbNameNest, regexString, + ) + os.Exit(1) + } + tr.regex = regex } - tr.regex = regex // For implode across records tr.otherKeysToOtherValuesToBuckets = lib.NewOrderedMap[*lib.OrderedMap[*tNestBucket]]() @@ -324,6 +355,25 @@ func (tr *TransformerNest) Transform( tr.recordTransformerFunc(inrecAndContext, outputRecordsAndContexts, inputDownstreamDoneChannel, outputDownstreamDoneChannel) } +// ---------------------------------------------------------------- +// getMatchingFieldNames returns field names matching tr.fieldRegex in record order. +// When !tr.doRegexes, returns [tr.fieldName] if present, else []. +func (tr *TransformerNest) getMatchingFieldNames(inrec *mlrval.Mlrmap) []string { + if !tr.doRegexes { + if inrec.Get(tr.fieldName) != nil { + return []string{tr.fieldName} + } + return nil + } + var names []string + for pe := inrec.Head; pe != nil; pe = pe.Next { + if tr.fieldRegex.MatchString(pe.Key) { + names = append(names, pe.Key) + } + } + return names +} + // ---------------------------------------------------------------- func (tr *TransformerNest) explodeValuesAcrossFields( inrecAndContext *types.RecordAndContext, @@ -334,27 +384,34 @@ func (tr *TransformerNest) explodeValuesAcrossFields( if !inrecAndContext.EndOfStream { inrec := inrecAndContext.Record - originalEntry := inrec.GetEntry(tr.fieldName) - if originalEntry == nil { + fieldNames := tr.getMatchingFieldNames(inrec) + if len(fieldNames) == 0 { *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) return } - recordEntry := originalEntry - mvalue := originalEntry.Value - svalue := mvalue.String() + for _, fieldName := range fieldNames { + originalEntry := inrec.GetEntry(fieldName) + if originalEntry == nil { + continue + } - // Not lib.SplitString so 'x=' will map to 'x_1=', rather than no field at all - pieces := strings.Split(svalue, tr.nestedFS) - i := 1 - for _, piece := range pieces { - key := tr.fieldName + "_" + strconv.Itoa(i) - value := mlrval.FromString(piece) - recordEntry = inrec.PutReferenceAfter(recordEntry, key, value) - i++ - } + recordEntry := originalEntry + mvalue := originalEntry.Value + svalue := mvalue.String() + + // Not lib.SplitString so 'x=' will map to 'x_1=', rather than no field at all + pieces := strings.Split(svalue, tr.nestedFS) + i := 1 + for _, piece := range pieces { + key := fieldName + "_" + strconv.Itoa(i) + value := mlrval.FromString(piece) + recordEntry = inrec.PutReferenceAfter(recordEntry, key, value) + i++ + } - inrec.Unlink(originalEntry) + inrec.Unlink(originalEntry) + } *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) } else { @@ -371,7 +428,14 @@ func (tr *TransformerNest) explodeValuesAcrossRecords( ) { if !inrecAndContext.EndOfStream { inrec := inrecAndContext.Record - mvalue := inrec.Get(tr.fieldName) + fieldNames := tr.getMatchingFieldNames(inrec) + if len(fieldNames) == 0 { + *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) + return + } + fieldName := fieldNames[0] + + mvalue := inrec.Get(fieldName) if mvalue == nil { *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) return @@ -382,7 +446,7 @@ func (tr *TransformerNest) explodeValuesAcrossRecords( pieces := strings.Split(svalue, tr.nestedFS) for _, piece := range pieces { outrec := inrec.Copy() - outrec.PutReference(tr.fieldName, mlrval.FromString(piece)) + outrec.PutReference(fieldName, mlrval.FromString(piece)) *outputRecordsAndContexts = append(*outputRecordsAndContexts, types.NewRecordAndContext(outrec, &inrecAndContext.Context)) } @@ -401,35 +465,42 @@ func (tr *TransformerNest) explodePairsAcrossFields( if !inrecAndContext.EndOfStream { inrec := inrecAndContext.Record - originalEntry := inrec.GetEntry(tr.fieldName) - if originalEntry == nil { + fieldNames := tr.getMatchingFieldNames(inrec) + if len(fieldNames) == 0 { *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) return } - mvalue := originalEntry.Value - svalue := mvalue.String() + for _, fieldName := range fieldNames { + originalEntry := inrec.GetEntry(fieldName) + if originalEntry == nil { + continue + } - recordEntry := originalEntry - pieces := lib.SplitString(svalue, tr.nestedFS) - for _, piece := range pieces { - pair := strings.SplitN(piece, tr.nestedPS, 2) - if len(pair) == 2 { // there is a pair - recordEntry = inrec.PutReferenceAfter( - recordEntry, - pair[0], - mlrval.FromString(pair[1]), - ) - } else { // there is not a pair - recordEntry = inrec.PutReferenceAfter( - recordEntry, - tr.fieldName, - mlrval.FromString(piece), - ) + mvalue := originalEntry.Value + svalue := mvalue.String() + + recordEntry := originalEntry + pieces := lib.SplitString(svalue, tr.nestedFS) + for _, piece := range pieces { + pair := strings.SplitN(piece, tr.nestedPS, 2) + if len(pair) == 2 { // there is a pair + recordEntry = inrec.PutReferenceAfter( + recordEntry, + pair[0], + mlrval.FromString(pair[1]), + ) + } else { // there is not a pair + recordEntry = inrec.PutReferenceAfter( + recordEntry, + fieldName, + mlrval.FromString(piece), + ) + } } - } - inrec.Unlink(originalEntry) + inrec.Unlink(originalEntry) + } *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) } else { @@ -446,7 +517,14 @@ func (tr *TransformerNest) explodePairsAcrossRecords( ) { if !inrecAndContext.EndOfStream { inrec := inrecAndContext.Record - mvalue := inrec.Get(tr.fieldName) + fieldNames := tr.getMatchingFieldNames(inrec) + if len(fieldNames) == 0 { + *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) + return + } + fieldName := fieldNames[0] + + mvalue := inrec.Get(fieldName) if mvalue == nil { *outputRecordsAndContexts = append(*outputRecordsAndContexts, inrecAndContext) return @@ -457,7 +535,7 @@ func (tr *TransformerNest) explodePairsAcrossRecords( for _, piece := range pieces { outrec := inrec.Copy() - originalEntry := outrec.GetEntry(tr.fieldName) + originalEntry := outrec.GetEntry(fieldName) // Put the new field where the old one was -- unless there's already a field with the new // name, in which case replace its value. @@ -465,7 +543,7 @@ func (tr *TransformerNest) explodePairsAcrossRecords( if len(pair) == 2 { // there is a pair outrec.PutReferenceAfter(originalEntry, pair[0], mlrval.FromString(pair[1])) } else { // there is not a pair - outrec.PutReferenceAfter(originalEntry, tr.fieldName, mlrval.FromString(piece)) + outrec.PutReferenceAfter(originalEntry, fieldName, mlrval.FromString(piece)) } outrec.Unlink(originalEntry) From c7c9469c4db1dcd02f679be720c8c990801e2173 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Sat, 14 Feb 2026 19:45:26 -0500 Subject: [PATCH 2/3] fix up cli flag --- pkg/transformers/nest.go | 5 +++-- test/cases/verb-nest/epaf-regex/cmd | 1 + test/cases/verb-nest/epaf-regex/experr | 0 test/cases/verb-nest/epaf-regex/expout | 4 ++++ test/cases/verb-nest/epar-regex/cmd | 1 + test/cases/verb-nest/epar-regex/experr | 0 test/cases/verb-nest/epar-regex/expout | 6 ++++++ test/cases/verb-nest/evaf-regex/cmd | 1 + test/cases/verb-nest/evaf-regex/experr | 0 test/cases/verb-nest/evaf-regex/expout | 2 ++ test/cases/verb-nest/evar-regex/cmd | 1 + test/cases/verb-nest/evar-regex/experr | 0 test/cases/verb-nest/evar-regex/expout | 7 +++++++ test/input/nest-explode-regex.dkvp | 2 ++ 14 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 test/cases/verb-nest/epaf-regex/cmd create mode 100644 test/cases/verb-nest/epaf-regex/experr create mode 100644 test/cases/verb-nest/epaf-regex/expout create mode 100644 test/cases/verb-nest/epar-regex/cmd create mode 100644 test/cases/verb-nest/epar-regex/experr create mode 100644 test/cases/verb-nest/epar-regex/expout create mode 100644 test/cases/verb-nest/evaf-regex/cmd create mode 100644 test/cases/verb-nest/evaf-regex/experr create mode 100644 test/cases/verb-nest/evaf-regex/expout create mode 100644 test/cases/verb-nest/evar-regex/cmd create mode 100644 test/cases/verb-nest/evar-regex/experr create mode 100644 test/cases/verb-nest/evar-regex/expout create mode 100644 test/input/nest-explode-regex.dkvp diff --git a/pkg/transformers/nest.go b/pkg/transformers/nest.go index fc7530eec..1f9c8fbb5 100644 --- a/pkg/transformers/nest.go +++ b/pkg/transformers/nest.go @@ -37,8 +37,8 @@ func transformerNestUsage( fmt.Fprintf(o, " --values,--pairs One is required.\n") fmt.Fprintf(o, " --across-records,--across-fields One is required.\n") fmt.Fprintf(o, " -f {field name} Required.\n") - fmt.Fprintf(o, " -r Treat -f as regular expression. Match all field names\n") - fmt.Fprintf(o, " and operate on each in record order. Example: -f '^[xy]$' -r\n") + fmt.Fprintf(o, " -r {field names} Treat -f as regular expression. Match all field names\n") + fmt.Fprintf(o, " and operate on each in record order. Example: `-r '^[xy]$`'.\n") fmt.Fprintf(o, " --nested-fs {string} Defaults to \";\". Field separator for nested values.\n") fmt.Fprintf(o, " --nested-ps {string} Defaults to \":\". Pair separator for nested key-value pairs.\n") fmt.Fprintf(o, " --evar {string} Shorthand for --explode --values --across-records --nested-fs {string}\n") @@ -135,6 +135,7 @@ func transformerNestParseCLI( } else if opt == "-r" { doRegexes = true + fieldName = cli.VerbGetStringArgOrDie(verb, opt, args, &argi, argc) } else if opt == "--explode" || opt == "-e" { doExplode = true diff --git a/test/cases/verb-nest/epaf-regex/cmd b/test/cases/verb-nest/epaf-regex/cmd new file mode 100644 index 000000000..37573e91a --- /dev/null +++ b/test/cases/verb-nest/epaf-regex/cmd @@ -0,0 +1 @@ +mlr nest --explode --pairs --across-fields -r '^x$' test/input/nest-explode.dkvp diff --git a/test/cases/verb-nest/epaf-regex/experr b/test/cases/verb-nest/epaf-regex/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/verb-nest/epaf-regex/expout b/test/cases/verb-nest/epaf-regex/expout new file mode 100644 index 000000000..24d5905f7 --- /dev/null +++ b/test/cases/verb-nest/epaf-regex/expout @@ -0,0 +1,4 @@ +a=1,b=2,c=3,y=d:40 +y=d:50 +u=100,y=d:60 +a=4,b=5,y=d:70 diff --git a/test/cases/verb-nest/epar-regex/cmd b/test/cases/verb-nest/epar-regex/cmd new file mode 100644 index 000000000..e9de6a568 --- /dev/null +++ b/test/cases/verb-nest/epar-regex/cmd @@ -0,0 +1 @@ +mlr nest --explode --pairs --across-records -r '^x$' test/input/nest-explode.dkvp diff --git a/test/cases/verb-nest/epar-regex/experr b/test/cases/verb-nest/epar-regex/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/verb-nest/epar-regex/expout b/test/cases/verb-nest/epar-regex/expout new file mode 100644 index 000000000..24fc64ca2 --- /dev/null +++ b/test/cases/verb-nest/epar-regex/expout @@ -0,0 +1,6 @@ +a=1,y=d:40 +b=2,y=d:40 +c=3,y=d:40 +u=100,y=d:60 +a=4,y=d:70 +b=5,y=d:70 diff --git a/test/cases/verb-nest/evaf-regex/cmd b/test/cases/verb-nest/evaf-regex/cmd new file mode 100644 index 000000000..d430cb9c0 --- /dev/null +++ b/test/cases/verb-nest/evaf-regex/cmd @@ -0,0 +1 @@ +mlr nest --explode --values --across-fields -r '^[xy]$' test/input/nest-explode-regex.dkvp diff --git a/test/cases/verb-nest/evaf-regex/experr b/test/cases/verb-nest/evaf-regex/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/verb-nest/evaf-regex/expout b/test/cases/verb-nest/evaf-regex/expout new file mode 100644 index 000000000..07c39ab49 --- /dev/null +++ b/test/cases/verb-nest/evaf-regex/expout @@ -0,0 +1,2 @@ +x_1=a,x_2=b,x_3=c,y_1=d,y_2=e,y_3=f,z=z +x_1=1,x_2=2,y_1=3,z=other diff --git a/test/cases/verb-nest/evar-regex/cmd b/test/cases/verb-nest/evar-regex/cmd new file mode 100644 index 000000000..b6a67f303 --- /dev/null +++ b/test/cases/verb-nest/evar-regex/cmd @@ -0,0 +1 @@ +mlr nest --explode --values --across-records -r '^x$' test/input/nest-explode.dkvp diff --git a/test/cases/verb-nest/evar-regex/experr b/test/cases/verb-nest/evar-regex/experr new file mode 100644 index 000000000..e69de29bb diff --git a/test/cases/verb-nest/evar-regex/expout b/test/cases/verb-nest/evar-regex/expout new file mode 100644 index 000000000..6c6ab0ad6 --- /dev/null +++ b/test/cases/verb-nest/evar-regex/expout @@ -0,0 +1,7 @@ +x=a:1,y=d:40 +x=b:2,y=d:40 +x=c:3,y=d:40 +x=,y=d:50 +u=100,y=d:60 +x=a:4,y=d:70 +x=b:5,y=d:70 diff --git a/test/input/nest-explode-regex.dkvp b/test/input/nest-explode-regex.dkvp new file mode 100644 index 000000000..f33e782fd --- /dev/null +++ b/test/input/nest-explode-regex.dkvp @@ -0,0 +1,2 @@ +x=a;b;c,y=d;e;f,z=z +x=1;2,y=3,z=other From 9c7dfa1d132566e57a27d5d14c89e61105b63ca0 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Sat, 14 Feb 2026 19:50:03 -0500 Subject: [PATCH 3/3] make dev --- docs/src/data-diving-examples.md | 4 ++-- docs/src/date-time-examples.md | 2 +- docs/src/manpage.md | 4 +++- docs/src/manpage.txt | 4 +++- docs/src/reference-verbs.md | 6 ++++-- man/manpage.txt | 4 +++- man/mlr.1 | 6 ++++-- pkg/transformers/nest.go | 6 +++--- test/cases/cli-help/0001/expout | 2 ++ 9 files changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/src/data-diving-examples.md b/docs/src/data-diving-examples.md index 297eca211..100716ec2 100644 --- a/docs/src/data-diving-examples.md +++ b/docs/src/data-diving-examples.md @@ -26,7 +26,7 @@ Vertical-tabular format is good for a quick look at CSV data layout -- seeing wh wc -l data/flins.csv
-36635 data/flins.csv
+   36635 data/flins.csv
 
@@ -227,7 +227,7 @@ Peek at the data:
 wc -l data/colored-shapes.dkvp
 
-10078 data/colored-shapes.dkvp
+   10078 data/colored-shapes.dkvp
 
diff --git a/docs/src/date-time-examples.md b/docs/src/date-time-examples.md
index cab74de3c..5bcbdac01 100644
--- a/docs/src/date-time-examples.md
+++ b/docs/src/date-time-examples.md
@@ -68,7 +68,7 @@ date,qoh
 wc -l data/miss-date.csv
 
-1372 data/miss-date.csv
+    1372 data/miss-date.csv
 
Since there are 1372 lines in the data file, some automation is called for. To find the missing dates, you can convert the dates to seconds since the epoch using `strptime`, then compute adjacent differences (the `cat -n` simply inserts record-counters): diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 39203a0c9..9731ba46f 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -1498,6 +1498,8 @@ This is simply a copy of what you should see on running `man mlr` at a command p --values,--pairs One is required. --across-records,--across-fields One is required. -f {field name} Required. + -r {field names} Like -f but treat arguments as a regular expression. Match all + field names and operate on each in record order. Example: `-r '^[xy]$`'. --nested-fs {string} Defaults to ";". Field separator for nested values. --nested-ps {string} Defaults to ":". Pair separator for nested key-value pairs. --evar {string} Shorthand for --explode --values --across-records --nested-fs {string} @@ -3759,5 +3761,5 @@ This is simply a copy of what you should see on running `man mlr` at a command p MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-01-02 4mMILLER24m(1) + 2026-02-15 4mMILLER24m(1) diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index 90bff3293..e9d2641fe 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -1477,6 +1477,8 @@ --values,--pairs One is required. --across-records,--across-fields One is required. -f {field name} Required. + -r {field names} Like -f but treat arguments as a regular expression. Match all + field names and operate on each in record order. Example: `-r '^[xy]$`'. --nested-fs {string} Defaults to ";". Field separator for nested values. --nested-ps {string} Defaults to ":". Pair separator for nested key-value pairs. --evar {string} Shorthand for --explode --values --across-records --nested-fs {string} @@ -3738,4 +3740,4 @@ MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-01-02 4mMILLER24m(1) + 2026-02-15 4mMILLER24m(1) diff --git a/docs/src/reference-verbs.md b/docs/src/reference-verbs.md index b50c97d7d..fa724e4ad 100644 --- a/docs/src/reference-verbs.md +++ b/docs/src/reference-verbs.md @@ -2245,6 +2245,8 @@ Options: --values,--pairs One is required. --across-records,--across-fields One is required. -f {field name} Required. + -r {field names} Like -f but treat arguments as a regular expression. Match all + field names and operate on each in record order. Example: `-r '^[xy]$`'. --nested-fs {string} Defaults to ";". Field separator for nested values. --nested-ps {string} Defaults to ":". Pair separator for nested key-value pairs. --evar {string} Shorthand for --explode --values --across-records --nested-fs {string} @@ -4134,7 +4136,7 @@ There are two main ways to use `mlr uniq`: the first way is with `-g` to specify wc -l data/colored-shapes.csv
-10079 data/colored-shapes.csv
+   10079 data/colored-shapes.csv
 
@@ -4291,7 +4293,7 @@ color=purple,shape=square,flag=0
 wc -l data/repeats.dkvp
 
-57 data/repeats.dkvp
+      57 data/repeats.dkvp
 
diff --git a/man/manpage.txt b/man/manpage.txt
index 90bff3293..e9d2641fe 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -1477,6 +1477,8 @@
          --values,--pairs      One is required.
          --across-records,--across-fields One is required.
          -f {field name}       Required.
+         -r {field names}      Like -f but treat arguments as a regular expression. Match all
+                               field names and operate on each in record order. Example: `-r '^[xy]$`'.
          --nested-fs {string}  Defaults to ";". Field separator for nested values.
          --nested-ps {string}  Defaults to ":". Pair separator for nested key-value pairs.
          --evar {string}       Shorthand for --explode --values --across-records --nested-fs {string}
@@ -3738,4 +3740,4 @@
        MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
        https://miller.readthedocs.io
 
-                                  2026-01-02                         4mMILLER24m(1)
+                                  2026-02-15                         4mMILLER24m(1)
diff --git a/man/mlr.1 b/man/mlr.1
index f36d5e2f0..3b6256a7b 100644
--- a/man/mlr.1
+++ b/man/mlr.1
@@ -2,12 +2,12 @@
 .\"     Title: mlr
 .\"    Author: [see the "AUTHOR" section]
 .\" Generator: ./mkman.rb
-.\"      Date: 2026-01-02
+.\"      Date: 2026-02-15
 .\"    Manual: \ \&
 .\"    Source: \ \&
 .\"  Language: English
 .\"
-.TH "MILLER" "1" "2026-01-02" "\ \&" "\ \&"
+.TH "MILLER" "1" "2026-02-15" "\ \&" "\ \&"
 .\" -----------------------------------------------------------------
 .\" * Portability definitions
 .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1868,6 +1868,8 @@ Options:
   --values,--pairs      One is required.
   --across-records,--across-fields One is required.
   -f {field name}       Required.
+  -r {field names}      Like -f but treat arguments as a regular expression. Match all
+                        field names and operate on each in record order. Example: `-r '^[xy]$`'.
   --nested-fs {string}  Defaults to ";". Field separator for nested values.
   --nested-ps {string}  Defaults to ":". Pair separator for nested key-value pairs.
   --evar {string}       Shorthand for --explode --values --across-records --nested-fs {string}
diff --git a/pkg/transformers/nest.go b/pkg/transformers/nest.go
index 1f9c8fbb5..e765eb2b5 100644
--- a/pkg/transformers/nest.go
+++ b/pkg/transformers/nest.go
@@ -37,8 +37,8 @@ func transformerNestUsage(
 	fmt.Fprintf(o, "  --values,--pairs      One is required.\n")
 	fmt.Fprintf(o, "  --across-records,--across-fields One is required.\n")
 	fmt.Fprintf(o, "  -f {field name}       Required.\n")
-	fmt.Fprintf(o, "  -r {field names}      Treat -f as regular expression. Match all field names\n")
-	fmt.Fprintf(o, "                        and operate on each in record order. Example: `-r '^[xy]$`'.\n")
+	fmt.Fprintf(o, "  -r {field names}      Like -f but treat arguments as a regular expression. Match all\n")
+	fmt.Fprintf(o, "                        field names and operate on each in record order. Example: `-r '^[xy]$`'.\n")
 	fmt.Fprintf(o, "  --nested-fs {string}  Defaults to \";\". Field separator for nested values.\n")
 	fmt.Fprintf(o, "  --nested-ps {string}  Defaults to \":\". Pair separator for nested key-value pairs.\n")
 	fmt.Fprintf(o, "  --evar {string}       Shorthand for --explode --values --across-records --nested-fs {string}\n")
@@ -253,7 +253,7 @@ type TransformerNest struct {
 	nestedFS  string
 	nestedPS  string
 
-	doRegexes bool
+	doRegexes  bool
 	fieldRegex *regexp.Regexp // when doRegexes, for matching field names
 
 	// For implode across fields (when !doRegexes)
diff --git a/test/cases/cli-help/0001/expout b/test/cases/cli-help/0001/expout
index 19a201c62..5142c4559 100644
--- a/test/cases/cli-help/0001/expout
+++ b/test/cases/cli-help/0001/expout
@@ -615,6 +615,8 @@ Options:
   --values,--pairs      One is required.
   --across-records,--across-fields One is required.
   -f {field name}       Required.
+  -r {field names}      Like -f but treat arguments as a regular expression. Match all
+                        field names and operate on each in record order. Example: `-r '^[xy]$`'.
   --nested-fs {string}  Defaults to ";". Field separator for nested values.
   --nested-ps {string}  Defaults to ":". Pair separator for nested key-value pairs.
   --evar {string}       Shorthand for --explode --values --across-records --nested-fs {string}