Debug trait for tuples, array/slices, Vec, String (etc?) do not respect width parameter

528754a
Opened by Felix S Klock II at 2023-12-26 08:08:14

Consider the following println:

    let msg = "Hello";
    println!("(i,j,k): |{:30}|", msg);

This is using the width parameter to ensure that the output occupies at least width characters. (One can provide other arguments like fill/alignment to adjust the fill character or whether the output is left/right/center- alligned.) See docs here: https://doc.rust-lang.org/std/fmt/#width

In other words, it prints this (note the distance between the two occurrences of |):

(i,j,k): |Hello                         |

So, the problem:

The Debug trait seems to honor width for numeric and pointer types, but not for other types.

For example:

    println!("(i,j,k): |{:30?}|", msg);

does not necessarily put at least 30 characters between the two | characters that it prints; in the case of msg given above, it prints:

(i,j,k): |"Hello"|

Here is a playpen illustrating the problem on several inputs.

  1. Debug also doesn't work with other format modifiers, like LowerHex. So, I can print my hex dump with

    let dump = [byte1, byte2, byte3, byte4];
    println!("{:x?}", dump); // invalid format string
    

    , which is kind of ironic - a trait named Debug can't perform such a traditional debug task.

    Vadim Petrochenkov at 2015-12-08 13:05:42

  2. As a workaround for now I've been using

    println!("(i,j,k): |{:30}|", format!("{:?}", msg));
    

    which works fine. However, would be great if this would work natively

    David Poetzsch-Heffter at 2016-03-28 12:52:16

  3. I came here to file a similar issue with alignment specifiers. But it's likely the same bug. Example:

    println!("|{:^11?}|", 3);     // prints: |     3     |
    println!("|{:^11?}|", "hi");  // prints: |"hi"|
    

    Doodpants at 2016-09-06 03:51:56

  4. I'm not sure if there have been solutions proposed for this anywhere. I'm mostly thinking of derive(Debug) and not specifically debug for String or Vec..

    Notes:

    • Debug / derive(Debug) need to be libcore compatible; so we have no heap allocation or String to use in the implementation

    Bits of a solution?

    • We already separate the no-special-arguments and the other cases don't we, so no extra code is needed in the common path
    • When we want to prefix spaces or another filler character, we need to buffer the output following the filler, but the buffer only needs to be as large as the fill width. So with a small stack buffer we can support most short widths but not arbitrary widths.

    bluss at 2017-12-23 17:45:32

  5. I just got annoyed for 20 minutes or so that Duration ignores fill/align, 0-pad, and width. It does seem to obey precision for some reason.

    Brian Anderson at 2019-03-19 22:35:10

  6. I'm not sure if this is related, but I seem to encounter a similar problem when using custom impl Display. Here is an example:

    use std::fmt;
    
    struct Foo {
        bar: i32,
    }
    
    impl fmt::Display for Foo {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "{:f>5}", self.bar)
        }
    }
    
    fn main() {
        println!("|{:<30}|", "");
        println!("|{:<30}|", 12);
        println!("|{:<30}|", Foo {bar: 12});
        println!("|{:<30}|", format!("{}", Foo {bar: 12}));
    }
    

    Output:

    |                              |
    |12                            |
    |fff12|
    |fff12                         |
    

    In this case the aforementioned workaround to use format!("{}", x) works as well.

    Edit: I realize now, this is because my impl Display ignores the alignment. The correct implementation looks more like

    impl fmt::Display for Foo {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            f.pad(&format!("{:f>5}", self.bar))
        }
    }
    

    With this, the expected result is printed. Hopefully this helps anyone else trying to fix their impl Display like I was for quite some time.

    Ian Chamberlain at 2019-12-28 20:16:00

  7. This issue has been reported independently several times over the years: see issues #43909, #55584, #55749, #83361, #87905, #88059. Clearly people do try to apply formatting parameters to Debug-formatted values.

    I tested some of the most common types for how their Debug implementations currently handle width and precision. Here's a Playground link. In summary:

    | | width | precision | |---------------|-----------------------|-----------------------| | integers | pads as expected | irrelevant | | floats | pads as expected | handled as expected | | strings | ignored | ignored | | Duration | ignored | handled as expected | | booleans | pads as expected | weird: println!("{:.3?}", false) prints fal | | () | pads as expected | weird: println!("{:.1?}", ()) prints ( | | Slices / Vec | pads each element (!) | applied to each element | | Structs/enums using #[derive(Debug)] | pads each member (!) | applied to each member |

    To clarify the last 2: Slices, Vecs, and structs/enums using #[derive(Debug)] all simply forward the width and precision to a recursive Debug::fmt call that formats their member values. For example, println!("{:.5?}", [1., 2.]) prints [1.00000, 2.00000]. That seems reasonable for precision, but for width it pads (and aligns) each element of the slice/struct to width, which is kind of weird. It's unfortunate that width can't be implemented for slices and structs like it is for other types by padding the entire thing to the given width. That would require allocations, but Debug is part of std::core.

    I will try to implement the following changes:

    • Make Duration pad to width rather than ignore it (PR: #88999)
    • Make strings pad to width rather than ignore it
    • ~Fix the weird behavior of booleans and () for precision~ EDIT: On second thought, this may be intended behavior, at least according to the docs of std::fmt (see also https://github.com/rust-lang/rust/issues/78384#issuecomment-716463536):

    For non-numeric types, this can be considered a “maximum width”. If the resulting string is longer than this width, then it is truncated down to this many characters

    Michiel De Muynck at 2021-09-14 00:35:26