///|
priv struct Ctx {
out : Array[Byte]
args : Array[String]
mut arg_index : Int32
messages : Array[String]
mut exit_error : Bool
mut stop : Bool
mut fatal : String?
}
///|
fn help_message() -> String {
let message =
#|Usage: printf FORMAT [ARGUMENT...]
#| --help display this help and exit
#| --version display version and exit
#|
#|Format and print ARGUMENTs under control of FORMAT, like printf(1).
#|The FORMAT is reused as necessary to consume all arguments.
#|
#|Escapes: \a \b \e \f \n \r \t \v \\ \" \' \NNN \0NNN \xHH \uHHHH
#|\UHHHHHHHH, and \c (stop all output).
#|
#|Conversions: %s %c %b %d %i %u %o %x %X %f %F %e %E %g %G %a %A %%, with the
#|flags -+ 0#, field width, and precision (both accept *).
#|%b interprets escapes in its argument.
#|Bare %q quotes an argument for a shell under the C-byte profile.
#|
#|Use -- before a FORMAT that starts with '-'.
message
}
///|
fn Ctx::next_arg(self : Ctx) -> String? {
if self.arg_index < self.args.length() {
let value = self.args[self.arg_index]
self.arg_index += 1
Some(value)
} else {
None
}
}
///|
/// Record a diagnostic that also makes the final exit status 1.
fn Ctx::warn(self : Ctx, msg : String) -> Unit {
self.messages.push(msg)
self.exit_error = true
}
///|
/// Record a diagnostic that leaves the exit status unchanged.
fn Ctx::notice(self : Ctx, msg : String) -> Unit {
self.messages.push(msg)
}
///|
fn is_blank(c : Char) -> Bool {
c.to_int() is (0x20 | 0x09 | 0x0A | 0x0D | 0x0B | 0x0C)
}
///|
let uint64_max : UInt64 = 0xFFFF_FFFF_FFFF_FFFF
///|
let int64_max_mag : UInt64 = 0x7FFF_FFFF_FFFF_FFFF
///|
let int64_min_mag : UInt64 = 0x8000_0000_0000_0000
///|
let float_profile : @numfmt.Profile = @numfmt.Extended80
///|
/// Parse a printf integer argument like strtoimax/strtoumax: leading blanks,
/// 'C or "C for a character code, then the longest valid decimal, 0x hex, or
/// leading-0 octal prefix. Trailing text warns; overflow clamps and warns.
/// Returns the value's 64-bit pattern.
fn parse_integer(ctx : Ctx, text : String, unsigned~ : Bool) -> Int64 {
let chars : Array[Char] = text.iter().collect()
let mut i = 0
while i < chars.length() && is_blank(chars[i]) {
i += 1
}
if i >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0
}
if chars[i] == '\'' || chars[i] == '"' {
if i + 1 >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0
}
if i + 2 < chars.length() {
let sb = StringBuilder()
for k in (i + 2)..= 0 {
base = 16
digits_start = i + 2
} else if i < chars.length() && chars[i] == '0' {
base = 8
}
let mut j = digits_start
let mut any = false
let mut mag : UInt64 = 0
let mut overflow = false
while j < chars.length() {
let d = match base {
16 => hex_val(chars[j])
8 => if chars[j] is ('0'..<='7') { chars[j].to_int() - 0x30 } else { -1 }
_ => if chars[j] is ('0'..<='9') { chars[j].to_int() - 0x30 } else { -1 }
}
if d < 0 {
break
}
any = true
let big_base = base.to_int64().reinterpret_as_uint64()
let big_d = d.to_int64().reinterpret_as_uint64()
if mag > (uint64_max - big_d) / big_base {
overflow = true
} else {
mag = mag * big_base + big_d
}
j += 1
}
if !any {
ctx.warn("printf: '\{text}': expected a numeric value")
return 0
}
// A range error supersedes the partial-conversion warning, like GNU.
let range_error = if unsigned {
overflow
} else if neg {
overflow || mag > int64_min_mag
} else {
overflow || mag > int64_max_mag
}
if range_error {
ctx.warn("printf: '\{text}': Numerical result out of range")
return if unsigned {
uint64_max.reinterpret_as_int64()
} else if neg {
int64_min_mag.reinterpret_as_int64()
} else {
int64_max_mag.reinterpret_as_int64()
}
}
if j < chars.length() {
ctx.warn("printf: '\{text}': value not completely converted")
}
let bits = mag.reinterpret_as_int64()
if neg {
-bits
} else {
bits
}
}
///|
fn parse_float_arg(ctx : Ctx, text : String) -> @numfmt.Value {
let chars : Array[Char] = text.iter().collect()
let mut index = 0
while index < chars.length() && is_blank(chars[index]) {
index += 1
}
if index < chars.length() && chars[index] is ('\'' | '"') {
if index + 1 >= chars.length() {
ctx.warn("printf: '\{text}': expected a numeric value")
return @numfmt.zero()
}
if index + 2 < chars.length() {
let suffix = StringBuilder()
for next in (index + 2).. Unit {
let negf = n < 0
let mag = if negf {
(-n).reinterpret_as_uint64()
} else {
n.reinterpret_as_uint64()
}
let body = apply_precision(mag.to_string(), prec)
let sign = if negf {
"-"
} else if plus {
"+"
} else if space {
" "
} else {
""
}
let zero_eff = zero && !minus && prec is None
push_string_utf8(ctx.out, pad_field(sign, body, width, minus, zero_eff))
}
///|
fn emit_unsigned(
ctx : Ctx,
u : UInt64,
radix : Int32,
upper : Bool,
alt : Bool,
width : Int32?,
prec : Int32?,
minus : Bool,
zero : Bool,
) -> Unit {
let digits = u.to_string(radix~)
let mut body = apply_precision(
if upper {
digits.to_upper()
} else {
digits
},
prec,
)
let mut prefix = ""
if alt && radix == 8 && !body.has_prefix("0") {
body = "0" + body
}
if alt && radix == 16 && u != 0 {
prefix = if upper { "0X" } else { "0x" }
}
let zero_eff = zero && !minus && prec is None
push_string_utf8(ctx.out, pad_field(prefix, body, width, minus, zero_eff))
}
///|
fn emit_float(
ctx : Ctx,
conv : Char,
x : @numfmt.Value,
width : Int32?,
prec : Int32?,
minus : Bool,
plus : Bool,
space : Bool,
zero : Bool,
alt : Bool,
) -> Unit {
let body = @numfmt.format_magnitude(
x,
conv,
prec,
alt,
float_profile,
normalized_hex=false,
)
let nonfinite = match x {
@numfmt.Finite(_, _, _) => false
_ => true
}
let sign = if x.is_negative() {
"-"
} else if plus {
"+"
} else if space {
" "
} else {
""
}
let (prefix, body) = if conv is ('a' | 'A') && !nonfinite {
(sign + body[:2].to_owned(), body[2:].to_owned())
} else {
(sign, body)
}
push_string_utf8(
ctx.out,
pad_field(prefix, body, width, minus, zero && !minus && !nonfinite),
)
}
///|
fn emit_b(ctx : Ctx, text : String) -> Unit {
let chars : Array[Char] = text.iter().collect()
let mut i = 0
while i < chars.length() {
if chars[i] == '\\' {
let (esc, used) = decode_escape(chars, i, in_b=true)
match esc {
Out(chunk) => ctx.out.append(chunk)
Stop => {
ctx.stop = true
return
}
Invalid(msg) => {
ctx.fatal = Some(msg)
return
}
}
i += used
} else {
push_char_utf8(ctx.out, chars[i])
i += 1
}
}
}
///|
/// GNU printf's per-conversion modifier rules, derived empirically:
/// %b and %% take no modifiers; # is limited to o/x/X and floats; the zero
/// flag is invalid for s/c; %c takes no precision.
fn spec_valid(
conv : Char,
minus : Bool,
plus : Bool,
space : Bool,
zero : Bool,
alt : Bool,
width : Int32?,
prec : Int32?,
) -> Bool {
match conv {
'd' | 'i' | 'u' => !alt
'o' | 'x' | 'X' | 'e' | 'E' | 'f' | 'F' | 'g' | 'G' | 'a' | 'A' => true
's' => !zero && !alt
'c' => !zero && !alt && prec is None
'b' | 'q' | '%' =>
!minus &&
!plus &&
!space &&
!zero &&
!alt &&
width is None &&
prec is None
_ => true
}
}
///|
/// Field widths allocate real padding, so cap them far below INT_MAX.
let max_width : Int32 = 100000000
///|
/// One pass over the format string; conversions consume arguments from ctx.
fn scan_format(ctx : Ctx, chars : Array[Char]) -> Unit {
let mut i = 0
while i < chars.length() {
if ctx.stop || !(ctx.fatal is None) {
return
}
let c = chars[i]
if c == '\\' {
let (esc, used) = decode_escape(chars, i)
match esc {
Out(bytes) => ctx.out.append(bytes)
Stop => {
ctx.stop = true
return
}
Invalid(msg) => {
ctx.fatal = Some(msg)
return
}
}
i += used
continue
}
if c != '%' {
push_char_utf8(ctx.out, c)
i += 1
continue
}
let spec_start = i
let mut j = i + 1
let mut minus = false
let mut plus = false
let mut space = false
let mut zero = false
let mut alt = false
while j < chars.length() {
match chars[j] {
'-' => minus = true
'+' => plus = true
' ' => space = true
'0' => zero = true
'#' => alt = true
_ => break
}
j += 1
}
let mut width : Int32? = None
if j < chars.length() && chars[j] == '*' {
let v = match ctx.next_arg() {
Some(arg) => parse_integer(ctx, arg, unsigned=false)
None => 0
}
// Padding is materialized in memory, so widths are capped well below
// INT_MAX; GNU would print the gigabytes of spaces instead.
if v > max_width.to_int64() || v < -max_width.to_int64() {
ctx.fatal = Some("printf: invalid field width: '\{v}'")
return
}
if v < 0 {
minus = true
width = Some((-v).to_int())
} else {
width = Some(v.to_int())
}
j += 1
} else {
let mut w = 0
let mut any = false
let digits_start = j
while j < chars.length() && chars[j] is ('0'..<='9') {
let d = chars[j].to_int() - 0x30
if w > (max_width - d) / 10 {
let sb = StringBuilder()
for k in digits_start.. parse_integer(ctx, arg, unsigned=false)
None => 0
}
// Precision materializes zeros in memory, so it shares the width cap.
if v > max_width.to_int64() {
ctx.fatal = Some("printf: invalid precision: '\{v}'")
return
}
// Any negative precision means the precision is omitted.
prec = if v < 0 { None } else { Some(v.to_int()) }
j += 1
} else {
let mut p = 0
let digits_start = j
while j < chars.length() && chars[j] is ('0'..<='9') {
let d = chars[j].to_int() - 0x30
if p > (max_width - d) / 10 {
let sb = StringBuilder()
for k in digits_start..= chars.length() {
ctx.fatal = Some("printf: missing conversion specifier at end of format")
return
}
let conv = chars[j]
i = j + 1
if !spec_valid(conv, minus, plus, space, zero, alt, width, prec) {
let sb = StringBuilder()
for k in spec_start.. ctx.out.push(b'%')
'd' | 'i' => {
// A missing argument formats as zero without a diagnostic.
let n = match ctx.next_arg() {
Some(arg) => parse_integer(ctx, arg, unsigned=false)
None => 0
}
emit_signed(ctx, n, width, prec, minus, plus, space, zero)
}
'u' | 'o' | 'x' | 'X' => {
let n = match ctx.next_arg() {
Some(arg) => parse_integer(ctx, arg, unsigned=true)
None => 0
}
let radix = match conv {
'u' => 10
'o' => 8
_ => 16
}
emit_unsigned(
ctx,
n.reinterpret_as_uint64(),
radix,
conv == 'X',
alt && conv != 'u',
width,
prec,
minus,
zero,
)
}
's' => {
// GNU printf measures %s width and precision in bytes.
let arg = ctx.next_arg().unwrap_or("")
let bytes : Array[Byte] = []
push_string_utf8(bytes, arg)
let limited = match prec {
Some(p) =>
if bytes.length() > p {
bytes[0:p].to_owned()
} else {
bytes
}
None => bytes
}
emit_padded_bytes(ctx.out, limited, width, minus)
}
'c' => {
// GNU printf emits only the first byte of the argument; a missing
// or empty argument emits one NUL byte.
let arg = ctx.next_arg().unwrap_or("")
let bytes : Array[Byte] = []
push_string_utf8(bytes, arg)
let first = if bytes.length() > 0 { [bytes[0]] } else { [b'\x00'] }
emit_padded_bytes(ctx.out, first, width, minus)
}
'b' => {
let arg = ctx.next_arg().unwrap_or("")
emit_b(ctx, arg)
}
'q' =>
if ctx.next_arg() is Some(arg) {
push_string_utf8(ctx.out, shell_quote(arg))
}
'f' | 'F' | 'e' | 'E' | 'g' | 'G' | 'a' | 'A' => {
let x = match ctx.next_arg() {
Some(arg) => parse_float_arg(ctx, arg)
None => @numfmt.zero()
}
emit_float(ctx, conv, x, width, prec, minus, plus, space, zero, alt)
}
_ => {
ctx.fatal = Some("printf: %\{conv}: invalid conversion specification")
return
}
}
}
}
///|
async fn main {
let metadata_args = @env.args()[1:]
if metadata_args.length() == 1 && metadata_args[0] == "--version" {
@stdio.stdout.write("printf 0.2.2\n")
return
}
let argv = @env.args()[1:]
if argv.length() > 0 && argv[0] == "--help" {
@stdio.stdout.write(help_message() + "\n")
return
}
let operands = if argv.length() > 0 && argv[0] == "--" {
argv[1:]
} else {
argv
}
if operands.is_empty() {
@stdio.stderr.write(
"printf: missing operand\nTry 'printf --help' for more information.\n",
)
@sys.exit(1)
return
}
let format = operands[0]
let ctx : Ctx = {
out: [],
args: operands[1:].to_owned(),
arg_index: 0,
messages: [],
exit_error: false,
stop: false,
fatal: None,
}
let fmt_chars : Array[Char] = format.iter().collect()
for ;; {
let before = ctx.arg_index
scan_format(ctx, fmt_chars)
if ctx.stop || !(ctx.fatal is None) {
break
}
if ctx.arg_index >= ctx.args.length() {
break
}
if ctx.arg_index == before {
ctx.notice(
"printf: warning: ignoring excess arguments, starting with '\{ctx.args[ctx.arg_index]}'",
)
break
}
}
@stdio.stdout.write(Bytes::from_array(ctx.out))
for msg in ctx.messages {
@stdio.stderr.write(msg + "\n")
}
match ctx.fatal {
Some(msg) => {
@stdio.stderr.write(msg + "\n")
@sys.exit(1)
return
}
None => ()
}
if ctx.exit_error {
@sys.exit(1)
}
}