A C++20 semantic versioning library — a faithful translation of python-semanticversion, with deprecated features removed. See differences.md to see what we changed and which commit we based this library off of.
Parse, compare, and match versions against flexible range specifications following the SemVer 2.0.0 standard. Includes both a simple/intuitive spec syntax and full NPM-style range support.
- Full SemVer 2.0.0 — major, minor, patch, prerelease identifiers, and build metadata
- Version comparison with correct precedence rules
- SimpleSpec — comma-separated clauses with wildcards, caret, tilde, and compatible-release operators
- NpmSpec — the complete NPM range specification including hyphen ranges, X-ranges, and
||unions - Coercion — best-effort conversion of arbitrary strings into valid versions
- Static or shared library — configurable at build time via CMake
Build as a shared library:
cmake -B build
cmake --build buildBuild as a static library:
cmake -B build -DSEMVER_BUILD_SHARED=OFF
cmake --build buildBuild and run unit tests:
cmake -B build -DSEMVER_BUILD_TESTS=ON
cmake --build build
./build/tests/semver_testsBuild and run fuzzer:
cmake -B build-fuzz -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug -DSEMVER_BUILD_FUZZ=ON -DSEMVER_BUILD_SHARED=OFF
cmake --build build-fuzz
# Run the harnesses
tests/fuzz/run.sh --parallel # run all, indefinitely until Ctrl-C
tests/fuzz/run.sh --parallel -max_total_time=60 # 60 seconds per target
tests/fuzz/run.sh --parallel -max_total_time=0 -runs=10000 # 10k runs eachBuild and run sanitizers:
cmake -B build-san -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug -DSEMVER_BUILD_TESTS=ON -DSEMVER_BUILD_SANITIZERS=ON -DSEMVER_BUILD_SHARED=OFF
cmake --build build-san
ASAN_OPTIONS="detect_leaks=1:halt_on_error=1" \
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
build-san/tests/semver_testsAfter installing, consume from your own project:
find_package(semver REQUIRED)
target_link_libraries(myapp PRIVATE semver::semver)#include <semver/semver.hpp>All types live in the semver namespace.
Construct a Version from a string:
auto v = semver::Version("1.4.2-rc.1+build.47");
std::cout << v.major(); // 1
std::cout << v.minor(); // 4
std::cout << v.patch(); // 2
std::cout << v.prerelease()[0]; // "rc"
std::cout << v.prerelease()[1]; // "1"
std::cout << v.build()[0]; // "build"
std::cout << v.build()[1]; // "47"
std::cout << v.to_string(); // "1.4.2-rc.1+build.47"
std::cout << v; // "1.4.2-rc.1+build.47"Validate without throwing:
semver::Version::validate("1.2.3"); // true
semver::Version::validate("not.a.v"); // falseBest-effort conversion of messy strings:
auto v = semver::Version::coerce("02"); // 2.0.0
auto w = semver::Version::coerce("1.2.3.4"); // 1.2.3+4All the standard comparison operators work and follow SemVer 2.0.0 precedence:
auto a = semver::Version("1.0.0");
auto b = semver::Version("1.0.1");
auto c = semver::Version("1.0.0-alpha");
auto d = semver::Version("1.0.0-beta");
a < b; // true — patch bump
c < a; // true — prereleases sort before the release
c < d; // true — "alpha" < "beta" lexicographically
a == semver::Version("1.0.0+anything"); // false — build metadata is compared for equality
a >= c; // trueNumeric prerelease identifiers sort numerically, not lexicographically:
semver::Version("1.0.0-2") < semver::Version("1.0.0-10"); // true
semver::Version("1.0.0-rc") > semver::Version("1.0.0-10"); // true — alpha > numericauto v = semver::Version("1.2.3-rc.1+build.5");
v.next_major().to_string(); // "2.0.0"
v.next_minor().to_string(); // "1.3.0"
v.next_patch().to_string(); // "1.2.3" — strips the prereleaseWhen a prerelease is present the bump returns the smallest version that is strictly greater. For example 1.2.3-rc.1 already sorts below 1.2.3, so next_patch() returns 1.2.3 (not 1.2.4).
auto v = semver::Version("3.2.1-pre+build");
v.truncate("build").to_string(); // "3.2.1-pre+build" (copy)
v.truncate("prerelease").to_string(); // "3.2.1-pre"
v.truncate("patch").to_string(); // "3.2.1"
v.truncate("minor").to_string(); // "3.2.0"
v.truncate("major").to_string(); // "3.0.0"SimpleSpec supports an intuitive comma-separated syntax with wildcards and extended operators.
semver::SimpleSpec(">=1.2.0").match(semver::Version("1.3.0")); // true
semver::SimpleSpec(">=1.2.0").match(semver::Version("1.1.9")); // false
semver::SimpleSpec("<2.0.0").match(semver::Version("1.99.0")); // true
semver::SimpleSpec("!=1.5.0").match(semver::Version("1.5.0")); // falseCombine clauses with commas (logical AND):
semver::SimpleSpec(">=1.0.0,<2.0.0").match(semver::Version("1.7.3")); // true
semver::SimpleSpec(">=1.0.0,<2.0.0").match(semver::Version("2.0.0")); // falsesemver::SimpleSpec("==1.2.*").match(semver::Version("1.2.0")); // true
semver::SimpleSpec("==1.2.*").match(semver::Version("1.2.99")); // true
semver::SimpleSpec("==1.2.*").match(semver::Version("1.3.0")); // false
semver::SimpleSpec("==1.*").match(semver::Version("1.0.0")); // true
semver::SimpleSpec("==1.*").match(semver::Version("1.99.0")); // true~X.Y.Z matches >=X.Y.Z and <X.(Y+1).0:
auto spec = semver::SimpleSpec("~1.4.2");
spec.match(semver::Version("1.4.2")); // true
spec.match(semver::Version("1.4.9")); // true
spec.match(semver::Version("1.5.0")); // false^X.Y.Z allows changes that do not modify the left-most non-zero digit:
auto spec = semver::SimpleSpec("^1.2.3");
spec.match(semver::Version("1.2.3")); // true
spec.match(semver::Version("1.9.0")); // true
spec.match(semver::Version("2.0.0")); // false
// For 0.x the caret is more restrictive:
semver::SimpleSpec("^0.2.3").match(semver::Version("0.2.9")); // true
semver::SimpleSpec("^0.2.3").match(semver::Version("0.3.0")); // false~=X.Y is equivalent to >=X.Y.0,<(X+1).0.0:
auto spec = semver::SimpleSpec("~=1.4");
spec.match(semver::Version("1.4.0")); // true
spec.match(semver::Version("1.99.0")); // true
spec.match(semver::Version("2.0.0")); // falseBy default, a prerelease like 1.0.0-alpha does not satisfy <1.0.0 because the common expectation is that prereleases belong to their own release. Append a bare hyphen to opt in:
semver::SimpleSpec("<1.0.0").match(semver::Version("1.0.0-alpha")); // false
semver::SimpleSpec("<1.0.0-").match(semver::Version("1.0.0-alpha")); // trueBuild metadata has no ordering. The only meaningful operation is exact equality:
semver::SimpleSpec("==1.0.0+build.42").match(semver::Version("1.0.0+build.42")); // true
semver::SimpleSpec("==1.0.0+build.42").match(semver::Version("1.0.0+build.99")); // false
semver::SimpleSpec("<=1.0.0").match(semver::Version("1.0.0+anything")); // true — build ignoredstd::vector<semver::Version> versions = {
semver::Version("0.9.0"),
semver::Version("1.0.0"),
semver::Version("1.3.0"),
semver::Version("2.0.0"),
};
auto spec = semver::SimpleSpec(">=1.0.0,<2.0.0");
auto filtered = spec.filter(versions);
// filtered: [1.0.0, 1.3.0]
auto best = spec.select(versions);
// best: 1.3.0NpmSpec implements the full node-semver range specification.
Clauses separated by spaces are ANDed together:
auto spec = semver::NpmSpec(">=1.2.7 <1.3.0");
spec.match(semver::Version("1.2.7")); // true
spec.match(semver::Version("1.2.99")); // true
spec.match(semver::Version("1.3.0")); // falseauto spec = semver::NpmSpec("1.2.7 || >=1.2.9 <2.0.0");
spec.match(semver::Version("1.2.7")); // true
spec.match(semver::Version("1.2.8")); // false — not in either range
spec.match(semver::Version("1.4.6")); // trueWildcards (*, x, X) or missing components mean "any value":
semver::NpmSpec("*").match(semver::Version("99.99.99")); // true
semver::NpmSpec("1.x").match(semver::Version("1.0.0")); // true
semver::NpmSpec("1.x").match(semver::Version("1.99.0")); // true
semver::NpmSpec("1.x").match(semver::Version("2.0.0")); // false
semver::NpmSpec("1.2.x").match(semver::Version("1.2.0")); // true
semver::NpmSpec("1.2.x").match(semver::Version("1.3.0")); // falseA - B is equivalent to >=A <=B, with partial versions expanded:
auto spec = semver::NpmSpec("1.2.3 - 2.3.4");
spec.match(semver::Version("1.2.3")); // true
spec.match(semver::Version("2.3.4")); // true
spec.match(semver::Version("2.3.5")); // false
// Partial upper bound: "1.2.3 - 2.3" means ">=1.2.3 <2.4.0"
semver::NpmSpec("1.2.3 - 2.3").match(semver::Version("2.3.99")); // true
semver::NpmSpec("1.2.3 - 2.3").match(semver::Version("2.4.0")); // false~X.Y.Z allows patch-level changes. If minor is missing, minor-level changes are allowed:
semver::NpmSpec("~1.2.3").match(semver::Version("1.2.5")); // true
semver::NpmSpec("~1.2.3").match(semver::Version("1.3.0")); // false
semver::NpmSpec("~1.2").match(semver::Version("1.2.0")); // true
semver::NpmSpec("~1.2").match(semver::Version("1.3.0")); // false
semver::NpmSpec("~1").match(semver::Version("1.9.9")); // true
semver::NpmSpec("~1").match(semver::Version("2.0.0")); // false^X.Y.Z allows changes that do not modify the left-most non-zero digit:
semver::NpmSpec("^1.2.3").match(semver::Version("1.9.0")); // true
semver::NpmSpec("^1.2.3").match(semver::Version("2.0.0")); // false
semver::NpmSpec("^0.2.3").match(semver::Version("0.2.9")); // true
semver::NpmSpec("^0.2.3").match(semver::Version("0.3.0")); // false
semver::NpmSpec("^0.0.3").match(semver::Version("0.0.3")); // true
semver::NpmSpec("^0.0.3").match(semver::Version("0.0.4")); // falseIn NPM semantics, prereleases only satisfy a range if the comparator's version has a prerelease on the same major.minor.patch tuple:
auto spec = semver::NpmSpec(">1.2.3-alpha.3");
spec.match(semver::Version("1.2.3-alpha.7")); // true — same patch, higher prerelease
spec.match(semver::Version("3.4.5")); // true — release is above the range
spec.match(semver::Version("3.4.5-alpha.9")); // false — different patch, prerelease blockedauto spec = semver::NpmSpec("^1.2.0 || >=3.0.0-beta <3.0.1");
spec.match(semver::Version("1.5.0")); // true — matched by ^1.2.0
spec.match(semver::Version("2.0.0")); // false — outside both ranges
spec.match(semver::Version("3.0.0-beta.2")); // true — matched by the second range
spec.match(semver::Version("3.0.0")); // true
spec.match(semver::Version("3.0.1")); // falsemin_version() returns the lowest Version that can possibly satisfy the spec — useful for lock file resolution. Available on both SimpleSpec and NpmSpec (inherited from BaseSpec):
semver::NpmSpec("^1.2.3").min_version(); // Version("1.2.3")
semver::NpmSpec(">=1.0.0 <2.0.0").min_version(); // Version("1.0.0")
semver::NpmSpec(">1.0.0").min_version(); // Version("1.0.1")
semver::NpmSpec(">1.0.0-beta").min_version(); // Version("1.0.0-beta.0")
semver::NpmSpec("*").min_version(); // Version("0.0.0")
semver::NpmSpec(">4 <3").min_version(); // std::nullopt (impossible)
semver::SimpleSpec(">=1.0.0,<2.0.0").min_version(); // Version("1.0.0")
semver::SimpleSpec("^1.2.3").min_version(); // Version("1.2.3")subset() checks whether every version matched by the argument is also matched by *this — useful for dependency auditing. Available on both SimpleSpec and NpmSpec, but cross-spec comparison is not supported because the two use different prerelease matching policies, see differences.md for more info:
auto wide = semver::NpmSpec(">=1.0.0");
auto narrow = semver::NpmSpec("^1.2.3");
wide.subset(narrow); // true — everything ^1.2.3 matches is within >=1.0.0
narrow.subset(wide); // false — >=1.0.0 includes 2.0.0 which ^1.2.3 rejects
semver::NpmSpec("*").subset(semver::NpmSpec("^1.0.0")); // true
semver::NpmSpec("^2 || ^3 || ^4").subset(semver::NpmSpec("^3")); // true
semver::NpmSpec("^2 || ^3 || ^4").subset(semver::NpmSpec("^1")); // false
// Works with SimpleSpec too:
semver::SimpleSpec(">=1.0.0").subset(semver::SimpleSpec("^1.2.3")); // true
// Cross-spec does NOT compile — different prerelease semantics:
// semver::SimpleSpec(">=1.0.0").subset(semver::NpmSpec("^1.2.3")); // compile error
// semver::NpmSpec("^1.2.3").subset(semver::SimpleSpec(">=1.0.0")); // compile errorsemver::compare("1.2.0", "1.3.0"); // std::weak_ordering::less
semver::compare("2.0.0", "1.0.0"); // std::weak_ordering::greater
semver::compare("1.0.0", "1.0.0"); // std::weak_ordering::equivalent
semver::match(">=1.0.0,<2.0.0", "1.5.0"); // true (uses SimpleSpec)
semver::npm_match("^1.0.0", "1.5.0"); // true (uses NpmSpec)
semver::validate("1.2.3"); // true
semver::validate("nope"); // false
semver::Version v;
if(!semver::attempt_parse("1.2.3", v))
throw std::runtime_error("bad version"); // does not throw because parsing succeeded
std::cout << v; // 1.2.3
std::string reason;
if(!semver::attempt_parse("bad", v, reason))
throw std::runtime_error(reason); // If thrown, `reason` contains exception's `what()` the ctor threw
std::cout << v; // 1.2.3BSD-2-Clause. See LICENSE for details.