From 3e429f5b42b02eff9dc4e769be1ae66665a5ae60 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Sun, 19 Apr 2026 10:21:51 -0400 Subject: [PATCH] Use `"\n"` in replace for `gsub` and `sub` (#2042) * Fix issue 1805 * Run `make dev` --- docs/src/manpage.md | 11 +++++-- docs/src/manpage.txt | 11 +++++-- docs/src/reference-verbs.md | 9 +++++- man/manpage.txt | 11 +++++-- man/mlr.1 | 13 ++++++-- pkg/transformers/subs.go | 30 ++++++++++++++++--- test/cases/cli-help/0001/expout | 9 +++++- .../non-windows/gsub-escape-tab/cmd | 1 + .../non-windows/gsub-escape-tab/experr | 0 .../non-windows/gsub-escape-tab/expout | 5 ++++ .../non-windows/ssub-escape-tab/cmd | 1 + .../non-windows/ssub-escape-tab/experr | 0 .../non-windows/ssub-escape-tab/expout | 5 ++++ .../non-windows/sub-escape-hex/cmd | 1 + .../non-windows/sub-escape-hex/experr | 0 .../non-windows/sub-escape-hex/expout | 5 ++++ .../sub-escape-literal-backslash/cmd | 1 + .../sub-escape-literal-backslash/experr | 0 .../sub-escape-literal-backslash/expout | 5 ++++ .../non-windows/sub-escape-newline/cmd | 1 + .../non-windows/sub-escape-newline/experr | 0 .../non-windows/sub-escape-newline/expout | 3 ++ 22 files changed, 107 insertions(+), 15 deletions(-) create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/cmd create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/experr create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/expout create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/cmd create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/experr create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/expout create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/cmd create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/experr create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/expout create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/cmd create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/experr create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/expout create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/cmd create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/experr create mode 100644 test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/expout diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 0c41c8b2f..99d89ad63 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -1333,6 +1333,8 @@ This is simply a copy of what you should see on running `man mlr` at a command p Usage: mlr gsub [options] Replaces old string with new string in specified field(s), with regex support for the old string and handling multiple matches, like the `gsub` DSL function. + The replacement string supports C-style backslash escapes such as \n, \t, + and \x1f. Write \\ to get a literal backslash. See also the `sub` and `ssub` verbs. Options: -f {a,b,c} Field names to convert. @@ -1980,7 +1982,10 @@ This is simply a copy of what you should see on running `man mlr` at a command p 1mssub0m Usage: mlr ssub [options] Replaces old string with new string in specified field(s), without regex support for - the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs. + the old string, like the `ssub` DSL function. + Both the search and replacement strings support C-style backslash escapes such + as \n, \t, and \x1f. Write \\ to get a literal backslash. + See also the `gsub` and `sub` verbs. Options: -f {a,b,c} Field names to convert. -r {regex} Regular expression for field names to convert. @@ -2129,6 +2134,8 @@ This is simply a copy of what you should see on running `man mlr` at a command p Usage: mlr sub [options] Replaces old string with new string in specified field(s), with regex support for the old string and not handling multiple matches, like the `sub` DSL function. + The replacement string supports C-style backslash escapes such as \n, \t, + and \x1f. Write \\ to get a literal backslash. See also the `gsub` and `ssub` verbs. Options: -f {a,b,c} Field names to convert. @@ -3823,5 +3830,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-04-08 4mMILLER24m(1) + 2026-04-19 4mMILLER24m(1) diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index ae9d9c087..f9b11f18e 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -1312,6 +1312,8 @@ Usage: mlr gsub [options] Replaces old string with new string in specified field(s), with regex support for the old string and handling multiple matches, like the `gsub` DSL function. + The replacement string supports C-style backslash escapes such as \n, \t, + and \x1f. Write \\ to get a literal backslash. See also the `sub` and `ssub` verbs. Options: -f {a,b,c} Field names to convert. @@ -1959,7 +1961,10 @@ 1mssub0m Usage: mlr ssub [options] Replaces old string with new string in specified field(s), without regex support for - the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs. + the old string, like the `ssub` DSL function. + Both the search and replacement strings support C-style backslash escapes such + as \n, \t, and \x1f. Write \\ to get a literal backslash. + See also the `gsub` and `sub` verbs. Options: -f {a,b,c} Field names to convert. -r {regex} Regular expression for field names to convert. @@ -2108,6 +2113,8 @@ Usage: mlr sub [options] Replaces old string with new string in specified field(s), with regex support for the old string and not handling multiple matches, like the `sub` DSL function. + The replacement string supports C-style backslash escapes such as \n, \t, + and \x1f. Write \\ to get a literal backslash. See also the `gsub` and `ssub` verbs. Options: -f {a,b,c} Field names to convert. @@ -3802,4 +3809,4 @@ MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite https://miller.readthedocs.io - 2026-04-08 4mMILLER24m(1) + 2026-04-19 4mMILLER24m(1) diff --git a/docs/src/reference-verbs.md b/docs/src/reference-verbs.md index 0c1d90ee5..ab8e0ba1b 100644 --- a/docs/src/reference-verbs.md +++ b/docs/src/reference-verbs.md @@ -1461,6 +1461,8 @@ record_count resource Usage: mlr gsub [options] Replaces old string with new string in specified field(s), with regex support for the old string and handling multiple matches, like the `gsub` DSL function. +The replacement string supports C-style backslash escapes such as \n, \t, +and \x1f. Write \\ to get a literal backslash. See also the `sub` and `ssub` verbs. Options: -f {a,b,c} Field names to convert. @@ -3219,7 +3221,10 @@ See also the "tee" DSL function which lets you do more ad-hoc customization.
 Usage: mlr ssub [options]
 Replaces old string with new string in specified field(s), without regex support for
-the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs.
+the old string, like the `ssub` DSL function.
+Both the search and replacement strings support C-style backslash escapes such
+as \n, \t, and \x1f. Write \\ to get a literal backslash.
+See also the `gsub` and `sub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
 -r {regex}  Regular expression for field names to convert.
@@ -3725,6 +3730,8 @@ $ each 10 uptime | mlr -p step -a delta -f 11
 Usage: mlr sub [options]
 Replaces old string with new string in specified field(s), with regex support
 for the old string and not handling multiple matches, like the `sub` DSL function.
+The replacement string supports C-style backslash escapes such as \n, \t,
+and \x1f. Write \\ to get a literal backslash.
 See also the `gsub` and `ssub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
diff --git a/man/manpage.txt b/man/manpage.txt
index ae9d9c087..f9b11f18e 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -1312,6 +1312,8 @@
        Usage: mlr gsub [options]
        Replaces old string with new string in specified field(s), with regex support
        for the old string and handling multiple matches, like the `gsub` DSL function.
+       The replacement string supports C-style backslash escapes such as \n, \t,
+       and \x1f. Write \\ to get a literal backslash.
        See also the `sub` and `ssub` verbs.
        Options:
        -f {a,b,c}  Field names to convert.
@@ -1959,7 +1961,10 @@
    1mssub0m
        Usage: mlr ssub [options]
        Replaces old string with new string in specified field(s), without regex support for
-       the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs.
+       the old string, like the `ssub` DSL function.
+       Both the search and replacement strings support C-style backslash escapes such
+       as \n, \t, and \x1f. Write \\ to get a literal backslash.
+       See also the `gsub` and `sub` verbs.
        Options:
        -f {a,b,c}  Field names to convert.
        -r {regex}  Regular expression for field names to convert.
@@ -2108,6 +2113,8 @@
        Usage: mlr sub [options]
        Replaces old string with new string in specified field(s), with regex support
        for the old string and not handling multiple matches, like the `sub` DSL function.
+       The replacement string supports C-style backslash escapes such as \n, \t,
+       and \x1f. Write \\ to get a literal backslash.
        See also the `gsub` and `ssub` verbs.
        Options:
        -f {a,b,c}  Field names to convert.
@@ -3802,4 +3809,4 @@
        MIME Type for Comma-Separated Values (CSV) Files, the Miller docsite
        https://miller.readthedocs.io
 
-                                  2026-04-08                         4mMILLER24m(1)
+                                  2026-04-19                         4mMILLER24m(1)
diff --git a/man/mlr.1 b/man/mlr.1
index a4b8980ec..ad35cd210 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-04-08
+.\"      Date: 2026-04-19
 .\"    Manual: \ \&
 .\"    Source: \ \&
 .\"  Language: English
 .\"
-.TH "MILLER" "1" "2026-04-08" "\ \&" "\ \&"
+.TH "MILLER" "1" "2026-04-19" "\ \&" "\ \&"
 .\" -----------------------------------------------------------------
 .\" * Portability definitions
 .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1631,6 +1631,8 @@ Options:
 Usage: mlr gsub [options]
 Replaces old string with new string in specified field(s), with regex support
 for the old string and handling multiple matches, like the `gsub` DSL function.
+The replacement string supports C-style backslash escapes such as \en, \et,
+and \ex1f. Write \e\e to get a literal backslash.
 See also the `sub` and `ssub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
@@ -2464,7 +2466,10 @@ See also the "tee" DSL function which lets you do more ad-hoc customization.
 .nf
 Usage: mlr ssub [options]
 Replaces old string with new string in specified field(s), without regex support for
-the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs.
+the old string, like the `ssub` DSL function.
+Both the search and replacement strings support C-style backslash escapes such
+as \en, \et, and \ex1f. Write \e\e to get a literal backslash.
+See also the `gsub` and `sub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
 -r {regex}  Regular expression for field names to convert.
@@ -2637,6 +2642,8 @@ for more information on EWMA.
 Usage: mlr sub [options]
 Replaces old string with new string in specified field(s), with regex support
 for the old string and not handling multiple matches, like the `sub` DSL function.
+The replacement string supports C-style backslash escapes such as \en, \et,
+and \ex1f. Write \e\e to get a literal backslash.
 See also the `gsub` and `ssub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
diff --git a/pkg/transformers/subs.go b/pkg/transformers/subs.go
index ca89466a8..bc5d00fa5 100644
--- a/pkg/transformers/subs.go
+++ b/pkg/transformers/subs.go
@@ -44,6 +44,8 @@ func transformerSubUsage(
 	fmt.Fprintf(o, "Usage: %s %s [options]\n", "mlr", verbNameSub)
 	fmt.Fprintf(o, "Replaces old string with new string in specified field(s), with regex support\n")
 	fmt.Fprintf(o, "for the old string and not handling multiple matches, like the `sub` DSL function.\n")
+	fmt.Fprintf(o, "The replacement string supports C-style backslash escapes such as \\n, \\t,\n")
+	fmt.Fprintf(o, "and \\x1f. Write \\\\ to get a literal backslash.\n")
 	fmt.Fprintf(o, "See also the `gsub` and `ssub` verbs.\n")
 	fmt.Fprintf(o, "Options:\n")
 	fmt.Fprintf(o, "-f {a,b,c}  Field names to convert.\n")
@@ -58,6 +60,8 @@ func transformerGsubUsage(
 	fmt.Fprintf(o, "Usage: %s %s [options]\n", "mlr", verbNameGsub)
 	fmt.Fprintf(o, "Replaces old string with new string in specified field(s), with regex support\n")
 	fmt.Fprintf(o, "for the old string and handling multiple matches, like the `gsub` DSL function.\n")
+	fmt.Fprintf(o, "The replacement string supports C-style backslash escapes such as \\n, \\t,\n")
+	fmt.Fprintf(o, "and \\x1f. Write \\\\ to get a literal backslash.\n")
 	fmt.Fprintf(o, "See also the `sub` and `ssub` verbs.\n")
 	fmt.Fprintf(o, "Options:\n")
 	fmt.Fprintf(o, "-f {a,b,c}  Field names to convert.\n")
@@ -71,7 +75,10 @@ func transformerSsubUsage(
 ) {
 	fmt.Fprintf(o, "Usage: %s %s [options]\n", "mlr", verbNameSsub)
 	fmt.Fprintf(o, "Replaces old string with new string in specified field(s), without regex support for\n")
-	fmt.Fprintf(o, "the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs.\n")
+	fmt.Fprintf(o, "the old string, like the `ssub` DSL function.\n")
+	fmt.Fprintf(o, "Both the search and replacement strings support C-style backslash escapes such\n")
+	fmt.Fprintf(o, "as \\n, \\t, and \\x1f. Write \\\\ to get a literal backslash.\n")
+	fmt.Fprintf(o, "See also the `gsub` and `sub` verbs.\n")
 	fmt.Fprintf(o, "Options:\n")
 	fmt.Fprintf(o, "-f {a,b,c}  Field names to convert.\n")
 	fmt.Fprintf(o, "-r {regex}  Regular expression for field names to convert.\n")
@@ -98,7 +105,7 @@ func transformerSubParseCLI(
 	opts *cli.TOptions,
 	doConstruct bool, // false for first pass of CLI-parse, true for second pass
 ) (RecordTransformer, error) {
-	return transformerSubsParseCLI(pargi, argc, args, opts, doConstruct, transformerSubUsage, NewTransformerSub)
+	return transformerSubsParseCLI(pargi, argc, args, opts, doConstruct, transformerSubUsage, NewTransformerSub, false)
 }
 
 func transformerGsubParseCLI(
@@ -108,7 +115,7 @@ func transformerGsubParseCLI(
 	opts *cli.TOptions,
 	doConstruct bool, // false for first pass of CLI-parse, true for second pass
 ) (RecordTransformer, error) {
-	return transformerSubsParseCLI(pargi, argc, args, opts, doConstruct, transformerGsubUsage, NewTransformerGsub)
+	return transformerSubsParseCLI(pargi, argc, args, opts, doConstruct, transformerGsubUsage, NewTransformerGsub, false)
 }
 
 func transformerSsubParseCLI(
@@ -118,10 +125,12 @@ func transformerSsubParseCLI(
 	opts *cli.TOptions,
 	doConstruct bool, // false for first pass of CLI-parse, true for second pass
 ) (RecordTransformer, error) {
-	return transformerSubsParseCLI(pargi, argc, args, opts, doConstruct, transformerSsubUsage, NewTransformerSsub)
+	return transformerSubsParseCLI(pargi, argc, args, opts, doConstruct, transformerSsubUsage, NewTransformerSsub, true)
 }
 
 // transformerSubsParseCLI is a shared CLI-parser for the sub, gsub, and ssub verbs.
+// When unbackslashOldText is true (ssub only), the search string is also unescaped;
+// for sub/gsub the search string is a regex and Go's regexp engine handles \n/\t/etc.
 func transformerSubsParseCLI(
 	pargi *int,
 	argc int,
@@ -130,6 +139,7 @@ func transformerSubsParseCLI(
 	doConstruct bool, // false for first pass of CLI-parse, true for second pass
 	usageFunc TransformerUsageFunc,
 	constructorFunc subConstructorFunc,
+	unbackslashOldText bool,
 ) (RecordTransformer, error) {
 
 	// Skip the verb name from the current spot in the mlr command line
@@ -192,6 +202,18 @@ func transformerSubsParseCLI(
 	oldText = args[argi]
 	newText = args[argi+1]
 
+	// Interpret C-style backslash escapes ("\n", "\t", "\x1f", etc.) in the
+	// replacement string the same way the DSL string-literal parser does, so
+	// that e.g. `mlr sub -a r "\n"` matches `sub($x, "r", "\n")` in the DSL.
+	// For sub/gsub the search string is a regex and Go's regexp engine already
+	// handles \n/\r/\t inside patterns; pre-unescaping would corrupt user-
+	// supplied regex metachars like \d or \s. For ssub the search string is a
+	// literal, so we unescape it too.
+	newText = lib.UnbackslashStringLiteral(newText)
+	if unbackslashOldText {
+		oldText = lib.UnbackslashStringLiteral(oldText)
+	}
+
 	argi += 2
 
 	*pargi = argi
diff --git a/test/cases/cli-help/0001/expout b/test/cases/cli-help/0001/expout
index af5b35ee7..af9c48619 100644
--- a/test/cases/cli-help/0001/expout
+++ b/test/cases/cli-help/0001/expout
@@ -389,6 +389,8 @@ gsub
 Usage: mlr gsub [options]
 Replaces old string with new string in specified field(s), with regex support
 for the old string and handling multiple matches, like the `gsub` DSL function.
+The replacement string supports C-style backslash escapes such as \n, \t,
+and \x1f. Write \\ to get a literal backslash.
 See also the `sub` and `ssub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
@@ -1067,7 +1069,10 @@ See also the "tee" DSL function which lets you do more ad-hoc customization.
 ssub
 Usage: mlr ssub [options]
 Replaces old string with new string in specified field(s), without regex support for
-the old string, like the `ssub` DSL function. See also the `gsub` and `sub` verbs.
+the old string, like the `ssub` DSL function.
+Both the search and replacement strings support C-style backslash escapes such
+as \n, \t, and \x1f. Write \\ to get a literal backslash.
+See also the `gsub` and `sub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
 -r {regex}  Regular expression for field names to convert.
@@ -1220,6 +1225,8 @@ sub
 Usage: mlr sub [options]
 Replaces old string with new string in specified field(s), with regex support
 for the old string and not handling multiple matches, like the `sub` DSL function.
+The replacement string supports C-style backslash escapes such as \n, \t,
+and \x1f. Write \\ to get a literal backslash.
 See also the `gsub` and `ssub` verbs.
 Options:
 -f {a,b,c}  Field names to convert.
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/cmd b/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/cmd
new file mode 100644
index 000000000..2941a6472
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/cmd
@@ -0,0 +1 @@
+printf 'a=lorem\n' | mlr --ojson gsub -a '[lm]' '\t'
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/experr b/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/expout b/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/expout
new file mode 100644
index 000000000..a18d4ac5f
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/gsub-escape-tab/expout
@@ -0,0 +1,5 @@
+[
+{
+  "a": "\tore\t"
+}
+]
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/cmd b/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/cmd
new file mode 100644
index 000000000..0151ff41f
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/cmd
@@ -0,0 +1 @@
+printf 'a=X\tY\n' | mlr --ojson ssub -a '\t' '-'
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/experr b/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/expout b/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/expout
new file mode 100644
index 000000000..456fce2ab
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/ssub-escape-tab/expout
@@ -0,0 +1,5 @@
+[
+{
+  "a": "X-Y"
+}
+]
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/cmd b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/cmd
new file mode 100644
index 000000000..59f62ef07
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/cmd
@@ -0,0 +1 @@
+printf 'a=lorem\n' | mlr --ojson sub -a 'r' '\x41'
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/experr b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/expout b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/expout
new file mode 100644
index 000000000..1e652cb51
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-hex/expout
@@ -0,0 +1,5 @@
+[
+{
+  "a": "loAem"
+}
+]
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/cmd b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/cmd
new file mode 100644
index 000000000..d540feb5c
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/cmd
@@ -0,0 +1 @@
+printf 'a=lorem\n' | mlr --ojson sub -a 'r' '\\n'
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/experr b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/expout b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/expout
new file mode 100644
index 000000000..901f56809
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-literal-backslash/expout
@@ -0,0 +1,5 @@
+[
+{
+  "a": "lo\\nem"
+}
+]
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/cmd b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/cmd
new file mode 100644
index 000000000..91d4d0ccb
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/cmd
@@ -0,0 +1 @@
+printf 'a=lorem\n' | mlr --ocsv sub -a 'r' '\n'
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/experr b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/expout b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/expout
new file mode 100644
index 000000000..c3ca45413
--- /dev/null
+++ b/test/cases/verb-sub-gsub-ssub/non-windows/sub-escape-newline/expout
@@ -0,0 +1,3 @@
+a
+"lo
+em"