Comparing Program Versions in POSIX Shell

23 April 2014 in posix

Comparing version strings in POSIX shell - not as easy as you might think.

Motivation

The new version of xeno is written as a portable POSIX shell script, and amongst other things, it has some simplifications that require Git 1.7.6+. In order to ensure things work, a simple version check is required.

Initial complications

Of course, nothing is simple…

To start with, we’re stuck with the fact that Git versions are not printed out in simple x.y.z format:

$ git --version
git version 1.9.2

and of course, someone may have arrogantly tagged their customized version:

$ git --version
git version 1.8.5.2 (Apple Git-48)

Fortunately, the surrounding junk in both cases is pretty easily dispatched with a call to cut:

$ git --version | cut -d ' ' -f3
1.9.2

The non-existent semantics of strings

But now comes the tricky part - comparison. If you can ensure that you version components will always be 1-digit, i.e. 1.9.2 and not 1.9.12, then you can use a simple lexical comparison:

$ test '1.9.2' \< '1.9.3'
$ echo $?
0 # Comparison was true

When you get into the 2-digit range, this become problematic:

$ test '1.9.2' \< '1.9.12'
$ echo $?
1 # Comparison was false

Making the shell work for you

There are a few solutions available online, but generally they are Bash-specific, and as it turns out, a bit over-complicated for a simple version requirement comparison. Finally, I found this entry, which seemed to provide an elegant solution to version sorting. However, it required the -g flag, which is a GNU-extension to sort. Fortunately, the solution was not horrible, and is quite extensible:

# Check that the version of Git installed is supported
GIT_VERSION=$(git --version | cut -d ' ' -f3)
MIN_GIT_VERSION="1.7.6"
LOWEST_GIT_VERSION=$(printf "$GIT_VERSION\n$MIN_GIT_VERSION" \
                     | sort -t "." -n -k1,1 -k2,2 -k3,3 -k4,4 \
                     | head -n 1)
if [ "$LOWEST_GIT_VERSION" != "$MIN_GIT_VERSION" ]; then
  echo "error: Git version ($GIT_VERSION) too old, need $MIN_GIT_VERSION+"
  exit 1
fi

Basically, we create a list of the minimum required version and the installed version, then sort them and take the lesser of the two, requiring that it is the minimum required version. The important code is:

sort -t "." -n -k1,1 -k2,2 -k3,3 -k4,4

This sorts versions by splitting them on . characters, and treating the fields numerically. The -k flag also allows one to specify comparison type on a per-field basis, allowing one to customize this strategy based on the format of the individual version fields, making it extensible beyond simple semantic versioning.