foundry_common_fmt/
console.rs

1use super::UIfmt;
2use alloy_primitives::{Address, Bytes, FixedBytes, I256, U256};
3use std::fmt::{self, Write};
4
5/// A piece is a portion of the format string which represents the next part to emit.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum Piece<'a> {
8    /// A literal string which should directly be emitted.
9    String(&'a str),
10    /// A format specifier which should be replaced with the next argument.
11    NextArgument(FormatSpec),
12}
13
14/// A format specifier.
15#[derive(Clone, Debug, Default, PartialEq, Eq)]
16pub enum FormatSpec {
17    /// `%s`
18    #[default]
19    String,
20    /// `%d`
21    Number,
22    /// `%i`
23    Integer,
24    /// `%o`
25    Object,
26    /// `%e`, `%18e`
27    Exponential(Option<usize>),
28    /// `%x`
29    Hexadecimal,
30}
31
32impl fmt::Display for FormatSpec {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        f.write_str("%")?;
35        match *self {
36            Self::String => f.write_str("s"),
37            Self::Number => f.write_str("d"),
38            Self::Integer => f.write_str("i"),
39            Self::Object => f.write_str("o"),
40            Self::Exponential(Some(n)) => write!(f, "{n}e"),
41            Self::Exponential(None) => f.write_str("e"),
42            Self::Hexadecimal => f.write_str("x"),
43        }
44    }
45}
46
47enum ParseArgError {
48    /// Failed to parse the argument.
49    Err,
50    /// Escape `%%`.
51    Skip,
52}
53
54/// Parses a format string into a sequence of [pieces][Piece].
55#[derive(Debug)]
56pub struct Parser<'a> {
57    input: &'a str,
58    chars: std::str::CharIndices<'a>,
59}
60
61impl<'a> Parser<'a> {
62    /// Creates a new parser for the given input.
63    pub fn new(input: &'a str) -> Self {
64        Self { input, chars: input.char_indices() }
65    }
66
67    /// Parses a string until the next format specifier.
68    ///
69    /// `skip` is the number of format specifier characters (`%`) to ignore before returning the
70    /// string.
71    fn string(&mut self, start: usize, mut skip: usize) -> &'a str {
72        while let Some((pos, c)) = self.peek() {
73            if c == '%' {
74                if skip == 0 {
75                    return &self.input[start..pos];
76                }
77                skip -= 1;
78            }
79            self.chars.next();
80        }
81        &self.input[start..]
82    }
83
84    /// Parses a format specifier.
85    ///
86    /// If `Err` is returned, the internal iterator may have been advanced and it may be in an
87    /// invalid state.
88    fn argument(&mut self) -> Result<FormatSpec, ParseArgError> {
89        let (start, ch) = self.peek().ok_or(ParseArgError::Err)?;
90        let simple_spec = match ch {
91            's' => Some(FormatSpec::String),
92            'd' => Some(FormatSpec::Number),
93            'i' => Some(FormatSpec::Integer),
94            'o' => Some(FormatSpec::Object),
95            'e' => Some(FormatSpec::Exponential(None)),
96            'x' => Some(FormatSpec::Hexadecimal),
97            // "%%" is a literal '%'.
98            '%' => return Err(ParseArgError::Skip),
99            _ => None,
100        };
101        if let Some(spec) = simple_spec {
102            self.chars.next();
103            return Ok(spec);
104        }
105
106        // %<n>e
107        if ch.is_ascii_digit() {
108            let n = self.integer(start);
109            if let Some((_, 'e')) = self.peek() {
110                self.chars.next();
111                return Ok(FormatSpec::Exponential(n));
112            }
113        }
114
115        Err(ParseArgError::Err)
116    }
117
118    fn integer(&mut self, start: usize) -> Option<usize> {
119        let mut end = start;
120        while let Some((pos, ch)) = self.peek() {
121            if !ch.is_ascii_digit() {
122                end = pos;
123                break;
124            }
125            self.chars.next();
126        }
127        self.input[start..end].parse().ok()
128    }
129
130    fn current_pos(&mut self) -> usize {
131        self.peek().map(|(n, _)| n).unwrap_or(self.input.len())
132    }
133
134    fn peek(&mut self) -> Option<(usize, char)> {
135        self.peek_n(0)
136    }
137
138    fn peek_n(&mut self, n: usize) -> Option<(usize, char)> {
139        self.chars.clone().nth(n)
140    }
141}
142
143impl<'a> Iterator for Parser<'a> {
144    type Item = Piece<'a>;
145
146    fn next(&mut self) -> Option<Self::Item> {
147        let (mut start, ch) = self.peek()?;
148        let mut skip = 0;
149        if ch == '%' {
150            let prev = self.chars.clone();
151            self.chars.next();
152            match self.argument() {
153                Ok(arg) => {
154                    debug_assert_eq!(arg.to_string(), self.input[start..self.current_pos()]);
155                    return Some(Piece::NextArgument(arg));
156                }
157
158                // Skip the argument if we encountered "%%".
159                Err(ParseArgError::Skip) => {
160                    start = self.current_pos();
161                    skip += 1;
162                }
163
164                // Reset the iterator if we failed to parse the argument, and include any
165                // parsed and unparsed specifier in `String`.
166                Err(ParseArgError::Err) => {
167                    self.chars = prev;
168                    skip += 1;
169                }
170            }
171        }
172        Some(Piece::String(self.string(start, skip)))
173    }
174}
175
176/// Formats a value using a [FormatSpec].
177pub trait ConsoleFmt {
178    /// Formats a value using a [FormatSpec].
179    fn fmt(&self, spec: FormatSpec) -> String;
180}
181
182impl ConsoleFmt for String {
183    fn fmt(&self, spec: FormatSpec) -> String {
184        match spec {
185            FormatSpec::String => self.clone(),
186            FormatSpec::Object => format!("'{}'", self.clone()),
187            FormatSpec::Number |
188            FormatSpec::Integer |
189            FormatSpec::Exponential(_) |
190            FormatSpec::Hexadecimal => Self::from("NaN"),
191        }
192    }
193}
194
195impl ConsoleFmt for bool {
196    fn fmt(&self, spec: FormatSpec) -> String {
197        match spec {
198            FormatSpec::String => self.pretty(),
199            FormatSpec::Object => format!("'{}'", self.pretty()),
200            FormatSpec::Number => (*self as i32).to_string(),
201            FormatSpec::Integer | FormatSpec::Exponential(_) | FormatSpec::Hexadecimal => {
202                String::from("NaN")
203            }
204        }
205    }
206}
207
208impl ConsoleFmt for U256 {
209    fn fmt(&self, spec: FormatSpec) -> String {
210        match spec {
211            FormatSpec::String | FormatSpec::Object | FormatSpec::Number | FormatSpec::Integer => {
212                self.pretty()
213            }
214            FormatSpec::Hexadecimal => {
215                let hex = format!("{self:x}");
216                format!("0x{}", hex.trim_start_matches('0'))
217            }
218            FormatSpec::Exponential(None) => {
219                let log = self.pretty().len() - 1;
220                let exp10 = Self::from(10).pow(Self::from(log));
221                let amount = *self;
222                let integer = amount / exp10;
223                let decimal = (amount % exp10).to_string();
224                let decimal = format!("{decimal:0>log$}").trim_end_matches('0').to_string();
225                if !decimal.is_empty() {
226                    format!("{integer}.{decimal}e{log}")
227                } else {
228                    format!("{integer}e{log}")
229                }
230            }
231            FormatSpec::Exponential(Some(precision)) => {
232                let exp10 = Self::from(10).pow(Self::from(precision));
233                let amount = *self;
234                let integer = amount / exp10;
235                let decimal = (amount % exp10).to_string();
236                let decimal = format!("{decimal:0>precision$}").trim_end_matches('0').to_string();
237                if !decimal.is_empty() {
238                    format!("{integer}.{decimal}")
239                } else {
240                    format!("{integer}")
241                }
242            }
243        }
244    }
245}
246
247impl ConsoleFmt for I256 {
248    fn fmt(&self, spec: FormatSpec) -> String {
249        match spec {
250            FormatSpec::String | FormatSpec::Object | FormatSpec::Number | FormatSpec::Integer => {
251                self.pretty()
252            }
253            FormatSpec::Hexadecimal => {
254                let hex = format!("{self:x}");
255                format!("0x{}", hex.trim_start_matches('0'))
256            }
257            FormatSpec::Exponential(None) => {
258                let amount = *self;
259                let sign = if amount.is_negative() { "-" } else { "" };
260                let log = if amount.is_negative() {
261                    self.pretty().len() - 2
262                } else {
263                    self.pretty().len() - 1
264                };
265                let exp10 = Self::exp10(log);
266                let integer = (amount / exp10).twos_complement();
267                let decimal = (amount % exp10).twos_complement().to_string();
268                let decimal = format!("{decimal:0>log$}").trim_end_matches('0').to_string();
269                if !decimal.is_empty() {
270                    format!("{sign}{integer}.{decimal}e{log}")
271                } else {
272                    format!("{sign}{integer}e{log}")
273                }
274            }
275            FormatSpec::Exponential(Some(precision)) => {
276                let amount = *self;
277                let sign = if amount.is_negative() { "-" } else { "" };
278                let exp10 = Self::exp10(precision);
279                let integer = (amount / exp10).twos_complement();
280                let decimal = (amount % exp10).twos_complement().to_string();
281                let decimal = format!("{decimal:0>precision$}").trim_end_matches('0').to_string();
282                if !decimal.is_empty() {
283                    format!("{sign}{integer}.{decimal}")
284                } else {
285                    format!("{sign}{integer}")
286                }
287            }
288        }
289    }
290}
291
292impl ConsoleFmt for Address {
293    fn fmt(&self, spec: FormatSpec) -> String {
294        match spec {
295            FormatSpec::String | FormatSpec::Hexadecimal => self.pretty(),
296            FormatSpec::Object => format!("'{}'", self.pretty()),
297            FormatSpec::Number | FormatSpec::Integer | FormatSpec::Exponential(_) => {
298                String::from("NaN")
299            }
300        }
301    }
302}
303
304impl ConsoleFmt for Vec<u8> {
305    fn fmt(&self, spec: FormatSpec) -> String {
306        self[..].fmt(spec)
307    }
308}
309
310impl ConsoleFmt for Bytes {
311    fn fmt(&self, spec: FormatSpec) -> String {
312        self[..].fmt(spec)
313    }
314}
315
316impl<const N: usize> ConsoleFmt for [u8; N] {
317    fn fmt(&self, spec: FormatSpec) -> String {
318        self[..].fmt(spec)
319    }
320}
321
322impl<const N: usize> ConsoleFmt for FixedBytes<N> {
323    fn fmt(&self, spec: FormatSpec) -> String {
324        self[..].fmt(spec)
325    }
326}
327
328impl ConsoleFmt for [u8] {
329    fn fmt(&self, spec: FormatSpec) -> String {
330        match spec {
331            FormatSpec::String | FormatSpec::Hexadecimal => self.pretty(),
332            FormatSpec::Object => format!("'{}'", self.pretty()),
333            FormatSpec::Number | FormatSpec::Integer | FormatSpec::Exponential(_) => {
334                String::from("NaN")
335            }
336        }
337    }
338}
339
340/// Formats a string using the input values.
341///
342/// Formatting rules are the same as Hardhat. The supported format specifiers are as follows:
343/// - %s: Converts the value using its String representation. This is equivalent to applying
344///   [`UIfmt::pretty()`] on the format string.
345/// - %o: Treats the format value as a javascript "object" and converts it to its string
346///   representation.
347/// - %d, %i: Converts the value to an integer. If a non-numeric value, such as String or Address,
348///   is passed, then the spec is formatted as `NaN`.
349/// - %x: Converts the value to a hexadecimal string. If a non-numeric value, such as String or
350///   Address, is passed, then the spec is formatted as `NaN`.
351/// - %e: Converts the value to an exponential notation string. If a non-numeric value, such as
352///   String or Address, is passed, then the spec is formatted as `NaN`.
353/// - %%: This is parsed as a single percent sign ('%') without consuming any input value.
354///
355/// Unformatted values are appended to the end of the formatted output using [`UIfmt::pretty()`].
356/// If there are more format specifiers than values, then the remaining unparsed format specifiers
357/// appended to the formatted output as-is.
358///
359/// # Examples
360///
361/// ```ignore (not implemented for integers)
362/// let formatted = foundry_common::fmt::console_format("%s has %d characters", &[&"foo", &3]);
363/// assert_eq!(formatted, "foo has 3 characters");
364/// ```
365pub fn console_format(spec: &str, values: &[&dyn ConsoleFmt]) -> String {
366    let mut values = values.iter().copied();
367    let mut result = String::with_capacity(spec.len());
368
369    // for the first space
370    let mut write_space = if spec.is_empty() {
371        false
372    } else {
373        format_spec(spec, &mut values, &mut result);
374        true
375    };
376
377    // append any remaining values with the standard format
378    for v in values {
379        let fmt = v.fmt(FormatSpec::String);
380        if write_space {
381            result.push(' ');
382        }
383        result.push_str(&fmt);
384        write_space = true;
385    }
386
387    result
388}
389
390fn format_spec<'a>(
391    s: &str,
392    mut values: impl Iterator<Item = &'a dyn ConsoleFmt>,
393    result: &mut String,
394) {
395    for piece in Parser::new(s) {
396        match piece {
397            Piece::String(s) => result.push_str(s),
398            Piece::NextArgument(spec) => {
399                if let Some(value) = values.next() {
400                    result.push_str(&value.fmt(spec));
401                } else {
402                    // Write the format specifier as-is if there are no more values.
403                    write!(result, "{spec}").unwrap();
404                }
405            }
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use alloy_primitives::{address, B256};
414    use foundry_macros::ConsoleFmt;
415    use std::str::FromStr;
416
417    macro_rules! logf1 {
418        ($a:ident) => {
419            console_format(&$a.p_0, &[&$a.p_1])
420        };
421    }
422
423    macro_rules! logf2 {
424        ($a:ident) => {
425            console_format(&$a.p_0, &[&$a.p_1, &$a.p_2])
426        };
427    }
428
429    macro_rules! logf3 {
430        ($a:ident) => {
431            console_format(&$a.p_0, &[&$a.p_1, &$a.p_2, &$a.p_3])
432        };
433    }
434
435    #[derive(Clone, Debug, ConsoleFmt)]
436    struct Log1 {
437        p_0: String,
438        p_1: U256,
439    }
440
441    #[derive(Clone, Debug, ConsoleFmt)]
442    struct Log2 {
443        p_0: String,
444        p_1: bool,
445        p_2: U256,
446    }
447
448    #[derive(Clone, Debug, ConsoleFmt)]
449    struct Log3 {
450        p_0: String,
451        p_1: Address,
452        p_2: bool,
453        p_3: U256,
454    }
455
456    #[expect(unused)]
457    #[derive(Clone, Debug, ConsoleFmt)]
458    enum Logs {
459        Log1(Log1),
460        Log2(Log2),
461        Log3(Log3),
462    }
463
464    #[test]
465    fn test_console_log_format_specifiers() {
466        let fmt_1 = |spec: &str, arg: &dyn ConsoleFmt| console_format(spec, &[arg]);
467
468        assert_eq!("foo", fmt_1("%s", &String::from("foo")));
469        assert_eq!("NaN", fmt_1("%d", &String::from("foo")));
470        assert_eq!("NaN", fmt_1("%i", &String::from("foo")));
471        assert_eq!("NaN", fmt_1("%e", &String::from("foo")));
472        assert_eq!("NaN", fmt_1("%x", &String::from("foo")));
473        assert_eq!("'foo'", fmt_1("%o", &String::from("foo")));
474        assert_eq!("%s foo", fmt_1("%%s", &String::from("foo")));
475        assert_eq!("% foo", fmt_1("%", &String::from("foo")));
476        assert_eq!("% foo", fmt_1("%%", &String::from("foo")));
477
478        assert_eq!("true", fmt_1("%s", &true));
479        assert_eq!("1", fmt_1("%d", &true));
480        assert_eq!("0", fmt_1("%d", &false));
481        assert_eq!("NaN", fmt_1("%i", &true));
482        assert_eq!("NaN", fmt_1("%e", &true));
483        assert_eq!("NaN", fmt_1("%x", &true));
484        assert_eq!("'true'", fmt_1("%o", &true));
485
486        let b32 =
487            B256::from_str("0xdeadbeef00000000000000000000000000000000000000000000000000000000")
488                .unwrap();
489        assert_eq!(
490            "0xdeadbeef00000000000000000000000000000000000000000000000000000000",
491            fmt_1("%s", &b32)
492        );
493        assert_eq!(
494            "0xdeadbeef00000000000000000000000000000000000000000000000000000000",
495            fmt_1("%x", &b32)
496        );
497        assert_eq!("NaN", fmt_1("%d", &b32));
498        assert_eq!("NaN", fmt_1("%i", &b32));
499        assert_eq!("NaN", fmt_1("%e", &b32));
500        assert_eq!(
501            "'0xdeadbeef00000000000000000000000000000000000000000000000000000000'",
502            fmt_1("%o", &b32)
503        );
504
505        let addr = address!("0xdEADBEeF00000000000000000000000000000000");
506        assert_eq!("0xdEADBEeF00000000000000000000000000000000", fmt_1("%s", &addr));
507        assert_eq!("NaN", fmt_1("%d", &addr));
508        assert_eq!("NaN", fmt_1("%i", &addr));
509        assert_eq!("NaN", fmt_1("%e", &addr));
510        assert_eq!("0xdEADBEeF00000000000000000000000000000000", fmt_1("%x", &addr));
511        assert_eq!("'0xdEADBEeF00000000000000000000000000000000'", fmt_1("%o", &addr));
512
513        let bytes = Bytes::from_str("0xdeadbeef").unwrap();
514        assert_eq!("0xdeadbeef", fmt_1("%s", &bytes));
515        assert_eq!("NaN", fmt_1("%d", &bytes));
516        assert_eq!("NaN", fmt_1("%i", &bytes));
517        assert_eq!("NaN", fmt_1("%e", &bytes));
518        assert_eq!("0xdeadbeef", fmt_1("%x", &bytes));
519        assert_eq!("'0xdeadbeef'", fmt_1("%o", &bytes));
520
521        assert_eq!("100", fmt_1("%s", &U256::from(100)));
522        assert_eq!("100", fmt_1("%d", &U256::from(100)));
523        assert_eq!("100", fmt_1("%i", &U256::from(100)));
524        assert_eq!("1e2", fmt_1("%e", &U256::from(100)));
525        assert_eq!("1.0023e6", fmt_1("%e", &U256::from(1002300)));
526        assert_eq!("1.23e5", fmt_1("%e", &U256::from(123000)));
527        assert_eq!("0x64", fmt_1("%x", &U256::from(100)));
528        assert_eq!("100", fmt_1("%o", &U256::from(100)));
529
530        assert_eq!("100", fmt_1("%s", &I256::try_from(100).unwrap()));
531        assert_eq!("100", fmt_1("%d", &I256::try_from(100).unwrap()));
532        assert_eq!("100", fmt_1("%i", &I256::try_from(100).unwrap()));
533        assert_eq!("1e2", fmt_1("%e", &I256::try_from(100).unwrap()));
534        assert_eq!("-1e2", fmt_1("%e", &I256::try_from(-100).unwrap()));
535        assert_eq!("-1.0023e6", fmt_1("%e", &I256::try_from(-1002300).unwrap()));
536        assert_eq!("-1.23e5", fmt_1("%e", &I256::try_from(-123000).unwrap()));
537        assert_eq!("1.0023e6", fmt_1("%e", &I256::try_from(1002300).unwrap()));
538        assert_eq!("1.23e5", fmt_1("%e", &I256::try_from(123000).unwrap()));
539
540        // %ne
541        assert_eq!("10", fmt_1("%1e", &I256::try_from(100).unwrap()));
542        assert_eq!("-1", fmt_1("%2e", &I256::try_from(-100).unwrap()));
543        assert_eq!("123000", fmt_1("%0e", &I256::try_from(123000).unwrap()));
544        assert_eq!("12300", fmt_1("%1e", &I256::try_from(123000).unwrap()));
545        assert_eq!("0.0123", fmt_1("%7e", &I256::try_from(123000).unwrap()));
546        assert_eq!("-0.0123", fmt_1("%7e", &I256::try_from(-123000).unwrap()));
547
548        assert_eq!("0x64", fmt_1("%x", &I256::try_from(100).unwrap()));
549        assert_eq!(
550            "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9c",
551            fmt_1("%x", &I256::try_from(-100).unwrap())
552        );
553        assert_eq!(
554            "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffe8b7891800",
555            fmt_1("%x", &I256::try_from(-100000000000i64).unwrap())
556        );
557        assert_eq!("100", fmt_1("%o", &I256::try_from(100).unwrap()));
558
559        // make sure that %byte values are not consumed when there are no values
560        assert_eq!("%333d%3e%5F", console_format("%333d%3e%5F", &[]));
561        assert_eq!(
562            "%5d123456.789%2f%3f%e1",
563            console_format("%5d%3e%2f%3f%e1", &[&U256::from(123456789)])
564        );
565    }
566
567    #[test]
568    fn test_console_log_format() {
569        let mut log1 = Log1 { p_0: "foo %s".to_string(), p_1: U256::from(100) };
570        assert_eq!("foo 100", logf1!(log1));
571        log1.p_0 = String::from("foo");
572        assert_eq!("foo 100", logf1!(log1));
573        log1.p_0 = String::from("%s foo");
574        assert_eq!("100 foo", logf1!(log1));
575
576        let mut log2 = Log2 { p_0: "foo %s %s".to_string(), p_1: true, p_2: U256::from(100) };
577        assert_eq!("foo true 100", logf2!(log2));
578        log2.p_0 = String::from("foo");
579        assert_eq!("foo true 100", logf2!(log2));
580        log2.p_0 = String::from("%s %s foo");
581        assert_eq!("true 100 foo", logf2!(log2));
582
583        let log3 = Log3 {
584            p_0: String::from("foo %s %%s %s and %d foo %%"),
585            p_1: address!("0xdEADBEeF00000000000000000000000000000000"),
586            p_2: true,
587            p_3: U256::from(21),
588        };
589        assert_eq!(
590            "foo 0xdEADBEeF00000000000000000000000000000000 %s true and 21 foo %",
591            logf3!(log3)
592        );
593
594        // %ne
595        let log4 = Log1 { p_0: String::from("%5e"), p_1: U256::from(123456789) };
596        assert_eq!("1234.56789", logf1!(log4));
597
598        let log5 = Log1 { p_0: String::from("foo %3e bar"), p_1: U256::from(123456789) };
599        assert_eq!("foo 123456.789 bar", logf1!(log5));
600
601        let log6 =
602            Log2 { p_0: String::from("%e and %12e"), p_1: false, p_2: U256::from(123456789) };
603        assert_eq!("NaN and 0.000123456789", logf2!(log6));
604    }
605
606    #[test]
607    fn test_derive_format() {
608        let log1 = Log1 { p_0: String::from("foo %s bar"), p_1: U256::from(42) };
609        assert_eq!(log1.fmt(Default::default()), "foo 42 bar");
610        let call = Logs::Log1(log1);
611        assert_eq!(call.fmt(Default::default()), "foo 42 bar");
612    }
613}