Skip to content

Fmt

An immutable sequence of literal text, bound interpolations, and unbound parameters, produced by a t"..." string.

Every segment is a Str of literal text, a FmtValue interpolation, or a FmtParam still waiting to be filled. This type behaves like an immutable array: indexable, iterable, spreadable, and destructurable.

Trust

A sequence written as t"..." carries a guarantee: its Str segments are program text, and everything interpolated into it is a FmtValue or FmtParam. That is what lets a consumer treat the literal segments as the trusted skeleton of a command and each interpolation as data to be bound or quoted. sqlite is a prime example, as it relies on this guarantee to exclude SQL injection vulnerabilities.

Toward that end, no conversion implicitly expands a sequence. str and plain interpolation ("$seq", "${seq}", "${seq:s}") raise a TypeError; verbatim and dbg give the source form This avoids accidental expansion before the sequences reaches its consumer for safe structural quoting or binding of interpolations. format() must be used to explicitly expand a sequence.

Building a sequence yourself must uphold this guarantee. The constructor accepts whatever segments it is given, so a Str built from untrusted input would subsequently be treated as trusted. Programmatic construction of Fmt must be done with care.

# Fine: the query text is literal, the value is bound.
let query = t"select * from users where name = $name"

# Fine: assembled at runtime, but the untrusted value is still bound.
let assembled = Fmt ["select * from users where name = ", (FmtValue name)]

# Danger: untrusted input is treated as a trusted string literal
let injected = Fmt ["select * from users where name = ", name]

dbg gives the source form, reconstructed from the literal text and each interpolation's source. An interpolation built at runtime has no source, so its bound value's debug form stands in.

verbatim — and with it the ! conversion and command-argument position — gives the same source form, since that is what the sequence was written as. str and the display conversion raise a TypeError. format() is the only expansion. See Trust.

let count = 3
assert_eq (dbg t"n=${count:03d}") r#"t"n=${count:03d}""#
assert_eq (verbatim t"n=${count:03d}") (dbg t"n=${count:03d}")

Example

let user = "root"
let seq = t"user: ${user:>8}"

# Expanding gives what the equivalent `"..."` would have.
assert_eq $seq.format() "user:     root"

# The interpolated value is still there to be inspected.
assert_eq $seq[1].value $user
assert_eq $seq[1].width 8
Open in playground

Implements: Index[{...Int: FmtSegment, ...Range[Int]: Array[FmtSegment]}], Spread[FmtSegment], Unpack[FmtSegment]

Constructor

Fmt segments

Builds a sequence from an iterable of segments.

Writing a t"..." is the usual way to get one; the constructor is for assembling a sequence at runtime.

Errors

Exception Condition
TypeError A segment is not a Str, FmtValue, or FmtParam

Example

let built = Fmt ["a=", (FmtValue 42), ";"]
assert_eq $built.len 3
assert_eq $built.format() "a=42;"

Methods

(eq) other

Two sequences are equal when their segments are. How a sequence was assembled does not show. The text each interpolation records does: a segment carries its source, so t"$name" and t"${name}" are not equal.

(fmt)()

dbg gives the source form, reconstructed from the literal text and each interpolation's source. An interpolation built at runtime has no source, so its bound value's debug form stands in.

verbatim — and with it the ! conversion and command-argument position — gives the same source form, since that is what the sequence was written as. str and the display conversion raise a TypeError. format() is the only expansion. See Trust.

let count = 3
assert_eq (dbg t"n=${count:03d}") r#"t"n=${count:03d}""#
assert_eq (verbatim t"n=${count:03d}") (dbg t"n=${count:03d}")

(index) index

seq[i] returns segment i: a Str of literal text, a FmtValue, or a FmtParam.

let greeting = t"hello $name!"
assert_eq $greeting[0] "hello "
assert_eq $greeting[1].source r"$name"

Assigning to a segment raises an ImmutableError.

(iter)()

Iterating yields the segments in order, so a consumer can decide what to do with each.

for segment = greeting
  echo $ type $segment

The console is the worked example of such a consumer: term walks a sequence segment by segment rather than converting it, which is what lets styling interpolated into one survive to the terminal.

format ...bindings -> Str

Expands the sequence and returns the result: literal text as it stands, and each interpolation through its own specification. This is the only way to get the expansion — see Trust.

Given arguments, it fills the parameters first. Positional argument i fills ${#i}, and a keyword argument fills ${#name}:

  • Holes inside a bound Fmt are filled too, depth first. Names are never renumbered, so a nested ${#0} is filled by argument 0 as well.
  • A filled hole keeps the parameter's specification and source.
  • A hole left unfilled, or an argument no hole consumed, is an error.

Errors

Exception Condition
MissingPosError A numbered parameter unfilled
MissingKeyError A named parameter unfilled
UnexpectedPosError A positional argument unused
UnexpectedKeyError A keyword argument unused

Example

let count = 3
assert_eq $(t"n=${count:03d}").format() "n=003"

let stmt = t"select * from t where a = ${#0} and c = ${#name}"
assert_eq $stmt.format(1, name: "n") "select * from t where a = 1 and c = n"

len() -> Int

The number of segments.

let greeting = t"hello $name!"
assert_eq $greeting.len 3