From a0048f03935e2a44d666e418f4e49150136679ba Mon Sep 17 00:00:00 2001 From: John Kerl Date: Wed, 12 Jan 2022 22:40:59 -0500 Subject: [PATCH] New format DSL function (#869) * New format DSL function * Updated affected test cases involving on-line help on "for" prefix * doc-build artifacts for previous commit * regression-test cases --- docs/src/manpage.md | 13 +++-- docs/src/manpage.txt | 13 +++-- docs/src/reference-dsl-builtin-functions.md | 12 ++++- internal/pkg/bifs/strings.go | 48 +++++++++++++++++++ .../pkg/dsl/cst/builtin_function_manager.go | 13 +++++ man/manpage.txt | 13 +++-- man/mlr.1 | 19 ++++++-- test/cases/dsl-format/0001/cmd | 1 + test/cases/dsl-format/0001/experr | 0 test/cases/dsl-format/0001/expout | 1 + test/cases/dsl-format/0001/mlr | 3 ++ test/cases/dsl-format/0002/cmd | 1 + test/cases/dsl-format/0002/experr | 0 test/cases/dsl-format/0002/expout | 1 + test/cases/dsl-format/0002/mlr | 3 ++ test/cases/dsl-format/0003/cmd | 1 + test/cases/dsl-format/0003/experr | 0 test/cases/dsl-format/0003/expout | 1 + test/cases/dsl-format/0003/mlr | 3 ++ test/cases/dsl-format/0004/cmd | 1 + test/cases/dsl-format/0004/experr | 0 test/cases/dsl-format/0004/expout | 1 + test/cases/dsl-format/0004/mlr | 3 ++ test/cases/dsl-format/0005/cmd | 1 + test/cases/dsl-format/0005/experr | 0 test/cases/dsl-format/0005/expout | 1 + test/cases/dsl-format/0005/mlr | 3 ++ test/cases/dsl-format/0006/cmd | 1 + test/cases/dsl-format/0006/experr | 0 test/cases/dsl-format/0006/expout | 1 + test/cases/dsl-format/0006/mlr | 3 ++ test/cases/dsl-format/0007/cmd | 1 + test/cases/dsl-format/0007/experr | 0 test/cases/dsl-format/0007/expout | 1 + test/cases/dsl-format/0007/mlr | 3 ++ test/cases/dsl-format/0008/cmd | 1 + test/cases/dsl-format/0008/experr | 0 test/cases/dsl-format/0008/expout | 1 + test/cases/dsl-format/0008/mlr | 3 ++ test/cases/dsl-format/0009/cmd | 1 + test/cases/dsl-format/0009/experr | 0 test/cases/dsl-format/0009/expout | 1 + test/cases/dsl-format/0009/mlr | 3 ++ test/cases/dsl-format/0010/cmd | 1 + test/cases/dsl-format/0010/experr | 0 test/cases/dsl-format/0010/expout | 1 + test/cases/dsl-format/0010/mlr | 3 ++ test/cases/help/0012/expout | 5 ++ test/cases/repl-help/0012/expout | 5 ++ todo.txt | 2 + 50 files changed, 180 insertions(+), 13 deletions(-) create mode 100644 test/cases/dsl-format/0001/cmd create mode 100644 test/cases/dsl-format/0001/experr create mode 100644 test/cases/dsl-format/0001/expout create mode 100644 test/cases/dsl-format/0001/mlr create mode 100644 test/cases/dsl-format/0002/cmd create mode 100644 test/cases/dsl-format/0002/experr create mode 100644 test/cases/dsl-format/0002/expout create mode 100644 test/cases/dsl-format/0002/mlr create mode 100644 test/cases/dsl-format/0003/cmd create mode 100644 test/cases/dsl-format/0003/experr create mode 100644 test/cases/dsl-format/0003/expout create mode 100644 test/cases/dsl-format/0003/mlr create mode 100644 test/cases/dsl-format/0004/cmd create mode 100644 test/cases/dsl-format/0004/experr create mode 100644 test/cases/dsl-format/0004/expout create mode 100644 test/cases/dsl-format/0004/mlr create mode 100644 test/cases/dsl-format/0005/cmd create mode 100644 test/cases/dsl-format/0005/experr create mode 100644 test/cases/dsl-format/0005/expout create mode 100644 test/cases/dsl-format/0005/mlr create mode 100644 test/cases/dsl-format/0006/cmd create mode 100644 test/cases/dsl-format/0006/experr create mode 100644 test/cases/dsl-format/0006/expout create mode 100644 test/cases/dsl-format/0006/mlr create mode 100644 test/cases/dsl-format/0007/cmd create mode 100644 test/cases/dsl-format/0007/experr create mode 100644 test/cases/dsl-format/0007/expout create mode 100644 test/cases/dsl-format/0007/mlr create mode 100644 test/cases/dsl-format/0008/cmd create mode 100644 test/cases/dsl-format/0008/experr create mode 100644 test/cases/dsl-format/0008/expout create mode 100644 test/cases/dsl-format/0008/mlr create mode 100644 test/cases/dsl-format/0009/cmd create mode 100644 test/cases/dsl-format/0009/experr create mode 100644 test/cases/dsl-format/0009/expout create mode 100644 test/cases/dsl-format/0009/mlr create mode 100644 test/cases/dsl-format/0010/cmd create mode 100644 test/cases/dsl-format/0010/experr create mode 100644 test/cases/dsl-format/0010/expout create mode 100644 test/cases/dsl-format/0010/mlr diff --git a/docs/src/manpage.md b/docs/src/manpage.md index 573371750..fb9508a81 100644 --- a/docs/src/manpage.md +++ b/docs/src/manpage.md @@ -207,9 +207,9 @@ FUNCTION LIST asserting_present asserting_string atan atan2 atanh bitcount boolean capitalize cbrt ceil clean_whitespace collapse_whitespace concat cos cosh depth dhms2fsec dhms2sec erf erfc every exp expm1 flatten float floor fmtnum - fold fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub haskey - hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array is_bool - is_boolean is_empty is_empty_map is_error is_float is_int is_map + fold format fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub + haskey hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array + is_bool is_boolean is_empty is_empty_map is_error is_float is_int is_map is_nonempty_map is_not_array is_not_empty is_not_map is_not_null is_null is_numeric is_present is_string joink joinkv joinv json_parse json_stringify leafcount length localtime2gmt localtime2sec log log10 log1p logifit lstrip @@ -2139,6 +2139,13 @@ FUNCTIONS FOR FILTER/PUT Array example: fold([1,2,3,4,5], func(acc,e) {return acc + e**3}, 10000) returns 10225. Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": accv+ev**2}}, {"sum":10000}) returns 10035. + format + (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. + Examples: + format("{}:{}:{}", 1,2) gives "1:2:". + format("{}:{}:{}", 1,2,3) gives "1:2:3". + format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + fsec2dhms (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s" diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index 9cb512195..fa2ae8ead 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -186,9 +186,9 @@ FUNCTION LIST asserting_present asserting_string atan atan2 atanh bitcount boolean capitalize cbrt ceil clean_whitespace collapse_whitespace concat cos cosh depth dhms2fsec dhms2sec erf erfc every exp expm1 flatten float floor fmtnum - fold fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub haskey - hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array is_bool - is_boolean is_empty is_empty_map is_error is_float is_int is_map + fold format fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub + haskey hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array + is_bool is_boolean is_empty is_empty_map is_error is_float is_int is_map is_nonempty_map is_not_array is_not_empty is_not_map is_not_null is_null is_numeric is_present is_string joink joinkv joinv json_parse json_stringify leafcount length localtime2gmt localtime2sec log log10 log1p logifit lstrip @@ -2118,6 +2118,13 @@ FUNCTIONS FOR FILTER/PUT Array example: fold([1,2,3,4,5], func(acc,e) {return acc + e**3}, 10000) returns 10225. Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": accv+ev**2}}, {"sum":10000}) returns 10035. + format + (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded. + Examples: + format("{}:{}:{}", 1,2) gives "1:2:". + format("{}:{}:{}", 1,2,3) gives "1:2:3". + format("{}:{}:{}", 1,2,3,4) gives "1:2:3". + fsec2dhms (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s" diff --git a/docs/src/reference-dsl-builtin-functions.md b/docs/src/reference-dsl-builtin-functions.md index 0941efbcd..6bb13f158 100644 --- a/docs/src/reference-dsl-builtin-functions.md +++ b/docs/src/reference-dsl-builtin-functions.md @@ -74,7 +74,7 @@ is 2. Unary operators such as `!` and `~` show argument-count of 1; the ternary * [**Hashing functions**](#hashing-functions): [md5](#md5), [sha1](#sha1), [sha256](#sha256), [sha512](#sha512). * [**Higher-order-functions functions**](#higher-order-functions-functions): [any](#any), [apply](#apply), [every](#every), [fold](#fold), [reduce](#reduce), [select](#select), [sort](#sort). * [**Math functions**](#math-functions): [abs](#abs), [acos](#acos), [acosh](#acosh), [asin](#asin), [asinh](#asinh), [atan](#atan), [atan2](#atan2), [atanh](#atanh), [cbrt](#cbrt), [ceil](#ceil), [cos](#cos), [cosh](#cosh), [erf](#erf), [erfc](#erfc), [exp](#exp), [expm1](#expm1), [floor](#floor), [invqnorm](#invqnorm), [log](#log), [log10](#log10), [log1p](#log1p), [logifit](#logifit), [max](#max), [min](#min), [qnorm](#qnorm), [round](#round), [roundm](#roundm), [sgn](#sgn), [sin](#sin), [sinh](#sinh), [sqrt](#sqrt), [tan](#tan), [tanh](#tanh), [urand](#urand), [urand32](#urand32), [urandelement](#urandelement), [urandint](#urandint), [urandrange](#urandrange). -* [**String functions**](#string-functions): [capitalize](#capitalize), [clean_whitespace](#clean_whitespace), [collapse_whitespace](#collapse_whitespace), [gsub](#gsub), [lstrip](#lstrip), [regextract](#regextract), [regextract_or_else](#regextract_or_else), [rstrip](#rstrip), [ssub](#ssub), [strip](#strip), [strlen](#strlen), [sub](#sub), [substr](#substr), [substr0](#substr0), [substr1](#substr1), [tolower](#tolower), [toupper](#toupper), [truncate](#truncate), [\.](#dot). +* [**String functions**](#string-functions): [capitalize](#capitalize), [clean_whitespace](#clean_whitespace), [collapse_whitespace](#collapse_whitespace), [format](#format), [gsub](#gsub), [lstrip](#lstrip), [regextract](#regextract), [regextract_or_else](#regextract_or_else), [rstrip](#rstrip), [ssub](#ssub), [strip](#strip), [strlen](#strlen), [sub](#sub), [substr](#substr), [substr0](#substr0), [substr1](#substr1), [tolower](#tolower), [toupper](#toupper), [truncate](#truncate), [\.](#dot). * [**System functions**](#system-functions): [hostname](#hostname), [os](#os), [system](#system), [version](#version). * [**Time functions**](#time-functions): [dhms2fsec](#dhms2fsec), [dhms2sec](#dhms2sec), [fsec2dhms](#fsec2dhms), [fsec2hms](#fsec2hms), [gmt2localtime](#gmt2localtime), [gmt2sec](#gmt2sec), [hms2fsec](#hms2fsec), [hms2sec](#hms2sec), [localtime2gmt](#localtime2gmt), [localtime2sec](#localtime2sec), [sec2dhms](#sec2dhms), [sec2gmt](#sec2gmt), [sec2gmtdate](#sec2gmtdate), [sec2hms](#sec2hms), [sec2localdate](#sec2localdate), [sec2localtime](#sec2localtime), [strftime](#strftime), [strftime_local](#strftime_local), [strptime](#strptime), [strptime_local](#strptime_local), [systime](#systime), [systimeint](#systimeint), [uptime](#uptime). * [**Typing functions**](#typing-functions): [asserting_absent](#asserting_absent), [asserting_array](#asserting_array), [asserting_bool](#asserting_bool), [asserting_boolean](#asserting_boolean), [asserting_empty](#asserting_empty), [asserting_empty_map](#asserting_empty_map), [asserting_error](#asserting_error), [asserting_float](#asserting_float), [asserting_int](#asserting_int), [asserting_map](#asserting_map), [asserting_nonempty_map](#asserting_nonempty_map), [asserting_not_array](#asserting_not_array), [asserting_not_empty](#asserting_not_empty), [asserting_not_map](#asserting_not_map), [asserting_not_null](#asserting_not_null), [asserting_null](#asserting_null), [asserting_numeric](#asserting_numeric), [asserting_present](#asserting_present), [asserting_string](#asserting_string), [is_absent](#is_absent), [is_array](#is_array), [is_bool](#is_bool), [is_boolean](#is_boolean), [is_empty](#is_empty), [is_empty_map](#is_empty_map), [is_error](#is_error), [is_float](#is_float), [is_int](#is_int), [is_map](#is_map), [is_nonempty_map](#is_nonempty_map), [is_not_array](#is_not_array), [is_not_empty](#is_not_empty), [is_not_map](#is_not_map), [is_not_null](#is_not_null), [is_null](#is_null), [is_numeric](#is_numeric), [is_present](#is_present), [is_string](#is_string), [typeof](#typeof). @@ -927,6 +927,16 @@ collapse_whitespace (class=string #args=1) Strip repeated whitespace from strin +### format +
+format  (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.
+Examples:
+format("{}:{}:{}", 1,2)     gives "1:2:".
+format("{}:{}:{}", 1,2,3)   gives "1:2:3".
+format("{}:{}:{}", 1,2,3,4) gives "1:2:3".
+
+ + ### gsub
 gsub  (class=string #args=3) '$name=gsub($name, "old", "new")' (replace all).
diff --git a/internal/pkg/bifs/strings.go b/internal/pkg/bifs/strings.go
index 4c9c30a83..4fd17f062 100644
--- a/internal/pkg/bifs/strings.go
+++ b/internal/pkg/bifs/strings.go
@@ -1,6 +1,7 @@
 package bifs
 
 import (
+	"bytes"
 	"regexp"
 	"strconv"
 	"strings"
@@ -287,6 +288,53 @@ func BIF_clean_whitespace(input1 *mlrval.Mlrval) *mlrval.Mlrval {
 	)
 }
 
+// ================================================================
+func BIF_format(mlrvals []*mlrval.Mlrval) *mlrval.Mlrval {
+	if len(mlrvals) == 0 {
+		return mlrval.VOID
+	}
+	formatString, ok := mlrvals[0].GetStringValue()
+	if !ok { // not a string
+		return mlrval.ERROR
+	}
+
+	pieces := lib.SplitString(formatString, "{}")
+
+	var buffer bytes.Buffer
+
+	// Example: format("{}:{}", 8, 9)
+	//
+	// * piece[0] ""
+	// * piece[1] ":"
+	// * piece[2] ""
+	// * mlrval[1] 8
+	// * mlrval[2] 9
+	//
+	// So:
+	// * Write piece[0]
+	// * Write mlrvals[1]
+	// * Write piece[1]
+	// * Write mlrvals[2]
+	// * Write piece[2]
+
+	// Q: What if too few arguments for format?
+	// A: Leave them off
+	// Q: What if too many arguments for format?
+	// A: Leave them off
+
+	n := len(mlrvals)
+	for i, piece := range pieces {
+		if i > 0 {
+			if i < n {
+				buffer.WriteString(mlrvals[i].String())
+			}
+		}
+		buffer.WriteString(piece)
+	}
+
+	return mlrval.FromString(buffer.String())
+}
+
 // ================================================================
 func BIF_hexfmt(input1 *mlrval.Mlrval) *mlrval.Mlrval {
 	if input1.IsInt() {
diff --git a/internal/pkg/dsl/cst/builtin_function_manager.go b/internal/pkg/dsl/cst/builtin_function_manager.go
index 6b8da6085..287c76e47 100644
--- a/internal/pkg/dsl/cst/builtin_function_manager.go
+++ b/internal/pkg/dsl/cst/builtin_function_manager.go
@@ -510,6 +510,19 @@ Arrays are new in Miller 6; the substr function is older.`,
 			binaryFunc: bifs.BIF_truncate,
 		},
 
+		{
+			name:  "format",
+			class: FUNC_CLASS_STRING,
+			help: `Using first argument as format string, interpolate remaining arguments in place of
+each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.`,
+			examples: []string{
+				`format("{}:{}:{}", 1,2)     gives "1:2:".`,
+				`format("{}:{}:{}", 1,2,3)   gives "1:2:3".`,
+				`format("{}:{}:{}", 1,2,3,4) gives "1:2:3".`,
+			},
+			variadicFunc: bifs.BIF_format,
+		},
+
 		// ----------------------------------------------------------------
 		// FUNC_CLASS_HASHING
 
diff --git a/man/manpage.txt b/man/manpage.txt
index 9cb512195..fa2ae8ead 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -186,9 +186,9 @@ FUNCTION LIST
        asserting_present asserting_string atan atan2 atanh bitcount boolean
        capitalize cbrt ceil clean_whitespace collapse_whitespace concat cos cosh
        depth dhms2fsec dhms2sec erf erfc every exp expm1 flatten float floor fmtnum
-       fold fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub haskey
-       hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array is_bool
-       is_boolean is_empty is_empty_map is_error is_float is_int is_map
+       fold format fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub
+       haskey hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array
+       is_bool is_boolean is_empty is_empty_map is_error is_float is_int is_map
        is_nonempty_map is_not_array is_not_empty is_not_map is_not_null is_null
        is_numeric is_present is_string joink joinkv joinv json_parse json_stringify
        leafcount length localtime2gmt localtime2sec log log10 log1p logifit lstrip
@@ -2118,6 +2118,13 @@ FUNCTIONS FOR FILTER/PUT
        Array example: fold([1,2,3,4,5], func(acc,e) {return acc + e**3}, 10000) returns 10225.
        Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum": accv+ev**2}}, {"sum":10000}) returns 10035.
 
+   format
+        (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.
+       Examples:
+       format("{}:{}:{}", 1,2)     gives "1:2:".
+       format("{}:{}:{}", 1,2,3)   gives "1:2:3".
+       format("{}:{}:{}", 1,2,3,4) gives "1:2:3".
+
    fsec2dhms
         (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s"
 
diff --git a/man/mlr.1 b/man/mlr.1
index c4a90961e..13ad7ae79 100644
--- a/man/mlr.1
+++ b/man/mlr.1
@@ -233,9 +233,9 @@ asserting_not_map asserting_not_null asserting_null asserting_numeric
 asserting_present asserting_string atan atan2 atanh bitcount boolean
 capitalize cbrt ceil clean_whitespace collapse_whitespace concat cos cosh
 depth dhms2fsec dhms2sec erf erfc every exp expm1 flatten float floor fmtnum
-fold fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub haskey
-hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array is_bool
-is_boolean is_empty is_empty_map is_error is_float is_int is_map
+fold format fsec2dhms fsec2hms get_keys get_values gmt2localtime gmt2sec gsub
+haskey hexfmt hms2fsec hms2sec hostname int invqnorm is_absent is_array
+is_bool is_boolean is_empty is_empty_map is_error is_float is_int is_map
 is_nonempty_map is_not_array is_not_empty is_not_map is_not_null is_null
 is_numeric is_present is_string joink joinkv joinv json_parse json_stringify
 leafcount length localtime2gmt localtime2sec log log10 log1p logifit lstrip
@@ -2963,6 +2963,19 @@ Map example: fold({"a":1, "b":3, "c": 5}, func(acck,accv,ek,ev) {return {"sum":
 .fi
 .if n \{\
 .RE
+.SS "format"
+.if n \{\
+.RS 0
+.\}
+.nf
+ (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.
+Examples:
+format("{}:{}:{}", 1,2)     gives "1:2:".
+format("{}:{}:{}", 1,2,3)   gives "1:2:3".
+format("{}:{}:{}", 1,2,3,4) gives "1:2:3".
+.fi
+.if n \{\
+.RE
 .SS "fsec2dhms"
 .if n \{\
 .RS 0
diff --git a/test/cases/dsl-format/0001/cmd b/test/cases/dsl-format/0001/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0001/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0001/experr b/test/cases/dsl-format/0001/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0001/expout b/test/cases/dsl-format/0001/expout
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/test/cases/dsl-format/0001/expout
@@ -0,0 +1 @@
+
diff --git a/test/cases/dsl-format/0001/mlr b/test/cases/dsl-format/0001/mlr
new file mode 100644
index 000000000..05eda28c0
--- /dev/null
+++ b/test/cases/dsl-format/0001/mlr
@@ -0,0 +1,3 @@
+end {
+  print format()
+}
diff --git a/test/cases/dsl-format/0002/cmd b/test/cases/dsl-format/0002/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0002/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0002/experr b/test/cases/dsl-format/0002/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0002/expout b/test/cases/dsl-format/0002/expout
new file mode 100644
index 000000000..ffba14cee
--- /dev/null
+++ b/test/cases/dsl-format/0002/expout
@@ -0,0 +1 @@
+(error)
diff --git a/test/cases/dsl-format/0002/mlr b/test/cases/dsl-format/0002/mlr
new file mode 100644
index 000000000..0445d8d19
--- /dev/null
+++ b/test/cases/dsl-format/0002/mlr
@@ -0,0 +1,3 @@
+end {
+  print format(1)
+}
diff --git a/test/cases/dsl-format/0003/cmd b/test/cases/dsl-format/0003/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0003/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0003/experr b/test/cases/dsl-format/0003/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0003/expout b/test/cases/dsl-format/0003/expout
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/test/cases/dsl-format/0003/expout
@@ -0,0 +1 @@
+
diff --git a/test/cases/dsl-format/0003/mlr b/test/cases/dsl-format/0003/mlr
new file mode 100644
index 000000000..0c9977041
--- /dev/null
+++ b/test/cases/dsl-format/0003/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("")
+}
diff --git a/test/cases/dsl-format/0004/cmd b/test/cases/dsl-format/0004/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0004/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0004/experr b/test/cases/dsl-format/0004/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0004/expout b/test/cases/dsl-format/0004/expout
new file mode 100644
index 000000000..8baef1b4a
--- /dev/null
+++ b/test/cases/dsl-format/0004/expout
@@ -0,0 +1 @@
+abc
diff --git a/test/cases/dsl-format/0004/mlr b/test/cases/dsl-format/0004/mlr
new file mode 100644
index 000000000..6189e3692
--- /dev/null
+++ b/test/cases/dsl-format/0004/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("abc")
+}
diff --git a/test/cases/dsl-format/0005/cmd b/test/cases/dsl-format/0005/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0005/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0005/experr b/test/cases/dsl-format/0005/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0005/expout b/test/cases/dsl-format/0005/expout
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/test/cases/dsl-format/0005/expout
@@ -0,0 +1 @@
+
diff --git a/test/cases/dsl-format/0005/mlr b/test/cases/dsl-format/0005/mlr
new file mode 100644
index 000000000..d3aa5adcd
--- /dev/null
+++ b/test/cases/dsl-format/0005/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("{}")
+}
diff --git a/test/cases/dsl-format/0006/cmd b/test/cases/dsl-format/0006/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0006/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0006/experr b/test/cases/dsl-format/0006/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0006/expout b/test/cases/dsl-format/0006/expout
new file mode 100644
index 000000000..d00491fd7
--- /dev/null
+++ b/test/cases/dsl-format/0006/expout
@@ -0,0 +1 @@
+1
diff --git a/test/cases/dsl-format/0006/mlr b/test/cases/dsl-format/0006/mlr
new file mode 100644
index 000000000..3b97563a9
--- /dev/null
+++ b/test/cases/dsl-format/0006/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("{}", 1)
+}
diff --git a/test/cases/dsl-format/0007/cmd b/test/cases/dsl-format/0007/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0007/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0007/experr b/test/cases/dsl-format/0007/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0007/expout b/test/cases/dsl-format/0007/expout
new file mode 100644
index 000000000..d00491fd7
--- /dev/null
+++ b/test/cases/dsl-format/0007/expout
@@ -0,0 +1 @@
+1
diff --git a/test/cases/dsl-format/0007/mlr b/test/cases/dsl-format/0007/mlr
new file mode 100644
index 000000000..7925108eb
--- /dev/null
+++ b/test/cases/dsl-format/0007/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("{}", 1, 2)
+}
diff --git a/test/cases/dsl-format/0008/cmd b/test/cases/dsl-format/0008/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0008/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0008/experr b/test/cases/dsl-format/0008/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0008/expout b/test/cases/dsl-format/0008/expout
new file mode 100644
index 000000000..6f26419dd
--- /dev/null
+++ b/test/cases/dsl-format/0008/expout
@@ -0,0 +1 @@
+
diff --git a/test/cases/dsl-format/0008/mlr b/test/cases/dsl-format/0008/mlr
new file mode 100644
index 000000000..db3f2c42f
--- /dev/null
+++ b/test/cases/dsl-format/0008/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("<{}:{}>", "abc")
+}
diff --git a/test/cases/dsl-format/0009/cmd b/test/cases/dsl-format/0009/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0009/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0009/experr b/test/cases/dsl-format/0009/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0009/expout b/test/cases/dsl-format/0009/expout
new file mode 100644
index 000000000..441d571a9
--- /dev/null
+++ b/test/cases/dsl-format/0009/expout
@@ -0,0 +1 @@
+
diff --git a/test/cases/dsl-format/0009/mlr b/test/cases/dsl-format/0009/mlr
new file mode 100644
index 000000000..5ea69c557
--- /dev/null
+++ b/test/cases/dsl-format/0009/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("<{}:{}>", "abc", "def")
+}
diff --git a/test/cases/dsl-format/0010/cmd b/test/cases/dsl-format/0010/cmd
new file mode 100644
index 000000000..6add080d4
--- /dev/null
+++ b/test/cases/dsl-format/0010/cmd
@@ -0,0 +1 @@
+mlr -n put -f ${CASEDIR}/mlr
diff --git a/test/cases/dsl-format/0010/experr b/test/cases/dsl-format/0010/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/dsl-format/0010/expout b/test/cases/dsl-format/0010/expout
new file mode 100644
index 000000000..441d571a9
--- /dev/null
+++ b/test/cases/dsl-format/0010/expout
@@ -0,0 +1 @@
+
diff --git a/test/cases/dsl-format/0010/mlr b/test/cases/dsl-format/0010/mlr
new file mode 100644
index 000000000..1d11b5580
--- /dev/null
+++ b/test/cases/dsl-format/0010/mlr
@@ -0,0 +1,3 @@
+end {
+  print format("<{}:{}>", "abc", "def", "ghi")
+}
diff --git a/test/cases/help/0012/expout b/test/cases/help/0012/expout
index 245e2ab33..f3b0aa646 100644
--- a/test/cases/help/0012/expout
+++ b/test/cases/help/0012/expout
@@ -28,6 +28,11 @@ Options:
                     with s in them. Undefined behavior results otherwise.
 -n                  Coerce field values autodetected as int to float, and then
                     apply the float format.
+format  (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.
+Examples:
+format("{}:{}:{}", 1,2)     gives "1:2:".
+format("{}:{}:{}", 1,2,3)   gives "1:2:3".
+format("{}:{}:{}", 1,2,3,4) gives "1:2:3".
 for: defines a for-loop using one of three styles. The body statements must
 be wrapped in curly braces.
 For-loop over stream record:
diff --git a/test/cases/repl-help/0012/expout b/test/cases/repl-help/0012/expout
index 9f5c6b71f..0e4b952b0 100644
--- a/test/cases/repl-help/0012/expout
+++ b/test/cases/repl-help/0012/expout
@@ -13,3 +13,8 @@ For-loop over out-of-stream variables:
 C-style for-loop:
 
   Example:  'for (var i = 0, var b = 1; i < 10; i += 1, b *= 2) { ... }'
+format  (class=string #args=variadic) Using first argument as format string, interpolate remaining arguments in place of each "{}" in the format string. Too-few arguments are treated as the empty string; too-many arguments are discarded.
+Examples:
+format("{}:{}:{}", 1,2)     gives "1:2:".
+format("{}:{}:{}", 1,2,3)   gives "1:2:3".
+format("{}:{}:{}", 1,2,3,4) gives "1:2:3".
diff --git a/todo.txt b/todo.txt
index c6690657c..1acda5b61 100644
--- a/todo.txt
+++ b/todo.txt
@@ -80,6 +80,8 @@ UX
 ! bnf fix for '[[' ']]' etc -- make it a nesting of singles. since otherwise no '[[3,4]]' literals :(
 ! broadly rethink os.Exit, especially as affecting mlr repl
 
+* ?xyz and ??xyz in repl, for :help and :help find respectively
+
 * consider expanding '(error)' to have more useful error-text
 * sync-print option; or (yuck) another xprint variant; or ...; emph dump/eprint
 * strptime w/ ...00.Z -> error