diff --git a/common/BUILD.bazel b/common/BUILD.bazel index f43eb77dc2e8..aadaf3f9af79 100644 --- a/common/BUILD.bazel +++ b/common/BUILD.bazel @@ -69,6 +69,7 @@ drake_cc_package_library( drake_cc_library( name = "fmt", srcs = [ + "fmt.cc", "fmt_eigen.cc", ], hdrs = [ diff --git a/common/fmt.cc b/common/fmt.cc new file mode 100644 index 000000000000..843e307fe54b --- /dev/null +++ b/common/fmt.cc @@ -0,0 +1,49 @@ +#include "drake/common/fmt.h" + +namespace drake { + +// We can simplify this after we drop support for Ubuntu 22.04 Jammy. +std::string fmt_debug_string(std::string_view x) { +#if FMT_VERSION >= 90000 + return fmt::format("{:?}", x); +#else + std::string result; + result.reserve(x.size() + 2); + result.push_back('"'); + for (const char ch : x) { + // Check for characters with a custom escape sequence. + if (ch == '\n') { + result.push_back('\\'); + result.push_back('n'); + continue; + } + if (ch == '\r') { + result.push_back('\\'); + result.push_back('r'); + continue; + } + if (ch == '\t') { + result.push_back('\\'); + result.push_back('t'); + continue; + } + // Check for characters that require a leading backslash. + if (ch == '"' || ch == '\\') { + result.push_back('\\'); + result.push_back(ch); + continue; + } + // Check for any other non-printable characters. + if (ch < 0x20 || ch >= 0x7F) { + result.append(fmt::format("\\x{:02x}", static_cast(ch))); + continue; + } + // Normal character. + result.push_back(ch); + } + result.push_back('"'); + return result; +#endif +} + +} // namespace drake diff --git a/common/fmt.h b/common/fmt.h index 955113b8003a..46dbcb4e7885 100644 --- a/common/fmt.h +++ b/common/fmt.h @@ -41,6 +41,14 @@ std::string fmt_floating_point(T x) { return result; } + +/** Returns `fmt::("{:?}", x)`, i.e, using fmt's "debug string format"; see +https://fmt.dev docs for the '?' presentation type for details. We provide this +wrapper because not all of our supported platforms have a new-enough fmt +to rely on it. On platforms with older fmt, we use a Drake re-implementation +of the feature that does NOT handle unicode correctly. */ +std::string fmt_debug_string(std::string_view x); + namespace internal::formatter_as { /* The DRAKE_FORMATTER_AS macro specializes this for it's format_as types. diff --git a/common/test/fmt_test.cc b/common/test/fmt_test.cc index 8c2fbd328c34..c9032f3facd2 100644 --- a/common/test/fmt_test.cc +++ b/common/test/fmt_test.cc @@ -2,6 +2,7 @@ #include +#include #include #include @@ -78,5 +79,28 @@ GTEST_TEST(FmtTest, FloatingPoint) { EXPECT_EQ(fmt_floating_point(1.0f), "1.0"); } +GTEST_TEST(FmtTest, DebugString) { + // We'll use these named fmt args to help make our expected values clear. + fmt::dynamic_format_arg_store args; + args.push_back(fmt::arg("bs", '\\')); // backslash + args.push_back(fmt::arg("dq", '"')); // double quote + + // Plain string. + EXPECT_EQ(fmt_debug_string("Hello, world!"), + fmt::vformat("{dq}Hello, world!{dq}", args)); + + // Custom escape sequences. + EXPECT_EQ(fmt_debug_string("aa\nbb\rcc\tdd"), + fmt::vformat("{dq}aa{bs}nbb{bs}rcc{bs}tdd{dq}", args)); + + // Printable characters that require escaping. + EXPECT_EQ(fmt_debug_string("aa\"bb'cc\\dd"), + fmt::vformat("{dq}aa{bs}{dq}bb'cc{bs}{bs}dd{dq}", args)); + + // Non-printable characters. + EXPECT_EQ(fmt_debug_string("aa\x0e!\x7f_"), + fmt::vformat("{dq}aa{bs}x0e!{bs}x7f_{dq}", args)); +} + } // namespace } // namespace drake