From 860583ceacd092314deb2062bb472efdac51e07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micka=C3=ABl=20Germain?= Date: Sun, 6 Sep 2026 12:29:47 -0700 Subject: [PATCH] if: Add PATH_IS_PREFIX operator Testing whether one path is a prefix of another is possible today with cmake_path(IS_PREFIX), but the idiom needs a separate statement plus a scratch variable and takes the prefix as a variable name, so projects reach for if(path MATCHES "^${prefix}") instead. That is wrong whenever the prefix contains a regex metacharacter, and it accepts siblings, because '^/a/b' matches '/a/bc'. Add a binary operator where the left operand is the candidate prefix and the right is the path, matching the operand order of cmake_path(IS_PREFIX) and $ so that the same operation reads the same way on all three surfaces. Neither operand is normalized, matching the default of cmake_path(IS_PREFIX) and the existing PATH_EQUAL operator, so '.' and '..' are compared as ordinary components. The test is non-strict, purely lexical, and applies no relative-to-absolute reconciliation. Add policy CMP0222 for compatibility, modeled on CMP0139. The keyword is consumed as an operator only when it appears in the second argument slot, so a variable named PATH_IS_PREFIX keeps working in unary and left-operand position. What the policy covers is such a variable appearing after another token, as in if(NOT PATH_IS_PREFIX AND other), which becomes a configure error under NEW. Since the operator is an if() spelling of cmake_path(IS_PREFIX) and shares its implementation, test it by asserting the two agree over a corpus of inputs rather than by restating expected values. Those are pinned by the cmake_path(IS_PREFIX) test, so the operator's tests stay limited to what has no command equivalent, and no platform branching is needed because parity holds whatever the host path model answers. Fixes: #28040 --- Help/command/if.rst | 34 ++++++ Help/manual/cmake-policies.7.rst | 1 + Help/policy/CMP0222.rst | 17 +++ Help/release/dev/if-PATH_IS_PREFIX.rst | 6 + Source/cmConditionEvaluator.cxx | 22 ++++ Source/cmConditionEvaluator.h | 1 + Source/cmPolicies.h | 4 + .../CMP0222/CMP0222-NEW-churn-result.txt | 1 + .../CMP0222/CMP0222-NEW-churn-stderr.txt | 8 ++ .../RunCMake/CMP0222/CMP0222-NEW-churn.cmake | 8 ++ Tests/RunCMake/CMP0222/CMP0222-NEW.cmake | 13 ++ Tests/RunCMake/CMP0222/CMP0222-OLD-result.txt | 1 + Tests/RunCMake/CMP0222/CMP0222-OLD-stderr.txt | 8 ++ Tests/RunCMake/CMP0222/CMP0222-OLD.cmake | 13 ++ .../CMP0222/CMP0222-WARN-churn-stderr.txt | 12 ++ .../RunCMake/CMP0222/CMP0222-WARN-churn.cmake | 5 + .../RunCMake/CMP0222/CMP0222-WARN-result.txt | 1 + .../RunCMake/CMP0222/CMP0222-WARN-stderr.txt | 21 ++++ Tests/RunCMake/CMP0222/CMP0222-WARN.cmake | 3 + Tests/RunCMake/CMP0222/CMakeLists.txt | 3 + Tests/RunCMake/CMP0222/RunCMakeTest.cmake | 7 ++ Tests/RunCMake/CMakeLists.txt | 1 + Tests/RunCMake/if/PathIsPrefix.cmake | 111 ++++++++++++++++++ Tests/RunCMake/if/RunCMakeTest.cmake | 1 + 24 files changed, 302 insertions(+) create mode 100644 Help/policy/CMP0222.rst create mode 100644 Help/release/dev/if-PATH_IS_PREFIX.rst create mode 100644 Tests/RunCMake/CMP0222/CMP0222-NEW-churn-result.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-NEW-churn-stderr.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-NEW-churn.cmake create mode 100644 Tests/RunCMake/CMP0222/CMP0222-NEW.cmake create mode 100644 Tests/RunCMake/CMP0222/CMP0222-OLD-result.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-OLD-stderr.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-OLD.cmake create mode 100644 Tests/RunCMake/CMP0222/CMP0222-WARN-churn-stderr.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-WARN-churn.cmake create mode 100644 Tests/RunCMake/CMP0222/CMP0222-WARN-result.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-WARN-stderr.txt create mode 100644 Tests/RunCMake/CMP0222/CMP0222-WARN.cmake create mode 100644 Tests/RunCMake/CMP0222/CMakeLists.txt create mode 100644 Tests/RunCMake/CMP0222/RunCMakeTest.cmake create mode 100644 Tests/RunCMake/if/PathIsPrefix.cmake diff --git a/Help/command/if.rst b/Help/command/if.rst index 23f50925e7..04d72f7523 100644 --- a/Help/command/if.rst +++ b/Help/command/if.rst @@ -429,6 +429,40 @@ Path Comparisons See :command:`cmake_path(COMPARE)` for more details. +.. signature:: if( PATH_IS_PREFIX ) + :target: PATH_IS_PREFIX + + .. versionadded:: 4.5 + + True if the path on the left is a prefix of the path on the right. + + .. code-block:: cmake + + # comparison is TRUE + if ("/a/b" PATH_IS_PREFIX "/a/b/c") + ... + endif() + + # comparison is FALSE: '/a/bc' is a sibling, not a child + if ("/a/b" PATH_IS_PREFIX "/a/bc") + ... + endif() + + Component-wise comparison is superior to a regular expression match + against the start of the path. The ``if ( MATCHES "^")`` + idiom mishandles regex metacharacters in ```` and accepts + siblings. + + The test is lexical, not a containment check. No + :ref:`path normalization ` is performed, so a ``..`` + escape still tests true: ``"/a/b" PATH_IS_PREFIX "/a/b/../../etc"``. + Normalize with :command:`cmake_path(NORMAL_PATH)` first if that must be + rejected. + + Equivalent to :command:`cmake_path(IS_PREFIX)` and + ``$`` without their ``NORMALIZE`` option. See + :command:`cmake_path(IS_PREFIX)` for more details. + Variable Expansion ^^^^^^^^^^^^^^^^^^ diff --git a/Help/manual/cmake-policies.7.rst b/Help/manual/cmake-policies.7.rst index 05f1678c94..b79d720130 100644 --- a/Help/manual/cmake-policies.7.rst +++ b/Help/manual/cmake-policies.7.rst @@ -100,6 +100,7 @@ Policies Introduced by CMake 4.5 .. toctree:: :maxdepth: 1 + CMP0222: The if() command supports path prefix tests using PATH_IS_PREFIX operator. CMP0221: cmake_host_system_information() DISTRIB_* queries read the host os-release. CMP0220: Languages enabled in subdirectories propagate to the top-level directory. diff --git a/Help/policy/CMP0222.rst b/Help/policy/CMP0222.rst new file mode 100644 index 0000000000..b1a9529ae4 --- /dev/null +++ b/Help/policy/CMP0222.rst @@ -0,0 +1,17 @@ +CMP0222 +------- + +.. versionadded:: 4.5 + +The :command:`if` command supports path prefix tests using +``PATH_IS_PREFIX`` operator. + +The ``OLD`` behavior for this policy is to ignore the ``PATH_IS_PREFIX`` +operator. The ``NEW`` behavior is to interpret the ``PATH_IS_PREFIX`` +operator. + +.. |INTRODUCED_IN_CMAKE_VERSION| replace:: 4.5 +.. |WARNS_OR_DOES_NOT_WARN| replace:: warns +.. include:: include/STANDARD_ADVICE.rst + +.. include:: include/DEPRECATED.rst diff --git a/Help/release/dev/if-PATH_IS_PREFIX.rst b/Help/release/dev/if-PATH_IS_PREFIX.rst new file mode 100644 index 0000000000..0bb28b1420 --- /dev/null +++ b/Help/release/dev/if-PATH_IS_PREFIX.rst @@ -0,0 +1,6 @@ +if-PATH_IS_PREFIX +----------------- + +* The :command:`if` command gained a ``PATH_IS_PREFIX`` operator to test + whether one path is a prefix of another without accessing the filesystem. + See policy :policy:`CMP0222`. diff --git a/Source/cmConditionEvaluator.cxx b/Source/cmConditionEvaluator.cxx index 9cfd0fba5e..bcd6bebe16 100644 --- a/Source/cmConditionEvaluator.cxx +++ b/Source/cmConditionEvaluator.cxx @@ -66,6 +66,7 @@ auto const keyVERSION_GREATER_EQUAL = "VERSION_GREATER_EQUAL"_s; auto const keyVERSION_LESS = "VERSION_LESS"_s; auto const keyVERSION_LESS_EQUAL = "VERSION_LESS_EQUAL"_s; auto const keyPATH_EQUAL = "PATH_EQUAL"_s; +auto const keyPATH_IS_PREFIX = "PATH_IS_PREFIX"_s; cmSystemTools::CompareOp const MATCH2CMPOP[5] = { cmSystemTools::OP_LESS, cmSystemTools::OP_LESS_EQUAL, @@ -222,6 +223,7 @@ cmConditionEvaluator::cmConditionEvaluator(cmMakefile& makefile, : Makefile(makefile) , Backtrace(std::move(bt)) , Policy139Status(makefile.GetPolicyStatus(cmPolicies::CMP0139)) + , Policy222Status(makefile.GetPolicyStatus(cmPolicies::CMP0222)) { } @@ -679,6 +681,26 @@ bool cmConditionEvaluator::HandleLevel2(cmArgumentList& newArgs, "Since the policy is not set the OLD behavior will be used."_s); } } + + else if (this->IsKeyword(keyPATH_IS_PREFIX, *args.next)) { + + if (this->Policy222Status != cmPolicies::OLD && + this->Policy222Status != cmPolicies::WARN) { + + cmValue lhs = this->GetVariableOrString(*args.current); + cmValue rhs = this->GetVariableOrString(*args.nextnext); + auto const result = cmCMakePath{ *lhs }.IsPrefix(cmCMakePath{ *rhs }); + newArgs.ReduceTwoArgs(result, args); + } + + else if (this->Policy222Status == cmPolicies::WARN) { + this->Makefile.IssuePolicyWarning( + cmPolicies::CMP0222, {}, + "PATH_IS_PREFIX will be interpreted as an operator " + "when the policy is set to NEW. " + "Since the policy is not set the OLD behavior will be used."_s); + } + } } return true; } diff --git a/Source/cmConditionEvaluator.h b/Source/cmConditionEvaluator.h index 0d9c78d6d4..3dea41295c 100644 --- a/Source/cmConditionEvaluator.h +++ b/Source/cmConditionEvaluator.h @@ -67,4 +67,5 @@ private: cmMakefile& Makefile; cmListFileBacktrace Backtrace; cmPolicies::PolicyStatus Policy139Status; + cmPolicies::PolicyStatus Policy222Status; }; diff --git a/Source/cmPolicies.h b/Source/cmPolicies.h index 746ed45568..6f4f8526bc 100644 --- a/Source/cmPolicies.h +++ b/Source/cmPolicies.h @@ -666,6 +666,10 @@ class cmMakefile; SELECT(POLICY, CMP0221, \ "cmake_host_system_information() DISTRIB_* queries read the host " \ "os-release.", \ + 4, 5, 0, WARN) \ + SELECT(POLICY, CMP0222, \ + "The if() command supports path prefix tests using " \ + "PATH_IS_PREFIX operator.", \ 4, 5, 0, WARN) #define CM_SELECT_ID(F, A1, A2, A3, A4, A5, A6) F(A1) diff --git a/Tests/RunCMake/CMP0222/CMP0222-NEW-churn-result.txt b/Tests/RunCMake/CMP0222/CMP0222-NEW-churn-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-NEW-churn-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CMP0222/CMP0222-NEW-churn-stderr.txt b/Tests/RunCMake/CMP0222/CMP0222-NEW-churn-stderr.txt new file mode 100644 index 0000000000..1380d6753a --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-NEW-churn-stderr.txt @@ -0,0 +1,8 @@ +^CMake Error at CMP0222-NEW-churn\.cmake:[0-9]+ \(if\): + if given arguments: + + "NOT" "PATH_IS_PREFIX" "STREQUAL" "yes" + + Unknown arguments specified +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\)$ diff --git a/Tests/RunCMake/CMP0222/CMP0222-NEW-churn.cmake b/Tests/RunCMake/CMP0222/CMP0222-NEW-churn.cmake new file mode 100644 index 0000000000..98be33ad51 --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-NEW-churn.cmake @@ -0,0 +1,8 @@ +cmake_policy(SET CMP0222 NEW) + +# The compatibility break the policy covers: under NEW the keyword is +# consumed as an operator, so a variable of that name is a hard error. +set(PATH_IS_PREFIX "yes") +if(NOT PATH_IS_PREFIX STREQUAL "yes") + message(SEND_ERROR "unreachable") +endif() diff --git a/Tests/RunCMake/CMP0222/CMP0222-NEW.cmake b/Tests/RunCMake/CMP0222/CMP0222-NEW.cmake new file mode 100644 index 0000000000..88711adf6c --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-NEW.cmake @@ -0,0 +1,13 @@ +cmake_policy(SET CMP0222 NEW) + +# Under NEW the keyword is interpreted as an operator and evaluated. The +# operator's own behavior is covered by the RunCMake.if PathIsPrefix case, +# which outlives this directory: the OLD and WARN cases here are removed +# once support for the OLD behavior is dropped. +if(NOT "/a/b" PATH_IS_PREFIX "/a/b/c") + message(SEND_ERROR "if(PATH_IS_PREFIX): '/a/b' not a prefix of '/a/b/c'") +endif() + +if("/a/b" PATH_IS_PREFIX "/a/bc") + message(SEND_ERROR "if(PATH_IS_PREFIX): '/a/b' wrongly a prefix of '/a/bc'") +endif() diff --git a/Tests/RunCMake/CMP0222/CMP0222-OLD-result.txt b/Tests/RunCMake/CMP0222/CMP0222-OLD-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-OLD-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CMP0222/CMP0222-OLD-stderr.txt b/Tests/RunCMake/CMP0222/CMP0222-OLD-stderr.txt new file mode 100644 index 0000000000..4f46b2601c --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-OLD-stderr.txt @@ -0,0 +1,8 @@ +^CMake Error at CMP0222-OLD\.cmake:[0-9]+ \(if\): + if given arguments: + + "/path1" "PATH_IS_PREFIX" "/path2" + + Unknown arguments specified +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\)$ diff --git a/Tests/RunCMake/CMP0222/CMP0222-OLD.cmake b/Tests/RunCMake/CMP0222/CMP0222-OLD.cmake new file mode 100644 index 0000000000..c0866773d1 --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-OLD.cmake @@ -0,0 +1,13 @@ +cmake_policy(SET CMP0222 OLD) + +# Still usable as a value under OLD. Must come first: the case below is +# fatal. +set(PATH_IS_PREFIX "value") +if(PATH_IS_PREFIX STREQUAL "value") +else() + message(SEND_ERROR "PATH_IS_PREFIX not usable as a value under OLD") +endif() + +if("/path1" PATH_IS_PREFIX "/path2") + message("PATH_IS_PREFIX recognized") +endif() diff --git a/Tests/RunCMake/CMP0222/CMP0222-WARN-churn-stderr.txt b/Tests/RunCMake/CMP0222/CMP0222-WARN-churn-stderr.txt new file mode 100644 index 0000000000..088d17249f --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-WARN-churn-stderr.txt @@ -0,0 +1,12 @@ +CMake Warning \(policy\) at CMP0222-WARN-churn\.cmake:[0-9]+ \(if\): + Policy CMP0222 is not set: The if\(\) command supports path prefix tests + using PATH_IS_PREFIX operator\. Run "cmake --help-policy CMP0222" for + policy details\. Use the cmake_policy command to set the policy and + suppress this warning\. + + PATH_IS_PREFIX will be interpreted as an operator when the policy is set to + NEW\. Since the policy is not set the OLD behavior will be used\. +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\) +This warning is for project developers\. Use -Wno-author or -Wno-policy to +suppress it\. diff --git a/Tests/RunCMake/CMP0222/CMP0222-WARN-churn.cmake b/Tests/RunCMake/CMP0222/CMP0222-WARN-churn.cmake new file mode 100644 index 0000000000..6b5e0466a4 --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-WARN-churn.cmake @@ -0,0 +1,5 @@ +set(PATH_IS_PREFIX "yes") +set(other "yes") +if(NOT PATH_IS_PREFIX AND other) + message(SEND_ERROR "condition should be false") +endif() diff --git a/Tests/RunCMake/CMP0222/CMP0222-WARN-result.txt b/Tests/RunCMake/CMP0222/CMP0222-WARN-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-WARN-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CMP0222/CMP0222-WARN-stderr.txt b/Tests/RunCMake/CMP0222/CMP0222-WARN-stderr.txt new file mode 100644 index 0000000000..506ddcc04c --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-WARN-stderr.txt @@ -0,0 +1,21 @@ +CMake Warning \(policy\) at CMP0222-WARN\.cmake:[0-9]+ \(if\): + Policy CMP0222 is not set: The if\(\) command supports path prefix tests + using PATH_IS_PREFIX operator\. Run "cmake --help-policy CMP0222" for + policy details\. Use the cmake_policy command to set the policy and + suppress this warning\. + + PATH_IS_PREFIX will be interpreted as an operator when the policy is set to + NEW\. Since the policy is not set the OLD behavior will be used\. +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\) +This warning is for project developers\. Use -Wno-author or -Wno-policy to +suppress it\. + +CMake Error at CMP0222-WARN\.cmake:[0-9]+ \(if\): + if given arguments: + + "/path1" "PATH_IS_PREFIX" "/path2" + + Unknown arguments specified +Call Stack \(most recent call first\): + CMakeLists\.txt:[0-9]+ \(include\) diff --git a/Tests/RunCMake/CMP0222/CMP0222-WARN.cmake b/Tests/RunCMake/CMP0222/CMP0222-WARN.cmake new file mode 100644 index 0000000000..fad6d5d77f --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMP0222-WARN.cmake @@ -0,0 +1,3 @@ +if("/path1" PATH_IS_PREFIX "/path2") + message("PATH_IS_PREFIX recognized") +endif() diff --git a/Tests/RunCMake/CMP0222/CMakeLists.txt b/Tests/RunCMake/CMP0222/CMakeLists.txt new file mode 100644 index 0000000000..5ff8d3e0ff --- /dev/null +++ b/Tests/RunCMake/CMP0222/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.23) +project(${RunCMake_TEST} NONE) +include(${RunCMake_TEST}.cmake) diff --git a/Tests/RunCMake/CMP0222/RunCMakeTest.cmake b/Tests/RunCMake/CMP0222/RunCMakeTest.cmake new file mode 100644 index 0000000000..670ef911b0 --- /dev/null +++ b/Tests/RunCMake/CMP0222/RunCMakeTest.cmake @@ -0,0 +1,7 @@ +include(RunCMake) + +run_cmake(CMP0222-OLD) +run_cmake(CMP0222-WARN) +run_cmake(CMP0222-WARN-churn) +run_cmake(CMP0222-NEW) +run_cmake(CMP0222-NEW-churn) diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt index 1acdcc7884..877679084e 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -186,6 +186,7 @@ if(WIN32) endif() add_RunCMake_test(CMP0217) add_RunCMake_test(CMP0219) +add_RunCMake_test(CMP0222) if(CMAKE_C_COMPILER_ID STREQUAL "MSVC") add_RunCMake_test(CMP0194 -DCMAKE_C_COMPILER_VERSION=${CMAKE_C_COMPILER_VERSION}) diff --git a/Tests/RunCMake/if/PathIsPrefix.cmake b/Tests/RunCMake/if/PathIsPrefix.cmake new file mode 100644 index 0000000000..37f11e6ce4 --- /dev/null +++ b/Tests/RunCMake/if/PathIsPrefix.cmake @@ -0,0 +1,111 @@ +cmake_policy(SET CMP0222 NEW) + +# The operator is an if() spelling of cmake_path(IS_PREFIX), so assert that +# the two agree rather than repeating a table of expected values here. What +# each input should evaluate to is pinned by the cmake_path(IS_PREFIX) test; +# this asserts only that the two surfaces cannot drift apart. Neither +# normalizes, so the comparison is against the plain form of the command. +# +# Because nothing below states an expected value, the host path model needs +# no branching: parity has to hold on every platform whatever the answer is. +function(assert_parity prefix path) + set(prefix_var "${prefix}") + cmake_path(IS_PREFIX prefix_var "${path}" expected) + if(expected) + set(expected TRUE) + else() + set(expected FALSE) + endif() + if(prefix PATH_IS_PREFIX path) + set(actual TRUE) + else() + set(actual FALSE) + endif() + if(NOT actual STREQUAL expected) + message(SEND_ERROR + "if('${prefix}' PATH_IS_PREFIX '${path}') is ${actual}, but " + "cmake_path(IS_PREFIX) says ${expected}") + endif() +endfunction() + +# Each pair appears in both roles, so a reversed call cannot pass. +assert_parity("/a/b" "/a/b/c") +assert_parity("/a/b/c" "/a/b") +assert_parity("/a/b" "/a/b") + +# Trailing separators, which are significant and asymmetric. +assert_parity("/a/b" "/a/b/") +assert_parity("/a/b/" "/a/b") +assert_parity("/a/b/" "/a/b/c") + +# Without NORMALIZE, '.' and '..' are ordinary components. +assert_parity("/a/b" "/a/b/../../etc") +assert_parity("/a/b" "/a/./b//c") +assert_parity("." "./a/b") +assert_parity("." "a/b") + +# Duplicate separators are not components. +assert_parity("a/b" "a///b") + +# Sibling sharing a textual prefix. +assert_parity("/a/b" "/a/bc") + +# Relative and absolute paths are not reconciled. +assert_parity("/a" "b/c") +assert_parity("b" "b/c") +assert_parity("/" "b/c") +assert_parity("/" "/a/b") + +# Empty operands in each position. +assert_parity("" "/a/b") +assert_parity("/a" "") +assert_parity("" "") + +# Host path model: backslashes, case, drive-relative paths, UNC root-names. +assert_parity("C:\\a" "C:\\a\\b") +assert_parity("c:/a" "C:/A/b") +assert_parity("C:/" "C:foo") +assert_parity("//host/share" "//host/share/a") +assert_parity("/a" "//a/b") + +# Literal operands, not just variables. +if(NOT "/a/b" PATH_IS_PREFIX "/a/b/c") + message(SEND_ERROR "if(PATH_IS_PREFIX): literal operands rejected") +endif() + +# NOT composes with the operator, which is the motivating guard idiom. +set(guard_ok FALSE) +if(NOT "/a/b" PATH_IS_PREFIX "/a/bc") + set(guard_ok TRUE) +endif() +if(NOT guard_ok) + message(SEND_ERROR "if(NOT PATH_IS_PREFIX ) did not compose") +endif() + +# Works in an elseif() chain, which a helper function could not. +if("/a" PATH_IS_PREFIX "/x/y") + message(SEND_ERROR "if(PATH_IS_PREFIX): '/a' matched '/x/y'") +elseif("/a/b" PATH_IS_PREFIX "/a/b/c") +else() + message(SEND_ERROR "elseif( PATH_IS_PREFIX ) did not match") +endif() + +# The motivating case: a prefix that if(MATCHES) cannot express. +set(prefix "/proj/lib+ssl(v2)/inc.d") +if(NOT "${prefix}" PATH_IS_PREFIX "${prefix}/f.h") + message(SEND_ERROR "if(PATH_IS_PREFIX): regex metacharacters in prefix") +endif() + +# The keyword is consumed as an operator only with both a preceding and a +# following token, so it stays usable as a variable otherwise. Neither +# condition may be written with a leading NOT: that would put the keyword in +# the operator slot and make these a hard error. +set(PATH_IS_PREFIX "yes") +if(PATH_IS_PREFIX) +else() + message(SEND_ERROR "if(PATH_IS_PREFIX): unary use broken") +endif() +if(PATH_IS_PREFIX STREQUAL "yes") +else() + message(SEND_ERROR "if(PATH_IS_PREFIX STREQUAL ...): left-operand use broken") +endif() diff --git a/Tests/RunCMake/if/RunCMakeTest.cmake b/Tests/RunCMake/if/RunCMakeTest.cmake index ed01091f22..e52e8e69a6 100644 --- a/Tests/RunCMake/if/RunCMakeTest.cmake +++ b/Tests/RunCMake/if/RunCMakeTest.cmake @@ -13,6 +13,7 @@ if(NOT MSYS) endif() run_cmake(IsDirectory) run_cmake(IsDirectoryLong) +run_cmake(PathIsPrefix) run_cmake(duplicate-deep-else) run_cmake(duplicate-else) run_cmake(duplicate-else-after-elseif)