]> git.ipfire.org Git - thirdparty/gcc.git/commitdiff
libstdc++: implement formatter for std::filesystem::path
authorIvan Lazaric <ivan.lazaric1@gmail.com>
Mon, 9 Feb 2026 12:26:36 +0000 (13:26 +0100)
committerTomasz Kamiński <tkaminsk@redhat.com>
Mon, 16 Feb 2026 11:34:18 +0000 (12:34 +0100)
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.
(std::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 <tkaminsk@redhat.com>
Signed-off-by: Ivan Lazaric <ivan.lazaric1@gmail.com>
Co-authored-by: Jonathan Wakely <jwakely@redhat.com>
libstdc++-v3/include/bits/fs_path.h
libstdc++-v3/include/bits/version.def
libstdc++-v3/include/bits/version.h
libstdc++-v3/include/std/filesystem
libstdc++-v3/testsuite/std/format/fs_path.cc [new file with mode: 0644]

index 07b74de6cbe9ade51d3963fe3021c774de1ddb4a..5c0d5c9d5f1e3d5fa5379edd89b6fe3be50bdd76 100644 (file)
 # 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
 
index 4b8e9d43ec20c6d0a0bbc6061afc3657c444187f..c7709ba3a070bbcf85ec8243b05a51a81b6f1764 100644 (file)
@@ -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 = {
index 7602225cb6df306b6f7412d7ab12d566438ee2f4..c72cda506f1eb94cfc1c67933e46a76b095b72cb 100644 (file)
 #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
index f902c6feb77a9a8dfb13f4dad2598fc874642983..b9900f49c3364c2efe008a73c12b3d53f2f4cf08 100644 (file)
@@ -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 (file)
index 0000000..b91ae6f
--- /dev/null
@@ -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();
+}