On Mon, Feb 16, 2026 at 11:28 AM Jonathan Wakely <[email protected]> wrote:

> On Mon, 16 Feb 2026 at 09:25 +0100, Tomasz Kamiński wrote:
> >From: Ivan Lazaric <[email protected]>
> >
> >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 <filesystem>.
> >
> >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.
> >       formatter<filesystem::path, _CharT>): 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 <[email protected]>
> >Signed-off-by: Ivan Lazaric <[email protected]>
> >Co-authored-by: Jonathan Wakely <[email protected]>
> >---
> >v3:
> > - updates formatting, replace spaces with tabs
> > - remove __always_inline from parse method
> > - extract _ViewT at begining of the format function
> > - use from_range constructor to create __out_str
> > - add new test for invalid UTF-8 sequence
> >
> >Testing on x86_64-linux. fs_patch.cc test passed with all standard
> >modes, cxx11 string API, and _GLIBCXX_DEBUG.
> >
> >OK for trunk when all test passes?
>
>
> OK, thank you both for getting this done.
>
Patch is now pushed to the trunk.

>
>
> > libstdc++-v3/include/bits/fs_path.h          | 107 +++++++++++++++
> > libstdc++-v3/include/bits/version.def        |  10 ++
> > libstdc++-v3/include/bits/version.h          |  10 ++
> > libstdc++-v3/include/std/filesystem          |   1 +
> > libstdc++-v3/testsuite/std/format/fs_path.cc | 136 +++++++++++++++++++
> > 5 files changed, 264 insertions(+)
> > create mode 100644 libstdc++-v3/testsuite/std/format/fs_path.cc
> >
> >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 <compare>
> > #endif
> >
> >+#ifdef __glibcxx_format_path // C++ >= 26 && HOSTED
> >+# include <bits/formatfwd.h>
> >+#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<filesystem::path, _CharT>
> >+    {
> >+      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 _Out>
> >+      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 <bits/requires_hosted.h>
> >
> > #define __glibcxx_want_filesystem
> >+#define __glibcxx_want_format_path
> > #include <bits/version.h>
> >
> > #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 <filesystem>
> >+#include <format>
> >+#include <testsuite_hooks.h>
> >+
> >+using std::filesystem::path;
> >+
> >+template<typename... Args>
> >+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<typename... Args>
> >+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<C>(S, L##S)
> >+#define WFMT(S) WIDEN_(_FCharT, S)
> >+#define WPATH(S) WIDEN_(_PCharT, S)
> >+
> >+template<typename _FCharT, typename _PCharT>
> >+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<path::value_type, char>)
> >+  {
> >+    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<char, char>();
> >+  test_format<char, wchar_t>();
> >+  test_format<wchar_t, char>();
> >+  test_format<wchar_t, wchar_t>();
> >+  test_format_invalid();
> >+}
> >--
> >2.53.0
> >
> >
>
>

Reply via email to