/commands/semver/sort-version-numbers
Sort version numbers correctly: sort -V vs semver
- tool
- SemVer
- versions
- GNU coreutils 8+, semver (npm) 7
- os
- linux, macos, windows
Plain sort orders version numbers as text, so 1.10.0 comes before 1.9.3. Use sort -V (GNU coreutils version sort) for numeric order. sort -V does not know Semantic Versioning pre-release rules and puts 1.2.0 before 1.2.0-alpha; when the list contains pre-releases, sort with a SemVer tool such as npx semver, which follows the SemVer precedence rules.
Compare the three orders
printf '1.10.0\n1.2.0\n1.9.3\n1.2.0-beta.2\n1.2.0-beta.10\n1.2.0-alpha\n' > versions.txt
echo "sort: $(sort versions.txt | tr '\n' ' ')"
echo "sort -V: $(sort -V versions.txt | tr '\n' ' ')"
echo "semver: $(npx -y semver@7 $(cat versions.txt) | tr '\n' ' ')"Output
sort: 1.10.0 1.2.0 1.2.0-alpha 1.2.0-beta.10 1.2.0-beta.2 1.9.3 sort -V: 1.2.0 1.2.0-alpha 1.2.0-beta.2 1.2.0-beta.10 1.9.3 1.10.0 semver: 1.2.0-alpha 1.2.0-beta.2 1.2.0-beta.10 1.2.0 1.9.3 1.10.0
sortcompares character by character:1.1sorts before1.2and1.9, so1.10.0is first.sort -Vcompares the numbers as numbers andbeta.10afterbeta.2, but treats the shorter1.2.0as smaller than1.2.0-alpha.semverputs every pre-release before its release (1.2.0-alpha < 1.2.0-beta.2 < 1.2.0-beta.10 < 1.2.0), as the specification says.
Latest version, filtered by range
npx -y semver@7 -r ">=1.2.0 <2.0.0" 1.10.0 1.2.0 2.0.0-rc.1 0.9.0 | tail -1
printf 'v1.2.0\nv1.10.0\nv1.9.3\n' | sort -V | tail -1Output
1.10.0 v1.10.0
semver -r prints only versions that satisfy the range, sorted ascending; tail -1 keeps the highest. Pre-releases are excluded unless you add -p (--include-prerelease).
Tools at a glance
| Tool | Numeric order | Pre-releases | Notes |
|---|---|---|---|
| sort | no | no | text order; only correct when all parts have the same width |
| sort -V | yes | no (1.2.0 before 1.2.0-alpha) | GNU coreutils; handles prefixes like app-1.10.0.tar.gz |
| sort -t. -k1,1n -k2,2n -k3,3n | yes | no | POSIX; fails on prefixes and suffixes |
| npx semver | yes | yes (SemVer rules) | drops strings that are not valid SemVer; -c coerces them |
| git tag --sort=version:refname | yes | configurable | versionsort.suffix sets where suffixes like -rc go |
File names, prefixes and invalid versions
printf 'app-1.10.0.tar.gz\napp-1.9.3.tar.gz\napp-1.2.0.tar.gz\napp-2.0.0.tar.gz\n' > files.txt
echo "newest first: $(sort -rV files.txt | tr '\n' ' ')"
echo "mixed v: $(printf 'v1.2.0\n1.2.0\nv1.10.0\n' | sort -V | tr '\n' ' ')"
echo "semver: $(npx -y semver@7 v1.2.0 1.10.0 1.2 | tr '\n' ' ')"
echo "semver -c: $(npx -y semver@7 -c 1.2 v1.10.0 release-2.0 | tr '\n' ' ')"Output
newest first: app-2.0.0.tar.gz app-1.10.0.tar.gz app-1.9.3.tar.gz app-1.2.0.tar.gz mixed v: 1.2.0 v1.2.0 v1.10.0 semver: 1.2.0 1.10.0 semver -c: 1.2.0 1.10.0 2.0.0
sort -rVreverses version order, so the newest archive comes first.- With a mixed
vprefix,sort -Vkeeps the plain1.2.0apart from thevversions; normalise the prefix first. semverstrips a leadingvbut silently drops1.2, which is not valid SemVer. With-cit coerces each string to the first version-like part it finds.
Git tags and other systems
git tag --sort=version:refname # Git's own version sort, ascending
git tag --sort=-version:refname | head -1 # newest tag
sort -t. -k1,1n -k2,2n -k3,3n file # POSIX sort without -V (no pre-releases)Where sort -V is not available, use GNU coreutils (gsort -V from Homebrew on macOS) or the POSIX key form above. In PowerShell, cast to [version]: '1.10.0','1.9.3' | Sort-Object { [version]$_ } (this type does not accept pre-release suffixes).
FAQ
How do I get the newest version first?
Add -r to version sort (sort -rV) and take the first line with head -1. With semver, keep the ascending output and use tail -1.
Why does npx semver print nothing for 1.2?
1.2 is not a valid SemVer version (it needs three numbers), so semver drops it and exits with 1 when nothing valid is left. Add -c to coerce such strings into 1.2.0 first.
Does sort -V handle a leading v?
Only if every line has it. Mixed lists put 1.2.0 before v1.2.0 because the comparison starts with the characters; strip the prefix first, for example with sed 's/^v//'.