list(SORT): Add COMPARATOR option for user-defined comparisons Allow users to provide a custom comparison function for `list(SORT)`. The comparator is validated for strict weak ordering at runtime to produce a clear CMake error rather than a platform-dependent abort. Closes: #27761
diff --git a/Help/command/list.rst b/Help/command/list.rst index a84e2cb..1280c40 100644 --- a/Help/command/list.rst +++ b/Help/command/list.rst
@@ -399,6 +399,7 @@ .. signature:: list(SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>]) + list(SORT <list> COMPARATOR <function> [CASE <case>] [ORDER <order>]) Sorts the list in-place alphabetically. @@ -447,3 +448,47 @@ ``DESCENDING`` Sorts the list in descending order. + + Instead of the built-in ``COMPARE`` methods, a user-defined comparison + function may be used with the ``COMPARATOR`` keyword. + + .. versionadded:: 4.4 + + ``COMPARATOR`` is mutually exclusive with ``COMPARE``. The ``CASE`` + and ``ORDER`` options may still be used alongside ``COMPARATOR``: + the ``CASE`` filter is applied to both values before they are passed to + the callable, and ``ORDER DESCENDING`` reverses the comparison by + swapping the two arguments. + + The callable must accept exactly three parameters: two input values and + the name of an output variable. It must set the output variable to a + boolean value (``TRUE`` if the first value should come before the second, + ``FALSE`` otherwise) in the calling scope. + If the callable does not set the output variable, it is an error. + + The comparator must define a + `strict weak ordering <https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings>`_. + In particular: + + * It must return ``FALSE`` when comparing an element to itself + (irreflexivity). + * If it returns ``TRUE`` for ``(a, b)``, it must return ``FALSE`` + for ``(b, a)`` (asymmetry). + + An error is raised if the comparator violates these requirements. + + Example: + + .. code-block:: cmake + + function(version_less a b result) + if("${a}" VERSION_LESS "${b}") + set(${result} TRUE PARENT_SCOPE) + else() + set(${result} FALSE PARENT_SCOPE) + endif() + endfunction() + + set(versions 3.1 1.2 2.0 1.10) + list(SORT versions COMPARATOR version_less) + # versions is now: 1.2;1.10;2.0;3.1
diff --git a/Help/release/dev/list-SORT-COMPARATOR.rst b/Help/release/dev/list-SORT-COMPARATOR.rst new file mode 100644 index 0000000..61a658d --- /dev/null +++ b/Help/release/dev/list-SORT-COMPARATOR.rst
@@ -0,0 +1,6 @@ +list-SORT-COMPARATOR +-------------------- + +* The :command:`list(SORT)` command gained a new ``COMPARATOR`` keyword + that invokes a user-defined :command:`function` to compare elements, complementing + the built-in ``COMPARE`` methods.
diff --git a/Source/cmList.cxx b/Source/cmList.cxx index 4dfeaa2..5b4eecf 100644 --- a/Source/cmList.cxx +++ b/Source/cmList.cxx
@@ -167,6 +167,60 @@ PredicateEvaluator& Evaluator; bool IncludeMatches; }; + +class ComparatorEvaluator +{ +public: + ComparatorEvaluator(std::string functionName, cmMakefile& makefile) + : FunctionName(std::move(functionName)) + , Makefile(&makefile) + , OutputVar(OutputVarFor("_cmake_comparator_out_", makefile)) + { + if (!makefile.GetState()->GetCommand(this->FunctionName)) { + throw cmList::transform_error( + cmStrCat("sub-command SORT, COMPARATOR: unknown function \"", + this->FunctionName, "\".")); + } + } + + bool operator()(std::string const& a, std::string const& b) + { + this->Makefile->RemoveDefinition(this->OutputVar); + + cmListFileContext context = this->Makefile->GetBacktrace().Top(); + std::vector<cmListFileArgument> funcArgs; + funcArgs.emplace_back(a, cmListFileArgument::Quoted, context.Line); + funcArgs.emplace_back(b, cmListFileArgument::Quoted, context.Line); + funcArgs.emplace_back(this->OutputVar, cmListFileArgument::Quoted, + context.Line); + cmListFileFunction func{ this->FunctionName, context.Line, context.Line, + std::move(funcArgs) }; + + cmExecutionStatus status(*this->Makefile); + if (!this->Makefile->ExecuteCommand(func, status) || + status.GetNestedError()) { + throw cmList::transform_error( + cmStrCat("sub-command SORT, COMPARATOR: function \"", + this->FunctionName, "\" failed during execution.")); + } + + cmValue result = this->Makefile->GetDefinition(this->OutputVar); + if (!result) { + throw cmList::transform_error( + cmStrCat("sub-command SORT, COMPARATOR: function \"", + this->FunctionName, "\" did not set the output variable.")); + } + + bool boolResult = cmIsOn(*result); + this->Makefile->RemoveDefinition(this->OutputVar); + return boolResult; + } + +private: + std::string FunctionName; + cmMakefile* Makefile = nullptr; + std::string OutputVar; +}; } cmList& cmList::filter(cm::string_view pattern, FilterMode mode) @@ -249,6 +303,13 @@ { } + StringSorter(cmList::SortConfiguration config, ComparisonFunction comparator) + : Filters{ nullptr, this->GetCaseFilter(config.Case) } + , SortMethod(std::move(comparator)) + , Descending(config.Order == OrderMode::DESCENDING) + { + } + std::string ApplyFilter(std::string const& argument) { std::string result = argument; @@ -308,6 +369,38 @@ return *this; } +cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile) +{ + SortConfiguration config{ cfg }; + + if (config.Order == SortConfiguration::OrderMode::DEFAULT) { + config.Order = SortConfiguration::OrderMode::ASCENDING; + } + if (config.Case == SortConfiguration::CaseSensitivity::DEFAULT) { + config.Case = SortConfiguration::CaseSensitivity::SENSITIVE; + } + + try { + ComparatorEvaluator evaluator(config.ComparatorFunction, makefile); + StringSorter sorter( + config, [&evaluator](std::string const& a, std::string const& b) { + bool result = evaluator(a, b); + if (result && evaluator(b, a)) { + throw cmList::transform_error( + "sub-command SORT, COMPARATOR: function does not induce a strict " + "weak ordering. The comparator returned TRUE for both (a, b) and " + "(b, a)."); + } + return result; + }); + std::sort(this->Values.begin(), this->Values.end(), sorter); + } catch (transform_error& e) { + throw std::invalid_argument(e.what()); + } + + return *this; +} + namespace { using transform_type = std::function<std::string(std::string const&)>; using transform_error = cmList::transform_error;
diff --git a/Source/cmList.h b/Source/cmList.h index b34a6ca..cccfb2c 100644 --- a/Source/cmList.h +++ b/Source/cmList.h
@@ -848,6 +848,7 @@ STRING, FILE_BASENAME, NATURAL, + COMPARATOR, } Compare = CompareMethod::DEFAULT; enum class CaseSensitivity @@ -857,6 +858,8 @@ INSENSITIVE, } Case = CaseSensitivity::DEFAULT; + std::string ComparatorFunction; + // declare the default constructor to work-around clang bug SortConfiguration(); @@ -869,6 +872,7 @@ } }; cmList& sort(SortConfiguration config = SortConfiguration{}); + cmList& sort(SortConfiguration config, cmMakefile& makefile); // exception raised on error during transform operations class transform_error : public std::runtime_error
diff --git a/Source/cmListCommand.cxx b/Source/cmListCommand.cxx index e5338d2..aed1230 100644 --- a/Source/cmListCommand.cxx +++ b/Source/cmListCommand.cxx
@@ -726,6 +726,7 @@ using SortConfig = cmList::SortConfiguration; SortConfig sortConfig; + bool hasComparator = false; size_t argumentIndex = 2; std::string const messageHint = "sub-command SORT "; @@ -733,6 +734,12 @@ while (argumentIndex < args.size()) { std::string const& option = args[argumentIndex++]; if (option == "COMPARE") { + if (hasComparator) { + status.SetError(cmStrCat(messageHint, + "option \"COMPARE\" is incompatible " + "with \"COMPARATOR\".")); + return false; + } if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) { std::string error = cmStrCat(messageHint, "option \"", option, "\" has been specified multiple times."); @@ -806,6 +813,27 @@ option, "\".")); return false; } + } else if (option == "COMPARATOR") { + if (hasComparator) { + status.SetError(cmStrCat(messageHint, "option \"", option, + "\" has been specified multiple times.")); + return false; + } + if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) { + status.SetError(cmStrCat(messageHint, + "option \"COMPARATOR\" is incompatible " + "with \"COMPARE\".")); + return false; + } + if (argumentIndex < args.size()) { + sortConfig.ComparatorFunction = args[argumentIndex++]; + sortConfig.Compare = SortConfig::CompareMethod::COMPARATOR; + hasComparator = true; + } else { + status.SetError(cmStrCat(messageHint, "missing argument for option \"", + option, "\".")); + return false; + } } else { status.SetError( cmStrCat(messageHint, "option \"", option, "\" is unknown.")); @@ -821,6 +849,17 @@ return true; } + if (hasComparator) { + try { + status.GetMakefile().AddDefinition( + listName, list->sort(sortConfig, status.GetMakefile()).to_string()); + return true; + } catch (std::invalid_argument& e) { + status.SetError(e.what()); + return false; + } + } + status.GetMakefile().AddDefinition(listName, list->sort(sortConfig).to_string()); return true;
diff --git a/Tests/RunCMake/list/RunCMakeTest.cmake b/Tests/RunCMake/list/RunCMakeTest.cmake index 1fd86a9..9705b55 100644 --- a/Tests/RunCMake/list/RunCMakeTest.cmake +++ b/Tests/RunCMake/list/RunCMakeTest.cmake
@@ -114,9 +114,18 @@ run_cmake(SORT-DuplicateCompareOption) run_cmake(SORT-DuplicateCaseOption) run_cmake(SORT-NoCaseOption) +run_cmake(SORT-COMPARATOR-UnknownFunction) +run_cmake(SORT-COMPARATOR-NoOutput) +run_cmake(SORT-COMPARATOR-CompareConflict) +run_cmake(SORT-COMPARATOR-CompareConflictReverse) +run_cmake(SORT-COMPARATOR-NoFunction) +run_cmake(SORT-COMPARATOR-DuplicateOption) +run_cmake(SORT-COMPARATOR-NotStrictWeak) +run_cmake(SORT-COMPARATOR-NotStrictWeakLate) # Successful tests run_cmake(SORT) +run_cmake(SORT-COMPARATOR) # argument tests run_cmake(PREPEND-NoArgs)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-stderr.txt new file mode 100644 index 0000000..0224d8c --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict-stderr.txt
@@ -0,0 +1,4 @@ +^CMake Error at SORT-COMPARATOR-CompareConflict\.cmake:10 \(list\): + list sub-command SORT option "COMPARATOR" is incompatible with "COMPARE"\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict.cmake new file mode 100644 index 0000000..2e62821 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflict.cmake
@@ -0,0 +1,10 @@ +function(my_cmp a b out) + if("${a}" STRLESS "${b}") + set(${out} TRUE PARENT_SCOPE) + else() + set(${out} FALSE PARENT_SCOPE) + endif() +endfunction() + +set(mylist c a b) +list(SORT mylist COMPARE STRING COMPARATOR my_cmp)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-stderr.txt new file mode 100644 index 0000000..30659dc --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse-stderr.txt
@@ -0,0 +1,4 @@ +^CMake Error at SORT-COMPARATOR-CompareConflictReverse\.cmake:10 \(list\): + list sub-command SORT option "COMPARE" is incompatible with "COMPARATOR"\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse.cmake new file mode 100644 index 0000000..c08314d --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-CompareConflictReverse.cmake
@@ -0,0 +1,10 @@ +function(my_cmp a b out) + if("${a}" STRLESS "${b}") + set(${out} TRUE PARENT_SCOPE) + else() + set(${out} FALSE PARENT_SCOPE) + endif() +endfunction() + +set(mylist c a b) +list(SORT mylist COMPARATOR my_cmp COMPARE STRING)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-stderr.txt new file mode 100644 index 0000000..b387e8e --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption-stderr.txt
@@ -0,0 +1,5 @@ +^CMake Error at SORT-COMPARATOR-DuplicateOption\.cmake:10 \(list\): + list sub-command SORT option "COMPARATOR" has been specified multiple + times\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption.cmake new file mode 100644 index 0000000..418a843 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-DuplicateOption.cmake
@@ -0,0 +1,10 @@ +function(my_cmp a b out) + if("${a}" STRLESS "${b}") + set(${out} TRUE PARENT_SCOPE) + else() + set(${out} FALSE PARENT_SCOPE) + endif() +endfunction() + +set(mylist c a b) +list(SORT mylist COMPARATOR my_cmp COMPARATOR my_cmp)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-stderr.txt new file mode 100644 index 0000000..7b74e3b --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction-stderr.txt
@@ -0,0 +1,4 @@ +^CMake Error at SORT-COMPARATOR-NoFunction\.cmake:2 \(list\): + list sub-command SORT missing argument for option "COMPARATOR"\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction.cmake new file mode 100644 index 0000000..0c834f1 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoFunction.cmake
@@ -0,0 +1,2 @@ +set(mylist a b c) +list(SORT mylist COMPARATOR)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-stderr.txt new file mode 100644 index 0000000..af5e3cc --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput-stderr.txt
@@ -0,0 +1,5 @@ +^CMake Error at SORT-COMPARATOR-NoOutput\.cmake:6 \(list\): + list sub-command SORT, COMPARATOR: function "bad_comparator" did not set + the output variable\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput.cmake new file mode 100644 index 0000000..9c279f8 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NoOutput.cmake
@@ -0,0 +1,6 @@ +function(bad_comparator a b out) + # Deliberately does NOT set ${out} +endfunction() + +set(mylist c a b) +list(SORT mylist COMPARATOR bad_comparator)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-stderr.txt new file mode 100644 index 0000000..2978265 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak-stderr.txt
@@ -0,0 +1,5 @@ +^CMake Error at SORT-COMPARATOR-NotStrictWeak\.cmake:6 \(list\): + list sub-command SORT, COMPARATOR: function does not induce a strict weak + ordering\. The comparator returned TRUE for both \(a, b\) and \(b, a\)\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak.cmake new file mode 100644 index 0000000..bf7e88f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeak.cmake
@@ -0,0 +1,6 @@ +function(reflexive_comparator a b out) + set(${out} TRUE PARENT_SCOPE) +endfunction() + +set(mylist c a b) +list(SORT mylist COMPARATOR reflexive_comparator)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-stderr.txt new file mode 100644 index 0000000..6188b8d --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate-stderr.txt
@@ -0,0 +1,5 @@ +^CMake Error at SORT-COMPARATOR-NotStrictWeakLate\.cmake:19 \(list\): + list sub-command SORT, COMPARATOR: function does not induce a strict weak + ordering\. The comparator returned TRUE for both \(a, b\) and \(b, a\)\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate.cmake new file mode 100644 index 0000000..c0193ed --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-NotStrictWeakLate.cmake
@@ -0,0 +1,19 @@ +# Comparator that is correct for most pairs but violates strict weak ordering +# when both elements start with "b". +function(late_violation a b result) + string(SUBSTRING "${a}" 0 1 a_first) + string(SUBSTRING "${b}" 0 1 b_first) + if(a_first STREQUAL "b" AND b_first STREQUAL "b") + # Violates asymmetry: always returns TRUE for b-vs-b pairs + set(${result} TRUE PARENT_SCOPE) + else() + if("${a}" STRLESS "${b}") + set(${result} TRUE PARENT_SCOPE) + else() + set(${result} FALSE PARENT_SCOPE) + endif() + endif() +endfunction() + +set(mylist "a" "c" "bb" "ba") +list(SORT mylist COMPARATOR late_violation)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-result.txt b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-result.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-result.txt
@@ -0,0 +1 @@ +1
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-stderr.txt b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-stderr.txt new file mode 100644 index 0000000..e5c9543 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction-stderr.txt
@@ -0,0 +1,4 @@ +^CMake Error at SORT-COMPARATOR-UnknownFunction\.cmake:2 \(list\): + list sub-command SORT, COMPARATOR: unknown function "no_such_function"\. +Call Stack \(most recent call first\): + CMakeLists\.txt:3 \(include\)$
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction.cmake b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction.cmake new file mode 100644 index 0000000..8e5901b --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR-UnknownFunction.cmake
@@ -0,0 +1,2 @@ +set(mylist a b c) +list(SORT mylist COMPARATOR no_such_function)
diff --git a/Tests/RunCMake/list/SORT-COMPARATOR.cmake b/Tests/RunCMake/list/SORT-COMPARATOR.cmake new file mode 100644 index 0000000..0303846 --- /dev/null +++ b/Tests/RunCMake/list/SORT-COMPARATOR.cmake
@@ -0,0 +1,135 @@ +# Comparator: returns TRUE if a < b (string comparison) +function(string_less a b result) + if("${a}" STRLESS "${b}") + set(${result} TRUE PARENT_SCOPE) + else() + set(${result} FALSE PARENT_SCOPE) + endif() +endfunction() + +# Comparator as macro +macro(string_less_macro a b result) + if(${a} STRLESS ${b}) + set(${result} TRUE) + else() + set(${result} FALSE) + endif() +endmacro() + +# Comparator: string length comparison +function(shorter_first a b result) + string(LENGTH "${a}" len_a) + string(LENGTH "${b}" len_b) + if(len_a LESS len_b) + set(${result} TRUE PARENT_SCOPE) + else() + set(${result} FALSE PARENT_SCOPE) + endif() +endfunction() + +## Basic COMPARATOR with function +set(mylist c a b) +list(SORT mylist COMPARATOR string_less) +if(NOT mylist STREQUAL "a;b;c") + message(FATAL_ERROR "SORT(COMPARATOR function) is \"${mylist}\", expected \"a;b;c\"") +endif() + +## COMPARATOR with macro +set(mylist c a b) +list(SORT mylist COMPARATOR string_less_macro) +if(NOT mylist STREQUAL "a;b;c") + message(FATAL_ERROR "SORT(COMPARATOR macro) is \"${mylist}\", expected \"a;b;c\"") +endif() + +## COMPARATOR with ORDER DESCENDING +set(mylist c a b) +list(SORT mylist COMPARATOR string_less ORDER DESCENDING) +if(NOT mylist STREQUAL "c;b;a") + message(FATAL_ERROR "SORT(COMPARATOR ORDER DESCENDING) is \"${mylist}\", expected \"c;b;a\"") +endif() + +## COMPARATOR with CASE INSENSITIVE +# With CASE INSENSITIVE, values are lowercased before being passed to the comparator +set(mylist "C" "a" "B") +list(SORT mylist COMPARATOR string_less CASE INSENSITIVE) +if(NOT mylist STREQUAL "a;B;C") + message(FATAL_ERROR "SORT(COMPARATOR CASE INSENSITIVE) is \"${mylist}\", expected \"a;B;C\"") +endif() + +## COMPARATOR with CASE INSENSITIVE and ORDER DESCENDING +set(mylist "C" "a" "B") +list(SORT mylist COMPARATOR string_less CASE INSENSITIVE ORDER DESCENDING) +if(NOT mylist STREQUAL "C;B;a") + message(FATAL_ERROR "SORT(COMPARATOR CASE INSENSITIVE ORDER DESCENDING) is \"${mylist}\", expected \"C;B;a\"") +endif() + +## Custom comparator (sort by string length) +set(mylist "bbb" "a" "cc") +list(SORT mylist COMPARATOR shorter_first) +if(NOT mylist STREQUAL "a;cc;bbb") + message(FATAL_ERROR "SORT(COMPARATOR shorter_first) is \"${mylist}\", expected \"a;cc;bbb\"") +endif() + +## Custom comparator with ORDER DESCENDING (sort by length descending) +set(mylist "bbb" "a" "cc") +list(SORT mylist COMPARATOR shorter_first ORDER DESCENDING) +if(NOT mylist STREQUAL "bbb;cc;a") + message(FATAL_ERROR "SORT(COMPARATOR shorter_first ORDER DESCENDING) is \"${mylist}\", expected \"bbb;cc;a\"") +endif() + +## Empty list (no-op) +set(empty_list "") +list(SORT empty_list COMPARATOR string_less) +if(NOT empty_list STREQUAL "") + message(FATAL_ERROR "SORT(COMPARATOR empty) is \"${empty_list}\", expected \"\"") +endif() + +## Single element list (no-op) +set(mylist "only") +list(SORT mylist COMPARATOR string_less) +if(NOT mylist STREQUAL "only") + message(FATAL_ERROR "SORT(COMPARATOR single) is \"${mylist}\", expected \"only\"") +endif() + +## Already sorted list +set(mylist a b c) +list(SORT mylist COMPARATOR string_less) +if(NOT mylist STREQUAL "a;b;c") + message(FATAL_ERROR "SORT(COMPARATOR already-sorted) is \"${mylist}\", expected \"a;b;c\"") +endif() + +## Recursive COMPARATOR - verify inner and outer output variables are distinct +# Inner comparator: checks that neither element equals its own output variable +# name (which would mean the outer and inner names collided), then performs a +# valid string comparison to satisfy strict weak ordering. +function(check_outer_name a b out) + if("${a}" STREQUAL "${out}") + message(FATAL_ERROR "Recursive COMPARATOR: inner and outer output variable names are the same: ${out}") + endif() + if("${b}" STREQUAL "${out}") + message(FATAL_ERROR "Recursive COMPARATOR: inner and outer output variable names are the same: ${out}") + endif() + if("${a}" STRLESS "${b}") + set(${out} TRUE PARENT_SCOPE) + else() + set(${out} FALSE PARENT_SCOPE) + endif() +endfunction() + +# Outer comparator: passes its own output variable name into a nested sort +# as a list element, so the inner comparator can compare the two names. +function(nested_comparator a b out) + set(_inner "${out}" dummy) + list(SORT _inner COMPARATOR check_outer_name) + if("${a}" STRLESS "${b}") + set(${out} TRUE PARENT_SCOPE) + else() + set(${out} FALSE PARENT_SCOPE) + endif() +endfunction() + +set(mylist c a b) +list(SORT mylist COMPARATOR nested_comparator) +if(NOT mylist STREQUAL "a;b;c") + message(FATAL_ERROR "SORT(COMPARATOR nested) is \"${mylist}\", expected \"a;b;c\"") +endif()