diff --git a/README.md b/README.md index 76c1e3a..dc2bf75 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Semantic Versioning utility. ## Overview -The `semver` command line utility generates, modifies, parses, sorts, and validates [Semantic Version](https://semver.org/) strings. +The `semver` command line utility finds, generates, modifies, parses, sorts, and validates [Semantic Version](https://semver.org/) strings. The Semantic Versioning format is: @@ -37,6 +37,7 @@ Coming soon. ## Usage ```bash +semver find [expression] semver grep [-coq] - semver printf semver sort [-r] - @@ -51,7 +52,7 @@ man semver ## Examples -Find the latest Git tag: +See the latest Git tag: ```bash git tag | semver grep -o | semver sort -r | head -n 1 @@ -85,3 +86,9 @@ semver printf '%major %minor %patch' '1.2.3-alpha+1' | awk '{ print ++$1 "." 0 " semver printf '%major %minor %patch' '1.2.3-alpha+1' | awk '{ print $1 "." ++$2 "." 0 }' # => 1.3.0 semver printf '%major %minor %patch' '1.2.3-alpha+1' | awk '{ print $1 "." $2 "." ++$3 }' # => 1.2.4 ``` + +Find filenames containing Semantic Versions inside a directory: + +```bash +semver find . -type f +``` \ No newline at end of file diff --git a/semver b/semver index b5b4169..5859aed 100755 --- a/semver +++ b/semver @@ -5,11 +5,18 @@ use warnings; use feature qw(say switch); use Scalar::Util qw(looks_like_number); use List::Util qw(max); +use File::Find; use Getopt::Long qw(GetOptionsFromArray); no if $] >= 5.018, warnings => qw(experimental::smartmatch); +# for the convenience of &wanted calls, including -eval statements: +use vars qw/*name *dir *prune/; +*name = *File::Find::name; +*dir = *File::Find::dir; +*prune = *File::Find::prune; + # Regex from semver.org: https://github.com/semver/semver/pull/460 -my $semver_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-]+)*))?$'; +my $semver_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-]+)*))?'; my $semver_precedence_regex_head = '^([0-9a-zA-Z-]*)'; my $semver_precedence_regex_tail = '\.(0|[1-9a-zA-Z-][0-9a-zA-Z-]*)'; @@ -23,6 +30,7 @@ sub usage() { say STDERR "Semantic Versioning utility."; say STDERR ""; say STDERR "Usage:"; + say STDERR " $program find [expression]"; say STDERR " $program grep [-coq] -"; say STDERR " $program printf "; say STDERR " $program sort [-r] -"; @@ -37,7 +45,7 @@ sub usage() { sub semver_get { my ($str) = @_; - my @matches = $str =~ $semver_regex; + my @matches = $str =~ /^$semver_regex$/; if ((defined $matches[0]) && (defined $matches[1]) && (defined $matches[2])) { return @matches; @@ -183,12 +191,44 @@ sub semver::printf { printf $format; } +sub semver::find { + my ($dir, $depth, $type) = @_; + + *wanted = sub { + my ($dev, $ino, $mode); + + my $is_match = /$semver_regex/s && (($dev, $ino, $mode) = lstat($_)); + + if ($type) { + if ($type eq "d") { + $is_match = $is_match && -d _; + } elsif ($type eq "f") { + $is_match = $is_match && -f _; + } elsif ($type eq "l") { + $is_match = $is_match && -l _; + } elsif ($type eq "p") { + $is_match = $is_match && -p _; + } + } + + if ($is_match) { + say "$name"; + } + }; + + if ($depth) { + finddepth({ wanted => \&wanted }, $dir); + } else { + find({ wanted => \&wanted }, $dir); + } +} + sub semver::grep_count { my $num_matches = 0; while (my $line = ) { my @words = split(/\s+/, $line); - if (grep(/$semver_regex/, @words)) { + if (grep(/^$semver_regex$/, @words)) { $num_matches++; } } @@ -203,7 +243,7 @@ sub semver::grep_count { sub semver::grep_quiet { while (my $line = ) { my @words = split(/\s+/, $line); - if (grep(/$semver_regex/, @words)) { + if (grep(/^$semver_regex$/, @words)) { exit 0; } } @@ -220,7 +260,7 @@ sub semver::grep { my @words = split(/\s+/, $line); if ($only_matching) { foreach (@words) { - if (/$semver_regex/) { + if (/^$semver_regex$/) { $num_matches++; print "$1.$2.$3"; print "-$4" if (length $4 // ''); @@ -229,7 +269,7 @@ sub semver::grep { } } } else { - if (grep(/$semver_regex/, @words)) { + if (grep(/^$semver_regex$/, @words)) { $num_matches++; print $line; } @@ -249,7 +289,7 @@ sub semver::sort(&@) { chomp(@lines); # Extra validation if just 1 element, because Perl does not sort unless there are at least 2 elements. - if ((scalar(@lines) eq 1) && ($lines[0] !~ /$semver_regex/)) { + if ((scalar(@lines) eq 1) && ($lines[0] !~ /^$semver_regex$/)) { exit 1; } @@ -278,6 +318,22 @@ sub main { my $subcommand = shift @args // ''; for ($subcommand) { + when (/^find$/) { + my $depth, my $type, my $help; + + GetOptionsFromArray( + \@args, + "depth" => \$depth, + "type=s" => \$type, + "h" => \$help, + ) or exit 1; + + usage() if $help or (scalar(@args) < 1); + + my $dir = $args[0]; + + semver::find($dir, $depth, $type); + } when (/^grep$/) { my $count, my $only_matching, my $quiet, my $help; diff --git a/semver.1 b/semver.1 index 143056c..27a7b78 100644 --- a/semver.1 +++ b/semver.1 @@ -6,6 +6,10 @@ .Nd Semantic Versioning utility .Sh SYNOPSIS .Nm +find +.Ar path +.Op Ar expression +.Nm grep .Op Fl coq .Fl @@ -22,7 +26,55 @@ sort .Sh DESCRIPTION The .Nm -utility generates, modifies, parses, sorts, and validates Semantic Version strings. +utility finds, generates, modifies, parses, sorts, and validates Semantic Version strings. +.Ss find +The +.Nm +utility can recursively descend the directory hierarchy specified by +.Ar path , +select any paths where (a) the basename (file name) contains one or more Semantic Version strings and (b) the file matches the optional boolean +.Op Ar expression , +and print them to the standard output, in the style of +.Xr find 1 +with +.Ic -name . +.Pp +The +.Op Ar expression +is made of one or more operands as described below. +.Pp +Operands: +.Bl -tag -width indent -offset indent +.It Ic -depth +Causes descent of the directory hierarchy to be done so that all entries in a directory are acted on BEFORE the directory itself. (If a +.Ic -depth +primary is not specified, all entries in a directory shall be acted on AFTER the directory itself.) +.It Ic -type Ar t +True if the file is of the specified type. Possible file types are as follows: +.Pp +.Bl -tag -width indent -compact +.It Cm d +directory +.It Cm f +regular file +.It Cm l +symbolic link +.It Cm p +FIFO pipe +.El +.El +.Pp +The primaries are combined with an implicit conjunction; the AND operator is implied by the juxtaposition of two primaries. Other operators from +.Xr find 1 +including negations are not supported. The primaries shall evaluate their respective arguments only once. +.Pp +Exit status: +.Bl -tag -width Ds -offset indent -compact +.It 0 +Success. +.It >0 +An error occurred, or an invalid option or operand was specified. +.El .Ss grep The .Nm @@ -62,8 +114,8 @@ Exit status: .Bl -tag -width Ds -offset indent -compact .It 0 One or more lines were selected (i.e. there was at least one valid Semantic Version). -.It 1 -No lines were selected (i.e. there were no valid Semantic Versions), or an invalid option was specified. +.It >0 +No lines were selected (i.e. there were no valid Semantic Versions), or an invalid option was specified, or an error occurred. .El .Ss printf The @@ -146,12 +198,12 @@ Exit status: .Bl -tag -width Ds -offset indent -compact .It 0 Success. -.It 1 +.It >0 The .Ar format string contained invalid specifiers, or .Ar version -was invalid. +was invalid, or an error occurred. .El .Ss sort The @@ -173,8 +225,8 @@ Exit status: .Bl -tag -width Ds -offset indent -compact .It 0 Success. -.It 1 -An invalid option was specified, or the input was invalid (i.e. it contained something besides Semantic Versions and line delimiter characters). +.It >0 +An invalid option was specified, or the input was invalid (i.e. it contained something besides Semantic Versions and line delimiter characters), or an error occurred. .El .Sh OPTIONS .Pp @@ -186,6 +238,16 @@ utility understands the following command-line options: Display the usage screen. .El .Sh EXAMPLES +.Ss Find +.Pp +Find only regular file names containing Semantic Version strings: +.Pp +.Bd -literal -offset indent -compact +$ semver find . -type f +foo-1.2.3 +bar-4.5.6 +7.8.9 +.Ed .Ss Grep Given a line-separated text stream: .Bd -literal -offset indent @@ -262,6 +324,7 @@ The Semantic Versioning standard does not define an ordering for two versions th .Nm utility applies an additional natural sort on top of the Semantic Version precedence sort. This additional sort is IMPLEMENTATION-SPECIFIC and SUBJECT TO CHANGE between releases, so its algorithm is deliberately left undocumented. You should not rely on it. .Sh SEE ALSO +.Xr find 1 , .Xr grep 1 , .Xr printf 1 , .Xr sort 1 diff --git a/test/find.bats b/test/find.bats new file mode 100644 index 0000000..ce1d39d --- /dev/null +++ b/test/find.bats @@ -0,0 +1,124 @@ +#!/usr/bin/env bats + +semver() { + ./semver "$@" +} + +@test "find: should work when there are no files" { + dir="$(mktemp -d)" + + [[ $(semver find "$dir") = "" ]] +} + +@test "find: should match all files with semvers in their names, like find(1) -name" { + dir="$(mktemp -d)" + touch "$dir/1.0.0" + touch "$dir/foo" + mkdir "$dir/2.0.0" + touch "$dir/2.0.0/3.0.0" + touch "$dir/2.0.0/bar" + + expected=$(cat <