From: Ivan Lazaric Date: Mon, 9 Feb 2026 12:26:36 +0000 (+0100) Subject: libstdc++: implement formatter for std::filesystem::path X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=642020ab7e5ba659a626a0808347e0488ae69394;p=thirdparty%2Fgcc.git libstdc++: implement formatter for std::filesystem::path This patch implements formatting for std::filesystem::path from P2845R8, and defines the feature test macro __cpp_lib_format_path to 202403L, provided only in . Formatting options are performed (if applicable) in order: 'g', '?', transcoding, fill-and-align & width The standard specifies transcoding behaviour only when literal encoding is UTF-8, leaving all other cases implementation defined. Current implementation of filesystem::path assumes: * char encoding is UTF-8 * wchar_t encoding is either UTF-32 or UTF-16 libstdc++-v3/ChangeLog: * include/bits/fs_path.h: Include bits/formatfwd.h. (std::formatter): Define. * include/bits/version.def (format_path): Define. * include/bits/version.h: Regenerate. * include/std/filesystem: Expose __cpp_lib_format_path. * testsuite/std/format/fs_path.cc: New test. Reviewed-by: Tomasz KamiƄski Signed-off-by: Ivan Lazaric Co-authored-by: Jonathan Wakely --- diff --git a/libstdc++-v3/include/bits/fs_path.h b/libstdc++-v3/include/bits/fs_path.h index 07b74de6cbe..5c0d5c9d5f1 100644 --- a/libstdc++-v3/include/bits/fs_path.h +++ b/libstdc++-v3/include/bits/fs_path.h @@ -50,6 +50,10 @@ # include #endif +#ifdef __glibcxx_format_path // C++ >= 26 && HOSTED +# include +#endif + #if defined(_WIN32) && !defined(__CYGWIN__) # define _GLIBCXX_FILESYSTEM_IS_WINDOWS 1 #endif @@ -1451,6 +1455,109 @@ template<> { return filesystem::hash_value(__p); } }; +#ifdef __glibcxx_format_path // C++ >= 26 && HOSTED + template<__format::__char _CharT> + struct formatter + { + formatter() = default; + + constexpr typename basic_format_parse_context<_CharT>::iterator + parse(basic_format_parse_context<_CharT>& __pc) + { + auto __first = __pc.begin(); + const auto __last = __pc.end(); + __format::_Spec<_CharT> __spec{}; + + auto __finalize = [this, &__spec] { + _M_spec = __spec; + }; + + auto __finished = [&] { + if (__first == __last || *__first == '}') + { + __finalize(); + return true; + } + return false; + }; + + if (__finished()) + return __first; + + __first = __spec._M_parse_fill_and_align(__first, __last); + if (__finished()) + return __first; + + __first = __spec._M_parse_width(__first, __last, __pc); + if (__finished()) + return __first; + + if (*__first == '?') + { + __spec._M_debug = true; + ++__first; + } + if (__finished()) + return __first; + + if (*__first == 'g') + { + __spec._M_type = __format::_Pres_g; + ++__first; + } + if (__finished()) + return __first; + + __format::__failed_to_parse_format_spec(); + } + + template + typename basic_format_context<_Out, _CharT>::iterator + format(const filesystem::path& __p, + basic_format_context<_Out, _CharT>& __fc) const + { + using _ValueT = filesystem::path::value_type; + using _ViewT = basic_string_view<_ValueT>; + using _FmtStrT = __format::__formatter_str<_CharT>; + + _ViewT __sv; + filesystem::path::string_type __s; + if (_M_spec._M_type == __format::_Pres_g) + __sv = __s = __p.generic_string<_ValueT>(); + else + __sv = __p.native(); + + auto __spec = _M_spec; + // 'g' should not be passed along. + __spec._M_type = __format::_Pres_none; + + if constexpr (is_same_v<_CharT, _ValueT>) + return _FmtStrT(__spec).format(__sv, __fc); + else + { + __format::_Str_sink<_ValueT> __sink; + if (__spec._M_debug) + { + using __format::_Term_quote; + __format::__write_escaped(__sink.out(), __sv, _Term_quote); + __sv = __sink.view(); + __spec._M_debug = 0; + } + basic_string<_CharT> __out_str + (std::from_range, __unicode::_Utf_view<_CharT, _ViewT>(__sv)); + return _FmtStrT(__spec).format(__out_str, __fc); + } + } + + constexpr void + set_debug_format() noexcept + { _M_spec._M_debug = true; } + + private: + __format::_Spec<_CharT> _M_spec{}; + }; +#endif // __glibcxx_format_path + _GLIBCXX_END_NAMESPACE_VERSION } // namespace std diff --git a/libstdc++-v3/include/bits/version.def b/libstdc++-v3/include/bits/version.def index 4b8e9d43ec2..c7709ba3a07 100644 --- a/libstdc++-v3/include/bits/version.def +++ b/libstdc++-v3/include/bits/version.def @@ -1561,6 +1561,16 @@ ftms = { }; }; +ftms = { + name = format_path; + // 202403 P2845R8 Formatting of std::filesystem::path + values = { + v = 202403; + cxxmin = 26; + hosted = yes; + }; +}; + ftms = { name = freestanding_algorithm; values = { diff --git a/libstdc++-v3/include/bits/version.h b/libstdc++-v3/include/bits/version.h index 7602225cb6d..c72cda506f1 100644 --- a/libstdc++-v3/include/bits/version.h +++ b/libstdc++-v3/include/bits/version.h @@ -1721,6 +1721,16 @@ #endif /* !defined(__cpp_lib_format_ranges) */ #undef __glibcxx_want_format_ranges +#if !defined(__cpp_lib_format_path) +# if (__cplusplus > 202302L) && _GLIBCXX_HOSTED +# define __glibcxx_format_path 202403L +# if defined(__glibcxx_want_all) || defined(__glibcxx_want_format_path) +# define __cpp_lib_format_path 202403L +# endif +# endif +#endif /* !defined(__cpp_lib_format_path) */ +#undef __glibcxx_want_format_path + #if !defined(__cpp_lib_freestanding_algorithm) # if (__cplusplus >= 202100L) # define __glibcxx_freestanding_algorithm 202311L diff --git a/libstdc++-v3/include/std/filesystem b/libstdc++-v3/include/std/filesystem index f902c6feb77..b9900f49c33 100644 --- a/libstdc++-v3/include/std/filesystem +++ b/libstdc++-v3/include/std/filesystem @@ -37,6 +37,7 @@ #include #define __glibcxx_want_filesystem +#define __glibcxx_want_format_path #include #ifdef __cpp_lib_filesystem // C++ >= 17 && HOSTED diff --git a/libstdc++-v3/testsuite/std/format/fs_path.cc b/libstdc++-v3/testsuite/std/format/fs_path.cc new file mode 100644 index 00000000000..b91ae6fd449 --- /dev/null +++ b/libstdc++-v3/testsuite/std/format/fs_path.cc @@ -0,0 +1,136 @@ +// { dg-do run { target c++26 } } +// { dg-options "-fexec-charset=UTF-8" } + +#include +#include +#include + +using std::filesystem::path; + +template +bool +is_format_string_for(const char* str, Args&&... args) +{ + try { + (void) std::vformat(str, std::make_format_args(args...)); + return true; + } catch (const std::format_error&) { + return false; + } +} + +template +bool +is_format_string_for(const wchar_t* str, Args&&... args) +{ + try { + (void) std::vformat(str, std::make_wformat_args(args...)); + return true; + } catch (const std::format_error&) { + return false; + } +} + +void +test_format_spec() +{ + // [fs.path.fmtr.funcs] + // \nontermdef{path-format-spec}\br + // \opt{fill-and-align} \opt{width} \opt{\terminal{?}} \opt{\terminal{g}} + path p; + VERIFY( is_format_string_for("{}", p) ); + VERIFY( is_format_string_for("{:}", p) ); + VERIFY( is_format_string_for("{:?}", p) ); + VERIFY( is_format_string_for("{:g}", p) ); + VERIFY( is_format_string_for("{:?g}", p) ); + VERIFY( is_format_string_for("{:?g}", p) ); + VERIFY( is_format_string_for("{:F^32?g}", p) ); + VERIFY( is_format_string_for("{:G<{}?g}", p, 32) ); + VERIFY( is_format_string_for(L"{:G<{}?g}", p, 32) ); + + VERIFY( ! is_format_string_for("{:g?}", p) ); +} + +#define WIDEN_(C, S) ::std::__format::_Widen(S, L##S) +#define WFMT(S) WIDEN_(_FCharT, S) +#define WPATH(S) WIDEN_(_PCharT, S) + +template +void +test_format() +{ + std::basic_string<_FCharT> res; + res = std::format(WFMT("{}"), path(WPATH("/usr/include"))); + VERIFY( res == WFMT("/usr/include") ); + res = std::format(WFMT("{:.<10}"), path(WPATH("foo/bar"))); + VERIFY( res == WFMT("foo/bar...") ); + res = std::format(WFMT("{}"), path(WPATH("foo///bar"))); + VERIFY( res == WFMT("foo///bar") ); + res = std::format(WFMT("{:g}"), path(WPATH("foo///bar"))); + VERIFY( res == WFMT("foo/bar") ); + res = std::format(WFMT("{}"), path(WPATH("/path/with/new\nline"))); + VERIFY( res == WFMT("/path/with/new\nline") ); + res = std::format(WFMT("{:?}"), path(WPATH("multi\nline"))); + VERIFY( res == WFMT("\"multi\\nline\"") ); + res = std::format(WFMT("{:?g}"), path(WPATH("mu///lti\nli///ne"))); + VERIFY( res == WFMT("\"mu/lti\\nli/ne\"") ); + res = std::format(WFMT("{}"), + path(WPATH("\u0428\u0447\u0443\u0447\u044B\u043D\u0448\u0447\u044B\u043D\u0430"))); + VERIFY( res == WFMT("\u0428\u0447\u0443\u0447\u044B\u043D\u0448\u0447\u044B\u043D\u0430")); + + if constexpr (path::preferred_separator == L'\\') + { + res = std::format(WFMT("{}"), path(WPATH("C:\\foo\\bar"))); + VERIFY( res == WFMT("C:\\foo\\bar") ); + res = std::format(WFMT("{:g}"), path(WPATH("C:\\foo\\bar"))); + VERIFY( res == WFMT("C:/foo/bar") ); + } +} + +void +test_format_invalid() +{ + if constexpr (std::is_same_v) + { + std::wstring res; + std::string_view seq = "\xf0\x9f\xa6\x84"; // \U0001F984 + + path p(seq.substr(1)); + res = std::format(L"{}", p); + VERIFY( res == L"\uFFFD\uFFFD\uFFFD" ); + res = std::format(L"{:?}", p); + VERIFY( res == LR"("\x{9f}\x{a6}\x{84}")" ); + } + else + { + std::string res; + + path p(L"\xd800"); + res = std::format("{}", p); + VERIFY( res == "\uFFFD" ); + res = std::format("{:?}", p); + VERIFY( res == "\"\\x{d800}\"" ); + + path p2(L"///\xd800"); + res = std::format("{}", p2); + VERIFY( res == "///\uFFFD" ); + res = std::format("{:g}", p2); + VERIFY( res == "/\uFFFD" ); + res = std::format("{:?}", p2); + VERIFY( res == "\"///\\x{d800}\"" ); + res = std::format("{:?g}", p2); + VERIFY( res == "\"/\\x{d800}\"" ); + res = std::format("{:C>14?g}", p2); + VERIFY( res == "CCC\"/\\x{d800}\"" ); + } +} + +int main() +{ + test_format_spec(); + test_format(); + test_format(); + test_format(); + test_format(); + test_format_invalid(); +}