Developer Toolbox

Regex: Semantic version

Semantic versioning writes a version as MAJOR.MINOR.PATCH, optionally followed by a pre-release after - and build metadata after +. This is the pattern published on semver.org: numbers have no leading zeros, and neither do numeric parts of a pre-release.

Open in Regex Tester The pattern and every example below are filled in.

Pattern

^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$

How it works

^
Start of the string.
(0|[1-9]\d*)
MAJOR: 0, or a number without a leading zero.
\.
A dot.
(0|[1-9]\d*)
MINOR, the same way.
\.
A dot.
(0|[1-9]\d*)
PATCH, the same way.
(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?
Optionally a pre-release after -: dot-separated parts, each a number without a leading zero or a run of letters, digits and dashes (beta.1, rc-2).
(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
Optionally build metadata after +: dot-separated runs of letters, digits and dashes.
$
End of the string.

Matches

  • 1.0.0
  • 2.10.3-beta.1
  • 1.0.0-alpha+001
  • 0.1.0+build.5

Doesn't match

  • 1.0
  • v1.0.0
  • 01.0.0
  • 1.0.0-01
  • 1.2.3.4

In your language

JavaScript
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/

A literal; new RegExp(source, flags) builds the same from a string.

Python
re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", re.ASCII)

A raw string, so backslashes reach re as written. re.ASCII keeps \d to 0-9, as in JavaScript (Python matches any Unicode digit otherwise). Use re.fullmatch to test a whole string.

Java
Pattern.compile("^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$")

A normal string literal, so every backslash is doubled. matcher(s).matches() tests the whole string.

Go
regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)

A raw string in backticks. RE2 runs in linear time but has no lookaround and no backreferences.

PHP
preg_match('/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/D', $input)

PCRE with / delimiters inside a single-quoted string. Without the D modifier, $ also matches before a final newline, so it is added to patterns that end in $.

C#
new Regex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", RegexOptions.ECMAScript)

A verbatim string: backslashes stay, a quote is doubled. RegexOptions.ECMAScript keeps \d to 0-9, as in JavaScript. $ also matches before a final newline; to reject one, end the pattern with \z instead.

Common mistakes

  • A leading v is not part of the version

    Git tags are often v1.2.3, but the version is 1.2.3, as the SemVer FAQ says. Strip the v before matching.

  • Comparing needs a library

    Text order puts 1.10.0 before 1.9.0, and 1.0.0-alpha is lower than 1.0.0 though it sorts after it as text. Use a semver library: semver on npm and on PyPI, golang.org/x/mod/semver in Go.

  • Ranges are another syntax

    ^1.2.0 and ~1.2.0 in package.json are ranges, not versions, and fail this pattern by design.