From 8d1bdb2e432b78a0bde09e2b9a89a056a2a86353 Mon Sep 17 00:00:00 2001 From: Changlin Hsieh Date: Tue, 11 Feb 2014 11:12:29 +0800 Subject: [PATCH 01/25] Implement the wrapper for direct return vector for issue #12 --- pystring.h | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pystring.h b/pystring.h index 45ffd9a..58bb5c0 100644 --- a/pystring.h +++ b/pystring.h @@ -178,6 +178,12 @@ namespace pystring /// not found, the original string will be returned with two empty strings. /// void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + inline std::vector< std::string > partition( const std::string & str, const std::string & sep ) + { + std::vector< std::string > result; + partition( str, sep, result ); + return result; + } ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with all occurrences of substring old replaced by new. If @@ -211,6 +217,12 @@ namespace pystring /// not found, the original string will be returned with two empty strings. /// void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + inline std::vector< std::string > rpartition ( const std::string & str, const std::string & sep ) + { + std::vector< std::string > result; + rpartition( str, sep, result ); + return result; + } ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with trailing characters removed. If chars is "", whitespace @@ -225,6 +237,12 @@ namespace pystring /// any whitespace string is a separator. /// void split( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); + inline std::vector< std::string > split( const std::string & str, const std::string & sep = "", int maxsplit = -1) + { + std::vector< std::string > result; + split( str, result, sep, maxsplit ); + return result; + } ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Fills the "result" list with the words in the string, using sep as the delimiter string. @@ -234,12 +252,24 @@ namespace pystring /// any whitespace string is a separator. /// void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); + inline std::vector< std::string > rsplit( const std::string & str, const std::string & sep = "", int maxsplit = -1) + { + std::vector< std::string > result; + rsplit( str, result, sep, maxsplit); + return result; + } ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a list of the lines in the string, breaking at line boundaries. Line breaks /// are not included in the resulting list unless keepends is given and true. /// void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends = false ); + inline std::vector< std::string > splitlines( const std::string & str, bool keepends = false ) + { + std::vector< std::string > result; + splitlines( str, result, keepends); + return result; + } ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return True if string starts with the prefix, otherwise return False. With optional start, From 851abee07a45e0077f5ddfb8c49040dc08eba449 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 21 Oct 2015 16:03:04 -0700 Subject: [PATCH 02/25] Allow default values in Makefile to be overridden This is handy when building this within an automated build system where creating and applying a patch is a PITA. --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 3742f3c..5ac4d7e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -LIBTOOL = libtool -LIBDIR = /usr/lib -CXX = g++ -CXXFLAGS = -g -O3 -Wall -Wextra -Wshadow -Wconversion -Wcast-qual -Wformat=2 +LIBTOOL ?= libtool +LIBDIR ?= /usr/lib +CXX ?= g++ +CXXFLAGS ?= -g -O3 -Wall -Wextra -Wshadow -Wconversion -Wcast-qual -Wformat=2 all: libpystring.la From b9b2a2eb469383e3abf93b9e101943e41f4fa329 Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Tue, 4 Feb 2020 05:32:18 +0000 Subject: [PATCH 03/25] declaring global string constants for performance improvement --- pystring.cpp | 127 +++++++++++++++++++++++++++------------------------ 1 file changed, 68 insertions(+), 59 deletions(-) diff --git a/pystring.cpp b/pystring.cpp index ec30d8b..23373e1 100644 --- a/pystring.cpp +++ b/pystring.cpp @@ -56,6 +56,15 @@ namespace pystring // This must also equal the size used in the end = MAX_32BIT_INT default arg. typedef int Py_ssize_t; +const std::string forward_slash = "/"; +const std::string double_forward_slash = "//"; +const std::string triple_forward_slash = "///"; +const std::string double_back_slash = "\\"; +const std::string empty_string = ""; +const std::string dot = "."; +const std::string double_dot = ".."; +const std::string colon = ":"; + /* helper macro to fixup start/end slice values */ #define ADJUST_INDICES(start, end, len) \ @@ -324,8 +333,8 @@ typedef int Py_ssize_t; if ( index < 0 ) { result[0] = str; - result[1] = ""; - result[2] = ""; + result[1] = empty_string; + result[2] = empty_string; } else { @@ -344,8 +353,8 @@ typedef int Py_ssize_t; int index = rfind( str, sep ); if ( index < 0 ) { - result[0] = ""; - result[1] = ""; + result[0] = empty_string; + result[1] = empty_string; result[2] = str; } else @@ -387,7 +396,7 @@ typedef int Py_ssize_t; { std::vector< std::string >::size_type seqlen = seq.size(), i; - if ( seqlen == 0 ) return ""; + if ( seqlen == 0 ) return empty_string; if ( seqlen == 1 ) return seq[0]; std::string result( seq[0] ); @@ -838,7 +847,7 @@ typedef int Py_ssize_t; std::string slice( const std::string & str, int start, int end ) { ADJUST_INDICES(start, end, (int) str.size()); - if ( start >= end ) return ""; + if ( start >= end ) return empty_string; return str.substr( start, end - start ); } @@ -922,7 +931,7 @@ typedef int Py_ssize_t; } else { - s.replace( i + offset, 1, "" ); + s.replace( i + offset, 1, empty_string ); offset -= 1; } @@ -1051,7 +1060,7 @@ typedef int Py_ssize_t; std::string mul( const std::string & str, int n ) { // Early exits - if (n <= 0) return ""; + if (n <= 0) return empty_string; if (n == 1) return str; std::ostringstream os; @@ -1080,7 +1089,7 @@ namespace path void splitdrive_nt(std::string & drivespec, std::string & pathspec, const std::string & p) { - if(pystring::slice(p, 1, 2) == ":") + if(pystring::slice(p, 1, 2) == colon) { std::string path = p; // In case drivespec == p drivespec = pystring::slice(path, 0, 2); @@ -1088,7 +1097,7 @@ namespace path } else { - drivespec = ""; + drivespec = empty_string; pathspec = p; } } @@ -1097,7 +1106,7 @@ namespace path void splitdrive_posix(std::string & drivespec, std::string & pathspec, const std::string & path) { - drivespec = ""; + drivespec = empty_string; pathspec = path; } @@ -1128,7 +1137,7 @@ namespace path bool isabs_posix(const std::string & s) { - return pystring::startswith(s, "/"); + return pystring::startswith(s, forward_slash); } bool isabs(const std::string & path) @@ -1175,7 +1184,7 @@ namespace path std::string join_nt(const std::vector< std::string > & paths) { - if(paths.empty()) return ""; + if(paths.empty()) return empty_string; if(paths.size() == 1) return paths[0]; std::string path = paths[0]; @@ -1200,15 +1209,15 @@ namespace path // 4. join('c:', 'd:/') = 'd:/' // 5. join('c:/', 'd:/') = 'd:/' - if( (pystring::slice(path, 1, 2) != ":") || - (pystring::slice(b, 1, 2) == ":") ) + if( (pystring::slice(path, 1, 2) != colon) || + (pystring::slice(b, 1, 2) == colon) ) { // Path doesnt start with a drive letter b_nts = true; } // Else path has a drive letter, and b doesn't but is absolute. else if((path.size()>3) || - ((path.size()==3) && !pystring::endswith(path, "/") && !pystring::endswith(path, "\\"))) + ((path.size()==3) && !pystring::endswith(path, forward_slash) && !pystring::endswith(path, double_back_slash))) { b_nts = true; } @@ -1222,9 +1231,9 @@ namespace path { // Join, and ensure there's a separator. // assert len(path) > 0 - if( pystring::endswith(path, "/") || pystring::endswith(path, "\\")) + if( pystring::endswith(path, forward_slash) || pystring::endswith(path, double_back_slash)) { - if(pystring::startswith(b,"/") || pystring::startswith(b,"\\")) + if(pystring::startswith(b,forward_slash) || pystring::startswith(b,double_back_slash)) { path += pystring::slice(b, 1); } @@ -1233,19 +1242,19 @@ namespace path path += b; } } - else if(pystring::endswith(path, ":")) + else if(pystring::endswith(path, colon)) { path += b; } else if(!b.empty()) { - if(pystring::startswith(b,"/") || pystring::startswith(b,"\\")) + if(pystring::startswith(b, forward_slash) || pystring::startswith(b,double_back_slash)) { path += b; } else { - path += "\\" + b; + path += double_back_slash + b; } } else @@ -1254,7 +1263,7 @@ namespace path // but b is empty; since, e.g., split('a/') produces // ('a', ''), it's best if join() adds a backslash in // this case. - path += "\\"; + path += double_back_slash; } } } @@ -1262,7 +1271,7 @@ namespace path return path; } - // Join two or more pathname components, inserting "\\" as needed. + // Join two or more pathname components, inserting double_back_slash as needed. std::string join_nt(const std::string & a, const std::string & b) { std::vector< std::string > paths(2); @@ -1279,7 +1288,7 @@ namespace path std::string join_posix(const std::vector< std::string > & paths) { - if(paths.empty()) return ""; + if(paths.empty()) return empty_string; if(paths.size() == 1) return paths[0]; std::string path = paths[0]; @@ -1287,17 +1296,17 @@ namespace path for(unsigned int i=1; i comps; - pystring::split(path, comps, "\\"); + pystring::split(path, comps, double_back_slash); int i = 0; while(i<(int)comps.size()) { - if(comps[i].empty() || comps[i] == ".") + if(comps[i].empty() || comps[i] == dot) { comps.erase(comps.begin()+i); } - else if(comps[i] == "..") + else if(comps[i] == double_dot) { - if(i>0 && comps[i-1] != "..") + if(i>0 && comps[i-1] != double_dot) { comps.erase(comps.begin()+i-1, comps.begin()+i+1); i -= 1; } - else if(i == 0 && pystring::endswith(prefix, "\\")) + else if(i == 0 && pystring::endswith(prefix, double_back_slash)) { comps.erase(comps.begin()+i); } @@ -1527,10 +1536,10 @@ namespace path // If the path is now empty, substitute '.' if(prefix.empty() && comps.empty()) { - comps.push_back("."); + comps.push_back(dot); } - return prefix + pystring::join("\\", comps); + return prefix + pystring::join(double_back_slash, comps); } // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B. @@ -1540,30 +1549,30 @@ namespace path std::string normpath_posix(const std::string & p) { - if(p.empty()) return "."; + if(p.empty()) return dot; std::string path = p; - int initial_slashes = pystring::startswith(path,"/") ? 1 : 0; + int initial_slashes = pystring::startswith(path, forward_slash) ? 1 : 0; // POSIX allows one or two initial slashes, but treats three or more // as single slash. - if (initial_slashes && pystring::startswith(path,"//") - && !pystring::startswith(path,"///")) + if (initial_slashes && pystring::startswith(path, double_forward_slash) + && !pystring::startswith(path, triple_forward_slash)) initial_slashes = 2; std::vector comps, new_comps; - pystring::split(path, comps, "/"); + pystring::split(path, comps, forward_slash); for(unsigned int i=0; i 0) - path = pystring::mul("/",initial_slashes) + path; + path = pystring::mul(forward_slash, initial_slashes) + path; - if(path.empty()) return "."; + if(path.empty()) return dot; return path; } @@ -1633,19 +1642,19 @@ namespace path } root = p; - ext = ""; + ext = empty_string; } void splitext_nt(std::string & root, std::string & ext, const std::string & path) { return splitext_generic(root, ext, path, - "\\", "/", "."); + double_back_slash, forward_slash, dot); } void splitext_posix(std::string & root, std::string & ext, const std::string & path) { return splitext_generic(root, ext, path, - "/", "", "."); + forward_slash, empty_string, dot); } void splitext(std::string & root, std::string & ext, const std::string & path) From 9bfc0b94ec2ca07782b2ad6124a412c3628b3b50 Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Tue, 4 Feb 2020 06:19:00 +0000 Subject: [PATCH 04/25] more efficient comparison with the colon string --- pystring.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pystring.cpp b/pystring.cpp index 23373e1..e17aa68 100644 --- a/pystring.cpp +++ b/pystring.cpp @@ -1089,7 +1089,7 @@ namespace path void splitdrive_nt(std::string & drivespec, std::string & pathspec, const std::string & p) { - if(pystring::slice(p, 1, 2) == colon) + if (p.size() >= 2 && p[1] == ':') { std::string path = p; // In case drivespec == p drivespec = pystring::slice(path, 0, 2); @@ -1209,8 +1209,8 @@ namespace path // 4. join('c:', 'd:/') = 'd:/' // 5. join('c:/', 'd:/') = 'd:/' - if( (pystring::slice(path, 1, 2) != colon) || - (pystring::slice(b, 1, 2) == colon) ) + + if ((path.size() >= 2 && path[1] != ':') || (b.size() >= 2 && b[1] == ':')) { // Path doesnt start with a drive letter b_nts = true; From 5a0840a73740d014692f4b7ea19f35878df131d4 Mon Sep 17 00:00:00 2001 From: Niclas Rosenvik Date: Fri, 4 Mar 2022 13:21:02 +0100 Subject: [PATCH 05/25] Add removeprefix and removesuffix to pystring Add removeprefix and removesuffix to pystring. Python 3.9 added removeprefix and removesuffix methods to Pythons string class. Add them to pystring to match the interfaces. --- pystring.cpp | 25 +++++++++++++++++++++++++ pystring.h | 12 ++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pystring.cpp b/pystring.cpp index e17aa68..ef3f98d 100644 --- a/pystring.cpp +++ b/pystring.cpp @@ -1071,6 +1071,31 @@ const std::string colon = ":"; return os.str(); } + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string removeprefix( const std::string & str, const std::string & prefix ) + { + if (pystring::startswith(str, prefix)) + { + return str.substr(prefix.length()); + } + + return str; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string removesuffix( const std::string & str, const std::string & suffix ) + { + if (pystring::endswith(str, suffix)) + { + return str.substr(0, str.length() - suffix.length()); + } + + return str; + } namespace os diff --git a/pystring.h b/pystring.h index 45ffd9a..fad270c 100644 --- a/pystring.h +++ b/pystring.h @@ -179,6 +179,18 @@ namespace pystring /// void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + ////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief If str starts with prefix return a copy of the string with prefix at the start + /// removed otherwise return an unmodified copy of the string. + /// + std::string removeprefix( const std::string & str, const std::string & prefix ); + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief If str ends with suffix return a copy of the string with suffix at the end removed + /// otherwise return an unmodified copy of the string. + /// + std::string removesuffix( const std::string & str, const std::string & suffix ); + ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with all occurrences of substring old replaced by new. If /// the optional argument count is given, only the first count occurrences are replaced. From e8e34d02a62763c6ad454a07825eae44ca2bcf14 Mon Sep 17 00:00:00 2001 From: Niclas Rosenvik Date: Fri, 4 Mar 2022 13:39:47 +0100 Subject: [PATCH 06/25] Add tests for removeprefix and removesuffix --- test.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test.cpp b/test.cpp index dea2dbe..ea3628a 100644 --- a/test.cpp +++ b/test.cpp @@ -94,6 +94,18 @@ PYSTRING_ADD_TEST(pystring, rfind) PYSTRING_CHECK_EQUAL(pystring::rfind("abcabcabc", "abc", 6, 8), -1); } +PYSTRING_ADD_TEST(pystring, removeprefix) +{ + PYSTRING_CHECK_EQUAL(pystring::removeprefix("abcdef", "abc"), "def"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("abcdef", "bcd"), "abcdef"); +} + +PYSTRING_ADD_TEST(pystring, removesuffix) +{ + PYSTRING_CHECK_EQUAL(pystring::removesuffix("abcdef", "def"), "abc"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("abcdef", "cde"), "abcdef"); +} + PYSTRING_ADD_TEST(pystring, replace) { PYSTRING_CHECK_EQUAL(pystring::replace("abcdef", "foo", "bar"), "abcdef"); From cce54a1bcb6f6b592475a4275870011b1b6081a3 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Wed, 8 Jun 2022 21:51:45 -0700 Subject: [PATCH 07/25] Revise license formatting to match other modern projects. (#33) No change to the fact that this project is BSD-3-clause licensed. But this slight rewording/reformatting: * Brings the notices in line with the exact wording we use in other projects such as OSL. * Now will be correctly recognized by GitHub and other automated license checkers as being EXACTLY the same as the canonical BSD 3-clause license. This was approved by SPI legal. --- LICENSE | 51 ++++++++++++++++++++++++--------------------------- README | 13 ++++++++++--- pystring.cpp | 36 ++++-------------------------------- pystring.h | 36 ++++-------------------------------- test.cpp | 4 ++++ unittest.h | 35 +++-------------------------------- 6 files changed, 49 insertions(+), 126 deletions(-) diff --git a/LICENSE b/LICENSE index bfdb07d..8269a9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,30 +1,27 @@ -Copyright (c) 2008-2010, Sony Pictures Imageworks Inc -All rights reserved. +Copyright (c) 2008-present Contributors to the Pystring project. +All Rights Reserved. Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: +modification, are permitted provided that the following conditions are met: -Redistributions of source code must retain the above copyright notice, -this list of conditions and the following disclaimer. -Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -Neither the name of the organization Sony Pictures Imageworks nor the -names of its contributors -may be used to endorse or promote products derived from this software -without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS -IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER -OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README b/README index 2c3b2c7..3862064 100644 --- a/README +++ b/README @@ -1,8 +1,15 @@ -Pystring is a collection of C++ functions which match the interface and behavior of python's string class methods using std::string. Implemented in C++, it does not require or make use of a python interpreter. It provides convenience and familiarity for common string operations not included in the standard C++ library. It's also useful in environments where both C++ and python are used. +Pystring is a collection of C++ functions which match the interface and +behavior of python's string class methods using std::string. Implemented in +C++, it does not require or make use of a python interpreter. It provides +convenience and familiarity for common string operations not included in the +standard C++ library. It's also useful in environments where both C++ and +python are used. -Overlapping functionality (such as index and slice/substr) of std::string is included to match python interfaces. +Overlapping functionality (such as index and slice/substr) of std::string is +included to match python interfaces. Originally developed at Sony Pictures Imageworks. http://opensource.imageworks.com/ -Note: Despite the infrequent updates, this repo is not dead/abandoned - just stable! We use it every day at Imageworks. \ No newline at end of file +Note: Despite the infrequent updates, this repo is not dead/abandoned - just +stable! We use it every day at Imageworks. diff --git a/pystring.cpp b/pystring.cpp index e17aa68..dfc5e09 100644 --- a/pystring.cpp +++ b/pystring.cpp @@ -1,35 +1,7 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright (c) 2008-2010, Sony Pictures Imageworks Inc -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// Neither the name of the organization Sony Pictures Imageworks nor the -// names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS -// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER -// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -/////////////////////////////////////////////////////////////////////////////// +// Copyright Contributors to the Pystring project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/imageworks/pystring/blob/master/LICENSE + #include "pystring.h" diff --git a/pystring.h b/pystring.h index 45ffd9a..7ca9607 100644 --- a/pystring.h +++ b/pystring.h @@ -1,35 +1,7 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright (c) 2008-2010, Sony Pictures Imageworks Inc -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// Neither the name of the organization Sony Pictures Imageworks nor the -// names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS -// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER -// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -/////////////////////////////////////////////////////////////////////////////// +// Copyright Contributors to the Pystring project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/imageworks/pystring/blob/master/LICENSE + #ifndef INCLUDED_PYSTRING_H #define INCLUDED_PYSTRING_H diff --git a/test.cpp b/test.cpp index dea2dbe..110c673 100644 --- a/test.cpp +++ b/test.cpp @@ -1,3 +1,7 @@ +// Copyright Contributors to the Pystring project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/imageworks/pystring/blob/master/LICENSE + #include #include "pystring.h" diff --git a/unittest.h b/unittest.h index 83090e9..536d9cb 100644 --- a/unittest.h +++ b/unittest.h @@ -1,35 +1,6 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright (c) 2008-2010, Sony Pictures Imageworks Inc -// All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// Neither the name of the organization Sony Pictures Imageworks nor the -// names of its contributors -// may be used to endorse or promote products derived from this software -// without specific prior written permission. -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS -// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED -// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER -// OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, -// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR -// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF -// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -/////////////////////////////////////////////////////////////////////////////// +// Copyright Contributors to the Pystring project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/imageworks/pystring/blob/master/LICENSE #ifndef INCLUDED_PYSTRING_UNITTEST_H #define INCLUDED_PYSTRING_UNITTEST_H From 22839311d1dab4b6d5f48f86e26433be3d7fcf56 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Mon, 13 Jun 2022 12:02:28 +0800 Subject: [PATCH 08/25] README: Add repology shield --- README => README.md | 2 ++ 1 file changed, 2 insertions(+) rename README => README.md (85%) diff --git a/README b/README.md similarity index 85% rename from README rename to README.md index 3862064..2805f5d 100644 --- a/README +++ b/README.md @@ -1,3 +1,5 @@ +[![Repology](https://img.shields.io/repology/repositories/pystring)](https://repology.org/project/pystring/versions) + Pystring is a collection of C++ functions which match the interface and behavior of python's string class methods using std::string. Implemented in C++, it does not require or make use of a python interpreter. It provides From bf5cd2d3b191c0917fc62d705ae6dae835383896 Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Mon, 11 Jul 2022 01:26:19 +0100 Subject: [PATCH 09/25] initial version of cmake config --- CMakeLists.txt | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ca4f506 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 2.8.12) +project(pystring CXX) + +set(BUILD_SHARED_LIBS YES) + +add_library(pystring + pystring.cpp + pystring.h +) + +add_executable (pystring_test test.cpp) +TARGET_LINK_LIBRARIES (pystring_test pystring) + +enable_testing() +add_test(NAME PyStringTest COMMAND pystring_test) + +include(GNUInstallDirs) + +install(TARGETS pystring + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + From fd8b46f79bc39eecacfd150dba0b889f20a7c801 Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Mon, 11 Jul 2022 23:38:17 +0100 Subject: [PATCH 10/25] moving cmake version to 3.2 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ca4f506..fed418f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 2.8.12) +cmake_minimum_required(VERSION 3.2) project(pystring CXX) set(BUILD_SHARED_LIBS YES) From 3b85d48cf1051fe30783f439352d4f990ae05cce Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Mon, 22 Aug 2022 00:39:52 +0100 Subject: [PATCH 11/25] adding aswf ci yaml file --- .github/workflows/ci.yml | 480 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..628f081 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,480 @@ +# borrowed this from: +# https://raw.githubusercontent.com/AcademySoftwareFoundation/Imath/main/.github/workflows/ci_workflow.yml +# with some slight simplification + +name: CI + +on: + push: + # Jobs are skipped when ONLY Markdown (*.md) files are changed + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' + +jobs: + # Linux jobs run in Docker containers, so the latest OS version is OK. macOS + # and Windows jobs need to be locked to specific virtual environment + # versions to mitigate issues from OS updates, and will require maintenance + # as OS versions are retired. + # + # GH Actions (Free plan) supports 20 concurrent jobs, with 5 concurrent macOS + # jobs. This workflow tries to utilize (but not exceed) that budget to + # promote timely CI. + + # --------------------------------------------------------------------------- + # Linux + # --------------------------------------------------------------------------- + # TODO: Add ARM build. Add sanitize build. + + linux: + name: 'Linux CentOS 7 VFX CY${{ matrix.vfx-cy }} + <${{ matrix.compiler-desc }} , + config=${{ matrix.build-type }}, + shared=${{ matrix.build-shared }}, + cxx=${{ matrix.cxx-standard }}>' + # GH-hosted VM. The build runs in CentOS 7 'container' defined below. + runs-on: ubuntu-latest + container: + # DockerHub: https://hub.docker.com/u/aswf + # Source: https://github.com/AcademySoftwareFoundation/aswf-docker + image: aswf/ci-openexr:${{ matrix.vfx-cy }} + strategy: + matrix: + build: [1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22] + include: + # ------------------------------------------------------------------- + # GCC, VFX CY2022 + # ------------------------------------------------------------------- + # C++17, Python 3.9 + - build: 1 + build-type: Release + build-shared: 'ON' + cxx-standard: 17 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc9.3.1 + vfx-cy: 2022 + + # C++17, Python 3.9.7, Debug + - build: 2 + build-type: Debug + build-shared: 'ON' + cxx-standard: 17 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc9.3.1 + vfx-cy: 2022 + + # C++17, Python 3.9.7, Static + - build: 3 + build-type: Release + build-shared: 'OFF' + cxx-standard: 17 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc9.3.1 + vfx-cy: 2022 + + # C++14, Python 3.9.7, Static + - build: 4 + build-type: Release + build-shared: 'ON' + cxx-standard: 14 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc9.3.1 + vfx-cy: 2022 + + # C++17, no Python + # - build: 5 + # build-type: Release + # build-shared: 'ON' + # cxx-standard: 17 + # cxx-compiler: g++ + # cc-compiler: gcc + # compiler-desc: gcc9.3.1 + # python: 'OFF' + # python-desc: no python + # vfx-cy: 2022 + + # ------------------------------------------------------------------- + # GCC, VFX CY2021 + # ------------------------------------------------------------------- + # C++17, Python 3.7.9 + - build: 9 + build-type: Release + build-shared: 'ON' + cxx-standard: 17 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc9.3.1 + python: 'ON' + vfx-cy: 2021 + + # ------------------------------------------------------------------- + # GCC, VFX CY2020 + # ------------------------------------------------------------------- + # C++14, Python 3.7 + - build: 10 + build-type: Release + build-shared: 'ON' + cxx-standard: 14 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc6.3.1 + vfx-cy: 2020 + + # ------------------------------------------------------------------- + # GCC, VFX CY2019 + # ------------------------------------------------------------------- + # C++11, Python 2.7 + - build: 11 + build-type: Release + build-shared: 'ON' + cxx-standard: 11 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc6.3.1 + vfx-cy: 2019 + + # ------------------------------------------------------------------- + # Clang, VFX CY2022 + # ------------------------------------------------------------------- + # C++17, Python 3.9 + - build: 12 + build-type: Release + build-shared: 'ON' + cxx-standard: 17 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang10.4 + vfx-cy: 2022 + + # C++17, Python 3.9.7, Debug + - build: 13 + build-type: Debug + build-shared: 'ON' + cxx-standard: 17 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang10.4 + vfx-cy: 2022 + + # C++17, Python 3.9.7, Static + - build: 14 + build-type: Release + build-shared: 'OFF' + cxx-standard: 17 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang10.4 + vfx-cy: 2022 + + # C++14, Python 3.9.7, Static + - build: 15 + build-type: Release + build-shared: 'ON' + cxx-standard: 14 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang10.4 + vfx-cy: 2022 + + # ------------------------------------------------------------------- + # Clang, VFX CY2021 + # ------------------------------------------------------------------- + # C++17, Python 3.7.9 + - build: 20 + build-type: Release + build-shared: 'ON' + cxx-standard: 17 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang10.4 + vfx-cy: 2021 + + # ------------------------------------------------------------------- + # Clang, VFX CY2020 + # ------------------------------------------------------------------- + # C++14, Python 3.7 + - build: 21 + build-type: Release + build-shared: 'ON' + cxx-standard: 14 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang7.8 + vfx-cy: 2020 + + # ------------------------------------------------------------------- + # Clang, VFX CY2019 + # ------------------------------------------------------------------- + # C++11, Python 2.7 + - build: 22 + build-type: Release + build-shared: 'ON' + cxx-standard: 11 + cxx-compiler: clang++ + cc-compiler: clang + compiler-desc: clang7.8 + vfx-cy: 2019 + + env: + CXX: ${{ matrix.cxx-compiler }} + CC: ${{ matrix.cc-compiler }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Create build directories + run: | + mkdir _install + mkdir _build + - name: Configure + run: | + cmake .. \ + -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ + -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + working-directory: _build + - name: Build + run: | + cmake --build . \ + --target install \ + --config ${{ matrix.build-type }} + working-directory: _build + - name: Test + run: | + ctest -C ${{ matrix.build_type }} + working-directory: _build + + # --------------------------------------------------------------------------- + # macOS + # --------------------------------------------------------------------------- + + macos_no_python: + name: 'macOS 10.15 + ' + runs-on: macos-10.15 + strategy: + matrix: + build: [1, 2, 3, 4, 5] + include: + # C++11 + - build: 1 + build-type: Release + build-shared: 'ON' + build-docs: 'ON' + cxx-standard: 17 + exclude-tests: + + # Debug + - build: 2 + build-type: Debug + build-shared: 'ON' + build-docs: 'OFF' + cxx-standard: 17 + exclude-tests: + + # Static + - build: 3 + build-type: Debug + build-shared: 'OFF' + build-docs: 'OFF' + cxx-standard: 17 + exclude-tests: + + # C++14 + - build: 4 + build-type: Release + build-shared: 'ON' + build-docs: 'OFF' + cxx-standard: 14 + exclude-tests: + + # C++11 + - build: 5 + build-type: Release + build-shared: 'ON' + build-docs: 'OFF' + cxx-standard: 11 + exclude-tests: + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Create build directories + run: | + mkdir _install + mkdir _build + + - name: Configure + run: | + cmake ../. \ + -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ + -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} + working-directory: _build + - name: Build + run: | + cmake --build . \ + --target install \ + --config ${{ matrix.build-type }} \ + -- -j2 + working-directory: _build + - name: Test + run: | + ctest -C ${{ matrix.build_type }} + working-directory: _build + + # --------------------------------------------------------------------------- + # Windows + # --------------------------------------------------------------------------- + + windows: + name: 'Windows 2019 + <${{ matrix.compiler-desc }}, + config=${{ matrix.build-type }}, + shared=${{ matrix.build-shared }}, + cxx=${{ matrix.cxx-standard }}, + docs=${{ matrix.build-docs }}>' + runs-on: windows-${{ matrix.osver }} + strategy: + matrix: + build: [1, 3, 4, 6, 7, 8] + include: + # ------------------------------------------------------------------- + # VFX CY2022 - C++17 - Windows 2019 + # ------------------------------------------------------------------- + # C++17, Release Shared + - build: 1 + build-type: Release + build-shared: 'ON' + build-docs: 'ON' + compiler-desc: msvc16.11 + cxx-standard: 17 + vfx-cy: 2022 + exclude-tests: + osver: 2019 + + # C++17, Debug - + ## - build: 2 + ## build-type: Debug + ## build-shared: 'ON' + ## build-docs: 'OFF' + ## cxx-standard: 17 + ## exclude-tests: + + # C++17, Release Static + - build: 3 + build-type: Release + build-shared: 'OFF' + build-docs: 'OFF' + compiler-desc: msvc16.11 + cxx-standard: 17 + vfx-cy: 2022 + exclude-tests: + osver: 2019 + + # ------------------------------------------------------------------- + # VFX CY2022 - C++17 - Windows 2022 + # ------------------------------------------------------------------- + # C++17, Release Shared + - build: 4 + build-type: Release + build-shared: 'ON' + build-docs: 'ON' + compiler-desc: msvc17.1 + cxx-standard: 17 + vfx-cy: 2022 + exclude-tests: + osver: 2022 + + # C++17, Debug - + ## - build: 5 + ## build-type: Debug + ## build-shared: 'ON' + ## build-docs: 'OFF' + ## cxx-standard: 17 + ## exclude-tests: + + # C++17, Release Static + - build: 6 + build-type: Release + build-shared: 'OFF' + build-docs: 'OFF' + compiler-desc: msvc17.1 + cxx-standard: 17 + vfx-cy: 2022 + exclude-tests: + osver: 2022 + + # ------------------------------------------------------------------- + # VFX CY2020 - C++14 - Windows 2019 + # ------------------------------------------------------------------- + # C++14, Release Shared + - build: 7 + build-type: Release + build-shared: 'ON' + build-docs: 'OFF' + compiler-desc: msvc16.11 + cxx-standard: 14 + vfx-cy: 2020 + exclude-tests: + osver: 2019 + + # ------------------------------------------------------------------- + # VFX CY2019 - C++11 - Windows 2019 + # ------------------------------------------------------------------- + # C++11, Release Shared + - build: 8 + build-type: Release + build-shared: 'ON' + build-docs: 'OFF' + compiler-desc: msvc16.11 + cxx-standard: 11 + exclude-tests: + osver: 2019 + + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Create build directories + run: | + mkdir _install + mkdir _build + shell: bash + - name: Configure + run: | + cmake ../. \ + -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS='ON'\ + -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ + -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ + -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ + -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} + shell: bash + working-directory: _build + - name: Build + run: | + cmake --build . \ + --target install \ + --config ${{ matrix.build-type }} + shell: bash + working-directory: _build + - name: Test + run: | + ctest -C Release + shell: bash + working-directory: _build \ No newline at end of file From e7b4ab950a94954c998df0300a245cedce9418fe Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Mon, 22 Aug 2022 00:44:57 +0100 Subject: [PATCH 12/25] trying out dynamic build type again --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 628f081..51c96f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -475,6 +475,6 @@ jobs: working-directory: _build - name: Test run: | - ctest -C Release + ctest -C ${{ matrix.build-type }} shell: bash working-directory: _build \ No newline at end of file From 87f735237b618515ec48204936e3e87a0d30b01b Mon Sep 17 00:00:00 2001 From: Daniel Greenstein Date: Mon, 22 Aug 2022 00:47:39 +0100 Subject: [PATCH 13/25] additional comment for future self --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51c96f6..868ffd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -455,6 +455,7 @@ jobs: mkdir _build shell: bash - name: Configure + # the windows build needs the -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS to work run: | cmake ../. \ -DCMAKE_INSTALL_PREFIX=../_install \ From 74700b2c04adcdfa9b081526c514859b62304ecd Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Thu, 4 May 2023 03:42:21 +0200 Subject: [PATCH 14/25] Proper installation paths It's Makefile convention to be able to install to `DESTDIR`. This is especially important for packaging on Linux distributions but might be convenient for other users as well. Also this introduces `PREFIX`, another convention. --- Makefile | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 5ac4d7e..a0df9aa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ LIBTOOL ?= libtool -LIBDIR ?= /usr/lib +PREFIX ?= /usr +INCLUDEDIR ?= ${PREFIX}/include/pystring +LIBDIR ?= ${PREFIX}/lib CXX ?= g++ CXXFLAGS ?= -g -O3 -Wall -Wextra -Wshadow -Wconversion -Wcast-qual -Wformat=2 @@ -12,7 +14,8 @@ libpystring.la: pystring.lo $(LIBTOOL) --mode=link --tag=CXX $(CXX) -o $@ $< -rpath $(LIBDIR) install: libpystring.la - $(LIBTOOL) --mode=install install -c $< $(LIBDIR)/$< + $(LIBTOOL) --mode=install install -Dm755 $< $(DESTDIR)$(LIBDIR)/$< + $(LIBTOOL) --mode=install install -Dm644 pystring.h $(DESTDIR)$(INCLUDEDIR)/pystring.h clean: $(RM) -fr pystring.lo pystring.o libpystring.la .libs From 4b8029a55c8a3e1839b1445606ec80f39995b09a Mon Sep 17 00:00:00 2001 From: Morozov Sergey Date: Mon, 15 May 2023 15:15:31 +0900 Subject: [PATCH 15/25] Added option to build static lib and installing header --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fed418f..0354c36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.2) project(pystring CXX) -set(BUILD_SHARED_LIBS YES) +option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) add_library(pystring pystring.cpp @@ -19,4 +19,8 @@ include(GNUInstallDirs) install(TARGETS pystring LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} ) +install (FILES pystring.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} + COMPONENT developer +) From e5df7dd77f239889713ab54fa5f23504759e252f Mon Sep 17 00:00:00 2001 From: Timo Gurr Date: Tue, 13 May 2025 16:30:22 +0200 Subject: [PATCH 16/25] cmake: fix build with cmake 4.0.0 --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0354c36..758b20a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.2) +cmake_minimum_required(VERSION 3.5) project(pystring CXX) option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) From 02ef1186d6b77bc35f385bd4db2da75b4736adb7 Mon Sep 17 00:00:00 2001 From: Danny Greenstein Date: Sun, 22 Jun 2025 19:44:53 -0700 Subject: [PATCH 17/25] Simplify github actions CI yaml file to use stock github runners and latest versions of windows/mac/ubuntu (#48) * simplified github actions to include just 8 tasks, 4 linux, 2 macos, 2 windows, always on the latest version of the os/runners that github gives us * using specific sha when pointing to the actions/checkout repo instead of a tag, a tag can be moved around, but a sha can't so it's more stable --- .github/workflows/ci.yml | 365 ++++++--------------------------------- 1 file changed, 52 insertions(+), 313 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 868ffd3..7cfe295 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ -# borrowed this from: -# https://raw.githubusercontent.com/AcademySoftwareFoundation/Imath/main/.github/workflows/ci_workflow.yml -# with some slight simplification +# simple ci config the idea is to use the latest ubuntu, macos, and windows that github runners give us +# test a few different compilers (gcc/clang) in debug and release +# and run our tests via ctest name: CI @@ -14,219 +14,69 @@ on: - '**.md' jobs: - # Linux jobs run in Docker containers, so the latest OS version is OK. macOS - # and Windows jobs need to be locked to specific virtual environment - # versions to mitigate issues from OS updates, and will require maintenance - # as OS versions are retired. - # - # GH Actions (Free plan) supports 20 concurrent jobs, with 5 concurrent macOS - # jobs. This workflow tries to utilize (but not exceed) that budget to - # promote timely CI. - - # --------------------------------------------------------------------------- - # Linux - # --------------------------------------------------------------------------- - # TODO: Add ARM build. Add sanitize build. linux: - name: 'Linux CentOS 7 VFX CY${{ matrix.vfx-cy }} - <${{ matrix.compiler-desc }} , - config=${{ matrix.build-type }}, - shared=${{ matrix.build-shared }}, - cxx=${{ matrix.cxx-standard }}>' - # GH-hosted VM. The build runs in CentOS 7 'container' defined below. + name: '${{ matrix.os }} / ${{ matrix.build-type }} / ${{ matrix.compiler-desc }} ' + runs-on: ubuntu-latest - container: - # DockerHub: https://hub.docker.com/u/aswf - # Source: https://github.com/AcademySoftwareFoundation/aswf-docker - image: aswf/ci-openexr:${{ matrix.vfx-cy }} strategy: matrix: - build: [1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22] + build: [1, 2, 3, 4] include: # ------------------------------------------------------------------- - # GCC, VFX CY2022 + # CLANG, Release # ------------------------------------------------------------------- - # C++17, Python 3.9 - build: 1 build-type: Release build-shared: 'ON' cxx-standard: 17 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 + cc_compiler: clang + cxx_compiler: clang++ + compiler-desc: clang + os: ubuntu-latest - # C++17, Python 3.9.7, Debug + # ------------------------------------------------------------------- + # CLANG, Debug + # ------------------------------------------------------------------- - build: 2 build-type: Debug build-shared: 'ON' cxx-standard: 17 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 - - # C++17, Python 3.9.7, Static - - build: 3 - build-type: Release - build-shared: 'OFF' - cxx-standard: 17 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 - - # C++14, Python 3.9.7, Static - - build: 4 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 - - # C++17, no Python - # - build: 5 - # build-type: Release - # build-shared: 'ON' - # cxx-standard: 17 - # cxx-compiler: g++ - # cc-compiler: gcc - # compiler-desc: gcc9.3.1 - # python: 'OFF' - # python-desc: no python - # vfx-cy: 2022 + cc_compiler: clang + cxx_compiler: clang++ + compiler-desc: clang + os: ubuntu-latest # ------------------------------------------------------------------- - # GCC, VFX CY2021 + # gcc, Release # ------------------------------------------------------------------- - # C++17, Python 3.7.9 - - build: 9 + - build: 3 build-type: Release build-shared: 'ON' cxx-standard: 17 cxx-compiler: g++ cc-compiler: gcc - compiler-desc: gcc9.3.1 - python: 'ON' - vfx-cy: 2021 + compiler-desc: gcc + os: ubuntu-latest # ------------------------------------------------------------------- - # GCC, VFX CY2020 + # gcc, Debug # ------------------------------------------------------------------- - # C++14, Python 3.7 - - build: 10 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc6.3.1 - vfx-cy: 2020 - - # ------------------------------------------------------------------- - # GCC, VFX CY2019 - # ------------------------------------------------------------------- - # C++11, Python 2.7 - - build: 11 - build-type: Release - build-shared: 'ON' - cxx-standard: 11 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc6.3.1 - vfx-cy: 2019 - - # ------------------------------------------------------------------- - # Clang, VFX CY2022 - # ------------------------------------------------------------------- - # C++17, Python 3.9 - - build: 12 - build-type: Release - build-shared: 'ON' - cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # C++17, Python 3.9.7, Debug - - build: 13 + - build: 4 build-type: Debug build-shared: 'ON' cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # C++17, Python 3.9.7, Static - - build: 14 - build-type: Release - build-shared: 'OFF' - cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # C++14, Python 3.9.7, Static - - build: 15 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # ------------------------------------------------------------------- - # Clang, VFX CY2021 - # ------------------------------------------------------------------- - # C++17, Python 3.7.9 - - build: 20 - build-type: Release - build-shared: 'ON' - cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2021 - - # ------------------------------------------------------------------- - # Clang, VFX CY2020 - # ------------------------------------------------------------------- - # C++14, Python 3.7 - - build: 21 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang7.8 - vfx-cy: 2020 - - # ------------------------------------------------------------------- - # Clang, VFX CY2019 - # ------------------------------------------------------------------- - # C++11, Python 2.7 - - build: 22 - build-type: Release - build-shared: 'ON' - cxx-standard: 11 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang7.8 - vfx-cy: 2019 + cxx-compiler: g++ + cc-compiler: gcc + compiler-desc: gcc + os: ubuntu-latest env: CXX: ${{ matrix.cxx-compiler }} CC: ${{ matrix.cc-compiler }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create build directories run: | mkdir _install @@ -249,32 +99,27 @@ jobs: working-directory: _build - name: Test run: | - ctest -C ${{ matrix.build_type }} + ctest --build-config ${{ matrix.build_type }} --verbose working-directory: _build # --------------------------------------------------------------------------- # macOS # --------------------------------------------------------------------------- - macos_no_python: - name: 'macOS 10.15 - ' - runs-on: macos-10.15 + macos: + name: '${{ matrix.os }} / ${{ matrix.build-type }}' + runs-on: macos-latest strategy: matrix: - build: [1, 2, 3, 4, 5] + build: [1, 2] include: - # C++11 + + # Release - build: 1 build-type: Release build-shared: 'ON' - build-docs: 'ON' cxx-standard: 17 - exclude-tests: + os: macos-latest # Debug - build: 2 @@ -282,34 +127,11 @@ jobs: build-shared: 'ON' build-docs: 'OFF' cxx-standard: 17 - exclude-tests: + os: macos-latest - # Static - - build: 3 - build-type: Debug - build-shared: 'OFF' - build-docs: 'OFF' - cxx-standard: 17 - exclude-tests: - - # C++14 - - build: 4 - build-type: Release - build-shared: 'ON' - build-docs: 'OFF' - cxx-standard: 14 - exclude-tests: - - # C++11 - - build: 5 - build-type: Release - build-shared: 'ON' - build-docs: 'OFF' - cxx-standard: 11 - exclude-tests: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create build directories run: | mkdir _install @@ -330,11 +152,10 @@ jobs: cmake --build . \ --target install \ --config ${{ matrix.build-type }} \ - -- -j2 working-directory: _build - name: Test run: | - ctest -C ${{ matrix.build_type }} + ctest --build-config ${{ matrix.build_type }} --verbose working-directory: _build # --------------------------------------------------------------------------- @@ -342,113 +163,31 @@ jobs: # --------------------------------------------------------------------------- windows: - name: 'Windows 2019 - <${{ matrix.compiler-desc }}, - config=${{ matrix.build-type }}, - shared=${{ matrix.build-shared }}, - cxx=${{ matrix.cxx-standard }}, - docs=${{ matrix.build-docs }}>' - runs-on: windows-${{ matrix.osver }} + name: '${{ matrix.os }} / ${{ matrix.build-type }}' + runs-on: windows-latest strategy: matrix: - build: [1, 3, 4, 6, 7, 8] + build: [1, 2] include: - # ------------------------------------------------------------------- - # VFX CY2022 - C++17 - Windows 2019 - # ------------------------------------------------------------------- - # C++17, Release Shared + + # Release - build: 1 build-type: Release build-shared: 'ON' - build-docs: 'ON' - compiler-desc: msvc16.11 cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2019 - - # C++17, Debug - - ## - build: 2 - ## build-type: Debug - ## build-shared: 'ON' - ## build-docs: 'OFF' - ## cxx-standard: 17 - ## exclude-tests: + os: windows-latest - # C++17, Release Static - - build: 3 - build-type: Release - build-shared: 'OFF' - build-docs: 'OFF' - compiler-desc: msvc16.11 - cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2019 - - # ------------------------------------------------------------------- - # VFX CY2022 - C++17 - Windows 2022 - # ------------------------------------------------------------------- - # C++17, Release Shared - - build: 4 - build-type: Release + # Debug + - build: 2 + build-type: Debug build-shared: 'ON' - build-docs: 'ON' - compiler-desc: msvc17.1 - cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2022 - - # C++17, Debug - - ## - build: 5 - ## build-type: Debug - ## build-shared: 'ON' - ## build-docs: 'OFF' - ## cxx-standard: 17 - ## exclude-tests: - - # C++17, Release Static - - build: 6 - build-type: Release - build-shared: 'OFF' - build-docs: 'OFF' - compiler-desc: msvc17.1 cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2022 - - # ------------------------------------------------------------------- - # VFX CY2020 - C++14 - Windows 2019 - # ------------------------------------------------------------------- - # C++14, Release Shared - - build: 7 - build-type: Release - build-shared: 'ON' - build-docs: 'OFF' - compiler-desc: msvc16.11 - cxx-standard: 14 - vfx-cy: 2020 - exclude-tests: - osver: 2019 - - # ------------------------------------------------------------------- - # VFX CY2019 - C++11 - Windows 2019 - # ------------------------------------------------------------------- - # C++11, Release Shared - - build: 8 - build-type: Release - build-shared: 'ON' - build-docs: 'OFF' - compiler-desc: msvc16.11 - cxx-standard: 11 - exclude-tests: - osver: 2019 + os: windows-latest + steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create build directories run: | mkdir _install From a09708a4870db7862e1a1aa42658c8e6e36547e7 Mon Sep 17 00:00:00 2001 From: Zephyr Lykos Date: Mon, 6 Oct 2025 12:30:32 +0800 Subject: [PATCH 18/25] Add meson build system (#50) * Add meson build system * Sync cmake's installation layout to Makefile and Meson builds * meson: remove cmake config target generation this pulls in an extra dependency on the cmake binary, CMake users can use the PkgConfig module instead. * ci: add meson tests * build: default to c++17 * ci: fix cmake builds * tests: fix memory leak in tests * ci: workaround clang sanitizer linking bug * build: install headers into a subdir --- .github/workflows/ci.yml | 22 ++++-- .github/workflows/meson.yml | 145 ++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 6 +- meson.build | 46 ++++++++++++ unittest.h | 16 ++-- 5 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/meson.yml create mode 100644 meson.build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cfe295..b980d84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,8 +30,9 @@ jobs: build-type: Release build-shared: 'ON' cxx-standard: 17 - cc_compiler: clang - cxx_compiler: clang++ + cxx-compiler: clang++ + cxx-flags: '' + cc-compiler: clang compiler-desc: clang os: ubuntu-latest @@ -42,8 +43,9 @@ jobs: build-type: Debug build-shared: 'ON' cxx-standard: 17 - cc_compiler: clang - cxx_compiler: clang++ + cxx-compiler: clang++ + cxx-flags: '' + cc-compiler: clang compiler-desc: clang os: ubuntu-latest @@ -55,6 +57,7 @@ jobs: build-shared: 'ON' cxx-standard: 17 cxx-compiler: g++ + cxx-flags: '' cc-compiler: gcc compiler-desc: gcc os: ubuntu-latest @@ -67,6 +70,7 @@ jobs: build-shared: 'ON' cxx-standard: 17 cxx-compiler: g++ + cxx-flags: '' cc-compiler: gcc compiler-desc: gcc os: ubuntu-latest @@ -99,7 +103,7 @@ jobs: working-directory: _build - name: Test run: | - ctest --build-config ${{ matrix.build_type }} --verbose + ctest --build-config ${{ matrix.build-type }} --verbose working-directory: _build # --------------------------------------------------------------------------- @@ -119,6 +123,7 @@ jobs: build-type: Release build-shared: 'ON' cxx-standard: 17 + cxx-flags: '' os: macos-latest # Debug @@ -127,6 +132,7 @@ jobs: build-shared: 'ON' build-docs: 'OFF' cxx-standard: 17 + cxx-flags: '' os: macos-latest steps: @@ -155,7 +161,7 @@ jobs: working-directory: _build - name: Test run: | - ctest --build-config ${{ matrix.build_type }} --verbose + ctest --build-config ${{ matrix.build-type }} --verbose working-directory: _build # --------------------------------------------------------------------------- @@ -175,6 +181,7 @@ jobs: build-type: Release build-shared: 'ON' cxx-standard: 17 + cxx-flags: '' os: windows-latest # Debug @@ -182,6 +189,7 @@ jobs: build-type: Debug build-shared: 'ON' cxx-standard: 17 + cxx-flags: '' os: windows-latest @@ -217,4 +225,4 @@ jobs: run: | ctest -C ${{ matrix.build-type }} shell: bash - working-directory: _build \ No newline at end of file + working-directory: _build diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml new file mode 100644 index 0000000..b653f4e --- /dev/null +++ b/.github/workflows/meson.yml @@ -0,0 +1,145 @@ +name: Meson + +on: + pull_request: + push: + +jobs: + meson-build-and-tests: + runs-on: ${{ matrix.platform }} + name: ${{ matrix.platform }}, ${{ matrix.mode.name }} ${{ matrix.flavor }} + strategy: + fail-fast: false + matrix: + flavor: + - debug + - release + mode: + - name: default + extra_envs: {} + + # Alternative compiler setups + - name: gcc + extra_envs: + CC: gcc + CXX: g++ + - name: clang + extra_envs: + CC: clang + CXX: clang++ + + - name: sanitize + args: >- + "-Db_sanitize=address,undefined" + extra_envs: {} + + # This is for MSVC, which only supports AddressSanitizer. + # https://learn.microsoft.com/en-us/cpp/sanitizers/ + - name: sanitize+asanonly + args: -Db_sanitize=address + extra_envs: + ASAN_OPTIONS: report_globals=0:halt_on_error=1:abort_on_error=1:print_summary=1 + + - name: clang+sanitize + args: >- + "-Db_sanitize=address,undefined" + extra_envs: + CC: clang + CXX: clang++ + + # default clang on GitHub hosted runners is from MSYS2. + # Use Visual Studio supplied clang-cl instead. + - name: clang-cl+sanitize + args: >- + "-Db_sanitize=address,undefined" + extra_envs: + CC: clang-cl + CXX: clang-cl + platform: + - ubuntu-22.04 + - windows-2022 + - macos-latest + + exclude: + # clang-cl only makes sense on windows. + - platform: ubuntu-22.04 + mode: + name: clang-cl+sanitize + - platform: macos-latest + mode: + name: clang-cl+sanitize + + # Use clang-cl instead of MSYS2 clang. + # + # we already tested clang+sanitize on linux, + # if this doesn't work, it should be an issue for MSYS2 team to consider. + - platform: windows-2022 + mode: + name: clang + - platform: windows-2022 + mode: + name: clang+sanitize + + # MSVC-only sanitizers + - platform: ubuntu-22.04 + mode: + name: sanitize+asanonly + - platform: macos-latest + mode: + name: sanitize+asanonly + - platform: windows-2022 + mode: + name: sanitize + + # clang is the default on macos + # also gcc is an alias to clang + - platform: macos-latest + mode: + name: clang + - platform: macos-latest + mode: + name: gcc + + # gcc is the default on linux + - platform: ubuntu-22.04 + mode: + name: gcc + + # only run sanitizer tests on linux + # + # gcc/clang's codegen shouldn't massively change across platforms, + # and linux supports most of the sanitizers. + - platform: macos-latest + mode: + name: clang+sanitize + - platform: macos-latest + mode: + name: sanitize + steps: + - name: Setup meson + run: | + pipx install meson ninja + - name: Checkout + uses: actions/checkout@v4 + - name: Activate MSVC and Configure + if: ${{ matrix.platform == 'windows-2022' }} + env: ${{ matrix.mode.extra_envs }} + run: | + meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} -Ddefault_library=static ${{ matrix.mode.args }} --vsenv + - name: Configuring + if: ${{ matrix.platform != 'windows-2022' }} + env: ${{ matrix.mode.extra_envs }} + run: | + meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} -Db_lundef=false ${{ matrix.mode.args }} + - name: Building + run: | + meson compile -C build-${{ matrix.flavor }} + - name: Running tests + env: ${{ matrix.mode.extra_envs }} + run: | + meson test -C build-${{ matrix.flavor }} --timeout-multiplier 0 + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.flavor }}-logs + path: build-${{ matrix.flavor }}/meson-logs diff --git a/CMakeLists.txt b/CMakeLists.txt index 758b20a..595a262 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.5) -project(pystring CXX) +project(pystring LANGUAGES CXX VERSION 1.1.4) option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) @@ -7,6 +7,10 @@ add_library(pystring pystring.cpp pystring.h ) +set_target_properties(pystring PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) add_executable (pystring_test test.cpp) TARGET_LINK_LIBRARIES (pystring_test pystring) diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..db5f121 --- /dev/null +++ b/meson.build @@ -0,0 +1,46 @@ +project( + 'pystring', + 'cpp', + version: '1.1.4', + license: 'BSD-3-Clause', + license_files: 'LICENSE', + meson_version: '>=1.3', + default_options: ['cpp_std=c++17,c++11', 'warning_level=3'], +) + +inc = include_directories('.') + +srcs = files('pystring.cpp') +hdrs = files('pystring.h') + +pystring_lib = library( + 'pystring', + srcs, + implicit_include_directories: false, + include_directories: inc, + version: meson.project_version(), + install: true, +) +pystring_dep = declare_dependency( + link_with: pystring_lib, + include_directories: inc, +) +meson.override_dependency('pystring', pystring_dep) + +test( + 'PyStringTest', + executable( + 'pystring_test', + 'test.cpp', + dependencies: pystring_dep, + build_by_default: false, + ), +) + +install_headers(hdrs, subdir: 'pystring') + +pkgconfig = import('pkgconfig') +pkgconfig.generate( + pystring_lib, + description: 'C++ functions matching the interface and behavior of python string methods with std::string', +) diff --git a/unittest.h b/unittest.h index 536d9cb..7412029 100644 --- a/unittest.h +++ b/unittest.h @@ -23,11 +23,11 @@ struct PYSTRINGTest PYSTRINGTestFunc function; }; -typedef std::vector UnitTests; +typedef std::vector UnitTests; UnitTests& GetUnitTests(); -struct AddTest { AddTest(PYSTRINGTest* test); }; +struct AddTest { AddTest(PYSTRINGTest&& test); }; /// PYSTRING_CHECK_* macros checks if the conditions is met, and if not, /// prints an error message indicating the module and line where the @@ -102,22 +102,22 @@ struct AddTest { AddTest(PYSTRINGTest* test); }; #define PYSTRING_ADD_TEST(group, name) \ static void pystringtest_##group##_##name(); \ - AddTest pystringaddtest_##group##_##name(new PYSTRINGTest(#group, #name, pystringtest_##group##_##name)); \ + AddTest pystringaddtest_##group##_##name(PYSTRINGTest(#group, #name, pystringtest_##group##_##name)); \ static void pystringtest_##group##_##name() #define PYSTRING_TEST_SETUP() \ int unit_test_failures = 0 #define PYSTRING_TEST_APP(app) \ - std::vector& GetUnitTests() { \ - static std::vector pystring_unit_tests; \ + std::vector& GetUnitTests() { \ + static std::vector pystring_unit_tests; \ return pystring_unit_tests; } \ - AddTest::AddTest(PYSTRINGTest* test){GetUnitTests().push_back(test);}; \ + AddTest::AddTest(PYSTRINGTest&& test){GetUnitTests().emplace_back(test);}; \ PYSTRING_TEST_SETUP(); \ int main(int, char **) { std::cerr << "\n" << #app <<"\n\n"; \ for(size_t i = 0; i < GetUnitTests().size(); ++i) { \ - int _tmp = unit_test_failures; GetUnitTests()[i]->function(); \ - std::cerr << "Test [" << GetUnitTests()[i]->group << "] [" << GetUnitTests()[i]->name << "] - "; \ + int _tmp = unit_test_failures; GetUnitTests()[i].function(); \ + std::cerr << "Test [" << GetUnitTests()[i].group << "] [" << GetUnitTests()[i].name << "] - "; \ std::cerr << (_tmp == unit_test_failures ? "PASSED" : "FAILED") << "\n"; } \ std::cerr << "\n" << unit_test_failures << " tests failed\n\n"; \ return unit_test_failures; } From c195f667090d220fa01615f4ac1e3bd37b402527 Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Tue, 11 Nov 2025 18:41:00 -0800 Subject: [PATCH 19/25] Install locally by default to avoid naively overwriting /usr/local (#52) Use `-DCMAKE_INSTALL_PREFIX=...` to set a non-local install destination. Signed-off-by: Larry Gritz --- CMakeLists.txt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 595a262..506075b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,18 @@ -cmake_minimum_required(VERSION 3.5) +cmake_minimum_required(VERSION 3.10) project(pystring LANGUAGES CXX VERSION 1.1.4) option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) +# If the user hasn't configured cmake with an explicit +# -DCMAKE_INSTALL_PREFIX=..., then set it to safely install into ./dist, to +# help prevent the user from accidentally writing over /usr/local or whatever. +if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT + AND PROJECT_IS_TOP_LEVEL) + set (CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/dist" CACHE PATH + "Installation location" FORCE) +endif() +message (STATUS "Installation path will be ${CMAKE_INSTALL_PREFIX}") + add_library(pystring pystring.cpp pystring.h From a9df4d1bb3cef589eba1fd9bfb45b37b2e209b01 Mon Sep 17 00:00:00 2001 From: Zephyr Lykos Date: Fri, 5 Dec 2025 11:32:22 +0800 Subject: [PATCH 20/25] ci: only bypass b_lundef if using clang+sanitize (#53) --- .github/workflows/meson.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml index b653f4e..bca9437 100644 --- a/.github/workflows/meson.yml +++ b/.github/workflows/meson.yml @@ -42,7 +42,7 @@ jobs: - name: clang+sanitize args: >- - "-Db_sanitize=address,undefined" + -Db_lundef=false "-Db_sanitize=address,undefined" extra_envs: CC: clang CXX: clang++ @@ -130,7 +130,7 @@ jobs: if: ${{ matrix.platform != 'windows-2022' }} env: ${{ matrix.mode.extra_envs }} run: | - meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} -Db_lundef=false ${{ matrix.mode.args }} + meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} ${{ matrix.mode.args }} - name: Building run: | meson compile -C build-${{ matrix.flavor }} From 64a428f62253fc1906ca3013e47f4e10b95dd2d2 Mon Sep 17 00:00:00 2001 From: Danny Greenstein Date: Sun, 22 Feb 2026 15:30:28 -0800 Subject: [PATCH 21/25] header only build option (#54) * new header only build option * removed extra semi colon * remaing marcos in new header file with a PYSTRING prefix so that they don't clash with other definitions when used in different contexts --- .github/workflows/ci.yml | 69 +- .github/workflows/meson.yml | 40 +- CMakeLists.txt | 60 +- meson.build | 69 +- meson_options.txt | 1 + pystring.cpp | 1669 +---------------------------------- pystring.h | 148 ++-- pystring_impl.h | 1669 +++++++++++++++++++++++++++++++++++ unittest.h | 2 +- 9 files changed, 1936 insertions(+), 1791 deletions(-) create mode 100644 meson_options.txt create mode 100644 pystring_impl.h diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b980d84..130df83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - build: [1, 2, 3, 4] + build: [1, 2, 3, 4, 5, 6] include: # ------------------------------------------------------------------- # CLANG, Release @@ -75,6 +75,36 @@ jobs: compiler-desc: gcc os: ubuntu-latest + + # ------------------------------------------------------------------- + # CLANG, Release header only + # ------------------------------------------------------------------- + - build: 5 + build-type: Release + build-shared: 'ON' + header-only: 'ON' + cxx-standard: 17 + cxx-compiler: clang++ + cxx-flags: '' + cc-compiler: clang + compiler-desc: clang + os: ubuntu-latest + + # ------------------------------------------------------------------- + # gcc, Release header only + # ------------------------------------------------------------------- + - build: 6 + build-type: Release + build-shared: 'ON' + header-only: 'ON' + cxx-standard: 17 + cxx-compiler: g++ + cxx-flags: '' + cc-compiler: gcc + compiler-desc: gcc + os: ubuntu-latest + + env: CXX: ${{ matrix.cxx-compiler }} CC: ${{ matrix.cc-compiler }} @@ -88,17 +118,17 @@ jobs: - name: Configure run: | cmake .. \ - -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install \ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} working-directory: _build - name: Build run: | cmake --build . \ - --target install \ --config ${{ matrix.build-type }} working-directory: _build - name: Test @@ -115,7 +145,7 @@ jobs: runs-on: macos-latest strategy: matrix: - build: [1, 2] + build: [1, 2, 3] include: # Release @@ -135,6 +165,16 @@ jobs: cxx-flags: '' os: macos-latest + + # Release header only + - build: 3 + build-type: Release + build-shared: 'ON' + header-only: 'ON' + cxx-standard: 17 + cxx-flags: '' + os: macos-latest + steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -146,17 +186,17 @@ jobs: - name: Configure run: | cmake ../. \ - -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install \ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ - -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} working-directory: _build - name: Build run: | cmake --build . \ - --target install \ --config ${{ matrix.build-type }} \ working-directory: _build - name: Test @@ -192,6 +232,15 @@ jobs: cxx-flags: '' os: windows-latest + # Release header only + - build: 3 + build-type: Release + build-shared: 'ON' + header-only: 'ON' + cxx-standard: 17 + cxx-flags: '' + os: windows-latest + steps: - name: Checkout @@ -205,19 +254,19 @@ jobs: # the windows build needs the -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS to work run: | cmake ../. \ - -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install \ -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS='ON'\ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ - -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} shell: bash working-directory: _build - name: Build run: | cmake --build . \ - --target install \ --config ${{ matrix.build-type }} shell: bash working-directory: _build diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml index bca9437..e020145 100644 --- a/.github/workflows/meson.yml +++ b/.github/workflows/meson.yml @@ -7,13 +7,16 @@ on: jobs: meson-build-and-tests: runs-on: ${{ matrix.platform }} - name: ${{ matrix.platform }}, ${{ matrix.mode.name }} ${{ matrix.flavor }} + name: ${{ matrix.platform }}, ${{ matrix.mode.name }} ${{ matrix.flavor }} ${{ matrix.library_mode }} strategy: fail-fast: false matrix: flavor: - debug - release + library_mode: + - compiled + - header-only mode: - name: default extra_envs: {} @@ -61,6 +64,29 @@ jobs: - macos-latest exclude: + # Only test header-only with a subset of configurations to reduce CI time + # Test header-only only with default compiler in release mode + - library_mode: header-only + flavor: debug + - library_mode: header-only + mode: + name: gcc + - library_mode: header-only + mode: + name: clang + - library_mode: header-only + mode: + name: sanitize + - library_mode: header-only + mode: + name: sanitize+asanonly + - library_mode: header-only + mode: + name: clang+sanitize + - library_mode: header-only + mode: + name: clang-cl+sanitize + # clang-cl only makes sense on windows. - platform: ubuntu-22.04 mode: @@ -125,21 +151,21 @@ jobs: if: ${{ matrix.platform == 'windows-2022' }} env: ${{ matrix.mode.extra_envs }} run: | - meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} -Ddefault_library=static ${{ matrix.mode.args }} --vsenv + meson setup build-${{ matrix.flavor }}-${{ matrix.library_mode }} --buildtype=${{ matrix.flavor }} -Ddefault_library=static -Dheader_only=${{ matrix.library_mode == 'header-only' && 'true' || 'false' }} ${{ matrix.mode.args }} --vsenv - name: Configuring if: ${{ matrix.platform != 'windows-2022' }} env: ${{ matrix.mode.extra_envs }} run: | - meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} ${{ matrix.mode.args }} + meson setup build-${{ matrix.flavor }}-${{ matrix.library_mode }} --buildtype=${{ matrix.flavor }} -Dheader_only=${{ matrix.library_mode == 'header-only' && 'true' || 'false' }} ${{ matrix.mode.args }} - name: Building run: | - meson compile -C build-${{ matrix.flavor }} + meson compile -C build-${{ matrix.flavor }}-${{ matrix.library_mode }} - name: Running tests env: ${{ matrix.mode.extra_envs }} run: | - meson test -C build-${{ matrix.flavor }} --timeout-multiplier 0 + meson test -C build-${{ matrix.flavor }}-${{ matrix.library_mode }} --timeout-multiplier 0 - uses: actions/upload-artifact@v4 if: failure() with: - name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.flavor }}-logs - path: build-${{ matrix.flavor }}/meson-logs + name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.flavor }}-${{ matrix.library_mode }}-logs + path: build-${{ matrix.flavor }}-${{ matrix.library_mode }}/meson-logs \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 506075b..4d35d72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.10) project(pystring LANGUAGES CXX VERSION 1.1.4) option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) +option(PYSTRING_HEADER_ONLY "Build as header-only library" OFF) # If the user hasn't configured cmake with an explicit # -DCMAKE_INSTALL_PREFIX=..., then set it to safely install into ./dist, to @@ -13,14 +14,48 @@ if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT endif() message (STATUS "Installation path will be ${CMAKE_INSTALL_PREFIX}") -add_library(pystring - pystring.cpp - pystring.h -) -set_target_properties(pystring PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} -) +if(PYSTRING_HEADER_ONLY) + message(STATUS "Building pystring as header-only library") + add_library(pystring INTERFACE) + + target_compile_definitions(pystring INTERFACE PYSTRING_HEADER_ONLY) + + target_include_directories(pystring INTERFACE + $ + $ + ) + + # Install both headers for header-only mode + install(FILES pystring.h pystring_impl.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} + ) +else() + message(STATUS "Building pystring as compiled library") + + add_library(pystring + pystring.cpp + pystring.h + ) + + set_target_properties(pystring PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) + + install(TARGETS pystring + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + ) + + install (FILES pystring.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} + COMPONENT developer + ) + +endif() + +# Test executable add_executable (pystring_test test.cpp) TARGET_LINK_LIBRARIES (pystring_test pystring) @@ -29,12 +64,3 @@ enable_testing() add_test(NAME PyStringTest COMMAND pystring_test) include(GNUInstallDirs) - -install(TARGETS pystring - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} -) -install (FILES pystring.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} - COMPONENT developer -) - diff --git a/meson.build b/meson.build index db5f121..5dab5b8 100644 --- a/meson.build +++ b/meson.build @@ -8,25 +8,58 @@ project( default_options: ['cpp_std=c++17,c++11', 'warning_level=3'], ) -inc = include_directories('.') +# Option to build as header-only library +header_only = get_option('header_only') -srcs = files('pystring.cpp') +inc = include_directories('.') hdrs = files('pystring.h') -pystring_lib = library( - 'pystring', - srcs, - implicit_include_directories: false, - include_directories: inc, - version: meson.project_version(), - install: true, -) -pystring_dep = declare_dependency( - link_with: pystring_lib, - include_directories: inc, -) +if header_only + # Header-only mode: create a header-only dependency + message('Building pystring as header-only library') + + pystring_dep = declare_dependency( + include_directories: inc, + compile_args: ['-DPYSTRING_HEADER_ONLY'], + ) + + # Install headers for header-only mode + install_headers(hdrs, files('pystring_impl.h'), subdir: 'pystring') + +else + # Compiled mode: build as normal library + message('Building pystring as compiled library') + + srcs = files('pystring.cpp') + + pystring_lib = library( + 'pystring', + srcs, + implicit_include_directories: false, + include_directories: inc, + version: meson.project_version(), + install: true, + ) + + pystring_dep = declare_dependency( + link_with: pystring_lib, + include_directories: inc, + ) + + # Install headers for compiled mode + install_headers(hdrs, subdir: 'pystring') + + # Generate pkg-config file + pkgconfig = import('pkgconfig') + pkgconfig.generate( + pystring_lib, + description: 'C++ functions matching the interface and behavior of python string methods with std::string', + ) +endif + meson.override_dependency('pystring', pystring_dep) +# Build and run tests test( 'PyStringTest', executable( @@ -36,11 +69,3 @@ test( build_by_default: false, ), ) - -install_headers(hdrs, subdir: 'pystring') - -pkgconfig = import('pkgconfig') -pkgconfig.generate( - pystring_lib, - description: 'C++ functions matching the interface and behavior of python string methods with std::string', -) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..77be15e --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('header_only', type: 'boolean', value: false, description: 'Build as header-only library') diff --git a/pystring.cpp b/pystring.cpp index 765e76a..1dbaad3 100644 --- a/pystring.cpp +++ b/pystring.cpp @@ -2,1671 +2,10 @@ // SPDX-License-Identifier: BSD-3-Clause // https://github.com/imageworks/pystring/blob/master/LICENSE - #include "pystring.h" -#include -#include -#include -#include -#include - -namespace pystring -{ - -#if defined(_WIN32) || defined(_WIN64) || defined(_WINDOWS) || defined(_MSC_VER) -#ifndef WINDOWS -#define WINDOWS -#endif +// when not in header only mode include the implementations as non inline +// functions +#ifndef PYSTRING_HEADER_ONLY +#include "pystring_impl.h" #endif - -// This definition codes from configure.in in the python src. -// Strictly speaking this limits us to str sizes of 2**31. -// Should we wish to handle this limit, we could use an architecture -// specific #defines and read from ssize_t (unistd.h) if the header exists. -// But in the meantime, the use of int assures maximum arch compatibility. -// This must also equal the size used in the end = MAX_32BIT_INT default arg. - -typedef int Py_ssize_t; -const std::string forward_slash = "/"; -const std::string double_forward_slash = "//"; -const std::string triple_forward_slash = "///"; -const std::string double_back_slash = "\\"; -const std::string empty_string = ""; -const std::string dot = "."; -const std::string double_dot = ".."; -const std::string colon = ":"; - - -/* helper macro to fixup start/end slice values */ -#define ADJUST_INDICES(start, end, len) \ - if (end > len) \ - end = len; \ - else if (end < 0) { \ - end += len; \ - if (end < 0) \ - end = 0; \ - } \ - if (start < 0) { \ - start += len; \ - if (start < 0) \ - start = 0; \ - } - - - namespace { - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// why doesn't the std::reverse work? - /// - void reverse_strings( std::vector< std::string > & result) - { - for (std::vector< std::string >::size_type i = 0; i < result.size() / 2; i++ ) - { - std::swap(result[i], result[result.size() - 1 - i]); - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void split_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) - { - std::string::size_type i, j, len = str.size(); - for (i = j = 0; i < len; ) - { - - while ( i < len && ::isspace( str[i] ) ) i++; - j = i; - - while ( i < len && ! ::isspace( str[i]) ) i++; - - - - if (j < i) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( j, i - j )); - - while ( i < len && ::isspace( str[i])) i++; - j = i; - } - } - if (j < len) - { - result.push_back( str.substr( j, len - j )); - } - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void rsplit_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) - { - std::string::size_type len = str.size(); - std::string::size_type i, j; - for (i = j = len; i > 0; ) - { - - while ( i > 0 && ::isspace( str[i - 1] ) ) i--; - j = i; - - while ( i > 0 && ! ::isspace( str[i - 1]) ) i--; - - - - if (j > i) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( i, j - i )); - - while ( i > 0 && ::isspace( str[i - 1])) i--; - j = i; - } - } - if (j > 0) - { - result.push_back( str.substr( 0, j )); - } - //std::reverse( result, result.begin(), result.end() ); - reverse_strings( result ); - } - - } //anonymous namespace - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void split( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) - { - result.clear(); - - if ( maxsplit < 0 ) maxsplit = MAX_32BIT_INT;//result.max_size(); - - - if ( sep.size() == 0 ) - { - split_whitespace( str, result, maxsplit ); - return; - } - - std::string::size_type i,j, len = str.size(), n = sep.size(); - - i = j = 0; - - while ( i+n <= len ) - { - if ( str[i] == sep[0] && str.substr( i, n ) == sep ) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( j, i - j ) ); - i = j = i + n; - } - else - { - i++; - } - } - - result.push_back( str.substr( j, len-j ) ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) - { - if ( maxsplit < 0 ) - { - split( str, result, sep, maxsplit ); - return; - } - - result.clear(); - - if ( sep.size() == 0 ) - { - rsplit_whitespace( str, result, maxsplit ); - return; - } - - Py_ssize_t i,j, len = (Py_ssize_t) str.size(), n = (Py_ssize_t) sep.size(); - - i = j = len; - - while ( i >= n ) - { - if ( str[i - 1] == sep[n - 1] && str.substr( i - n, n ) == sep ) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( i, j - i ) ); - i = j = i - n; - } - else - { - i--; - } - } - - result.push_back( str.substr( 0, j ) ); - reverse_strings( result ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - #define LEFTSTRIP 0 - #define RIGHTSTRIP 1 - #define BOTHSTRIP 2 - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string do_strip( const std::string & str, int striptype, const std::string & chars ) - { - Py_ssize_t len = (Py_ssize_t) str.size(), i, j, charslen = (Py_ssize_t) chars.size(); - - if ( charslen == 0 ) - { - i = 0; - if ( striptype != RIGHTSTRIP ) - { - while ( i < len && ::isspace( str[i] ) ) - { - i++; - } - } - - j = len; - if ( striptype != LEFTSTRIP ) - { - do - { - j--; - } - while (j >= i && ::isspace(str[j])); - - j++; - } - - - } - else - { - const char * sep = chars.c_str(); - - i = 0; - if ( striptype != RIGHTSTRIP ) - { - while ( i < len && memchr(sep, str[i], charslen) ) - { - i++; - } - } - - j = len; - if (striptype != LEFTSTRIP) - { - do - { - j--; - } - while (j >= i && memchr(sep, str[j], charslen) ); - j++; - } - - - } - - if ( i == 0 && j == len ) - { - return str; - } - else - { - return str.substr( i, j - i ); - } - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) - { - result.resize(3); - int index = find( str, sep ); - if ( index < 0 ) - { - result[0] = str; - result[1] = empty_string; - result[2] = empty_string; - } - else - { - result[0] = str.substr( 0, index ); - result[1] = sep; - result[2] = str.substr( index + sep.size(), str.size() ); - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) - { - result.resize(3); - int index = rfind( str, sep ); - if ( index < 0 ) - { - result[0] = empty_string; - result[1] = empty_string; - result[2] = str; - } - else - { - result[0] = str.substr( 0, index ); - result[1] = sep; - result[2] = str.substr( index + sep.size(), str.size() ); - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string strip( const std::string & str, const std::string & chars ) - { - return do_strip( str, BOTHSTRIP, chars ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string lstrip( const std::string & str, const std::string & chars ) - { - return do_strip( str, LEFTSTRIP, chars ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string rstrip( const std::string & str, const std::string & chars ) - { - return do_strip( str, RIGHTSTRIP, chars ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string join( const std::string & str, const std::vector< std::string > & seq ) - { - std::vector< std::string >::size_type seqlen = seq.size(), i; - - if ( seqlen == 0 ) return empty_string; - if ( seqlen == 1 ) return seq[0]; - - std::string result( seq[0] ); - - for ( i = 1; i < seqlen; ++i ) - { - result += str + seq[i]; - - } - - - return result; - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - namespace - { - /* Matches the end (direction >= 0) or start (direction < 0) of self - * against substr, using the start and end arguments. Returns - * -1 on error, 0 if not found and 1 if found. - */ - - int _string_tailmatch(const std::string & self, const std::string & substr, - Py_ssize_t start, Py_ssize_t end, - int direction) - { - Py_ssize_t len = (Py_ssize_t) self.size(); - Py_ssize_t slen = (Py_ssize_t) substr.size(); - - const char* sub = substr.c_str(); - const char* str = self.c_str(); - - ADJUST_INDICES(start, end, len); - - if (direction < 0) { - // startswith - if (start+slen > len) - return 0; - } else { - // endswith - if (end-start < slen || start > len) - return 0; - if (end-slen > start) - start = end - slen; - } - if (end-start >= slen) - return (!std::memcmp(str+start, sub, slen)); - - return 0; - } - } - - bool endswith( const std::string & str, const std::string & suffix, int start, int end ) - { - int result = _string_tailmatch(str, suffix, - (Py_ssize_t) start, (Py_ssize_t) end, +1); - //if (result == -1) // TODO: Error condition - - return static_cast(result); - } - - - bool startswith( const std::string & str, const std::string & prefix, int start, int end ) - { - int result = _string_tailmatch(str, prefix, - (Py_ssize_t) start, (Py_ssize_t) end, -1); - //if (result == -1) // TODO: Error condition - - return static_cast(result); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - bool isalnum( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - - - if( len == 1 ) - { - return ::isalnum( str[0] ); - } - - for ( i = 0; i < len; ++i ) - { - if ( !::isalnum( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isalpha( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isalpha( (int) str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::isalpha( (int) str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isdigit( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isdigit( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( ! ::isdigit( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool islower( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::islower( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::islower( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isspace( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isspace( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::isspace( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool istitle( const std::string & str ) - { - std::string::size_type len = str.size(), i; - - if ( len == 0 ) return false; - if ( len == 1 ) return ::isupper( str[0] ); - - bool cased = false, previous_is_cased = false; - - for ( i = 0; i < len; ++i ) - { - if ( ::isupper( str[i] ) ) - { - if ( previous_is_cased ) - { - return false; - } - - previous_is_cased = true; - cased = true; - } - else if ( ::islower( str[i] ) ) - { - if (!previous_is_cased) - { - return false; - } - - previous_is_cased = true; - cased = true; - - } - else - { - previous_is_cased = false; - } - } - - return cased; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isupper( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isupper( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::isupper( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string capitalize( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - - if ( len > 0) - { - if (::islower(s[0])) s[0] = (char) ::toupper( s[0] ); - } - - for ( i = 1; i < len; ++i ) - { - if (::isupper(s[i])) s[i] = (char) ::tolower( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string lower( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - - for ( i = 0; i < len; ++i ) - { - if ( ::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string upper( const std::string & str ) - { - std::string s( str ) ; - std::string::size_type len = s.size(), i; - - for ( i = 0; i < len; ++i ) - { - if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string swapcase( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - - for ( i = 0; i < len; ++i ) - { - if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); - else if (::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string title( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - bool previous_is_cased = false; - - for ( i = 0; i < len; ++i ) - { - int c = s[i]; - if ( ::islower(c) ) - { - if ( !previous_is_cased ) - { - s[i] = (char) ::toupper(c); - } - previous_is_cased = true; - } - else if ( ::isupper(c) ) - { - if ( previous_is_cased ) - { - s[i] = (char) ::tolower(c); - } - previous_is_cased = true; - } - else - { - previous_is_cased = false; - } - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string translate( const std::string & str, const std::string & table, const std::string & deletechars ) - { - std::string s; - std::string::size_type len = str.size(), dellen = deletechars.size(); - - if ( table.size() != 256 ) - { - // TODO : raise exception instead - return str; - } - - //if nothing is deleted, use faster code - if ( dellen == 0 ) - { - s = str; - for ( std::string::size_type i = 0; i < len; ++i ) - { - s[i] = table[ s[i] ]; - } - return s; - } - - - int trans_table[256]; - for ( int i = 0; i < 256; i++) - { - trans_table[i] = table[i]; - } - - for ( std::string::size_type i = 0; i < dellen; i++) - { - trans_table[(int) deletechars[i] ] = -1; - } - - for ( std::string::size_type i = 0; i < len; ++i ) - { - if ( trans_table[ (int) str[i] ] != -1 ) - { - s += table[ str[i] ]; - } - } - - return s; - - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string zfill( const std::string & str, int width ) - { - int len = (int)str.size(); - - if ( len >= width ) - { - return str; - } - - std::string s( str ); - - int fill = width - len; - - s = std::string( fill, '0' ) + s; - - - if ( s[fill] == '+' || s[fill] == '-' ) - { - s[0] = s[fill]; - s[fill] = '0'; - } - - return s; - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string ljust( const std::string & str, int width ) - { - std::string::size_type len = str.size(); - if ( (( int ) len ) >= width ) return str; - return str + std::string( width - len, ' ' ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string rjust( const std::string & str, int width ) - { - std::string::size_type len = str.size(); - if ( (( int ) len ) >= width ) return str; - return std::string( width - len, ' ' ) + str; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string center( const std::string & str, int width ) - { - int len = (int) str.size(); - int marg, left; - - if ( len >= width ) return str; - - marg = width - len; - left = marg / 2 + (marg & width & 1); - - return std::string( left, ' ' ) + str + std::string( marg - left, ' ' ); - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string slice( const std::string & str, int start, int end ) - { - ADJUST_INDICES(start, end, (int) str.size()); - if ( start >= end ) return empty_string; - return str.substr( start, end - start ); - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int find( const std::string & str, const std::string & sub, int start, int end ) - { - ADJUST_INDICES(start, end, (int) str.size()); - - std::string::size_type result = str.find( sub, start ); - - // If we cannot find the string, or if the end-point of our found substring is past - // the allowed end limit, return that it can't be found. - if( result == std::string::npos || - (result + sub.size() > (std::string::size_type)end) ) - { - return -1; - } - - return (int) result; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int index( const std::string & str, const std::string & sub, int start, int end ) - { - return find( str, sub, start, end ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int rfind( const std::string & str, const std::string & sub, int start, int end ) - { - ADJUST_INDICES(start, end, (int) str.size()); - - std::string::size_type result = str.rfind( sub, end ); - - if( result == std::string::npos || - result < (std::string::size_type)start || - (result + sub.size() > (std::string::size_type)end)) - return -1; - - return (int)result; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int rindex( const std::string & str, const std::string & sub, int start, int end ) - { - return rfind( str, sub, start, end ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string expandtabs( const std::string & str, int tabsize ) - { - std::string s( str ); - - std::string::size_type len = str.size(), i = 0; - int offset = 0; - - int j = 0; - - for ( i = 0; i < len; ++i ) - { - if ( str[i] == '\t' ) - { - - if ( tabsize > 0 ) - { - int fillsize = tabsize - (j % tabsize); - j += fillsize; - s.replace( i + offset, 1, std::string( fillsize, ' ' )); - offset += fillsize - 1; - } - else - { - s.replace( i + offset, 1, empty_string ); - offset -= 1; - } - - } - else - { - j++; - - if (str[i] == '\n' || str[i] == '\r') - { - j = 0; - } - } - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int count( const std::string & str, const std::string & substr, int start, int end ) - { - int nummatches = 0; - int cursor = start; - - while ( 1 ) - { - cursor = find( str, substr, cursor, end ); - - if ( cursor < 0 ) break; - - cursor += (int) substr.size(); - nummatches += 1; - } - - return nummatches; - - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count ) - { - int sofar = 0; - int cursor = 0; - std::string s( str ); - - std::string::size_type oldlen = oldstr.size(), newlen = newstr.size(); - - cursor = find( s, oldstr, cursor ); - - while ( cursor != -1 && cursor <= (int)s.size() ) - { - if ( count > -1 && sofar >= count ) - { - break; - } - - s.replace( cursor, oldlen, newstr ); - cursor += (int) newlen; - - if ( oldlen != 0) - { - cursor = find( s, oldstr, cursor ); - } - else - { - ++cursor; - } - - ++sofar; - } - - return s; - - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends ) - { - result.clear(); - std::string::size_type len = str.size(), i, j, eol; - - for (i = j = 0; i < len; ) - { - while (i < len && str[i] != '\n' && str[i] != '\r') i++; - - eol = i; - if (i < len) - { - if (str[i] == '\r' && i + 1 < len && str[i+1] == '\n') - { - i += 2; - } - else - { - i++; - } - if (keepends) - eol = i; - - } - - result.push_back( str.substr( j, eol - j ) ); - j = i; - - } - - if (j < len) - { - result.push_back( str.substr( j, len - j ) ); - } - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string mul( const std::string & str, int n ) - { - // Early exits - if (n <= 0) return empty_string; - if (n == 1) return str; - - std::ostringstream os; - for(int i=0; i= 2 && p[1] == ':') - { - std::string path = p; // In case drivespec == p - drivespec = pystring::slice(path, 0, 2); - pathspec = pystring::slice(path, 2); - } - else - { - drivespec = empty_string; - pathspec = p; - } - } - - // On Posix, drive is always empty - void splitdrive_posix(std::string & drivespec, std::string & pathspec, - const std::string & path) - { - drivespec = empty_string; - pathspec = path; - } - - void splitdrive(std::string & drivespec, std::string & pathspec, - const std::string & path) - { -#ifdef WINDOWS - return splitdrive_nt(drivespec, pathspec, path); -#else - return splitdrive_posix(drivespec, pathspec, path); -#endif - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - // Test whether a path is absolute - // In windows, if the character to the right of the colon - // is a forward or backslash it's absolute. - bool isabs_nt(const std::string & path) - { - std::string drivespec, pathspec; - splitdrive_nt(drivespec, pathspec, path); - if(pathspec.empty()) return false; - return ((pathspec[0] == '/') || (pathspec[0] == '\\')); - } - - bool isabs_posix(const std::string & s) - { - return pystring::startswith(s, forward_slash); - } - - bool isabs(const std::string & path) - { -#ifdef WINDOWS - return isabs_nt(path); -#else - return isabs_posix(path); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string abspath_nt(const std::string & path, const std::string & cwd) - { - std::string p = path; - if(!isabs_nt(p)) p = join_nt(cwd, p); - return normpath_nt(p); - } - - std::string abspath_posix(const std::string & path, const std::string & cwd) - { - std::string p = path; - if(!isabs_posix(p)) p = join_posix(cwd, p); - return normpath_posix(p); - } - - std::string abspath(const std::string & path, const std::string & cwd) - { -#ifdef WINDOWS - return abspath_nt(path, cwd); -#else - return abspath_posix(path, cwd); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string join_nt(const std::vector< std::string > & paths) - { - if(paths.empty()) return empty_string; - if(paths.size() == 1) return paths[0]; - - std::string path = paths[0]; - - for(unsigned int i=1; i= 2 && path[1] != ':') || (b.size() >= 2 && b[1] == ':')) - { - // Path doesnt start with a drive letter - b_nts = true; - } - // Else path has a drive letter, and b doesn't but is absolute. - else if((path.size()>3) || - ((path.size()==3) && !pystring::endswith(path, forward_slash) && !pystring::endswith(path, double_back_slash))) - { - b_nts = true; - } - } - - if(b_nts) - { - path = b; - } - else - { - // Join, and ensure there's a separator. - // assert len(path) > 0 - if( pystring::endswith(path, forward_slash) || pystring::endswith(path, double_back_slash)) - { - if(pystring::startswith(b,forward_slash) || pystring::startswith(b,double_back_slash)) - { - path += pystring::slice(b, 1); - } - else - { - path += b; - } - } - else if(pystring::endswith(path, colon)) - { - path += b; - } - else if(!b.empty()) - { - if(pystring::startswith(b, forward_slash) || pystring::startswith(b,double_back_slash)) - { - path += b; - } - else - { - path += double_back_slash + b; - } - } - else - { - // path is not empty and does not end with a backslash, - // but b is empty; since, e.g., split('a/') produces - // ('a', ''), it's best if join() adds a backslash in - // this case. - path += double_back_slash; - } - } - } - - return path; - } - - // Join two or more pathname components, inserting double_back_slash as needed. - std::string join_nt(const std::string & a, const std::string & b) - { - std::vector< std::string > paths(2); - paths[0] = a; - paths[1] = b; - return join_nt(paths); - } - - // Join pathnames. - // If any component is an absolute path, all previous path components - // will be discarded. - // Ignore the previous parts if a part is absolute. - // Insert a '/' unless the first part is empty or already ends in '/'. - - std::string join_posix(const std::vector< std::string > & paths) - { - if(paths.empty()) return empty_string; - if(paths.size() == 1) return paths[0]; - - std::string path = paths[0]; - - for(unsigned int i=1; i paths(2); - paths[0] = a; - paths[1] = b; - return join_posix(paths); - } - - std::string join(const std::string & path1, const std::string & path2) - { -#ifdef WINDOWS - return join_nt(path1, path2); -#else - return join_posix(path1, path2); -#endif - } - - - std::string join(const std::vector< std::string > & paths) - { -#ifdef WINDOWS - return join_nt(paths); -#else - return join_posix(paths); -#endif - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - - // Split a pathname. - // Return (head, tail) where tail is everything after the final slash. - // Either part may be empty - - void split_nt(std::string & head, std::string & tail, const std::string & path) - { - std::string d, p; - splitdrive_nt(d, p, path); - - // set i to index beyond p's last slash - int i = (int)p.size(); - - // walk back to find the index of the first slash from the end - while(i>0 && (p[i-1] != '\\') && (p[i-1] != '/')) - { - i = i - 1; - } - - head = pystring::slice(p,0,i); - tail = pystring::slice(p,i); // now tail has no slashes - - // remove trailing slashes from head, unless it's all slashes - std::string head2 = head; - while(!head2.empty() && ((pystring::slice(head2,-1) == forward_slash) || - (pystring::slice(head2,-1) == double_back_slash))) - { - head2 = pystring::slice(head2,0,-1); - } - - if(!head2.empty()) head = head2; - head = d + head; - } - - - // Split a path in head (everything up to the last '/') and tail (the - // rest). If the path ends in '/', tail will be empty. If there is no - // '/' in the path, head will be empty. - // Trailing '/'es are stripped from head unless it is the root. - - void split_posix(std::string & head, std::string & tail, const std::string & p) - { - int i = pystring::rfind(p, forward_slash) + 1; - - head = pystring::slice(p,0,i); - tail = pystring::slice(p,i); - - if(!head.empty() && (head != pystring::mul(forward_slash, (int) head.size()))) - { - head = pystring::rstrip(head, forward_slash); - } - } - - void split(std::string & head, std::string & tail, const std::string & path) - { -#ifdef WINDOWS - return split_nt(head, tail, path); -#else - return split_posix(head, tail, path); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string basename_nt(const std::string & path) - { - std::string head, tail; - split_nt(head, tail, path); - return tail; - } - - std::string basename_posix(const std::string & path) - { - std::string head, tail; - split_posix(head, tail, path); - return tail; - } - - std::string basename(const std::string & path) - { -#ifdef WINDOWS - return basename_nt(path); -#else - return basename_posix(path); -#endif - } - - std::string dirname_nt(const std::string & path) - { - std::string head, tail; - split_nt(head, tail, path); - return head; - } - - std::string dirname_posix(const std::string & path) - { - std::string head, tail; - split_posix(head, tail, path); - return head; - } - - std::string dirname(const std::string & path) - { -#ifdef WINDOWS - return dirname_nt(path); -#else - return dirname_posix(path); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. - std::string normpath_nt(const std::string & p) - { - std::string path = p; - path = pystring::replace(path, forward_slash,double_back_slash); - - std::string prefix; - splitdrive_nt(prefix, path, path); - - // We need to be careful here. If the prefix is empty, and the path starts - // with a backslash, it could either be an absolute path on the current - // drive (\dir1\dir2\file) or a UNC filename (\\server\mount\dir1\file). It - // is therefore imperative NOT to collapse multiple backslashes blindly in - // that case. - // The code below preserves multiple backslashes when there is no drive - // letter. This means that the invalid filename \\\a\b is preserved - // unchanged, where a\\\b is normalised to a\b. It's not clear that there - // is any better behaviour for such edge cases. - - if(prefix.empty()) - { - // No drive letter - preserve initial backslashes - while(pystring::slice(path,0,1) == double_back_slash) - { - prefix = prefix + double_back_slash; - path = pystring::slice(path,1); - } - } - else - { - // We have a drive letter - collapse initial backslashes - if(pystring::startswith(path, double_back_slash)) - { - prefix = prefix + double_back_slash; - path = pystring::lstrip(path, double_back_slash); - } - } - - std::vector comps; - pystring::split(path, comps, double_back_slash); - - int i = 0; - - while(i<(int)comps.size()) - { - if(comps[i].empty() || comps[i] == dot) - { - comps.erase(comps.begin()+i); - } - else if(comps[i] == double_dot) - { - if(i>0 && comps[i-1] != double_dot) - { - comps.erase(comps.begin()+i-1, comps.begin()+i+1); - i -= 1; - } - else if(i == 0 && pystring::endswith(prefix, double_back_slash)) - { - comps.erase(comps.begin()+i); - } - else - { - i += 1; - } - } - else - { - i += 1; - } - } - - // If the path is now empty, substitute '.' - if(prefix.empty() && comps.empty()) - { - comps.push_back(dot); - } - - return prefix + pystring::join(double_back_slash, comps); - } - - // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B. - // It should be understood that this may change the meaning of the path - // if it contains symbolic links! - // Normalize path, eliminating double slashes, etc. - - std::string normpath_posix(const std::string & p) - { - if(p.empty()) return dot; - - std::string path = p; - - int initial_slashes = pystring::startswith(path, forward_slash) ? 1 : 0; - - // POSIX allows one or two initial slashes, but treats three or more - // as single slash. - - if (initial_slashes && pystring::startswith(path, double_forward_slash) - && !pystring::startswith(path, triple_forward_slash)) - initial_slashes = 2; - - std::vector comps, new_comps; - pystring::split(path, comps, forward_slash); - - for(unsigned int i=0; i 0) - path = pystring::mul(forward_slash, initial_slashes) + path; - - if(path.empty()) return dot; - return path; - } - - std::string normpath(const std::string & path) - { -#ifdef WINDOWS - return normpath_nt(path); -#else - return normpath_posix(path); -#endif - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - // Split the extension from a pathname. - // Extension is everything from the last dot to the end, ignoring - // leading dots. Returns "(root, ext)"; ext may be empty. - // It is always true that root + ext == p - - void splitext_generic(std::string & root, std::string & ext, - const std::string & p, - const std::string & sep, - const std::string & altsep, - const std::string & extsep) - { - int sepIndex = pystring::rfind(p, sep); - if(!altsep.empty()) - { - int altsepIndex = pystring::rfind(p, altsep); - sepIndex = std::max(sepIndex, altsepIndex); - } - - int dotIndex = pystring::rfind(p, extsep); - if(dotIndex > sepIndex) - { - // Skip all leading dots - int filenameIndex = sepIndex + 1; - - while(filenameIndex < dotIndex) - { - if(pystring::slice(p,filenameIndex) != extsep) - { - root = pystring::slice(p, 0, dotIndex); - ext = pystring::slice(p, dotIndex); - return; - } - - filenameIndex += 1; - } - } - - root = p; - ext = empty_string; - } - - void splitext_nt(std::string & root, std::string & ext, const std::string & path) - { - return splitext_generic(root, ext, path, - double_back_slash, forward_slash, dot); - } - - void splitext_posix(std::string & root, std::string & ext, const std::string & path) - { - return splitext_generic(root, ext, path, - forward_slash, empty_string, dot); - } - - void splitext(std::string & root, std::string & ext, const std::string & path) - { -#ifdef WINDOWS - return splitext_nt(root, ext, path); -#else - return splitext_posix(root, ext, path); -#endif - } - -} // namespace path -} // namespace os - - -}//namespace pystring - - diff --git a/pystring.h b/pystring.h index 12dee5e..84e0743 100644 --- a/pystring.h +++ b/pystring.h @@ -6,6 +6,12 @@ #ifndef INCLUDED_PYSTRING_H #define INCLUDED_PYSTRING_H +#ifdef PYSTRING_HEADER_ONLY +#define PYSTRING_INLINE inline +#else +#define PYSTRING_INLINE +#endif + #include #include @@ -32,102 +38,102 @@ namespace pystring ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with only its first character capitalized. /// - std::string capitalize( const std::string & str ); + PYSTRING_INLINE std::string capitalize( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return centered in a string of length width. Padding is done using spaces. /// - std::string center( const std::string & str, int width ); + PYSTRING_INLINE std::string center( const std::string & str, int width ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the number of occurrences of substring sub in string S[start:end]. Optional /// arguments start and end are interpreted as in slice notation. /// - int count( const std::string & str, const std::string & substr, int start = 0, int end = MAX_32BIT_INT); + PYSTRING_INLINE int count( const std::string & str, const std::string & substr, int start = 0, int end = MAX_32BIT_INT); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return True if the string ends with the specified suffix, otherwise return False. With /// optional start, test beginning at that position. With optional end, stop comparing at that position. /// - bool endswith( const std::string & str, const std::string & suffix, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE bool endswith( const std::string & str, const std::string & suffix, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string where all tab characters are expanded using spaces. If tabsize /// is not given, a tab size of 8 characters is assumed. /// - std::string expandtabs( const std::string & str, int tabsize = 8); + PYSTRING_INLINE std::string expandtabs( const std::string & str, int tabsize = 8); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the lowest index in the string where substring sub is found, such that sub is /// contained in the range [start, end). Optional arguments start and end are interpreted as /// in slice notation. Return -1 if sub is not found. /// - int find( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int find( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Synonym of find right now. Python version throws exceptions. This one currently doesn't /// - int index( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int index( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all characters in the string are alphanumeric and there is at least one /// character, false otherwise. /// - bool isalnum( const std::string & str ); + PYSTRING_INLINE bool isalnum( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all characters in the string are alphabetic and there is at least one /// character, false otherwise /// - bool isalpha( const std::string & str ); + PYSTRING_INLINE bool isalpha( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all characters in the string are digits and there is at least one /// character, false otherwise. /// - bool isdigit( const std::string & str ); + PYSTRING_INLINE bool isdigit( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all cased characters in the string are lowercase and there is at least one /// cased character, false otherwise. /// - bool islower( const std::string & str ); + PYSTRING_INLINE bool islower( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if there are only whitespace characters in the string and there is at least /// one character, false otherwise. /// - bool isspace( const std::string & str ); + PYSTRING_INLINE bool isspace( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if the string is a titlecased string and there is at least one character, /// i.e. uppercase characters may only follow uncased characters and lowercase characters only /// cased ones. Return false otherwise. /// - bool istitle( const std::string & str ); + PYSTRING_INLINE bool istitle( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all cased characters in the string are uppercase and there is at least one /// cased character, false otherwise. /// - bool isupper( const std::string & str ); + PYSTRING_INLINE bool isupper( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a string which is the concatenation of the strings in the sequence seq. /// The separator between elements is the str argument /// - std::string join( const std::string & str, const std::vector< std::string > & seq ); + PYSTRING_INLINE std::string join( const std::string & str, const std::vector< std::string > & seq ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the string left justified in a string of length width. Padding is done using /// spaces. The original string is returned if width is less than str.size(). /// - std::string ljust( const std::string & str, int width ); + PYSTRING_INLINE std::string ljust( const std::string & str, int width ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string converted to lowercase. /// - std::string lower( const std::string & str ); + PYSTRING_INLINE std::string lower( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with leading characters removed. If chars is omitted or None, @@ -135,13 +141,13 @@ namespace pystring /// characters in the string will be stripped from the beginning of the string this method /// is called on (argument "str" ). /// - std::string lstrip( const std::string & str, const std::string & chars = "" ); + PYSTRING_INLINE std::string lstrip( const std::string & str, const std::string & chars = "" ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string, concatenated N times, together. /// Corresponds to the __mul__ operator. /// - std::string mul( const std::string & str, int n); + PYSTRING_INLINE std::string mul( const std::string & str, int n); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the string around first occurance of sep. @@ -149,7 +155,7 @@ namespace pystring /// be the text before sep, sep itself, and the remaining text. If sep is /// not found, the original string will be returned with two empty strings. /// - void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + PYSTRING_INLINE void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); inline std::vector< std::string > partition( const std::string & str, const std::string & sep ) { std::vector< std::string > result; @@ -161,38 +167,38 @@ namespace pystring /// @brief If str starts with prefix return a copy of the string with prefix at the start /// removed otherwise return an unmodified copy of the string. /// - std::string removeprefix( const std::string & str, const std::string & prefix ); + PYSTRING_INLINE std::string removeprefix( const std::string & str, const std::string & prefix ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief If str ends with suffix return a copy of the string with suffix at the end removed /// otherwise return an unmodified copy of the string. /// - std::string removesuffix( const std::string & str, const std::string & suffix ); + PYSTRING_INLINE std::string removesuffix( const std::string & str, const std::string & suffix ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with all occurrences of substring old replaced by new. If /// the optional argument count is given, only the first count occurrences are replaced. /// - std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count = -1); + PYSTRING_INLINE std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count = -1); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the highest index in the string where substring sub is found, such that sub is /// contained within s[start,end]. Optional arguments start and end are interpreted as in /// slice notation. Return -1 on failure. /// - int rfind( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int rfind( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Currently a synonym of rfind. The python version raises exceptions. This one currently /// does not /// - int rindex( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int rindex( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the string right justified in a string of length width. Padding is done using /// spaces. The original string is returned if width is less than str.size(). /// - std::string rjust( const std::string & str, int width); + PYSTRING_INLINE std::string rjust( const std::string & str, int width); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the string around last occurance of sep. @@ -200,7 +206,7 @@ namespace pystring /// be the text before sep, sep itself, and the remaining text. If sep is /// not found, the original string will be returned with two empty strings. /// - void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + PYSTRING_INLINE void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); inline std::vector< std::string > rpartition ( const std::string & str, const std::string & sep ) { std::vector< std::string > result; @@ -213,14 +219,14 @@ namespace pystring /// characters are removed. If not "", the characters in the string will be stripped from the /// end of the string this method is called on. /// - std::string rstrip( const std::string & str, const std::string & chars = "" ); + PYSTRING_INLINE std::string rstrip( const std::string & str, const std::string & chars = "" ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Fills the "result" list with the words in the string, using sep as the delimiter string. /// If maxsplit is > -1, at most maxsplit splits are done. If sep is "", /// any whitespace string is a separator. /// - void split( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); + PYSTRING_INLINE void split( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); inline std::vector< std::string > split( const std::string & str, const std::string & sep = "", int maxsplit = -1) { std::vector< std::string > result; @@ -235,7 +241,7 @@ namespace pystring /// If maxsplit is > -1, at most maxsplit splits are done. If sep is "", /// any whitespace string is a separator. /// - void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); + PYSTRING_INLINE void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); inline std::vector< std::string > rsplit( const std::string & str, const std::string & sep = "", int maxsplit = -1) { std::vector< std::string > result; @@ -247,7 +253,7 @@ namespace pystring /// @brief Return a list of the lines in the string, breaking at line boundaries. Line breaks /// are not included in the resulting list unless keepends is given and true. /// - void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends = false ); + PYSTRING_INLINE void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends = false ); inline std::vector< std::string > splitlines( const std::string & str, bool keepends = false ) { std::vector< std::string > result; @@ -260,48 +266,48 @@ namespace pystring /// test string beginning at that position. With optional end, stop comparing string at that /// position /// - bool startswith( const std::string & str, const std::string & prefix, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE bool startswith( const std::string & str, const std::string & prefix, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with leading and trailing characters removed. If chars is "", /// whitespace characters are removed. If given not "", the characters in the string will be /// stripped from the both ends of the string this method is called on. /// - std::string strip( const std::string & str, const std::string & chars = "" ); + PYSTRING_INLINE std::string strip( const std::string & str, const std::string & chars = "" ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with uppercase characters converted to lowercase and vice versa. /// - std::string swapcase( const std::string & str ); + PYSTRING_INLINE std::string swapcase( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a titlecased version of the string: words start with uppercase characters, /// all remaining cased characters are lowercase. /// - std::string title( const std::string & str ); + PYSTRING_INLINE std::string title( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string where all characters occurring in the optional argument /// deletechars are removed, and the remaining characters have been mapped through the given /// translation table, which must be a string of length 256. /// - std::string translate( const std::string & str, const std::string & table, const std::string & deletechars = ""); + PYSTRING_INLINE std::string translate( const std::string & str, const std::string & table, const std::string & deletechars = ""); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string converted to uppercase. /// - std::string upper( const std::string & str ); + PYSTRING_INLINE std::string upper( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the numeric string left filled with zeros in a string of length width. The original /// string is returned if width is less than str.size(). /// - std::string zfill( const std::string & str, int width ); + PYSTRING_INLINE std::string zfill( const std::string & str, int width ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief function matching python's slice functionality. /// - std::string slice( const std::string & str, int start = 0, int end = MAX_32BIT_INT); + PYSTRING_INLINE std::string slice( const std::string & str, int start = 0, int end = MAX_32BIT_INT); /// /// @ } @@ -339,26 +345,26 @@ namespace path /// program; where basename for '/foo/bar/' returns 'bar', the basename() function returns an /// empty string (''). - std::string basename(const std::string & path); - std::string basename_nt(const std::string & path); - std::string basename_posix(const std::string & path); + PYSTRING_INLINE std::string basename(const std::string & path); + PYSTRING_INLINE std::string basename_nt(const std::string & path); + PYSTRING_INLINE std::string basename_posix(const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the directory name of pathname path. This is the first half of the pair /// returned by split(path). - std::string dirname(const std::string & path); - std::string dirname_nt(const std::string & path); - std::string dirname_posix(const std::string & path); + PYSTRING_INLINE std::string dirname(const std::string & path); + PYSTRING_INLINE std::string dirname_nt(const std::string & path); + PYSTRING_INLINE std::string dirname_posix(const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return True if path is an absolute pathname. On Unix, that means it begins with a /// slash, on Windows that it begins with a (back)slash after chopping off a potential drive /// letter. - bool isabs(const std::string & path); - bool isabs_nt(const std::string & path); - bool isabs_posix(const std::string & s); + PYSTRING_INLINE bool isabs(const std::string & path); + PYSTRING_INLINE bool isabs_nt(const std::string & path); + PYSTRING_INLINE bool isabs_posix(const std::string & s); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a normalized absolutized version of the pathname path. @@ -366,9 +372,9 @@ namespace path /// NOTE: This differs from the interface of the python equivalent in that it requires you /// to pass in the current working directory as an argument. - std::string abspath(const std::string & path, const std::string & cwd); - std::string abspath_nt(const std::string & path, const std::string & cwd); - std::string abspath_posix(const std::string & path, const std::string & cwd); + PYSTRING_INLINE std::string abspath(const std::string & path, const std::string & cwd); + PYSTRING_INLINE std::string abspath_nt(const std::string & path, const std::string & cwd); + PYSTRING_INLINE std::string abspath_posix(const std::string & path, const std::string & cwd); ////////////////////////////////////////////////////////////////////////////////////////////// @@ -381,13 +387,13 @@ namespace path /// current directory on drive C: (c:foo), not c:\foo. /// This dispatches based on the compilation OS - std::string join(const std::string & path1, const std::string & path2); - std::string join_nt(const std::string & path1, const std::string & path2); - std::string join_posix(const std::string & path1, const std::string & path2); + PYSTRING_INLINE std::string join(const std::string & path1, const std::string & path2); + PYSTRING_INLINE std::string join_nt(const std::string & path1, const std::string & path2); + PYSTRING_INLINE std::string join_posix(const std::string & path1, const std::string & path2); - std::string join(const std::vector< std::string > & paths); - std::string join_nt(const std::vector< std::string > & paths); - std::string join_posix(const std::vector< std::string > & paths); + PYSTRING_INLINE std::string join(const std::vector< std::string > & paths); + PYSTRING_INLINE std::string join_nt(const std::vector< std::string > & paths); + PYSTRING_INLINE std::string join_posix(const std::vector< std::string > & paths); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Normalize a pathname. This collapses redundant separators and up-level references @@ -396,9 +402,9 @@ namespace path /// It should be understood that this may change the meaning of the path if it contains /// symbolic links! - std::string normpath(const std::string & path); - std::string normpath_nt(const std::string & path); - std::string normpath_posix(const std::string & path); + PYSTRING_INLINE std::string normpath(const std::string & path); + PYSTRING_INLINE std::string normpath_nt(const std::string & path); + PYSTRING_INLINE std::string normpath_posix(const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the pathname path into a pair, (head, tail) where tail is the last pathname @@ -409,9 +415,9 @@ namespace path /// join(head, tail) returns a path to the same location as path (but the strings may /// differ). - void split(std::string & head, std::string & tail, const std::string & path); - void split_nt(std::string & head, std::string & tail, const std::string & path); - void split_posix(std::string & head, std::string & tail, const std::string & path); + PYSTRING_INLINE void split(std::string & head, std::string & tail, const std::string & path); + PYSTRING_INLINE void split_nt(std::string & head, std::string & tail, const std::string & path); + PYSTRING_INLINE void split_posix(std::string & head, std::string & tail, const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the pathname path into a pair (drive, tail) where drive is either a drive @@ -419,18 +425,18 @@ namespace path /// drive will always be the empty string. In all cases, drive + tail will be the same as /// path. - void splitdrive(std::string & drivespec, std::string & pathspec, const std::string & path); - void splitdrive_nt(std::string & drivespec, std::string & pathspec, const std::string & p); - void splitdrive_posix(std::string & drivespec, std::string & pathspec, const std::string & path); + PYSTRING_INLINE void splitdrive(std::string & drivespec, std::string & pathspec, const std::string & path); + PYSTRING_INLINE void splitdrive_nt(std::string & drivespec, std::string & pathspec, const std::string & p); + PYSTRING_INLINE void splitdrive_posix(std::string & drivespec, std::string & pathspec, const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the pathname path into a pair (root, ext) such that root + ext == path, and /// ext is empty or begins with a period and contains at most one period. Leading periods on /// the basename are ignored; splitext('.cshrc') returns ('.cshrc', ''). - void splitext(std::string & root, std::string & ext, const std::string & path); - void splitext_nt(std::string & root, std::string & ext, const std::string & path); - void splitext_posix(std::string & root, std::string & ext, const std::string & path); + PYSTRING_INLINE void splitext(std::string & root, std::string & ext, const std::string & path); + PYSTRING_INLINE void splitext_nt(std::string & root, std::string & ext, const std::string & path); + PYSTRING_INLINE void splitext_posix(std::string & root, std::string & ext, const std::string & path); /// /// @ } @@ -440,4 +446,8 @@ namespace path } // namespace pystring +#if PYSTRING_HEADER_ONLY +#include "pystring_impl.h" +#endif + #endif diff --git a/pystring_impl.h b/pystring_impl.h new file mode 100644 index 0000000..43844a6 --- /dev/null +++ b/pystring_impl.h @@ -0,0 +1,1669 @@ +// Copyright Contributors to the Pystring project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/imageworks/pystring/blob/master/LICENSE + +#include +#include +#include +#include +#include + +namespace pystring +{ + +#if defined(_WIN32) || defined(_WIN64) || defined(_WINDOWS) || defined(_MSC_VER) +#ifndef WINDOWS +#define WINDOWS +#endif +#endif + +// This definition codes from configure.in in the python src. +// Strictly speaking this limits us to str sizes of 2**31. +// Should we wish to handle this limit, we could use an architecture +// specific #defines and read from ssize_t (unistd.h) if the header exists. +// But in the meantime, the use of int assures maximum arch compatibility. +// This must also equal the size used in the end = MAX_32BIT_INT default arg. + +typedef int Py_ssize_t; +const std::string forward_slash = "/"; +const std::string double_forward_slash = "//"; +const std::string triple_forward_slash = "///"; +const std::string double_back_slash = "\\"; +const std::string empty_string = ""; +const std::string dot = "."; +const std::string double_dot = ".."; +const std::string colon = ":"; + + +/* helper macro to fixup start/end slice values */ +#define PYSTRING_ADJUST_INDICES(start, end, len) \ + if (end > len) \ + end = len; \ + else if (end < 0) { \ + end += len; \ + if (end < 0) \ + end = 0; \ + } \ + if (start < 0) { \ + start += len; \ + if (start < 0) \ + start = 0; \ + } + + + namespace { + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// why doesn't the std::reverse work? + /// + void reverse_strings( std::vector< std::string > & result) + { + for (std::vector< std::string >::size_type i = 0; i < result.size() / 2; i++ ) + { + std::swap(result[i], result[result.size() - 1 - i]); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void split_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) + { + std::string::size_type i, j, len = str.size(); + for (i = j = 0; i < len; ) + { + + while ( i < len && ::isspace( str[i] ) ) i++; + j = i; + + while ( i < len && ! ::isspace( str[i]) ) i++; + + + + if (j < i) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( j, i - j )); + + while ( i < len && ::isspace( str[i])) i++; + j = i; + } + } + if (j < len) + { + result.push_back( str.substr( j, len - j )); + } + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void rsplit_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) + { + std::string::size_type len = str.size(); + std::string::size_type i, j; + for (i = j = len; i > 0; ) + { + + while ( i > 0 && ::isspace( str[i - 1] ) ) i--; + j = i; + + while ( i > 0 && ! ::isspace( str[i - 1]) ) i--; + + + + if (j > i) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( i, j - i )); + + while ( i > 0 && ::isspace( str[i - 1])) i--; + j = i; + } + } + if (j > 0) + { + result.push_back( str.substr( 0, j )); + } + //std::reverse( result, result.begin(), result.end() ); + reverse_strings( result ); + } + + } //anonymous namespace + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void split( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) + { + result.clear(); + + if ( maxsplit < 0 ) maxsplit = MAX_32BIT_INT;//result.max_size(); + + + if ( sep.size() == 0 ) + { + split_whitespace( str, result, maxsplit ); + return; + } + + std::string::size_type i,j, len = str.size(), n = sep.size(); + + i = j = 0; + + while ( i+n <= len ) + { + if ( str[i] == sep[0] && str.substr( i, n ) == sep ) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( j, i - j ) ); + i = j = i + n; + } + else + { + i++; + } + } + + result.push_back( str.substr( j, len-j ) ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) + { + if ( maxsplit < 0 ) + { + split( str, result, sep, maxsplit ); + return; + } + + result.clear(); + + if ( sep.size() == 0 ) + { + rsplit_whitespace( str, result, maxsplit ); + return; + } + + Py_ssize_t i,j, len = (Py_ssize_t) str.size(), n = (Py_ssize_t) sep.size(); + + i = j = len; + + while ( i >= n ) + { + if ( str[i - 1] == sep[n - 1] && str.substr( i - n, n ) == sep ) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( i, j - i ) ); + i = j = i - n; + } + else + { + i--; + } + } + + result.push_back( str.substr( 0, j ) ); + reverse_strings( result ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + #define PYSTRING_LEFTSTRIP 0 + #define PYSTRING_RIGHTSTRIP 1 + #define PYSTRING_BOTHSTRIP 2 + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string do_strip( const std::string & str, int striptype, const std::string & chars ) + { + Py_ssize_t len = (Py_ssize_t) str.size(), i, j, charslen = (Py_ssize_t) chars.size(); + + if ( charslen == 0 ) + { + i = 0; + if ( striptype != PYSTRING_RIGHTSTRIP ) + { + while ( i < len && ::isspace( str[i] ) ) + { + i++; + } + } + + j = len; + if ( striptype != PYSTRING_LEFTSTRIP ) + { + do + { + j--; + } + while (j >= i && ::isspace(str[j])); + + j++; + } + + + } + else + { + const char * sep = chars.c_str(); + + i = 0; + if ( striptype != PYSTRING_RIGHTSTRIP ) + { + while ( i < len && memchr(sep, str[i], charslen) ) + { + i++; + } + } + + j = len; + if (striptype != PYSTRING_LEFTSTRIP) + { + do + { + j--; + } + while (j >= i && memchr(sep, str[j], charslen) ); + j++; + } + + + } + + if ( i == 0 && j == len ) + { + return str; + } + else + { + return str.substr( i, j - i ); + } + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) + { + result.resize(3); + int index = find( str, sep ); + if ( index < 0 ) + { + result[0] = str; + result[1] = empty_string; + result[2] = empty_string; + } + else + { + result[0] = str.substr( 0, index ); + result[1] = sep; + result[2] = str.substr( index + sep.size(), str.size() ); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) + { + result.resize(3); + int index = rfind( str, sep ); + if ( index < 0 ) + { + result[0] = empty_string; + result[1] = empty_string; + result[2] = str; + } + else + { + result[0] = str.substr( 0, index ); + result[1] = sep; + result[2] = str.substr( index + sep.size(), str.size() ); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string strip( const std::string & str, const std::string & chars ) + { + return do_strip( str, PYSTRING_BOTHSTRIP, chars ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string lstrip( const std::string & str, const std::string & chars ) + { + return do_strip( str, PYSTRING_LEFTSTRIP, chars ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string rstrip( const std::string & str, const std::string & chars ) + { + return do_strip( str, PYSTRING_RIGHTSTRIP, chars ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string join( const std::string & str, const std::vector< std::string > & seq ) + { + std::vector< std::string >::size_type seqlen = seq.size(), i; + + if ( seqlen == 0 ) return empty_string; + if ( seqlen == 1 ) return seq[0]; + + std::string result( seq[0] ); + + for ( i = 1; i < seqlen; ++i ) + { + result += str + seq[i]; + + } + + + return result; + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + namespace + { + /* Matches the end (direction >= 0) or start (direction < 0) of self + * against substr, using the start and end arguments. Returns + * -1 on error, 0 if not found and 1 if found. + */ + + int _string_tailmatch(const std::string & self, const std::string & substr, + Py_ssize_t start, Py_ssize_t end, + int direction) + { + Py_ssize_t len = (Py_ssize_t) self.size(); + Py_ssize_t slen = (Py_ssize_t) substr.size(); + + const char* sub = substr.c_str(); + const char* str = self.c_str(); + + PYSTRING_ADJUST_INDICES(start, end, len); + + if (direction < 0) { + // startswith + if (start+slen > len) + return 0; + } else { + // endswith + if (end-start < slen || start > len) + return 0; + if (end-slen > start) + start = end - slen; + } + if (end-start >= slen) + return (!std::memcmp(str+start, sub, slen)); + + return 0; + } + } + + bool endswith( const std::string & str, const std::string & suffix, int start, int end ) + { + int result = _string_tailmatch(str, suffix, + (Py_ssize_t) start, (Py_ssize_t) end, +1); + //if (result == -1) // TODO: Error condition + + return static_cast(result); + } + + + bool startswith( const std::string & str, const std::string & prefix, int start, int end ) + { + int result = _string_tailmatch(str, prefix, + (Py_ssize_t) start, (Py_ssize_t) end, -1); + //if (result == -1) // TODO: Error condition + + return static_cast(result); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + bool isalnum( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + + + if( len == 1 ) + { + return ::isalnum( str[0] ); + } + + for ( i = 0; i < len; ++i ) + { + if ( !::isalnum( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isalpha( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isalpha( (int) str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( !::isalpha( (int) str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isdigit( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isdigit( str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( ! ::isdigit( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool islower( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::islower( str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( !::islower( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isspace( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isspace( str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( !::isspace( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool istitle( const std::string & str ) + { + std::string::size_type len = str.size(), i; + + if ( len == 0 ) return false; + if ( len == 1 ) return ::isupper( str[0] ); + + bool cased = false, previous_is_cased = false; + + for ( i = 0; i < len; ++i ) + { + if ( ::isupper( str[i] ) ) + { + if ( previous_is_cased ) + { + return false; + } + + previous_is_cased = true; + cased = true; + } + else if ( ::islower( str[i] ) ) + { + if (!previous_is_cased) + { + return false; + } + + previous_is_cased = true; + cased = true; + + } + else + { + previous_is_cased = false; + } + } + + return cased; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isupper( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isupper( str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( !::isupper( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string capitalize( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + + if ( len > 0) + { + if (::islower(s[0])) s[0] = (char) ::toupper( s[0] ); + } + + for ( i = 1; i < len; ++i ) + { + if (::isupper(s[i])) s[i] = (char) ::tolower( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string lower( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + + for ( i = 0; i < len; ++i ) + { + if ( ::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string upper( const std::string & str ) + { + std::string s( str ) ; + std::string::size_type len = s.size(), i; + + for ( i = 0; i < len; ++i ) + { + if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string swapcase( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + + for ( i = 0; i < len; ++i ) + { + if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); + else if (::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string title( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + bool previous_is_cased = false; + + for ( i = 0; i < len; ++i ) + { + int c = s[i]; + if ( ::islower(c) ) + { + if ( !previous_is_cased ) + { + s[i] = (char) ::toupper(c); + } + previous_is_cased = true; + } + else if ( ::isupper(c) ) + { + if ( previous_is_cased ) + { + s[i] = (char) ::tolower(c); + } + previous_is_cased = true; + } + else + { + previous_is_cased = false; + } + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string translate( const std::string & str, const std::string & table, const std::string & deletechars ) + { + std::string s; + std::string::size_type len = str.size(), dellen = deletechars.size(); + + if ( table.size() != 256 ) + { + // TODO : raise exception instead + return str; + } + + //if nothing is deleted, use faster code + if ( dellen == 0 ) + { + s = str; + for ( std::string::size_type i = 0; i < len; ++i ) + { + s[i] = table[ s[i] ]; + } + return s; + } + + + int trans_table[256]; + for ( int i = 0; i < 256; i++) + { + trans_table[i] = table[i]; + } + + for ( std::string::size_type i = 0; i < dellen; i++) + { + trans_table[(int) deletechars[i] ] = -1; + } + + for ( std::string::size_type i = 0; i < len; ++i ) + { + if ( trans_table[ (int) str[i] ] != -1 ) + { + s += table[ str[i] ]; + } + } + + return s; + + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string zfill( const std::string & str, int width ) + { + int len = (int)str.size(); + + if ( len >= width ) + { + return str; + } + + std::string s( str ); + + int fill = width - len; + + s = std::string( fill, '0' ) + s; + + + if ( s[fill] == '+' || s[fill] == '-' ) + { + s[0] = s[fill]; + s[fill] = '0'; + } + + return s; + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string ljust( const std::string & str, int width ) + { + std::string::size_type len = str.size(); + if ( (( int ) len ) >= width ) return str; + return str + std::string( width - len, ' ' ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string rjust( const std::string & str, int width ) + { + std::string::size_type len = str.size(); + if ( (( int ) len ) >= width ) return str; + return std::string( width - len, ' ' ) + str; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string center( const std::string & str, int width ) + { + int len = (int) str.size(); + int marg, left; + + if ( len >= width ) return str; + + marg = width - len; + left = marg / 2 + (marg & width & 1); + + return std::string( left, ' ' ) + str + std::string( marg - left, ' ' ); + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string slice( const std::string & str, int start, int end ) + { + PYSTRING_ADJUST_INDICES(start, end, (int) str.size()); + if ( start >= end ) return empty_string; + return str.substr( start, end - start ); + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int find( const std::string & str, const std::string & sub, int start, int end ) + { + PYSTRING_ADJUST_INDICES(start, end, (int) str.size()); + + std::string::size_type result = str.find( sub, start ); + + // If we cannot find the string, or if the end-point of our found substring is past + // the allowed end limit, return that it can't be found. + if( result == std::string::npos || + (result + sub.size() > (std::string::size_type)end) ) + { + return -1; + } + + return (int) result; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int index( const std::string & str, const std::string & sub, int start, int end ) + { + return find( str, sub, start, end ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int rfind( const std::string & str, const std::string & sub, int start, int end ) + { + PYSTRING_ADJUST_INDICES(start, end, (int) str.size()); + + std::string::size_type result = str.rfind( sub, end ); + + if( result == std::string::npos || + result < (std::string::size_type)start || + (result + sub.size() > (std::string::size_type)end)) + return -1; + + return (int)result; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int rindex( const std::string & str, const std::string & sub, int start, int end ) + { + return rfind( str, sub, start, end ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string expandtabs( const std::string & str, int tabsize ) + { + std::string s( str ); + + std::string::size_type len = str.size(), i = 0; + int offset = 0; + + int j = 0; + + for ( i = 0; i < len; ++i ) + { + if ( str[i] == '\t' ) + { + + if ( tabsize > 0 ) + { + int fillsize = tabsize - (j % tabsize); + j += fillsize; + s.replace( i + offset, 1, std::string( fillsize, ' ' )); + offset += fillsize - 1; + } + else + { + s.replace( i + offset, 1, empty_string ); + offset -= 1; + } + + } + else + { + j++; + + if (str[i] == '\n' || str[i] == '\r') + { + j = 0; + } + } + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int count( const std::string & str, const std::string & substr, int start, int end ) + { + int nummatches = 0; + int cursor = start; + + while ( 1 ) + { + cursor = find( str, substr, cursor, end ); + + if ( cursor < 0 ) break; + + cursor += (int) substr.size(); + nummatches += 1; + } + + return nummatches; + + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count ) + { + int sofar = 0; + int cursor = 0; + std::string s( str ); + + std::string::size_type oldlen = oldstr.size(), newlen = newstr.size(); + + cursor = find( s, oldstr, cursor ); + + while ( cursor != -1 && cursor <= (int)s.size() ) + { + if ( count > -1 && sofar >= count ) + { + break; + } + + s.replace( cursor, oldlen, newstr ); + cursor += (int) newlen; + + if ( oldlen != 0) + { + cursor = find( s, oldstr, cursor ); + } + else + { + ++cursor; + } + + ++sofar; + } + + return s; + + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends ) + { + result.clear(); + std::string::size_type len = str.size(), i, j, eol; + + for (i = j = 0; i < len; ) + { + while (i < len && str[i] != '\n' && str[i] != '\r') i++; + + eol = i; + if (i < len) + { + if (str[i] == '\r' && i + 1 < len && str[i+1] == '\n') + { + i += 2; + } + else + { + i++; + } + if (keepends) + eol = i; + + } + + result.push_back( str.substr( j, eol - j ) ); + j = i; + + } + + if (j < len) + { + result.push_back( str.substr( j, len - j ) ); + } + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string mul( const std::string & str, int n ) + { + // Early exits + if (n <= 0) return empty_string; + if (n == 1) return str; + + std::ostringstream os; + for(int i=0; i= 2 && p[1] == ':') + { + std::string path = p; // In case drivespec == p + drivespec = pystring::slice(path, 0, 2); + pathspec = pystring::slice(path, 2); + } + else + { + drivespec = empty_string; + pathspec = p; + } + } + + // On Posix, drive is always empty + void splitdrive_posix(std::string & drivespec, std::string & pathspec, + const std::string & path) + { + drivespec = empty_string; + pathspec = path; + } + + void splitdrive(std::string & drivespec, std::string & pathspec, + const std::string & path) + { +#ifdef WINDOWS + return splitdrive_nt(drivespec, pathspec, path); +#else + return splitdrive_posix(drivespec, pathspec, path); +#endif + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + // Test whether a path is absolute + // In windows, if the character to the right of the colon + // is a forward or backslash it's absolute. + bool isabs_nt(const std::string & path) + { + std::string drivespec, pathspec; + splitdrive_nt(drivespec, pathspec, path); + if(pathspec.empty()) return false; + return ((pathspec[0] == '/') || (pathspec[0] == '\\')); + } + + bool isabs_posix(const std::string & s) + { + return pystring::startswith(s, forward_slash); + } + + bool isabs(const std::string & path) + { +#ifdef WINDOWS + return isabs_nt(path); +#else + return isabs_posix(path); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string abspath_nt(const std::string & path, const std::string & cwd) + { + std::string p = path; + if(!isabs_nt(p)) p = join_nt(cwd, p); + return normpath_nt(p); + } + + std::string abspath_posix(const std::string & path, const std::string & cwd) + { + std::string p = path; + if(!isabs_posix(p)) p = join_posix(cwd, p); + return normpath_posix(p); + } + + std::string abspath(const std::string & path, const std::string & cwd) + { +#ifdef WINDOWS + return abspath_nt(path, cwd); +#else + return abspath_posix(path, cwd); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string join_nt(const std::vector< std::string > & paths) + { + if(paths.empty()) return empty_string; + if(paths.size() == 1) return paths[0]; + + std::string path = paths[0]; + + for(unsigned int i=1; i= 2 && path[1] != ':') || (b.size() >= 2 && b[1] == ':')) + { + // Path doesnt start with a drive letter + b_nts = true; + } + // Else path has a drive letter, and b doesn't but is absolute. + else if((path.size()>3) || + ((path.size()==3) && !pystring::endswith(path, forward_slash) && !pystring::endswith(path, double_back_slash))) + { + b_nts = true; + } + } + + if(b_nts) + { + path = b; + } + else + { + // Join, and ensure there's a separator. + // assert len(path) > 0 + if( pystring::endswith(path, forward_slash) || pystring::endswith(path, double_back_slash)) + { + if(pystring::startswith(b,forward_slash) || pystring::startswith(b,double_back_slash)) + { + path += pystring::slice(b, 1); + } + else + { + path += b; + } + } + else if(pystring::endswith(path, colon)) + { + path += b; + } + else if(!b.empty()) + { + if(pystring::startswith(b, forward_slash) || pystring::startswith(b,double_back_slash)) + { + path += b; + } + else + { + path += double_back_slash + b; + } + } + else + { + // path is not empty and does not end with a backslash, + // but b is empty; since, e.g., split('a/') produces + // ('a', ''), it's best if join() adds a backslash in + // this case. + path += double_back_slash; + } + } + } + + return path; + } + + // Join two or more pathname components, inserting double_back_slash as needed. + std::string join_nt(const std::string & a, const std::string & b) + { + std::vector< std::string > paths(2); + paths[0] = a; + paths[1] = b; + return join_nt(paths); + } + + // Join pathnames. + // If any component is an absolute path, all previous path components + // will be discarded. + // Ignore the previous parts if a part is absolute. + // Insert a '/' unless the first part is empty or already ends in '/'. + + std::string join_posix(const std::vector< std::string > & paths) + { + if(paths.empty()) return empty_string; + if(paths.size() == 1) return paths[0]; + + std::string path = paths[0]; + + for(unsigned int i=1; i paths(2); + paths[0] = a; + paths[1] = b; + return join_posix(paths); + } + + std::string join(const std::string & path1, const std::string & path2) + { +#ifdef WINDOWS + return join_nt(path1, path2); +#else + return join_posix(path1, path2); +#endif + } + + + std::string join(const std::vector< std::string > & paths) + { +#ifdef WINDOWS + return join_nt(paths); +#else + return join_posix(paths); +#endif + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + + // Split a pathname. + // Return (head, tail) where tail is everything after the final slash. + // Either part may be empty + + void split_nt(std::string & head, std::string & tail, const std::string & path) + { + std::string d, p; + splitdrive_nt(d, p, path); + + // set i to index beyond p's last slash + int i = (int)p.size(); + + // walk back to find the index of the first slash from the end + while(i>0 && (p[i-1] != '\\') && (p[i-1] != '/')) + { + i = i - 1; + } + + head = pystring::slice(p,0,i); + tail = pystring::slice(p,i); // now tail has no slashes + + // remove trailing slashes from head, unless it's all slashes + std::string head2 = head; + while(!head2.empty() && ((pystring::slice(head2,-1) == forward_slash) || + (pystring::slice(head2,-1) == double_back_slash))) + { + head2 = pystring::slice(head2,0,-1); + } + + if(!head2.empty()) head = head2; + head = d + head; + } + + + // Split a path in head (everything up to the last '/') and tail (the + // rest). If the path ends in '/', tail will be empty. If there is no + // '/' in the path, head will be empty. + // Trailing '/'es are stripped from head unless it is the root. + + void split_posix(std::string & head, std::string & tail, const std::string & p) + { + int i = pystring::rfind(p, forward_slash) + 1; + + head = pystring::slice(p,0,i); + tail = pystring::slice(p,i); + + if(!head.empty() && (head != pystring::mul(forward_slash, (int) head.size()))) + { + head = pystring::rstrip(head, forward_slash); + } + } + + void split(std::string & head, std::string & tail, const std::string & path) + { +#ifdef WINDOWS + return split_nt(head, tail, path); +#else + return split_posix(head, tail, path); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string basename_nt(const std::string & path) + { + std::string head, tail; + split_nt(head, tail, path); + return tail; + } + + std::string basename_posix(const std::string & path) + { + std::string head, tail; + split_posix(head, tail, path); + return tail; + } + + std::string basename(const std::string & path) + { +#ifdef WINDOWS + return basename_nt(path); +#else + return basename_posix(path); +#endif + } + + std::string dirname_nt(const std::string & path) + { + std::string head, tail; + split_nt(head, tail, path); + return head; + } + + std::string dirname_posix(const std::string & path) + { + std::string head, tail; + split_posix(head, tail, path); + return head; + } + + std::string dirname(const std::string & path) + { +#ifdef WINDOWS + return dirname_nt(path); +#else + return dirname_posix(path); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. + std::string normpath_nt(const std::string & p) + { + std::string path = p; + path = pystring::replace(path, forward_slash,double_back_slash); + + std::string prefix; + splitdrive_nt(prefix, path, path); + + // We need to be careful here. If the prefix is empty, and the path starts + // with a backslash, it could either be an absolute path on the current + // drive (\dir1\dir2\file) or a UNC filename (\\server\mount\dir1\file). It + // is therefore imperative NOT to collapse multiple backslashes blindly in + // that case. + // The code below preserves multiple backslashes when there is no drive + // letter. This means that the invalid filename \\\a\b is preserved + // unchanged, where a\\\b is normalised to a\b. It's not clear that there + // is any better behaviour for such edge cases. + + if(prefix.empty()) + { + // No drive letter - preserve initial backslashes + while(pystring::slice(path,0,1) == double_back_slash) + { + prefix = prefix + double_back_slash; + path = pystring::slice(path,1); + } + } + else + { + // We have a drive letter - collapse initial backslashes + if(pystring::startswith(path, double_back_slash)) + { + prefix = prefix + double_back_slash; + path = pystring::lstrip(path, double_back_slash); + } + } + + std::vector comps; + pystring::split(path, comps, double_back_slash); + + int i = 0; + + while(i<(int)comps.size()) + { + if(comps[i].empty() || comps[i] == dot) + { + comps.erase(comps.begin()+i); + } + else if(comps[i] == double_dot) + { + if(i>0 && comps[i-1] != double_dot) + { + comps.erase(comps.begin()+i-1, comps.begin()+i+1); + i -= 1; + } + else if(i == 0 && pystring::endswith(prefix, double_back_slash)) + { + comps.erase(comps.begin()+i); + } + else + { + i += 1; + } + } + else + { + i += 1; + } + } + + // If the path is now empty, substitute '.' + if(prefix.empty() && comps.empty()) + { + comps.push_back(dot); + } + + return prefix + pystring::join(double_back_slash, comps); + } + + // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B. + // It should be understood that this may change the meaning of the path + // if it contains symbolic links! + // Normalize path, eliminating double slashes, etc. + + std::string normpath_posix(const std::string & p) + { + if(p.empty()) return dot; + + std::string path = p; + + int initial_slashes = pystring::startswith(path, forward_slash) ? 1 : 0; + + // POSIX allows one or two initial slashes, but treats three or more + // as single slash. + + if (initial_slashes && pystring::startswith(path, double_forward_slash) + && !pystring::startswith(path, triple_forward_slash)) + initial_slashes = 2; + + std::vector comps, new_comps; + pystring::split(path, comps, forward_slash); + + for(unsigned int i=0; i 0) + path = pystring::mul(forward_slash, initial_slashes) + path; + + if(path.empty()) return dot; + return path; + } + + std::string normpath(const std::string & path) + { +#ifdef WINDOWS + return normpath_nt(path); +#else + return normpath_posix(path); +#endif + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + // Split the extension from a pathname. + // Extension is everything from the last dot to the end, ignoring + // leading dots. Returns "(root, ext)"; ext may be empty. + // It is always true that root + ext == p + + void splitext_generic(std::string & root, std::string & ext, + const std::string & p, + const std::string & sep, + const std::string & altsep, + const std::string & extsep) + { + int sepIndex = pystring::rfind(p, sep); + if(!altsep.empty()) + { + int altsepIndex = pystring::rfind(p, altsep); + sepIndex = std::max(sepIndex, altsepIndex); + } + + int dotIndex = pystring::rfind(p, extsep); + if(dotIndex > sepIndex) + { + // Skip all leading dots + int filenameIndex = sepIndex + 1; + + while(filenameIndex < dotIndex) + { + if(pystring::slice(p,filenameIndex) != extsep) + { + root = pystring::slice(p, 0, dotIndex); + ext = pystring::slice(p, dotIndex); + return; + } + + filenameIndex += 1; + } + } + + root = p; + ext = empty_string; + } + + void splitext_nt(std::string & root, std::string & ext, const std::string & path) + { + return splitext_generic(root, ext, path, + double_back_slash, forward_slash, dot); + } + + void splitext_posix(std::string & root, std::string & ext, const std::string & path) + { + return splitext_generic(root, ext, path, + forward_slash, empty_string, dot); + } + + void splitext(std::string & root, std::string & ext, const std::string & path) + { +#ifdef WINDOWS + return splitext_nt(root, ext, path); +#else + return splitext_posix(root, ext, path); +#endif + } + +} // namespace path +} // namespace os + + +}//namespace pystring + + diff --git a/unittest.h b/unittest.h index 7412029..585f417 100644 --- a/unittest.h +++ b/unittest.h @@ -112,7 +112,7 @@ struct AddTest { AddTest(PYSTRINGTest&& test); }; std::vector& GetUnitTests() { \ static std::vector pystring_unit_tests; \ return pystring_unit_tests; } \ - AddTest::AddTest(PYSTRINGTest&& test){GetUnitTests().emplace_back(test);}; \ + AddTest::AddTest(PYSTRINGTest&& test){GetUnitTests().emplace_back(test);} \ PYSTRING_TEST_SETUP(); \ int main(int, char **) { std::cerr << "\n" << #app <<"\n\n"; \ for(size_t i = 0; i < GetUnitTests().size(); ++i) { \ From f64d7e3130f3c4b74c976e86a301d3ea43a03f12 Mon Sep 17 00:00:00 2001 From: Danny Greenstein Date: Sun, 22 Feb 2026 16:33:16 -0800 Subject: [PATCH 22/25] enforcing min c++ 11 to match the new meson build system (#56) --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d35d72..a0318b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,10 @@ cmake_minimum_required(VERSION 3.10) project(pystring LANGUAGES CXX VERSION 1.1.4) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) option(PYSTRING_HEADER_ONLY "Build as header-only library" OFF) @@ -13,6 +17,7 @@ if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT "Installation location" FORCE) endif() message (STATUS "Installation path will be ${CMAKE_INSTALL_PREFIX}") +include(GNUInstallDirs) if(PYSTRING_HEADER_ONLY) message(STATUS "Building pystring as header-only library") @@ -63,4 +68,3 @@ TARGET_LINK_LIBRARIES (pystring_test pystring) enable_testing() add_test(NAME PyStringTest COMMAND pystring_test) -include(GNUInstallDirs) From 381829c2c1696ffec9277b339952a9588e6e67cf Mon Sep 17 00:00:00 2001 From: Danny Greenstein Date: Mon, 23 Feb 2026 17:04:14 -0800 Subject: [PATCH 23/25] Match our isupper with python behavior (#55) * tightening compatbility with python's behavior in edge cases * a couple of more edge cases --- pystring_impl.h | 40 +++- test.cpp | 594 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 626 insertions(+), 8 deletions(-) diff --git a/pystring_impl.h b/pystring_impl.h index 43844a6..e6d6a48 100644 --- a/pystring_impl.h +++ b/pystring_impl.h @@ -504,11 +504,18 @@ const std::string colon = ":"; if ( len == 0 ) return false; if( len == 1 ) return ::islower( str[0] ); - for ( i = 0; i < len; ++i ) - { - if ( !::islower( str[i] ) ) return false; + // python's islower is a lot more leniant than c++ + // this will match the python behavior so that something like + // islower("hello123") is true + + bool has_cased = false; + for (i = 0; i < len; ++i) { + if (::islower(str[i])) + has_cased = true; + else if (::isupper(str[i])) + return false; } - return true; + return has_cased; } ////////////////////////////////////////////////////////////////////////////////////////////// @@ -580,11 +587,18 @@ const std::string colon = ":"; if ( len == 0 ) return false; if( len == 1 ) return ::isupper( str[0] ); - for ( i = 0; i < len; ++i ) - { - if ( !::isupper( str[i] ) ) return false; + // python's isupper is a lot more leniant than c++ + // this will match the python behavior so that something like + // isupper("HELLO123") is true + + bool has_cased = false; + for (std::string::size_type i = 0; i < str.size(); ++i) { + if (::isupper(str[i])) + has_cased = true; + else if (::islower(str[i])) + return false; } - return true; + return has_cased; } ////////////////////////////////////////////////////////////////////////////////////////////// @@ -927,6 +941,16 @@ const std::string colon = ":"; int nummatches = 0; int cursor = start; + // special handling for an empty substring + // this will match python's behavior of + // "bob".count("") == 4 + // "".count("") == 1 + if ( substr.empty() ) + { + PYSTRING_ADJUST_INDICES(start, end, (int)str.size()); + return end - start + 1; + } + while ( 1 ) { cursor = find( str, substr, cursor, end ); diff --git a/test.cpp b/test.cpp index ed702de..8c59046 100644 --- a/test.cpp +++ b/test.cpp @@ -7,6 +7,25 @@ #include "pystring.h" #include "unittest.h" +// Helper wrappers (for pystring functions that don't have direct return values) + +namespace { +static std::vector pystring_split(const std::string& s, int maxsplit) + { std::vector r; pystring::split(s, r, "", maxsplit); return r; } +static std::vector pystring_split(const std::string& s, const std::string& sep, int maxsplit) + { std::vector r; pystring::split(s, r, sep, maxsplit); return r; } +static std::vector pystring_rsplit(const std::string& s, int maxsplit) + { std::vector r; pystring::rsplit(s, r, "", maxsplit); return r; } +static std::vector pystring_rsplit(const std::string& s, const std::string& sep, int maxsplit) + { std::vector r; pystring::rsplit(s, r, sep, maxsplit); return r; } +static std::vector pystring_partition(const std::string& s, const std::string& sep) + { std::vector r; pystring::partition(s, sep, r); return r; } +static std::vector pystring_rpartition(const std::string& s, const std::string& sep) + { std::vector r; pystring::rpartition(s, sep, r); return r; } +static std::vector pystring_splitlines(const std::string& s, bool keepends) + { std::vector r; pystring::splitlines(s, r, keepends); return r; } +} // namespace + PYSTRING_TEST_APP(PyStringUnitTests) PYSTRING_ADD_TEST(pystring, endswith) @@ -33,6 +52,17 @@ PYSTRING_ADD_TEST(pystring, endswith) PYSTRING_CHECK_EQUAL(pystring::endswith("abcdef", "cdef", -10), true); } +PYSTRING_ADD_TEST(pystring, isupper) +{ + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("AB-C"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("HELLO 123"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("123"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("HELLO!"), true); +} + PYSTRING_ADD_TEST(pystring, find) { PYSTRING_CHECK_EQUAL(pystring::find("", ""), 0); @@ -670,3 +700,567 @@ PYSTRING_ADD_TEST(pystring_os_path, splitext) splitext_nt(root, ext, "c:\\a.b.c"); PYSTRING_CHECK_EQUAL(root, "c:\\a.b"); PYSTRING_CHECK_EQUAL(ext, ".c"); splitext_nt(root, ext, "c:\\a_b.c"); PYSTRING_CHECK_EQUAL(root, "c:\\a_b"); PYSTRING_CHECK_EQUAL(ext, ".c"); } + +// the python3_compat tests were auto generated from python +// to make sure that our functions exactly match the values that are being returned in python 3.11 + +PYSTRING_ADD_TEST(python3_compat, capitalize) +{ + PYSTRING_CHECK_EQUAL(pystring::capitalize(""), ""); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hello"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("HELLO"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("Hello World"), "Hello world"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hello123"), "Hello123"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hElLo"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hello world!"), "Hello world!"); +} + +PYSTRING_ADD_TEST(python3_compat, center) +{ + PYSTRING_CHECK_EQUAL(pystring::center("hello", 10), " hello "); + PYSTRING_CHECK_EQUAL(pystring::center("hello", 3), "hello"); + PYSTRING_CHECK_EQUAL(pystring::center("", 5), " "); + PYSTRING_CHECK_EQUAL(pystring::center("hello", 5), "hello"); +} + +PYSTRING_ADD_TEST(python3_compat, count_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::count("", "", 0), 1); + PYSTRING_CHECK_EQUAL(pystring::count("bob", ""), 4); + PYSTRING_CHECK_EQUAL(pystring::count("", "bob"), 0); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "o", 0), 2); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "l", 0), 3); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "xyz", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "", 0), 12); + PYSTRING_CHECK_EQUAL(pystring::count("aaa", "a", 0), 3); + PYSTRING_CHECK_EQUAL(pystring::count("aaa", "aa", 0), 1); + PYSTRING_CHECK_EQUAL(pystring::count("hello", "l", 2, 5), 2); +} + +PYSTRING_ADD_TEST(python3_compat, endswith_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "hello", 0), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "world", 0), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "hello", 1), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "ello", 1), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello", "hello world", 0), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "world", 0, 5), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "hello", 0, 5), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello", "hello", 0), true); +} + +PYSTRING_ADD_TEST(python3_compat, expandtabs) +{ + PYSTRING_CHECK_EQUAL(pystring::expandtabs("hello\tworld", 8), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("hello\tworld", 4), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("\t\t", 4), " "); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("no tabs", 4), "no tabs"); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("a\tb\tc", 4), "a b c"); +} + +PYSTRING_ADD_TEST(python3_compat, find_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "world", 0), 6); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "hello", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "xyz", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "o", 0), 4); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "o", 5), 7); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "o", 0, 5), 4); + PYSTRING_CHECK_EQUAL(pystring::find("", "hello", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::find("hello", "", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::find("abcabc", "bc", 0), 1); + PYSTRING_CHECK_EQUAL(pystring::find("abcabc", "bc", 2), 4); +} + +PYSTRING_ADD_TEST(python3_compat, isalnum) +{ + PYSTRING_CHECK_EQUAL(pystring::isalnum(""), false); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc"), true); + PYSTRING_CHECK_EQUAL(pystring::isalnum("123"), true); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc123"), true); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc!"), false); + PYSTRING_CHECK_EQUAL(pystring::isalnum(" "), false); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc 123"), false); +} + +PYSTRING_ADD_TEST(python3_compat, isalpha) +{ + PYSTRING_CHECK_EQUAL(pystring::isalpha(""), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abc"), true); + PYSTRING_CHECK_EQUAL(pystring::isalpha("ABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abcABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abc1"), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abc!"), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha(" "), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha("a b"), false); +} + +PYSTRING_ADD_TEST(python3_compat, isdigit) +{ + PYSTRING_CHECK_EQUAL(pystring::isdigit(""), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("0"), true); + PYSTRING_CHECK_EQUAL(pystring::isdigit("123"), true); + PYSTRING_CHECK_EQUAL(pystring::isdigit("12.3"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("12 3"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("123abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit(" "), false); +} + +PYSTRING_ADD_TEST(python3_compat, islower) +{ + PYSTRING_CHECK_EQUAL(pystring::islower(""), false); + PYSTRING_CHECK_EQUAL(pystring::islower("a"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("A"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("ABC"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abC"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc123"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("123"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc 123"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("hello!"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("HELLO!"), false); + PYSTRING_CHECK_EQUAL(pystring::islower(" "), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc\n"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("!@#$"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("a!b"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("1a"), true); +} + +PYSTRING_ADD_TEST(python3_compat, isspace) +{ + PYSTRING_CHECK_EQUAL(pystring::isspace(""), false); + PYSTRING_CHECK_EQUAL(pystring::isspace(" "), true); + PYSTRING_CHECK_EQUAL(pystring::isspace(" "), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("\t"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("\n"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("\r"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace(" \t\n"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("a"), false); + PYSTRING_CHECK_EQUAL(pystring::isspace(" a "), false); + PYSTRING_CHECK_EQUAL(pystring::isspace(" a"), false); +} + +PYSTRING_ADD_TEST(python3_compat, istitle) +{ + PYSTRING_CHECK_EQUAL(pystring::istitle(""), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title Case"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("title"), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("TITLE"), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title123"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("A"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("a"), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title Case Here"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title Case"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("123 Title"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("Already A Title"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("not A Title"), false); +} + +PYSTRING_ADD_TEST(python3_compat, isupper) +{ + PYSTRING_CHECK_EQUAL(pystring::isupper(""), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("A"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("a"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC123"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("123"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC 123"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("HELLO!"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("hello!"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper(" "), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC\n"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC\tDEF"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("!@#$"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("A!B"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("1A"), true); +} + +PYSTRING_ADD_TEST(python3_compat, join_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::join(",", {}), ""); + PYSTRING_CHECK_EQUAL(pystring::join(",", {""}), ""); + PYSTRING_CHECK_EQUAL(pystring::join(",", {"a"}), "a"); + PYSTRING_CHECK_EQUAL(pystring::join(",", {"a", "b", "c"}), "a,b,c"); + PYSTRING_CHECK_EQUAL(pystring::join("", {"a", "b", "c"}), "abc"); + PYSTRING_CHECK_EQUAL(pystring::join(", ", {"hello", "world"}), "hello, world"); + PYSTRING_CHECK_EQUAL(pystring::join("-", {"one", "two", "three"}), "one-two-three"); + PYSTRING_CHECK_EQUAL(pystring::join("/", {"path", "to", "file"}), "path/to/file"); +} + +PYSTRING_ADD_TEST(python3_compat, ljust) +{ + PYSTRING_CHECK_EQUAL(pystring::ljust("hello", 10), "hello "); + PYSTRING_CHECK_EQUAL(pystring::ljust("hello", 3), "hello"); + PYSTRING_CHECK_EQUAL(pystring::ljust("", 5), " "); + PYSTRING_CHECK_EQUAL(pystring::ljust("hello", 5), "hello"); +} + +PYSTRING_ADD_TEST(python3_compat, lower) +{ + PYSTRING_CHECK_EQUAL(pystring::lower(""), ""); + PYSTRING_CHECK_EQUAL(pystring::lower("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lower("HELLO"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lower("Hello World"), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::lower("hello123"), "hello123"); + PYSTRING_CHECK_EQUAL(pystring::lower("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::lower("hElLo"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lower("hello world!"), "hello world!"); +} + +PYSTRING_ADD_TEST(python3_compat, lstrip) +{ + PYSTRING_CHECK_EQUAL(pystring::lstrip(""), ""); + PYSTRING_CHECK_EQUAL(pystring::lstrip(" hello "), "hello "); + PYSTRING_CHECK_EQUAL(pystring::lstrip(" hello ", " "), "hello "); + PYSTRING_CHECK_EQUAL(pystring::lstrip("xxhelloxx", "x"), "helloxx"); + PYSTRING_CHECK_EQUAL(pystring::lstrip("\t\nhello\t\n"), "hello\t\n"); + PYSTRING_CHECK_EQUAL(pystring::lstrip("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lstrip(" "), ""); + PYSTRING_CHECK_EQUAL(pystring::lstrip("aabbcc", "ac"), "bbcc"); + PYSTRING_CHECK_EQUAL(pystring::lstrip("hello world "), "hello world "); +} + +PYSTRING_ADD_TEST(python3_compat, mul) +{ + PYSTRING_CHECK_EQUAL(pystring::mul("ab", 3), "ababab"); + PYSTRING_CHECK_EQUAL(pystring::mul("ab", 0), ""); + PYSTRING_CHECK_EQUAL(pystring::mul("ab", 1), "ab"); + PYSTRING_CHECK_EQUAL(pystring::mul("", 5), ""); + PYSTRING_CHECK_EQUAL(pystring::mul("x", 5), "xxxxx"); +} + + +PYSTRING_ADD_TEST(python3_compat, removeprefix_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello world", "hello "), "world"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello world", "xyz"), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello world", ""), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello", "hello"), ""); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("", "x"), ""); +} + +PYSTRING_ADD_TEST(python3_compat, removesuffix_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello world", "hello "), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello world", "xyz"), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello world", ""), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello", "hello"), ""); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("", "x"), ""); +} + +PYSTRING_ADD_TEST(python3_compat, replace_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "world", "python", -1), "hello python"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "o", "0", -1), "hell0 w0rld"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "o", "0", 1), "hell0 world"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "xyz", "abc", -1), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "", "_", -1), "_h_e_l_l_o_ _w_o_r_l_d_"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaa", "a", "bb", -1), "bbbbbb"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaa", "a", "bb", 2), "bbbba"); + PYSTRING_CHECK_EQUAL(pystring::replace("", "a", "b", -1), ""); + PYSTRING_CHECK_EQUAL(pystring::replace("hello", "hello", "", -1), ""); +} + +PYSTRING_ADD_TEST(python3_compat, rfind_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "world", 0), 6); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "hello", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "xyz", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "", 0), 11); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "o", 0), 7); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "o", 5), 7); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "o", 0, 5), 4); + PYSTRING_CHECK_EQUAL(pystring::rfind("", "hello", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello", "", 0), 5); + PYSTRING_CHECK_EQUAL(pystring::rfind("abcabc", "bc", 0), 4); + PYSTRING_CHECK_EQUAL(pystring::rfind("abcabc", "bc", 2), 4); +} + +PYSTRING_ADD_TEST(python3_compat, rjust) +{ + PYSTRING_CHECK_EQUAL(pystring::rjust("hello", 10), " hello"); + PYSTRING_CHECK_EQUAL(pystring::rjust("hello", 3), "hello"); + PYSTRING_CHECK_EQUAL(pystring::rjust("", 5), " "); + PYSTRING_CHECK_EQUAL(pystring::rjust("hello", 5), "hello"); +} + + +PYSTRING_ADD_TEST(python3_compat, rstrip) +{ + PYSTRING_CHECK_EQUAL(pystring::rstrip(""), ""); + PYSTRING_CHECK_EQUAL(pystring::rstrip(" hello "), " hello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip(" hello ", " "), " hello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("xxhelloxx", "x"), "xxhello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("\t\nhello\t\n"), "\t\nhello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip(" "), ""); + PYSTRING_CHECK_EQUAL(pystring::rstrip("aabbcc", "ac"), "aabb"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("hello world "), "hello world"); +} + +PYSTRING_ADD_TEST(python3_compat, slice_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 0, 5), "hello"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 1, 4), "ell"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 0, -1), "hell"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", -3, -1), "ll"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 2, 2), ""); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 3, 1), ""); + PYSTRING_CHECK_EQUAL(pystring::slice("", 0, 0), ""); + PYSTRING_CHECK_EQUAL(pystring::slice("hello world", 6, 11), "world"); +} + + +PYSTRING_ADD_TEST(python3_compat, startswith_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "hello", 0), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "world", 0), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "hello", 1), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "ello", 1), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello", "hello world", 0), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "world", 0, 5), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "hello", 0, 5), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello", "hello", 0), true); +} + +PYSTRING_ADD_TEST(python3_compat, strip) +{ + PYSTRING_CHECK_EQUAL(pystring::strip(""), ""); + PYSTRING_CHECK_EQUAL(pystring::strip(" hello "), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip(" hello ", " "), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip("xxhelloxx", "x"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip("\t\nhello\t\n"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip(" "), ""); + PYSTRING_CHECK_EQUAL(pystring::strip("aabbcc", "ac"), "bb"); + PYSTRING_CHECK_EQUAL(pystring::strip("hello world "), "hello world"); +} + +PYSTRING_ADD_TEST(python3_compat, swapcase) +{ + PYSTRING_CHECK_EQUAL(pystring::swapcase(""), ""); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hello"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("HELLO"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("Hello World"), "hELLO wORLD"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hello123"), "HELLO123"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hElLo"), "HeLlO"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hello world!"), "HELLO WORLD!"); +} + +PYSTRING_ADD_TEST(python3_compat, title) +{ + PYSTRING_CHECK_EQUAL(pystring::title(""), ""); + PYSTRING_CHECK_EQUAL(pystring::title("hello world"), "Hello World"); + PYSTRING_CHECK_EQUAL(pystring::title("HELLO WORLD"), "Hello World"); + PYSTRING_CHECK_EQUAL(pystring::title("hello"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::title("it's a test"), "It'S A Test"); + PYSTRING_CHECK_EQUAL(pystring::title("they're bill's friends from the UK"), "They'Re Bill'S Friends From The Uk"); + PYSTRING_CHECK_EQUAL(pystring::title("hello123world"), "Hello123World"); +} + +PYSTRING_ADD_TEST(python3_compat, upper) +{ + PYSTRING_CHECK_EQUAL(pystring::upper(""), ""); + PYSTRING_CHECK_EQUAL(pystring::upper("hello"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::upper("HELLO"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::upper("Hello World"), "HELLO WORLD"); + PYSTRING_CHECK_EQUAL(pystring::upper("hello123"), "HELLO123"); + PYSTRING_CHECK_EQUAL(pystring::upper("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::upper("hElLo"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::upper("hello world!"), "HELLO WORLD!"); +} + +PYSTRING_ADD_TEST(python3_compat, zfill) +{ + PYSTRING_CHECK_EQUAL(pystring::zfill("42", 5), "00042"); + PYSTRING_CHECK_EQUAL(pystring::zfill("42", 2), "42"); + PYSTRING_CHECK_EQUAL(pystring::zfill("-42", 5), "-0042"); + PYSTRING_CHECK_EQUAL(pystring::zfill("+42", 5), "+0042"); + PYSTRING_CHECK_EQUAL(pystring::zfill("hello", 8), "000hello"); + PYSTRING_CHECK_EQUAL(pystring::zfill("", 3), "000"); +} + + +PYSTRING_ADD_TEST(python3_compat, rpartition_python3) +{ + { + std::vector _e17 = {"hello", " ", "world"}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello world", " ")) == _e17); + } + { + std::vector _e19 = {"", "", "hello world"}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello world", "xyz")) == _e19); + } + { + std::vector _e21 = {"", "hello", ""}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello", "hello")) == _e21); + } + { + std::vector _e23 = {"", "", ""}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("", "x")) == _e23); + } + { + std::vector _e25 = {"aXb", "X", "c"}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("aXbXc", "X")) == _e25); + } + { + std::vector _e27 = {"hello ", "world", ""}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello world", "world")) == _e27); + } +} + + +PYSTRING_ADD_TEST(python3_compat, rsplit_python3) +{ + { + std::vector _e12 = {"hello world", "foo"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit("hello world foo", " ", 1)) == _e12); + } + { + std::vector _e13 = {"a,b", "c", "d"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit("a,b,c,d", ",", 2)) == _e13); + } + { + std::vector _e14 = {" hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit(" hello world ", 1)) == _e14); + } + { + std::vector _e15 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit("hello world", -1)) == _e15); + } +} + + +PYSTRING_ADD_TEST(python3_compat, split_python3) +{ + { + std::vector _e0 = {}; + PYSTRING_CHECK_ASSERT((pystring_split("", -1)) == _e0); + } + { + std::vector _e1 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world", -1)) == _e1); + } + { + std::vector _e2 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split(" hello world ", -1)) == _e2); + } + { + std::vector _e3 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world", " ", -1)) == _e3); + } + { + std::vector _e4 = {"hello world"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world", " ", 0)) == _e4); + } + { + std::vector _e5 = {"hello", "world foo"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world foo", " ", 1)) == _e5); + } + { + std::vector _e6 = {"a", "b", "c", "d"}; + PYSTRING_CHECK_ASSERT((pystring_split("a,b,c,d", ",", -1)) == _e6); + } + { + std::vector _e7 = {"a", "b", "c,d"}; + PYSTRING_CHECK_ASSERT((pystring_split("a,b,c,d", ",", 2)) == _e7); + } + { + std::vector _e8 = {"a", "b", "c"}; + PYSTRING_CHECK_ASSERT((pystring_split("aXXbXXc", "XX", -1)) == _e8); + } + { + std::vector _e9 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello", "x", -1)) == _e9); + } + { + std::vector _e10 = {}; + PYSTRING_CHECK_ASSERT((pystring_split(" ", -1)) == _e10); + } + { + std::vector _e11 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split("\thello\tworld\t", -1)) == _e11); + } +} + + + +PYSTRING_ADD_TEST(python3_compat, splitlines) +{ + { + std::vector _e28 = {}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("", false)) == _e28); + } + { + std::vector _e29 = {}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("", true)) == _e29); + } + { + std::vector _e30 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello", false)) == _e30); + } + { + std::vector _e31 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello", true)) == _e31); + } + { + std::vector _e32 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\nworld", false)) == _e32); + } + { + std::vector _e33 = {"hello\n", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\nworld", true)) == _e33); + } + { + std::vector _e34 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\r\nworld", false)) == _e34); + } + { + std::vector _e35 = {"hello\r\n", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\r\nworld", true)) == _e35); + } + { + std::vector _e36 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\rworld", false)) == _e36); + } + { + std::vector _e37 = {"hello\r", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\rworld", true)) == _e37); + } + { + std::vector _e38 = {"line1", "line2", "line3"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("line1\nline2\nline3", false)) == _e38); + } + { + std::vector _e39 = {"line1\n", "line2\n", "line3"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("line1\nline2\nline3", true)) == _e39); + } + { + std::vector _e40 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n", false)) == _e40); + } + { + std::vector _e41 = {"hello\n"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n", true)) == _e41); + } + { + std::vector _e42 = {"", "hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("\nhello", false)) == _e42); + } + { + std::vector _e43 = {"\n", "hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("\nhello", true)) == _e43); + } +} + From 266d23c9b4668e9582de0f3fbfc91c09d9a4b963 Mon Sep 17 00:00:00 2001 From: Danny Greenstein Date: Sat, 13 Jun 2026 15:38:34 -0700 Subject: [PATCH 24/25] Add permanent header-only and compiled library targets with CMake export support (#62) * updating version numbers * no need for meson options now that we are building both compiled and header only together and providing them as options * no need for header only ci tasks, each ci task now builds and tests two different targets, regular pystring and pystring_header_only * no need for header only ci tasks, each ci task now builds and tests two different targets, regular pystring and pystring_header_only * updated meson and cmake build systems to have two available targets for the user to pick from compiled and header only * adding cmake configuration file * removing empty ci target * adding bit in pystringConfig to prevent double definition error * adding newline in file end for consistency --- .github/workflows/ci.yml | 56 +-------------- .github/workflows/meson.yml | 40 ++--------- CMakeLists.txt | 129 +++++++++++++++++++++------------- cmake/pystringConfig.cmake.in | 8 +++ meson.build | 88 ++++++++++++----------- meson_options.txt | 1 - test_header_only_define.cpp | 5 ++ 7 files changed, 149 insertions(+), 178 deletions(-) create mode 100644 cmake/pystringConfig.cmake.in delete mode 100644 meson_options.txt create mode 100644 test_header_only_define.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 130df83..ed7e9ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - build: [1, 2, 3, 4, 5, 6] + build: [1, 2, 3, 4] include: # ------------------------------------------------------------------- # CLANG, Release @@ -75,36 +75,6 @@ jobs: compiler-desc: gcc os: ubuntu-latest - - # ------------------------------------------------------------------- - # CLANG, Release header only - # ------------------------------------------------------------------- - - build: 5 - build-type: Release - build-shared: 'ON' - header-only: 'ON' - cxx-standard: 17 - cxx-compiler: clang++ - cxx-flags: '' - cc-compiler: clang - compiler-desc: clang - os: ubuntu-latest - - # ------------------------------------------------------------------- - # gcc, Release header only - # ------------------------------------------------------------------- - - build: 6 - build-type: Release - build-shared: 'ON' - header-only: 'ON' - cxx-standard: 17 - cxx-compiler: g++ - cxx-flags: '' - cc-compiler: gcc - compiler-desc: gcc - os: ubuntu-latest - - env: CXX: ${{ matrix.cxx-compiler }} CC: ${{ matrix.cc-compiler }} @@ -124,7 +94,6 @@ jobs: -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ - -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} working-directory: _build - name: Build run: | @@ -145,7 +114,7 @@ jobs: runs-on: macos-latest strategy: matrix: - build: [1, 2, 3] + build: [1, 2] include: # Release @@ -165,16 +134,6 @@ jobs: cxx-flags: '' os: macos-latest - - # Release header only - - build: 3 - build-type: Release - build-shared: 'ON' - header-only: 'ON' - cxx-standard: 17 - cxx-flags: '' - os: macos-latest - steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -192,7 +151,6 @@ jobs: -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ - -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} working-directory: _build - name: Build run: | @@ -232,15 +190,6 @@ jobs: cxx-flags: '' os: windows-latest - # Release header only - - build: 3 - build-type: Release - build-shared: 'ON' - header-only: 'ON' - cxx-standard: 17 - cxx-flags: '' - os: windows-latest - steps: - name: Checkout @@ -261,7 +210,6 @@ jobs: -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ - -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} shell: bash working-directory: _build - name: Build diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml index e020145..bca9437 100644 --- a/.github/workflows/meson.yml +++ b/.github/workflows/meson.yml @@ -7,16 +7,13 @@ on: jobs: meson-build-and-tests: runs-on: ${{ matrix.platform }} - name: ${{ matrix.platform }}, ${{ matrix.mode.name }} ${{ matrix.flavor }} ${{ matrix.library_mode }} + name: ${{ matrix.platform }}, ${{ matrix.mode.name }} ${{ matrix.flavor }} strategy: fail-fast: false matrix: flavor: - debug - release - library_mode: - - compiled - - header-only mode: - name: default extra_envs: {} @@ -64,29 +61,6 @@ jobs: - macos-latest exclude: - # Only test header-only with a subset of configurations to reduce CI time - # Test header-only only with default compiler in release mode - - library_mode: header-only - flavor: debug - - library_mode: header-only - mode: - name: gcc - - library_mode: header-only - mode: - name: clang - - library_mode: header-only - mode: - name: sanitize - - library_mode: header-only - mode: - name: sanitize+asanonly - - library_mode: header-only - mode: - name: clang+sanitize - - library_mode: header-only - mode: - name: clang-cl+sanitize - # clang-cl only makes sense on windows. - platform: ubuntu-22.04 mode: @@ -151,21 +125,21 @@ jobs: if: ${{ matrix.platform == 'windows-2022' }} env: ${{ matrix.mode.extra_envs }} run: | - meson setup build-${{ matrix.flavor }}-${{ matrix.library_mode }} --buildtype=${{ matrix.flavor }} -Ddefault_library=static -Dheader_only=${{ matrix.library_mode == 'header-only' && 'true' || 'false' }} ${{ matrix.mode.args }} --vsenv + meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} -Ddefault_library=static ${{ matrix.mode.args }} --vsenv - name: Configuring if: ${{ matrix.platform != 'windows-2022' }} env: ${{ matrix.mode.extra_envs }} run: | - meson setup build-${{ matrix.flavor }}-${{ matrix.library_mode }} --buildtype=${{ matrix.flavor }} -Dheader_only=${{ matrix.library_mode == 'header-only' && 'true' || 'false' }} ${{ matrix.mode.args }} + meson setup build-${{ matrix.flavor }} --buildtype=${{ matrix.flavor }} ${{ matrix.mode.args }} - name: Building run: | - meson compile -C build-${{ matrix.flavor }}-${{ matrix.library_mode }} + meson compile -C build-${{ matrix.flavor }} - name: Running tests env: ${{ matrix.mode.extra_envs }} run: | - meson test -C build-${{ matrix.flavor }}-${{ matrix.library_mode }} --timeout-multiplier 0 + meson test -C build-${{ matrix.flavor }} --timeout-multiplier 0 - uses: actions/upload-artifact@v4 if: failure() with: - name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.flavor }}-${{ matrix.library_mode }}-logs - path: build-${{ matrix.flavor }}-${{ matrix.library_mode }}/meson-logs \ No newline at end of file + name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.flavor }}-logs + path: build-${{ matrix.flavor }}/meson-logs diff --git a/CMakeLists.txt b/CMakeLists.txt index a0318b9..be8c306 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,11 @@ cmake_minimum_required(VERSION 3.10) -project(pystring LANGUAGES CXX VERSION 1.1.4) +project(pystring LANGUAGES CXX VERSION 1.2.0) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) -option(PYSTRING_HEADER_ONLY "Build as header-only library" OFF) # If the user hasn't configured cmake with an explicit # -DCMAKE_INSTALL_PREFIX=..., then set it to safely install into ./dist, to @@ -19,52 +18,86 @@ endif() message (STATUS "Installation path will be ${CMAKE_INSTALL_PREFIX}") include(GNUInstallDirs) -if(PYSTRING_HEADER_ONLY) - message(STATUS "Building pystring as header-only library") - add_library(pystring INTERFACE) - - target_compile_definitions(pystring INTERFACE PYSTRING_HEADER_ONLY) - - target_include_directories(pystring INTERFACE - $ - $ - ) - - # Install both headers for header-only mode - install(FILES pystring.h pystring_impl.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} - ) -else() - message(STATUS "Building pystring as compiled library") - - add_library(pystring - pystring.cpp - pystring.h - ) - - set_target_properties(pystring PROPERTIES - VERSION ${PROJECT_VERSION} - SOVERSION ${PROJECT_VERSION_MAJOR} - ) - - install(TARGETS pystring - LIBRARY DESTINATION lib - RUNTIME DESTINATION bin - ARCHIVE DESTINATION lib - ) - - install (FILES pystring.h - DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} - COMPONENT developer - ) - -endif() - -# Test executable - -add_executable (pystring_test test.cpp) -TARGET_LINK_LIBRARIES (pystring_test pystring) +# --- Compiled library target: pystring::pystring --- +add_library(pystring + pystring.cpp + pystring.h +) + +set_target_properties(pystring PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} +) + +target_include_directories(pystring PUBLIC + $ + $ +) + +install(TARGETS pystring + EXPORT pystringTargets + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + + +# --- Header-only target: pystring::pystring_header_only --- +add_library(pystring_header_only INTERFACE) + +target_compile_definitions(pystring_header_only INTERFACE PYSTRING_HEADER_ONLY) + +target_include_directories(pystring_header_only INTERFACE + $ + $ +) + +install(TARGETS pystring_header_only + EXPORT pystringTargets +) + +install(FILES pystring.h pystring_impl.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} +) + +# --- Export & package config --- +install(EXPORT pystringTargets + FILE pystringTargets.cmake + NAMESPACE pystring:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pystring +) + +include(CMakePackageConfigHelpers) + +configure_package_config_file( + cmake/pystringConfig.cmake.in + pystringConfig.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pystring +) + +write_basic_package_version_file( + pystringConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion +) + +install(FILES + ${CMAKE_CURRENT_BINARY_DIR}/pystringConfig.cmake + ${CMAKE_CURRENT_BINARY_DIR}/pystringConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/pystring +) + +# --- Tests --- +add_executable(pystring_test test.cpp) +target_link_libraries(pystring_test pystring) + +add_executable(pystring_test_header_only test.cpp) +target_link_libraries(pystring_test_header_only pystring_header_only) + +# Compile-time check that PYSTRING_HEADER_ONLY propagates correctly +add_executable(pystring_test_header_only_define test_header_only_define.cpp) +target_link_libraries(pystring_test_header_only_define pystring_header_only) enable_testing() add_test(NAME PyStringTest COMMAND pystring_test) - +add_test(NAME PyStringTestHeaderOnly COMMAND pystring_test_header_only) diff --git a/cmake/pystringConfig.cmake.in b/cmake/pystringConfig.cmake.in new file mode 100644 index 0000000..b62792d --- /dev/null +++ b/cmake/pystringConfig.cmake.in @@ -0,0 +1,8 @@ +@PACKAGE_INIT@ + +# Prevent double-definition errors +if(NOT TARGET pystring::pystring AND NOT TARGET pystring::pystring_header_only) + include("${CMAKE_CURRENT_LIST_DIR}/pystringTargets.cmake") +endif() + +check_required_components(pystring) diff --git a/meson.build b/meson.build index 5dab5b8..d0da73d 100644 --- a/meson.build +++ b/meson.build @@ -1,65 +1,51 @@ project( 'pystring', 'cpp', - version: '1.1.4', + version: '1.2.0', license: 'BSD-3-Clause', license_files: 'LICENSE', meson_version: '>=1.3', default_options: ['cpp_std=c++17,c++11', 'warning_level=3'], ) -# Option to build as header-only library -header_only = get_option('header_only') - inc = include_directories('.') hdrs = files('pystring.h') -if header_only - # Header-only mode: create a header-only dependency - message('Building pystring as header-only library') - - pystring_dep = declare_dependency( - include_directories: inc, - compile_args: ['-DPYSTRING_HEADER_ONLY'], - ) - - # Install headers for header-only mode - install_headers(hdrs, files('pystring_impl.h'), subdir: 'pystring') - -else - # Compiled mode: build as normal library - message('Building pystring as compiled library') +# --- Compiled library target --- +pystring_lib = library( + 'pystring', + files('pystring.cpp'), + implicit_include_directories: false, + include_directories: inc, + version: meson.project_version(), + install: true, +) - srcs = files('pystring.cpp') +pystring_dep = declare_dependency( + link_with: pystring_lib, + include_directories: inc, +) - pystring_lib = library( - 'pystring', - srcs, - implicit_include_directories: false, - include_directories: inc, - version: meson.project_version(), - install: true, - ) +install_headers(hdrs, subdir: 'pystring') - pystring_dep = declare_dependency( - link_with: pystring_lib, - include_directories: inc, - ) +pkgconfig = import('pkgconfig') +pkgconfig.generate( + pystring_lib, + description: 'C++ functions matching the interface and behavior of python string methods with std::string', +) - # Install headers for compiled mode - install_headers(hdrs, subdir: 'pystring') +# --- Header-only target --- +pystring_header_only_dep = declare_dependency( + include_directories: inc, + compile_args: ['-DPYSTRING_HEADER_ONLY'], +) - # Generate pkg-config file - pkgconfig = import('pkgconfig') - pkgconfig.generate( - pystring_lib, - description: 'C++ functions matching the interface and behavior of python string methods with std::string', - ) -endif +install_headers(hdrs, files('pystring_impl.h'), subdir: 'pystring') +# --- Override default dependency --- meson.override_dependency('pystring', pystring_dep) -# Build and run tests +# --- Tests --- test( 'PyStringTest', executable( @@ -69,3 +55,21 @@ test( build_by_default: false, ), ) + +test( + 'PyStringTestHeaderOnly', + executable( + 'pystring_test_header_only', + 'test.cpp', + dependencies: pystring_header_only_dep, + build_by_default: false, + ), +) + +# Compile-time check that PYSTRING_HEADER_ONLY propagates correctly +executable( + 'pystring_test_header_only_define', + 'test_header_only_define.cpp', + dependencies: pystring_header_only_dep, + build_by_default: true, +) diff --git a/meson_options.txt b/meson_options.txt deleted file mode 100644 index 77be15e..0000000 --- a/meson_options.txt +++ /dev/null @@ -1 +0,0 @@ -option('header_only', type: 'boolean', value: false, description: 'Build as header-only library') diff --git a/test_header_only_define.cpp b/test_header_only_define.cpp new file mode 100644 index 0000000..5a364ce --- /dev/null +++ b/test_header_only_define.cpp @@ -0,0 +1,5 @@ +// test_header_only_mode.cpp +#ifndef PYSTRING_HEADER_ONLY +#error "PYSTRING_HEADER_ONLY must be defined when using the header-only target" +#endif +int main() { return 0; } From 1922c8f2b48e3beb6831c27b4811b58995e986cf Mon Sep 17 00:00:00 2001 From: Supan Roy <140494154+Supan-Roy@users.noreply.github.com> Date: Sun, 14 Jun 2026 05:01:32 +0600 Subject: [PATCH 25/25] Add additional replace overlap edge case tests (#63) * add additional replace overlap edge case tests * Add additional Python compatibility edge case tests --- test.cpp | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/test.cpp b/test.cpp index 8c59046..2f562b0 100644 --- a/test.cpp +++ b/test.cpp @@ -969,6 +969,25 @@ PYSTRING_ADD_TEST(python3_compat, replace_python3) PYSTRING_CHECK_EQUAL(pystring::replace("hello", "hello", "", -1), ""); } +PYSTRING_ADD_TEST(python3_compat, replace_overlap) +{ + PYSTRING_CHECK_EQUAL(pystring::replace("aaaaa", "aa", "b", -1), "bba"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaaa", "aa", "b", -1), "bb"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaa", "aa", "b", -1), "ba"); + PYSTRING_CHECK_EQUAL(pystring::replace("abababa", "aba", "X", -1), "XbX"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaaaaa", "aaa", "x", -1), "xx"); + PYSTRING_CHECK_EQUAL(pystring::replace("abcabcabc", "abc", "x", 2), "xxabc"); + PYSTRING_CHECK_EQUAL(pystring::replace("abcabcabc", "abc", "x", 1), "xabcabc"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaaa", "a", "", -1), ""); + PYSTRING_CHECK_EQUAL(pystring::replace("aaaa", "aa", "", -1), ""); + PYSTRING_CHECK_EQUAL(pystring::replace("abc", "d", "x", -1), "abc"); + PYSTRING_CHECK_EQUAL(pystring::replace("", "a", "x", -1), ""); + PYSTRING_CHECK_EQUAL(pystring::replace("", "", "x", -1), "x"); + PYSTRING_CHECK_EQUAL(pystring::replace("a", "", "-", -1), "-a-"); + PYSTRING_CHECK_EQUAL(pystring::replace("mississippi", "issi", "X", -1), "mXssippi"); + PYSTRING_CHECK_EQUAL(pystring::replace("11111", "11", "x", -1), "xx1"); +} + PYSTRING_ADD_TEST(python3_compat, rfind_python3) { PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "world", 0), 6); @@ -1262,5 +1281,20 @@ PYSTRING_ADD_TEST(python3_compat, splitlines) std::vector _e43 = {"\n", "hello"}; PYSTRING_CHECK_ASSERT((pystring_splitlines("\nhello", true)) == _e43); } + { + std::vector _e44 = {"hello", "", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n\nworld", false)) == _e44); + } + { + std::vector _e45 = {"hello\n", "\n", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n\nworld", true)) == _e45); + } + { + std::vector _e46 = {"hello", ""}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n\n", false)) == _e46); + } + { + std::vector _e47 = {"hello\n", "\n"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n\n", true)) == _e47); + } } -