From de013dc35cba72bfb0a3b6834bb9e9bb0df50f42 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Mon, 6 Jul 2026 16:45:14 -0400 Subject: [PATCH] Right-align headers over all-numeric columns with --right-align-numeric (#380) (#2167) With --right-align-numeric, PPRINT data cells right-align but headers stayed left-aligned, so a header did not line up with its own column's data -- the original ask in #380. Now a header is right-aligned when every value in its column is numeric, for both non-barred and barred PPRINT output. Mixed columns keep left-aligned headers. For --omd-aligned, the raw header text of right-aligned columns is now right-justified too, matching how Markdown viewers render the ---: marker; this follows the same all-values-numeric per-column rule already used for the separator markers. Man-page regeneration also picks up previously-merged reorder help-text edits that had not been regenerated. Co-authored-by: Claude Fable 5 --- docs/src/file-formats.md | 12 +++-- docs/src/file-formats.md.in | 6 ++- docs/src/manpage.md | 21 +++++--- docs/src/manpage.txt | 18 ++++--- docs/src/reference-main-flag-list.md | 2 +- man/manpage.txt | 18 ++++--- man/mlr.1 | 18 ++++--- pkg/cli/option_parse.go | 7 +-- pkg/output/record_writer_markdown.go | 13 +++-- pkg/output/record_writer_pprint.go | 54 +++++++++++++++++-- test/cases/io-markdown/0004/expout | 2 +- .../io-pprint-right-align-numeric/0001/expout | 2 +- .../io-pprint-right-align-numeric/0002/expout | 2 +- .../io-pprint-right-align-numeric/0003/cmd | 1 + .../io-pprint-right-align-numeric/0003/experr | 0 .../io-pprint-right-align-numeric/0003/expout | 3 ++ .../io-pprint-right-align-numeric/0003/input | 3 ++ .../io-pprint-right-align-numeric/0004/cmd | 1 + .../io-pprint-right-align-numeric/0004/experr | 0 .../io-pprint-right-align-numeric/0004/expout | 6 +++ .../io-pprint-right-align-numeric/0004/input | 3 ++ 21 files changed, 142 insertions(+), 50 deletions(-) create mode 100644 test/cases/io-pprint-right-align-numeric/0003/cmd create mode 100644 test/cases/io-pprint-right-align-numeric/0003/experr create mode 100644 test/cases/io-pprint-right-align-numeric/0003/expout create mode 100644 test/cases/io-pprint-right-align-numeric/0003/input create mode 100644 test/cases/io-pprint-right-align-numeric/0004/cmd create mode 100644 test/cases/io-pprint-right-align-numeric/0004/experr create mode 100644 test/cases/io-pprint-right-align-numeric/0004/expout create mode 100644 test/cases/io-pprint-right-align-numeric/0004/input diff --git a/docs/src/file-formats.md b/docs/src/file-formats.md index ef1eacd8b..c39278211 100644 --- a/docs/src/file-formats.md +++ b/docs/src/file-formats.md @@ -532,13 +532,14 @@ Since Miller 6.11.0, you can use `--barred-input` with pprint input format: Use `--right` to right-align all cells, or `--right-align-numeric` to right-align only the cells -having numeric values, leaving other cells (and the header line) left-aligned: +having numeric values, leaving other cells left-aligned. Headers are right-aligned over columns +whose values are all numeric, so that header and data share the same alignment:
 mlr --icsv --opprint --right-align-numeric cat example.csv
 
-color  shape    flag  k  index quantity rate
+color  shape    flag   k index quantity   rate
 yellow triangle true   1    11  43.6498 9.8870
 red    square   true   2    15  79.2778 0.0130
 red    circle   true   3    16  13.8103 2.9010
@@ -556,7 +557,7 @@ purple square   false 10    91  72.3735 8.2430
 
 +--------+----------+-------+----+-------+----------+--------+
-| color  | shape    | flag  | k  | index | quantity | rate   |
+| color  | shape    | flag  |  k | index | quantity |   rate |
 +--------+----------+-------+----+-------+----------+--------+
 | yellow | triangle | true  |  1 |    11 |  43.6498 | 9.8870 |
 | red    | square   | true  |  2 |    15 |  79.2778 | 0.0130 |
@@ -624,13 +625,14 @@ The `--right-align-numeric` flag also applies to markdown output: numeric column
 right-alignment marker (`---:`) in the header-separator line, so they render right-aligned in
 Markdown viewers. With `--omd`, since output is streaming, the marker for each column is chosen
 from the first record of each same-schema group; with `--omd-aligned`, a column gets the marker
-when all its values are numeric, and its cell text is right-justified in the raw markdown as well:
+when all its values are numeric, and its header and cell text are right-justified in the raw
+markdown as well:
 
 
 mlr --icsv --omd-aligned --right-align-numeric cat example.csv
 
-| color  | shape    | flag  | k    | index | quantity | rate   |
+| color  | shape    | flag  |    k | index | quantity |   rate |
 | ---    | ---      | ---   | ---: |  ---: |     ---: |   ---: |
 | yellow | triangle | true  |    1 |    11 |  43.6498 | 9.8870 |
 | red    | square   | true  |    2 |    15 |  79.2778 | 0.0130 |
diff --git a/docs/src/file-formats.md.in b/docs/src/file-formats.md.in
index 90cfc846f..2322ff474 100644
--- a/docs/src/file-formats.md.in
+++ b/docs/src/file-formats.md.in
@@ -213,7 +213,8 @@ mlr -o pprint --barred cat data/small | mlr -i pprint --barred-input -o json fil
 GENMD-EOF
 
 Use `--right` to right-align all cells, or `--right-align-numeric` to right-align only the cells
-having numeric values, leaving other cells (and the header line) left-aligned:
+having numeric values, leaving other cells left-aligned. Headers are right-aligned over columns
+whose values are all numeric, so that header and data share the same alignment:
 
 GENMD-RUN-COMMAND
 mlr --icsv --opprint --right-align-numeric cat example.csv
@@ -258,7 +259,8 @@ The `--right-align-numeric` flag also applies to markdown output: numeric column
 right-alignment marker (`---:`) in the header-separator line, so they render right-aligned in
 Markdown viewers. With `--omd`, since output is streaming, the marker for each column is chosen
 from the first record of each same-schema group; with `--omd-aligned`, a column gets the marker
-when all its values are numeric, and its cell text is right-justified in the raw markdown as well:
+when all its values are numeric, and its header and cell text are right-justified in the raw
+markdown as well:
 
 GENMD-RUN-COMMAND
 mlr --icsv --omd-aligned --right-align-numeric cat example.csv
diff --git a/docs/src/manpage.md b/docs/src/manpage.md
index 871becd15..f283ec6f8 100644
--- a/docs/src/manpage.md
+++ b/docs/src/manpage.md
@@ -802,10 +802,12 @@ This is simply a copy of what you should see on running `man mlr` at a command p
        --fw {string}            Shortcut for --fixed left-align-multi-word
        --right                  Right-justifies all fields for PPRINT output.
        --right-align-numeric    Right-justifies fields with numeric values for PPRINT
-                                output, leaving other fields (and header lines)
-                                left-justified. Also applies to markdown output,
-                                where numeric columns get right-alignment markers
-                                (`---:`) in the header-separator line.
+                                output, leaving other fields left-justified. Headers
+                                are right-justified over columns whose values are all
+                                numeric, so that header and data share the same
+                                alignment. Also applies to markdown output, where
+                                numeric columns get right-alignment markers (`---:`)
+                                in the header-separator line.
 
 1mPROFILING FLAGS0m
        These are flags for profiling Miller performance.
@@ -1848,8 +1850,10 @@ This is simply a copy of what you should see on running `man mlr` at a command p
                   record start.
        -f {a,b,c} Field names to reorder.
        -r {a,b,c} Treat field names as regular expressions. Matched fields are moved to
-                  start or end in record order. Example: -r '^YYY,^XXX' puts all YYY-
-                  and XXX-prefixed fields first (in record order), then the rest.
+                  start or end, grouped by the order the regexes are given; within each
+                  group, fields keep their record order. Example: -r '^YYY,^XXX' puts
+                  all YYY-prefixed fields first, then all XXX-prefixed fields, then the
+                  rest.
        -b {x}     Put field names specified with -f before field name specified by {x},
                   if any. If {x} isn't present in a given record, the specified fields
                   will not be moved.
@@ -1861,7 +1865,7 @@ This is simply a copy of what you should see on running `man mlr` at a command p
        Examples:
        mlr reorder    -f a,b sends input record "d=4,b=2,a=1,c=3" to "a=1,b=2,d=4,c=3".
        mlr reorder -e -f a,b sends input record "d=4,b=2,a=1,c=3" to "d=4,c=3,a=1,b=2".
-       mlr reorder -r '^YYY,^XXX' puts YYY- and XXX-prefixed fields first (record order), then rest.
+       mlr reorder -r '^YYY,^XXX' puts YYY-prefixed fields first, then XXX-prefixed fields, then rest.
 
    1mrepeat0m
        Usage: mlr repeat [options]
@@ -2457,6 +2461,9 @@ This is simply a copy of what you should see on running `man mlr` at a command p
        count-distinct. For uniq, -f is a synonym for -g. Output fields are
        written in the order in which they are named with -g or -f, not in the
        order in which they appear in the input records.
+       To deduplicate records by one or more fields while keeping all other
+       fields, use head: e.g. "mlr head -n 1 -g hash" keeps the first record
+       for each distinct value of the hash field, with all fields intact.
 
        Options:
        -g {d,e,f} Group-by field names for uniq counts.
diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt
index 8307f65a5..b0d6322ca 100644
--- a/docs/src/manpage.txt
+++ b/docs/src/manpage.txt
@@ -781,10 +781,12 @@
        --fw {string}            Shortcut for --fixed left-align-multi-word
        --right                  Right-justifies all fields for PPRINT output.
        --right-align-numeric    Right-justifies fields with numeric values for PPRINT
-                                output, leaving other fields (and header lines)
-                                left-justified. Also applies to markdown output,
-                                where numeric columns get right-alignment markers
-                                (`---:`) in the header-separator line.
+                                output, leaving other fields left-justified. Headers
+                                are right-justified over columns whose values are all
+                                numeric, so that header and data share the same
+                                alignment. Also applies to markdown output, where
+                                numeric columns get right-alignment markers (`---:`)
+                                in the header-separator line.
 
 1mPROFILING FLAGS0m
        These are flags for profiling Miller performance.
@@ -1827,8 +1829,10 @@
                   record start.
        -f {a,b,c} Field names to reorder.
        -r {a,b,c} Treat field names as regular expressions. Matched fields are moved to
-                  start or end in record order. Example: -r '^YYY,^XXX' puts all YYY-
-                  and XXX-prefixed fields first (in record order), then the rest.
+                  start or end, grouped by the order the regexes are given; within each
+                  group, fields keep their record order. Example: -r '^YYY,^XXX' puts
+                  all YYY-prefixed fields first, then all XXX-prefixed fields, then the
+                  rest.
        -b {x}     Put field names specified with -f before field name specified by {x},
                   if any. If {x} isn't present in a given record, the specified fields
                   will not be moved.
@@ -1840,7 +1844,7 @@
        Examples:
        mlr reorder    -f a,b sends input record "d=4,b=2,a=1,c=3" to "a=1,b=2,d=4,c=3".
        mlr reorder -e -f a,b sends input record "d=4,b=2,a=1,c=3" to "d=4,c=3,a=1,b=2".
-       mlr reorder -r '^YYY,^XXX' puts YYY- and XXX-prefixed fields first (record order), then rest.
+       mlr reorder -r '^YYY,^XXX' puts YYY-prefixed fields first, then XXX-prefixed fields, then rest.
 
    1mrepeat0m
        Usage: mlr repeat [options]
diff --git a/docs/src/reference-main-flag-list.md b/docs/src/reference-main-flag-list.md
index c2f26e842..d402919e3 100644
--- a/docs/src/reference-main-flag-list.md
+++ b/docs/src/reference-main-flag-list.md
@@ -415,7 +415,7 @@ These are flags which are applicable to PPRINT format.
 * `--fixed {string}`: Fixed width specification. One of 'widths:,,...', left-align, left-align-multi-word, right-align, right-align-multi-word
 * `--fw {string}`: Shortcut for --fixed left-align-multi-word
 * `--right`: Right-justifies all fields for PPRINT output.
-* `--right-align-numeric`: Right-justifies fields with numeric values for PPRINT output, leaving other fields (and header lines) left-justified. Also applies to markdown output, where numeric columns get right-alignment markers (`---:`) in the header-separator line.
+* `--right-align-numeric`: Right-justifies fields with numeric values for PPRINT output, leaving other fields left-justified. Headers are right-justified over columns whose values are all numeric, so that header and data share the same alignment. Also applies to markdown output, where numeric columns get right-alignment markers (`---:`) in the header-separator line.
 
 ## Profiling flags
 
diff --git a/man/manpage.txt b/man/manpage.txt
index 8307f65a5..b0d6322ca 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -781,10 +781,12 @@
        --fw {string}            Shortcut for --fixed left-align-multi-word
        --right                  Right-justifies all fields for PPRINT output.
        --right-align-numeric    Right-justifies fields with numeric values for PPRINT
-                                output, leaving other fields (and header lines)
-                                left-justified. Also applies to markdown output,
-                                where numeric columns get right-alignment markers
-                                (`---:`) in the header-separator line.
+                                output, leaving other fields left-justified. Headers
+                                are right-justified over columns whose values are all
+                                numeric, so that header and data share the same
+                                alignment. Also applies to markdown output, where
+                                numeric columns get right-alignment markers (`---:`)
+                                in the header-separator line.
 
 1mPROFILING FLAGS0m
        These are flags for profiling Miller performance.
@@ -1827,8 +1829,10 @@
                   record start.
        -f {a,b,c} Field names to reorder.
        -r {a,b,c} Treat field names as regular expressions. Matched fields are moved to
-                  start or end in record order. Example: -r '^YYY,^XXX' puts all YYY-
-                  and XXX-prefixed fields first (in record order), then the rest.
+                  start or end, grouped by the order the regexes are given; within each
+                  group, fields keep their record order. Example: -r '^YYY,^XXX' puts
+                  all YYY-prefixed fields first, then all XXX-prefixed fields, then the
+                  rest.
        -b {x}     Put field names specified with -f before field name specified by {x},
                   if any. If {x} isn't present in a given record, the specified fields
                   will not be moved.
@@ -1840,7 +1844,7 @@
        Examples:
        mlr reorder    -f a,b sends input record "d=4,b=2,a=1,c=3" to "a=1,b=2,d=4,c=3".
        mlr reorder -e -f a,b sends input record "d=4,b=2,a=1,c=3" to "d=4,c=3,a=1,b=2".
-       mlr reorder -r '^YYY,^XXX' puts YYY- and XXX-prefixed fields first (record order), then rest.
+       mlr reorder -r '^YYY,^XXX' puts YYY-prefixed fields first, then XXX-prefixed fields, then rest.
 
    1mrepeat0m
        Usage: mlr repeat [options]
diff --git a/man/mlr.1 b/man/mlr.1
index f8ded7292..1578a7e64 100644
--- a/man/mlr.1
+++ b/man/mlr.1
@@ -933,10 +933,12 @@ These are flags which are applicable to PPRINT format.
 --fw {string}            Shortcut for --fixed left-align-multi-word
 --right                  Right-justifies all fields for PPRINT output.
 --right-align-numeric    Right-justifies fields with numeric values for PPRINT
-                         output, leaving other fields (and header lines)
-                         left-justified. Also applies to markdown output,
-                         where numeric columns get right-alignment markers
-                         (`---:`) in the header-separator line.
+                         output, leaving other fields left-justified. Headers
+                         are right-justified over columns whose values are all
+                         numeric, so that header and data share the same
+                         alignment. Also applies to markdown output, where
+                         numeric columns get right-alignment markers (`---:`)
+                         in the header-separator line.
 .fi
 .if n \{\
 .RE
@@ -2267,8 +2269,10 @@ Options:
            record start.
 -f {a,b,c} Field names to reorder.
 -r {a,b,c} Treat field names as regular expressions. Matched fields are moved to
-           start or end in record order. Example: -r '^YYY,^XXX' puts all YYY-
-           and XXX-prefixed fields first (in record order), then the rest.
+           start or end, grouped by the order the regexes are given; within each
+           group, fields keep their record order. Example: -r '^YYY,^XXX' puts
+           all YYY-prefixed fields first, then all XXX-prefixed fields, then the
+           rest.
 -b {x}     Put field names specified with -f before field name specified by {x},
            if any. If {x} isn't present in a given record, the specified fields
            will not be moved.
@@ -2280,7 +2284,7 @@ Options:
 Examples:
 mlr reorder    -f a,b sends input record "d=4,b=2,a=1,c=3" to "a=1,b=2,d=4,c=3".
 mlr reorder -e -f a,b sends input record "d=4,b=2,a=1,c=3" to "d=4,c=3,a=1,b=2".
-mlr reorder -r '^YYY,^XXX' puts YYY- and XXX-prefixed fields first (record order), then rest.
+mlr reorder -r '^YYY,^XXX' puts YYY-prefixed fields first, then XXX-prefixed fields, then rest.
 .fi
 .if n \{\
 .RE
diff --git a/pkg/cli/option_parse.go b/pkg/cli/option_parse.go
index 97ec7a66e..a1cadab22 100644
--- a/pkg/cli/option_parse.go
+++ b/pkg/cli/option_parse.go
@@ -537,9 +537,10 @@ var PPRINTOnlyFlagSection = FlagSection{
 		{
 			name: "--right-align-numeric",
 			help: "Right-justifies fields with numeric values for PPRINT output, leaving " +
-				"other fields (and header lines) left-justified. Also applies to markdown " +
-				"output, where numeric columns get right-alignment markers (`---:`) in the " +
-				"header-separator line.",
+				"other fields left-justified. Headers are right-justified over columns " +
+				"whose values are all numeric, so that header and data share the same " +
+				"alignment. Also applies to markdown output, where numeric columns get " +
+				"right-alignment markers (`---:`) in the header-separator line.",
 			parser: func(args []string, argc int, pargi *int, options *TOptions) {
 				options.WriterOptions.RightAlignNumericOutput = true
 				*pargi += 1
diff --git a/pkg/output/record_writer_markdown.go b/pkg/output/record_writer_markdown.go
index 51cfa971a..f60fd7a2c 100644
--- a/pkg/output/record_writer_markdown.go
+++ b/pkg/output/record_writer_markdown.go
@@ -181,12 +181,19 @@ func (writer *RecordWriterMarkdown) flushBatch(
 		}
 	}
 
-	// Header
+	// Header. Right-aligned columns get right-justified header text so that
+	// header and data share the same alignment in the raw markdown, matching
+	// how Markdown viewers render the `---:` marker.
 	bufferedOutputStream.WriteString("|")
 	for pe := first.Head; pe != nil; pe = pe.Next {
 		bufferedOutputStream.WriteString(" ")
-		bufferedOutputStream.WriteString(colorizer.MaybeColorizeKey(pe.Key, outputIsStdout))
-		writePadding(bufferedOutputStream, maxWidths[pe.Key]-lib.DisplayWidth(pe.Key))
+		if columnRightAligned[pe.Key] {
+			writePadding(bufferedOutputStream, maxWidths[pe.Key]-lib.DisplayWidth(pe.Key))
+			bufferedOutputStream.WriteString(colorizer.MaybeColorizeKey(pe.Key, outputIsStdout))
+		} else {
+			bufferedOutputStream.WriteString(colorizer.MaybeColorizeKey(pe.Key, outputIsStdout))
+			writePadding(bufferedOutputStream, maxWidths[pe.Key]-lib.DisplayWidth(pe.Key))
+		}
 		bufferedOutputStream.WriteString(" |")
 	}
 	bufferedOutputStream.WriteString(writer.writerOptions.ORS)
diff --git a/pkg/output/record_writer_pprint.go b/pkg/output/record_writer_pprint.go
index cd1718be0..47d1afa79 100644
--- a/pkg/output/record_writer_pprint.go
+++ b/pkg/output/record_writer_pprint.go
@@ -122,14 +122,42 @@ func (writer *RecordWriterPPRINT) writeHeterogenousList(
 			maxWidths[key] = width
 		}
 	}
+	rightAlignedHeaders := writer.computeRightAlignedHeaders(records)
 	if barred {
-		writer.writeHeterogenousListBarred(records, maxWidths, bufferedOutputStream, outputIsStdout)
+		writer.writeHeterogenousListBarred(records, maxWidths, rightAlignedHeaders,
+			bufferedOutputStream, outputIsStdout)
 	} else {
-		writer.writeHeterogenousListNonBarred(records, maxWidths, bufferedOutputStream, outputIsStdout)
+		writer.writeHeterogenousListNonBarred(records, maxWidths, rightAlignedHeaders,
+			bufferedOutputStream, outputIsStdout)
 	}
 	return true
 }
 
+// computeRightAlignedHeaders returns, for --right-align-numeric, the set of
+// columns whose every value in the batch is numeric. Headers over such
+// columns are right-aligned so that they line up with their data cells;
+// headers over mixed columns stay left-aligned. See issue #380.
+func (writer *RecordWriterPPRINT) computeRightAlignedHeaders(
+	records []*mlrval.Mlrmap,
+) map[string]bool {
+	rightAlignedHeaders := make(map[string]bool)
+	if !writer.writerOptions.RightAlignNumericOutput {
+		return rightAlignedHeaders
+	}
+	for _, outrec := range records {
+		for pe := outrec.Head; pe != nil; pe = pe.Next {
+			isNumeric := pe.Value.IsNumeric()
+			previous, seen := rightAlignedHeaders[pe.Key]
+			if !seen {
+				rightAlignedHeaders[pe.Key] = isNumeric
+			} else {
+				rightAlignedHeaders[pe.Key] = previous && isNumeric
+			}
+		}
+	}
+	return rightAlignedHeaders
+}
+
 // Example:
 //
 // a   b   i  x                    y
@@ -142,6 +170,7 @@ func (writer *RecordWriterPPRINT) writeHeterogenousList(
 func (writer *RecordWriterPPRINT) writeHeterogenousListNonBarred(
 	records []*mlrval.Mlrmap,
 	maxWidths map[string]int,
+	rightAlignedHeaders map[string]bool,
 	bufferedOutputStream *bufio.Writer,
 	outputIsStdout bool,
 ) {
@@ -152,7 +181,7 @@ func (writer *RecordWriterPPRINT) writeHeterogenousListNonBarred(
 		// Print header line
 		if onFirst && !writer.writerOptions.HeaderlessOutput {
 			for pe := outrec.Head; pe != nil; pe = pe.Next {
-				if !writer.writerOptions.RightAlignedPPRINTOutput { // left-align
+				if !writer.headerIsRightAligned(pe.Key, rightAlignedHeaders) { // left-align
 					if pe.Next != nil {
 						// Header line, left-align, not last column
 						bufferedOutputStream.WriteString(colorizer.MaybeColorizeKey(pe.Key, outputIsStdout))
@@ -247,6 +276,7 @@ type barredChars struct {
 func (writer *RecordWriterPPRINT) writeHeterogenousListBarred(
 	records []*mlrval.Mlrmap,
 	maxWidths map[string]int,
+	rightAlignedHeaders map[string]bool,
 	bufferedOutputStream *bufio.Writer,
 	outputIsStdout bool,
 ) {
@@ -311,7 +341,7 @@ func (writer *RecordWriterPPRINT) writeHeterogenousListBarred(
 
 			bufferedOutputStream.WriteString(bc.verticalStart)
 			for pe := outrec.Head; pe != nil; pe = pe.Next {
-				if !writer.writerOptions.RightAlignedPPRINTOutput { // left-align
+				if !writer.headerIsRightAligned(pe.Key, rightAlignedHeaders) { // left-align
 					bufferedOutputStream.WriteString(colorizer.MaybeColorizeKey(pe.Key, outputIsStdout))
 					writer.writePadding(pe.Key, maxWidths[pe.Key], bufferedOutputStream)
 				} else { // right-align
@@ -381,7 +411,7 @@ func (writer *RecordWriterPPRINT) writeHeterogenousListBarred(
 
 // cellIsRightAligned decides the alignment of a single data cell: everything
 // with --right; numeric values only with --right-align-numeric. Header cells
-// are not passed through here -- they right-align only with --right.
+// are not passed through here -- see headerIsRightAligned.
 func (writer *RecordWriterPPRINT) cellIsRightAligned(value *mlrval.Mlrval) bool {
 	if writer.writerOptions.RightAlignedPPRINTOutput {
 		return true
@@ -389,6 +419,20 @@ func (writer *RecordWriterPPRINT) cellIsRightAligned(value *mlrval.Mlrval) bool
 	return writer.writerOptions.RightAlignNumericOutput && value.IsNumeric()
 }
 
+// headerIsRightAligned decides the alignment of a header cell: everything
+// with --right; with --right-align-numeric, headers over all-numeric columns
+// (as precomputed by computeRightAlignedHeaders) so that header and data
+// share the same alignment.
+func (writer *RecordWriterPPRINT) headerIsRightAligned(
+	key string,
+	rightAlignedHeaders map[string]bool,
+) bool {
+	if writer.writerOptions.RightAlignedPPRINTOutput {
+		return true
+	}
+	return rightAlignedHeaders[key]
+}
+
 func (writer *RecordWriterPPRINT) writePadding(
 	text string,
 	fieldWidth int,
diff --git a/test/cases/io-markdown/0004/expout b/test/cases/io-markdown/0004/expout
index b206b3481..5b0727dfc 100644
--- a/test/cases/io-markdown/0004/expout
+++ b/test/cases/io-markdown/0004/expout
@@ -1,4 +1,4 @@
-| a   | b   | i     | x          |
+| a   | b   |     i | x          |
 | --- | --- |  ---: | ---        |
 | pan | pan |     1 | 0.34679000 |
 | eks | wye | 10000 | 0.75868000 |
diff --git a/test/cases/io-pprint-right-align-numeric/0001/expout b/test/cases/io-pprint-right-align-numeric/0001/expout
index 904116eb2..1f26a8e08 100644
--- a/test/cases/io-pprint-right-align-numeric/0001/expout
+++ b/test/cases/io-pprint-right-align-numeric/0001/expout
@@ -1,4 +1,4 @@
-a   b   i     x
+a   b       i x
 pan pan     1 0.34679000
 eks wye 10000 0.75868000
 wye hat    -3 N/A
diff --git a/test/cases/io-pprint-right-align-numeric/0002/expout b/test/cases/io-pprint-right-align-numeric/0002/expout
index db257f18f..b2bf8aa59 100644
--- a/test/cases/io-pprint-right-align-numeric/0002/expout
+++ b/test/cases/io-pprint-right-align-numeric/0002/expout
@@ -1,5 +1,5 @@
 +-----+-----+-------+------------+
-| a   | b   | i     | x          |
+| a   | b   |     i | x          |
 +-----+-----+-------+------------+
 | pan | pan |     1 | 0.34679000 |
 | eks | wye | 10000 | 0.75868000 |
diff --git a/test/cases/io-pprint-right-align-numeric/0003/cmd b/test/cases/io-pprint-right-align-numeric/0003/cmd
new file mode 100644
index 000000000..56d83155b
--- /dev/null
+++ b/test/cases/io-pprint-right-align-numeric/0003/cmd
@@ -0,0 +1 @@
+mlr --icsv --opprint --right-align-numeric cat ${CASEDIR}/input
diff --git a/test/cases/io-pprint-right-align-numeric/0003/experr b/test/cases/io-pprint-right-align-numeric/0003/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/io-pprint-right-align-numeric/0003/expout b/test/cases/io-pprint-right-align-numeric/0003/expout
new file mode 100644
index 000000000..8b16809b4
--- /dev/null
+++ b/test/cases/io-pprint-right-align-numeric/0003/expout
@@ -0,0 +1,3 @@
+name  count      ratio
+alpha     3 0.90000000
+b        14 0.05000000
diff --git a/test/cases/io-pprint-right-align-numeric/0003/input b/test/cases/io-pprint-right-align-numeric/0003/input
new file mode 100644
index 000000000..254256b7c
--- /dev/null
+++ b/test/cases/io-pprint-right-align-numeric/0003/input
@@ -0,0 +1,3 @@
+name,count,ratio
+alpha,3,0.9
+b,14,0.05
diff --git a/test/cases/io-pprint-right-align-numeric/0004/cmd b/test/cases/io-pprint-right-align-numeric/0004/cmd
new file mode 100644
index 000000000..ad12c83e8
--- /dev/null
+++ b/test/cases/io-pprint-right-align-numeric/0004/cmd
@@ -0,0 +1 @@
+mlr --icsv --opprint --barred --right-align-numeric cat ${CASEDIR}/input
diff --git a/test/cases/io-pprint-right-align-numeric/0004/experr b/test/cases/io-pprint-right-align-numeric/0004/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/io-pprint-right-align-numeric/0004/expout b/test/cases/io-pprint-right-align-numeric/0004/expout
new file mode 100644
index 000000000..64b7fe62f
--- /dev/null
+++ b/test/cases/io-pprint-right-align-numeric/0004/expout
@@ -0,0 +1,6 @@
++-------+-------+------------+
+| name  | count |      ratio |
++-------+-------+------------+
+| alpha |     3 | 0.90000000 |
+| b     |    14 | 0.05000000 |
++-------+-------+------------+
diff --git a/test/cases/io-pprint-right-align-numeric/0004/input b/test/cases/io-pprint-right-align-numeric/0004/input
new file mode 100644
index 000000000..254256b7c
--- /dev/null
+++ b/test/cases/io-pprint-right-align-numeric/0004/input
@@ -0,0 +1,3 @@
+name,count,ratio
+alpha,3,0.9
+b,14,0.05