1

I have a directory of mostly version numbers, and I'm trying to determine the one that is version-sorted just before another particular one.

The background here, for fear of making this an XY problem, is that these version numbers are derived from git tags for web content, and we symlink the public_html directory for the site's virtualhost to the versioned directory.

It looks something like this:

drwxr-xr-x   8 graham  www  17 Oct 17  2013 v2.0.10
drwxr-xr-x   8 graham  www  17 Oct 17  2013 v2.0.11
drwxrwxr-x   8 graham  www  17 Aug 29  2013 v2.0.8
drwxr-xr-x   8 brian   www  17 Oct 17  2013 v2.0.9
...
drwxr-xr-x   9 graham  www  21 Aug  5  2015 v4.19.0
drwxr-xr-x  10 graham  www  19 Dec 17  2014 v4.2.0
drwxr-xr-x   9 brian   www  21 Aug 10  2015 v4.20.0
drwxr-xr-x   9 brian   www  21 Aug 10  2015 v4.20.0-fail
drwxr-xr-x   9 graham  www  21 Aug 11  2015 v4.20.1
...
drwxr-xr-x   9 graham  www  19 Mar 14 11:00 v4.35.0
drwxr-xr-x   9 graham  www  19 Mar 14 13:28 v4.36.0
drwxr-xr-x   9 graham  www  19 Mar 16 10:58 v4.36.1
lrwxr-xr-x   1 graham  www  11 Mar 16 11:13 public_html -> v4.36.1

The symlink may not always be pointing at the most recent version (for example if we had to back out of a change). My goal is to find the properly-named directory (i.e. we'd skip *-fail and the like) that is version sorted before the one that public_html points to.

I'm in FreeBSD, so my stat command works nicely like this:

read target <<<"$(stat -f'%Y' "${base}/public_html")"

If you're in Linux, I gather you could get the same effect with something like this:

target="$(stat -c '%N' "${base}/public_html")"
target="${target#* -> ?}"; target="${target%?}"

But from here, I'm not sure how I gather the version number. Note that I'm in FreeBSD, so my sort command does not have a -V option. I've tried two methods.

The first is to process a while loop on the directory contents:

shopt -s extglob
while read this; do
  [[ "$this" = "$target" ]] && break
  previous="$this"  
done < $(ls -1d v+([0-9]).+([0-9]).+([0-9]) | sort -n -t . -k1,1 -k2,2 -k3,3)

The second is pretty similar, parsing an array instead:

shopt -s extglob
a=( $(ls -1d v+([0-9]).+([0-9]).+([0-9]) | sort -n -t . -k1,1 -k2,2 -k3,3) )
for (( n=0; n<${#a[@]}; n++ )); do
  [[ "${a[$n]}" = "$target" ]] && break
  previous="${a[$n]}"
done

Where both of these fall down is the sort, in which the first field does not appear to be sorted. I suspect it's because it's not numeric (with "v" at the start, delimited by "."). Is there a way I can specify multiple delimiters, or otherwise "ignore" the v somehow?

Or is there a better way to handle this than using the external sort command, which has non-portable options? Something internal to bash perhaps?

Thanks.

Graham
  • 1,631
  • 14
  • 23

3 Answers3

0

Perhaps not a complete solution, but you might be able to get away with the following:

ls -1d v+([0-9]).+([0-9]).+([0-9]) | sort -t . -k1,1 -k2,2n -k3,3n

The difference here is that the -n for numeric fields is applied only to the second and third field, so you'll continue sorting alphabetically on the first field.

This doesn't help you in the jump from v9.x.x to v10.x.x, but as you've gone from v2 to v4 in a little under three years, perhaps it's "good enough".

ALSO:

  • You might run this in two stages. First stage determines the most recent major version number with something like major=$(ls -1d v[0-9]* | tail -1); major=${major%%.*}", and then the subsequent sort is based on a=$( ls ${major}.+([0-9]).+([0-9]) ). Then you can safely sort on just the numeric fields.
  • There's a bash implementation of qsort which you might be able to adapt in order to make something that's entirely internal to bash. It'll require some hacking to make it work with your array.
Community
  • 1
  • 1
ghoti
  • 45,319
  • 8
  • 65
  • 104
0

You can separate alpha prefix, sort numerically by sub fields, and merge back

$ ls -1 v* | sed 's/^v/v./' | sort -t. -k2,2n -k3,3n -k4n | sed 's/^v\./v/'

v2.0.8
v2.0.9
v2.0.10
v2.0.11
v4.2.0
v4.19.0
v4.20.0
v4.20.0-fail
v4.20.1
v4.26.1
v4.35.0
v4.36.0
v10.0.1

note that I added v10.* to verify that it's future proof.

karakfa
  • 66,216
  • 7
  • 41
  • 56
0

This pure Bash (except for readlink) solution does a linear search instead of sorting:

VERSION_RX='^v([0-9][0-9]*)\.([0-9][0-9]*)\.([0-9][0-9]*)$'

target=$(readlink "$base/public_html")

# target == v1.22.333 -> target_key == 000000001_000000022_000000333
[[ $target =~ $VERSION_RX ]] || exit 1
printf -v target_key '%09d_%09d_%09d' \
    "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"

pred_dir=
pred_key=

for path in "$base"/v*.*.* ; do
    dir=${path##*/}
    [[ $dir == "$target" ]] && continue
    [[ $dir =~ $VERSION_RX ]] || continue
    printf -v key '%09d_%09d_%09d' \
        "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"

    [[ $key > $target_key ]] && continue

    if [[ -z $pred_key || $key > $pred_key ]] ; then
        pred_dir=$dir
        pred_key=$key
    fi
done

echo "$pred_dir"
pjh
  • 6,388
  • 2
  • 16
  • 17