mirror of https://github.com/Wilfred/difftastic/
433 lines
13 KiB
Plaintext
433 lines
13 KiB
Plaintext
// A format string consists of a string of literal characters, to be printed
|
|
// verbatim, and format sequences, which describe how to format arguments from
|
|
// a set of variadic parameters for printing.
|
|
//
|
|
// A format sequence is enclosed in curly braces '{}'. An empty sequence takes
|
|
// the next argument from the parameter list, in order. A specific parameter may
|
|
// be selected by indexing it from zero: '{0}', '{1}', and so on. To print '{',
|
|
// use '{{', and for '}', use '}}'.
|
|
//
|
|
// You may use a colon to add format modifiers; for example, '{:x}' will format
|
|
// an argument in hexadecimal, and '{3:-10}' will left-align the 3rd argument to
|
|
// at least 10 characters.
|
|
//
|
|
// The format modifiers takes the form of an optional flag character:
|
|
//
|
|
// 0: Numeric values are zero-padded up to the required width.
|
|
// -: The value shall be left-aligned, and spaces inserted on the right to meet
|
|
// the required width. '-' takes precedence over '0' if both are used.
|
|
// : (a space) insert a space before positive numbers, where '-' would be if it
|
|
// were negative.
|
|
// +: insert a '+' before positive numbers, where '-' would be if it were
|
|
// negative. '+' takes precedence over ' ' if both are used.
|
|
//
|
|
// Following the flag, an optional decimal number shall specify the minimum
|
|
// width of this field. If '0' or '-' were not given, the default behavior shall
|
|
// be to pad with spaces to achieve the necessary width.
|
|
//
|
|
// Following the width, an optional precision may be given as a decimal number
|
|
// following a '.' character. For integer types, this gives the minimum number
|
|
// of digits to include. For floating types, this gives the number of digits
|
|
// following the radix to include.
|
|
//
|
|
// Following the precision, an optional character controls the output format:
|
|
//
|
|
// x, X: print in lowercase or uppercase hexadecimal
|
|
// o, b: print in octal or binary
|
|
//
|
|
// TODO: Expand this with more format modifiers
|
|
use ascii;
|
|
use bufio;
|
|
use encoding::utf8;
|
|
use io;
|
|
use os;
|
|
use strconv;
|
|
use strings;
|
|
use types;
|
|
|
|
// Tagged union of all types which are formattable.
|
|
export type formattable =
|
|
(...types::numeric | uintptr | str | rune | bool | nullable *void);
|
|
|
|
// Formats text for printing and writes it to [os::stdout].
|
|
export fn printf(fmt: str, args: formattable...) (io::error | size) =
|
|
fprintf(os::stdout, fmt, args...);
|
|
|
|
// Formats text for printing and writes it to [os::stdout], followed by a line
|
|
// feed.
|
|
export fn printfln(fmt: str, args: formattable...) (io::error | size) =
|
|
fprintfln(os::stdout, fmt, args...);
|
|
|
|
// Formats text for printing and writes it to [os::stderr].
|
|
export fn errorf(fmt: str, args: formattable...) (io::error | size) =
|
|
fprintf(os::stderr, fmt, args...);
|
|
|
|
// Formats text for printing and writes it to [os::stderr], followed by a line
|
|
// feed.
|
|
export fn errorfln(fmt: str, args: formattable...) (io::error | size) =
|
|
fprintfln(os::stderr, fmt, args...);
|
|
|
|
// Formats text for printing and writes it into a heap-allocated string. The
|
|
// caller must free the return value.
|
|
export fn asprintf(fmt: str, args: formattable...) str = {
|
|
let buf = bufio::dynamic(io::mode::WRITE);
|
|
assert(fprintf(buf, fmt, args...) is size);
|
|
return strings::fromutf8_unsafe(bufio::finish(buf));
|
|
};
|
|
|
|
// Formats text for printing and writes it into a caller supplied buffer. The
|
|
// returned string is borrowed from this buffer.
|
|
export fn bsprintf(buf: []u8, fmt: str, args: formattable...) str = {
|
|
let sink = bufio::fixed(buf, io::mode::WRITE);
|
|
let l = fprintf(sink, fmt, args...) as size;
|
|
return strings::fromutf8_unsafe(buf[..l]);
|
|
};
|
|
|
|
// Formats text for printing and writes it to [os::stderr], followed by a line
|
|
// feed, then exits the program with an error status.
|
|
export @noreturn fn fatal(fmt: str, args: formattable...) void = {
|
|
fprintfln(os::stderr, fmt, args...);
|
|
os::exit(1);
|
|
};
|
|
|
|
// Formats text for printing and writes it to an [io::stream], followed by a
|
|
// line feed.
|
|
export fn fprintfln(
|
|
s: *io::stream,
|
|
fmt: str,
|
|
args: formattable...
|
|
) (io::error | size) = {
|
|
return fprintf(s, fmt, args...)? + io::write(s, ['\n': u32: u8])?;
|
|
};
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them to [os::stdout] separated by spaces
|
|
export fn print(args: formattable...) (io::error | size) =
|
|
fprint(os::stdout, args...);
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them to [os::stdout] separated by spaces and followed by a line feed
|
|
export fn println(args: formattable...) (io::error | size) =
|
|
fprintln(os::stdout, args...);
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them to [os::stderr] separated by spaces
|
|
export fn error(args: formattable...) (io::error | size) =
|
|
fprint(os::stderr, args...);
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them to [os::stderr] separated by spaces and followed by a line feed
|
|
export fn errorln(args: formattable...) (io::error | size) =
|
|
fprintln(os::stderr, args...);
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them into a heap-allocated string separated by spaces. The caller must free
|
|
// the return value.
|
|
export fn asprint(args: formattable...) str = {
|
|
let buf = bufio::dynamic(io::mode::WRITE);
|
|
assert(fprint(buf, args...) is size);
|
|
return strings::fromutf8_unsafe(bufio::finish(buf));
|
|
};
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them into a caller supplied buffer separated by spaces. The returned string
|
|
// is borrowed from this buffer.
|
|
export fn bsprint(buf: []u8, args: formattable...) str = {
|
|
let sink = bufio::fixed(buf, io::mode::WRITE);
|
|
assert(fprint(sink, args...) is size);
|
|
return strings::fromutf8_unsafe(buf);
|
|
};
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them to an [io::stream] separated by spaces and followed by a line feed
|
|
export fn fprintln(s: *io::stream, args: formattable...) (io::error | size) = {
|
|
return fprint(s, args...)? + io::write(s, ['\n': u32: u8])?;
|
|
};
|
|
|
|
// Formats values for printing using the default format modifiers and writes
|
|
// them to an [io::stream] separated by spaces
|
|
export fn fprint(s: *io::stream, args: formattable...) (io::error | size) = {
|
|
let mod = modifiers { base = strconv::base::DEC, ... };
|
|
let n = 0z;
|
|
for (let i = 0z; i < len(args); i += 1) {
|
|
n += format(s, args[i], &mod)?;
|
|
if (i != len(args) - 1) {
|
|
n += io::write(s, [' ': u32: u8])?;
|
|
};
|
|
};
|
|
return n;
|
|
};
|
|
|
|
type negation = enum {
|
|
NONE,
|
|
SPACE,
|
|
PLUS,
|
|
};
|
|
|
|
type padding = enum {
|
|
ALIGN_RIGHT,
|
|
ALIGN_LEFT,
|
|
ZEROES,
|
|
};
|
|
|
|
type modifiers = struct {
|
|
padding: padding,
|
|
negation: negation,
|
|
width: uint,
|
|
precision: uint,
|
|
base: strconv::base,
|
|
};
|
|
|
|
type modflags = enum uint {
|
|
NONE = 0,
|
|
ZERO = 1 << 0,
|
|
MINUS = 1 << 1,
|
|
SPACE = 1 << 2,
|
|
PLUS = 1 << 3,
|
|
};
|
|
|
|
// Formats text for printing and writes it to an [io::stream].
|
|
export fn fprintf(
|
|
s: *io::stream,
|
|
fmt: str,
|
|
args: formattable...
|
|
) (io::error | size) = {
|
|
let n = 0z, i = 0z;
|
|
let iter = strings::iter(fmt);
|
|
for (true) {
|
|
let r: rune = match (strings::next(&iter)) {
|
|
void => break,
|
|
r: rune => r,
|
|
};
|
|
|
|
if (r == '{') {
|
|
r = match (strings::next(&iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
|
|
const arg = if (r == '{') {
|
|
n += io::write(s, utf8::encoderune('{'))?;
|
|
continue;
|
|
} else if (ascii::isdigit(r)) {
|
|
strings::push(&iter, r);
|
|
args[scan_uint(&iter)];
|
|
} else {
|
|
strings::push(&iter, r);
|
|
i += 1;
|
|
args[i - 1];
|
|
};
|
|
|
|
let mod = modifiers { base = strconv::base::DEC, ... };
|
|
r = match (strings::next(&iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
switch (r) {
|
|
':' => scan_modifiers(&iter, &mod),
|
|
'}' => void,
|
|
* => abort("Invalid format string"),
|
|
};
|
|
|
|
n += format(s, arg, &mod)?;
|
|
} else if (r == '}') {
|
|
match (strings::next(&iter)) {
|
|
void => abort("Invalid format string (hanging '}')"),
|
|
r: rune => assert(r == '}', "Invalid format string (hanging '}')"),
|
|
};
|
|
|
|
n += io::write(s, utf8::encoderune('}'))?;
|
|
} else {
|
|
n += io::write(s, utf8::encoderune(r))?;
|
|
};
|
|
};
|
|
|
|
return n;
|
|
};
|
|
|
|
fn format(out: *io::stream, arg: formattable, mod: *modifiers) (size | io::error) = {
|
|
let z = format_raw(io::empty, arg, mod)?;
|
|
|
|
let pad: []u8 = [];
|
|
if (z < mod.width: size) {
|
|
pad = utf8::encoderune(switch (mod.padding) {
|
|
padding::ZEROES => '0',
|
|
* => ' ',
|
|
});
|
|
};
|
|
|
|
if (mod.padding == padding::ALIGN_LEFT) {
|
|
format_raw(out, arg, mod);
|
|
};
|
|
|
|
for (z < mod.width: size) {
|
|
z += io::write(out, pad)?;
|
|
};
|
|
|
|
if (mod.padding != padding::ALIGN_LEFT) {
|
|
format_raw(out, arg, mod);
|
|
};
|
|
|
|
return z;
|
|
};
|
|
|
|
fn format_raw(
|
|
out: *io::stream,
|
|
arg: formattable,
|
|
mod: *modifiers,
|
|
) (size | io::error) = match (arg) {
|
|
s: str => io::write(out, strings::toutf8(s)),
|
|
r: rune => io::write(out, utf8::encoderune(r)),
|
|
b: bool => io::write(out, strings::toutf8(if (b) "true" else "false")),
|
|
n: types::numeric => {
|
|
let s = strconv::numerictosb(n, mod.base);
|
|
io::write(out, strings::toutf8(s));
|
|
},
|
|
p: uintptr => {
|
|
let s = strconv::uptrtosb(p, mod.base);
|
|
io::write(out, strings::toutf8(s));
|
|
},
|
|
v: nullable *void => match (v) {
|
|
v: *void => {
|
|
let s = strconv::uptrtosb(v: uintptr,
|
|
strconv::base::HEX_LOWER);
|
|
let n = io::write(out, strings::toutf8("0x"))?;
|
|
n += io::write(out, strings::toutf8(s))?;
|
|
n;
|
|
},
|
|
null => format(out, "(null)", mod),
|
|
},
|
|
};
|
|
|
|
|
|
fn scan_uint(iter: *strings::iterator) uint = {
|
|
let num: []u8 = [];
|
|
defer free(num);
|
|
for (true) {
|
|
let r = match (strings::next(iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
|
|
if (ascii::isdigit(r)) {
|
|
append(num, r: u32: u8);
|
|
} else {
|
|
strings::push(iter, r);
|
|
match (strconv::stou(strings::fromutf8(num))) {
|
|
(strconv::invalid | strconv::overflow) =>
|
|
abort("Invalid format string (invalid index)"),
|
|
u: uint => return u,
|
|
};
|
|
};
|
|
};
|
|
abort("unreachable");
|
|
};
|
|
|
|
fn scan_modifier_flags(iter: *strings::iterator, mod: *modifiers) void = {
|
|
let flags = modflags::NONE;
|
|
|
|
for (true) {
|
|
let r = match (strings::next(iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
|
|
switch (r) {
|
|
'0' => flags |= modflags::ZERO,
|
|
'-' => flags |= modflags::MINUS,
|
|
' ' => flags |= modflags::SPACE,
|
|
'+' => flags |= modflags::PLUS,
|
|
* => {
|
|
strings::push(iter, r);
|
|
break;
|
|
},
|
|
};
|
|
};
|
|
|
|
mod.padding = if (flags & modflags::MINUS != 0)
|
|
padding::ALIGN_LEFT
|
|
else if (flags & modflags::ZERO != 0)
|
|
padding::ZEROES
|
|
else
|
|
padding::ALIGN_RIGHT;
|
|
|
|
mod.negation = if (flags & modflags::PLUS != 0)
|
|
negation::PLUS
|
|
else if (flags & modflags::SPACE != 0)
|
|
negation::SPACE
|
|
else
|
|
negation::NONE;
|
|
};
|
|
|
|
fn scan_modifier_width(iter: *strings::iterator, mod: *modifiers) void = {
|
|
let r = match (strings::next(iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
|
|
let is_digit = ascii::isdigit(r);
|
|
strings::push(iter, r);
|
|
|
|
if (is_digit) {
|
|
mod.width = scan_uint(iter);
|
|
};
|
|
};
|
|
|
|
fn scan_modifier_precision(iter: *strings::iterator, mod: *modifiers) void = {
|
|
let r = match (strings::next(iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
|
|
if (r == '.') {
|
|
mod.precision = scan_uint(iter);
|
|
} else {
|
|
strings::push(iter, r);
|
|
};
|
|
};
|
|
|
|
fn scan_modifier_base(iter: *strings::iterator, mod: *modifiers) void = {
|
|
let r = match (strings::next(iter)) {
|
|
void => abort("Invalid format string (unterminated '{')"),
|
|
r: rune => r,
|
|
};
|
|
|
|
switch (r) {
|
|
'x' => mod.base = strconv::base::HEX_LOWER,
|
|
'X' => mod.base = strconv::base::HEX_UPPER,
|
|
'o' => mod.base = strconv::base::OCT,
|
|
'b' => mod.base = strconv::base::BIN,
|
|
* => strings::push(iter, r),
|
|
};
|
|
};
|
|
|
|
fn scan_modifiers(iter: *strings::iterator, mod: *modifiers) void = {
|
|
scan_modifier_flags(iter, mod);
|
|
scan_modifier_width(iter, mod);
|
|
scan_modifier_precision(iter, mod);
|
|
scan_modifier_base(iter, mod);
|
|
|
|
// eat '}'
|
|
let terminated = match (strings::next(iter)) {
|
|
void => false,
|
|
r: rune => r == '}',
|
|
};
|
|
assert(terminated, "Invalid format string (unterminated '{')");
|
|
};
|
|
|
|
@test fn fmt() void = {
|
|
let buf: [1024]u8 = [0...];
|
|
assert(bsprintf(buf, "hello world") == "hello world");
|
|
assert(bsprintf(buf, "{} {}", "hello", "world") == "hello world");
|
|
assert(bsprintf(buf, "{0} {1}", "hello", "world") == "hello world");
|
|
assert(bsprintf(buf, "{0} {0}", "hello", "world") == "hello hello");
|
|
assert(bsprintf(buf, "{1} {0} {1}", "hello", "world") == "world hello world");
|
|
assert(bsprintf(buf, "x: {:08x}", 0xBEEF) == "x: 0000beef");
|
|
assert(bsprintf(buf, "x: {:8X}", 0xBEEF) == "x: BEEF");
|
|
assert(bsprintf(buf, "x: {:-8X}", 0xBEEF) == "x: BEEF ");
|
|
assert(bsprintf(buf, "x: {:o}", 0o755) == "x: 755");
|
|
assert(bsprintf(buf, "x: {:b}", 0b11011) == "x: 11011");
|
|
assert(bsprintf(buf, "{} {} {} {}", true, false, null, 'x')
|
|
== "true false (null) x");
|
|
};
|