├── .gitignore ├── .travis.yml ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE.txt ├── Makefile ├── README.md ├── vert.go └── vert_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8.x 5 | - 1.9.x 6 | - master 7 | 8 | script: 9 | - make setup 10 | - make bootstrap 11 | - make test 12 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/Masterminds/semver" 6 | packages = ["."] 7 | revision = "15d8430ab86497c5c0da827b748823945e1cf1e1" 8 | version = "v1.4.0" 9 | 10 | [[projects]] 11 | name = "github.com/urfave/cli" 12 | packages = ["."] 13 | revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" 14 | version = "v1.20.0" 15 | 16 | [solve-meta] 17 | analyzer-name = "dep" 18 | analyzer-version = 1 19 | inputs-digest = "c39ee2dc071b0651ae8526686c752e3701ef863549634dc761c2ff44e9b04fa9" 20 | solver-name = "gps-cdcl" 21 | solver-version = 1 22 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/Masterminds/semver" 26 | version = "^1.0.0" 27 | 28 | [[constraint]] 29 | name = "github.com/urfave/cli" 30 | version = "^1" 31 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Vert 2 | Copyright (C) 2015, Matt Butcher 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: bootstrap test install 2 | 3 | setup: 4 | go get -u github.com/golang/dep/cmd/dep 5 | 6 | bootstrap: 7 | dep ensure 8 | dep status 9 | 10 | build: 11 | go build -o vert vert.go 12 | 13 | test: 14 | go test . 15 | 16 | install: build 17 | install -d ${DESTDIR}/usr/local/bin/ 18 | install -m 755 ./vert ${DESTDIR}/usr/local/bin/vert 19 | 20 | .PHONY: bootstrap test build install all setup 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vert: A command-line version comparison tool 2 | [![Stability: Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html) 3 | [![Build Status](https://travis-ci.org/Masterminds/vert.svg?branch=master)](https://travis-ci.org/Masterminds/vert) 4 | 5 | 6 | vert (Version Tester) is a simple command line tool for comparing two or 7 | more versions, or testing versions against fuzzy version rules. 8 | 9 | ## Basic Usage 10 | 11 | Vert takes at least two arguments: A base version, and one or more 12 | versions to compare to the base. The base version is special, in that it 13 | can be a fuzzy version specification rather than an exact version. 14 | 15 | ``` 16 | $ vert 1.0.0 1.0.0 17 | 1.0.0 18 | $ echo $? 19 | 0 20 | ``` 21 | 22 | When `vert` runs, it will print a normalized string of any version that 23 | matches, and then will set the exit code to the number of version match 24 | failures that it saw. 25 | 26 | `vert` understands SemVer v2 versions, so the following will also pass: 27 | 28 | ``` 29 | $ vert v1.0.0 1 30 | 1.0.0 31 | $ echo $? 32 | 0 33 | ``` 34 | 35 | There are three things to note: 36 | 37 | - A leading `v` is ignored. 38 | - Numbers are expanded to a full SemVer string, thus `1` is expanded to 39 | `1.0.0` and `1.1` is expaneded to `1.1.0` 40 | - The output is normalized to the form `X.Y.Z[-PRERELEASE][+BUILD]` 41 | 42 | A failed comparison looks like this: 43 | 44 | ``` 45 | $ vert 1.0.0 1.2.0 46 | $ echo $? 47 | 1 48 | ``` 49 | 50 | Failed version comparisons to not print any text unless the given base 51 | version is malformed: 52 | 53 | ``` 54 | $ vert 1.zoo.cheese 1.1.1 55 | Could not parse constraint 1.zoo.cheese 56 | ``` 57 | 58 | Base versions can be fuzzy: 59 | 60 | - `vert ">1.0" 1.1` 61 | - `vert "^2" 2.1.3` 62 | - `vert ">1.1.2,<1.3.4" 1.2` 63 | 64 | And `vert` understands alpha/beta markers: 65 | 66 | ``` 67 | vert ">1.0.0-alpha.1" 1.0.0-beta.1 68 | 1.0.0-beta.1 69 | ``` 70 | 71 | Multiple versions can be compared at once, and using the `-s` flag, you 72 | can even sort the output: 73 | 74 | ``` 75 | $ vert ^1 1.1.1 1.0.1 1.2.3 1.0.2 0.9 2.0 76 | 1.1.1 77 | 1.0.1 78 | 1.2.3 79 | 1.0.2 80 | $ echo $? 81 | 2 82 | ``` 83 | 84 | In the above, we asked vert for all of the version in the `1.X.Y` range 85 | (`^1`), and then gave it a list of versions, including some outside of 86 | that range. 87 | 88 | `vert` returned a list of versions that match. Via the return code, we 89 | can see that two failed to match. To see which failed, we can use the 90 | `-f` flag: 91 | 92 | ``` 93 | $ vert -f ^1 1.1.1 1.0.1 1.2.3 1.0.2 0.9 2.0 94 | 0.9.0 95 | 2.0.0 96 | ``` 97 | 98 | We can sort output using the `-s` flag: 99 | 100 | ``` 101 | vert -s ^1 1.1.1 1.0.1 1.2.3 1.0.2 0.9 2.0 102 | 1.0.1 103 | 1.0.2 104 | 1.1.1 105 | 1.2.3 106 | ``` 107 | 108 | Finally, `vert` can transform `git describe` versions into SemVer, 109 | assuming the Git tags are SemVer: 110 | 111 | ``` 112 | $ vert -g ^1 $(git describe --tags) 113 | 1.0.1+32.fef45 114 | ``` 115 | 116 | In the future, we'd like to add more transformations. If you have any 117 | ideas, please let us know in the issue queue. 118 | 119 | ## Installation 120 | 121 | Assuming you have make, [Go](http://golang.org) version 1.5.1 or later and 122 | [dep](https://github.com/golang/dep), you can simply run `make`: 123 | 124 | ``` 125 | $ make all 126 | dep ensure 127 | dep status 128 | PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED 129 | github.com/Masterminds/semver ^1.0.0 v1.4.0 15d8430 15d8430 1 130 | github.com/urfave/cli ^1.0.0 v1.20.0 cfb3883 cfb3883 1 131 | go test . 132 | ok github.com/Masterminds/vert 0.006s 133 | go build -o vert vert.go 134 | install -d /usr/local/bin/ 135 | install -m 755 ./vert /usr/local/bin/vert 136 | ``` 137 | 138 | This will install into `/usr/local/bin/vert`. 139 | -------------------------------------------------------------------------------- /vert.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/Masterminds/semver" 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | const name = "vert" 15 | const version = "0.1.0" 16 | const description = `Version Tester. Compare versions. 17 | 18 | Vert is a tool for comparing two version strings, or comparing a version string 19 | to a version range. 20 | 21 | $ vert ">1.0.0" 1.1.0 22 | 1.1.0 23 | 24 | $ vert "<1.0.0" 1.1.0 25 | # No output because nothing matched. 26 | 27 | $ vert -f "<1.0.0" 1.1.0 28 | 1.1.0 # -f returns all failures, rather than matches. 29 | 30 | $ vert ">1.0.0" 1.1.0 1.1.1 1.2.3 0.1.1 31 | 1.1.0 32 | 1.1.1 33 | 1.2.3 34 | 35 | Vert can also convert a Git version to a SemVer 2 version. 36 | 37 | $ vert -g ">1" v1.10.0-123-g0239788 1 ↵ 38 | 1.10.0+123.g0239788 39 | 40 | Note that it assigns the git commit count and hash to the build metadata, not 41 | to a pre-release tag. 42 | 43 | See below for information about how to determine the number of failed tests. 44 | 45 | EXIT CODES: 46 | 47 | vert returns exit codes based on the number of failed matches. There are a few 48 | reserved exit codes: 49 | 50 | - 128: The command was not called correctly. 51 | - 256: A version failed to parse, and comparisons could not continue. This will 52 | occur if the original constraint version cannot be parsed. If any 53 | subsequent version fails to parse, it will simply be counted as a failure. 54 | 55 | Any other error codes indicate the number of failed tests. For example: 56 | 57 | $ vert 1.2.3 1.2.3 1.2.4 1.2.5 58 | 1.2.3 59 | $ echo $? 60 | 2 # <-- Two tests failed. 61 | 62 | BASE VERSIONS: 63 | 64 | The base version may be in any of the following formats: 65 | 66 | - An exact semantic version number 67 | - 1.2.3 68 | - v1.2.3 69 | - 1.2.3-alpha.1+10212015 70 | - A semantic version range 71 | - * 72 | - !=1.0.0 73 | - >=1.2.3 74 | - >1.2.3,<1.3.2 75 | - ~1.2.0 76 | - ^2.3 77 | 78 | VERSIONS: 79 | 80 | Other than the base version, all other supplied versions must follow the 81 | SemVer 2 spec. Examples: 82 | 83 | - 1.2.3 84 | - v1.2.3 85 | - 1.2.3-alpha.1+10212015 86 | - v1.2.3-alpha.1+10212015 87 | - 1 (equivalent to 1.0.0) 88 | ` 89 | 90 | func main() { 91 | app := cli.NewApp() 92 | app.Name = name 93 | app.Usage = description 94 | app.Action = func(c *cli.Context) { res := run(c); os.Exit(res) } 95 | app.Version = version 96 | app.ArgsUsage = "BASE VERSION [VERSION [VERSION [...]]" 97 | app.Flags = []cli.Flag{ 98 | cli.BoolFlag{ 99 | Name: "failed,f", 100 | Usage: "Show the versions that failed rather than the ones that passed.", 101 | }, 102 | cli.BoolFlag{ 103 | Name: "sort,s", 104 | Usage: "Sort the versions before printing. Without this, versions are returned in the order they were tested.", 105 | }, 106 | cli.BoolFlag{ 107 | Name: "git,g", 108 | Usage: "Assume that (non-base) versions are in Git `git describe --tags` version format, and convert to SemVer.", 109 | }, 110 | } 111 | app.Run(os.Args) 112 | } 113 | 114 | // context describes the relevant portion of a cli.Context. 115 | // 116 | // This abstraction makes mocking easy. 117 | type context interface { 118 | Bool(string) bool 119 | Args() cli.Args 120 | } 121 | 122 | // run handles all of the flags and then runs the main action. 123 | func run(c context) int { 124 | args := c.Args() 125 | if len(args) < 2 { 126 | perr("Not enough arguments") 127 | return 128 128 | } 129 | 130 | if c.Bool("git") { 131 | for i := 1; i < len(args); i++ { 132 | nv, err := git2semver(args[i]) 133 | if err != nil { 134 | perr("Not a recognize git version: %s", args[i]) 135 | continue 136 | } 137 | args[i] = nv.String() 138 | } 139 | } 140 | 141 | pass, fail, code := compare(args[0], args[1:]) 142 | 143 | out := pass 144 | if c.Bool("failed") { 145 | out = fail 146 | } 147 | 148 | if c.Bool("sort") { 149 | sort.Sort(semver.Collection(out)) 150 | } 151 | 152 | pvers(out) 153 | return code 154 | } 155 | 156 | // compare compiles a base version comparator, and then compares all cases to it. 157 | // 158 | // It retuns an array of versions that passed, and an array of versions that failed. 159 | func compare(base string, cases []string) ([]*semver.Version, []*semver.Version, int) { 160 | passed, failed := []*semver.Version{}, []*semver.Version{} 161 | 162 | constraint, err := semver.NewConstraint(base) 163 | if err != nil { 164 | perr("Could not parse constraint %s", base) 165 | return passed, failed, 128 166 | } 167 | 168 | for _, t := range cases { 169 | ver, err := semver.NewVersion(t) 170 | if err != nil { 171 | failed = append(failed, ver) 172 | perr("Failed to parse %s", t) 173 | continue 174 | } 175 | if constraint.Check(ver) { 176 | passed = append(passed, ver) 177 | continue 178 | } 179 | failed = append(failed, ver) 180 | } 181 | 182 | return passed, failed, len(failed) 183 | } 184 | 185 | var stdout io.Writer = os.Stdout 186 | var stderr io.Writer = os.Stderr 187 | 188 | // pvers prints a list of versions to standard out. 189 | func pvers(vers []*semver.Version) { 190 | for _, v := range vers { 191 | fmt.Fprintln(stdout, v.String()) 192 | } 193 | } 194 | 195 | // pout prints to stdout. 196 | func pout(msg string, args ...interface{}) { 197 | fmt.Fprintf(stdout, msg, args...) 198 | fmt.Fprintln(stdout) 199 | } 200 | 201 | // perr prints to stderr. 202 | func perr(msg string, args ...interface{}) { 203 | fmt.Fprintf(stderr, msg, args...) 204 | fmt.Fprintln(stderr) 205 | } 206 | 207 | // git2semver converts a Git version to a SemVer 2 version 208 | // 209 | // This assumes that the base tag is a semver tag. 210 | // 211 | // v1.2.3-3-afeee becomes 1.2.3+3.afeee 212 | func git2semver(ver string) (*semver.Version, error) { 213 | va := strings.Split(ver, "-") 214 | target := va[0] 215 | if len(va) > 1 { 216 | md := strings.Join(va[1:], ".") 217 | target += "+" + md 218 | } 219 | return semver.NewVersion(target) 220 | } 221 | -------------------------------------------------------------------------------- /vert_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/urfave/cli" 8 | ) 9 | 10 | type mockContext struct { 11 | bools map[string]bool 12 | args cli.Args 13 | } 14 | 15 | func (c *mockContext) Args() cli.Args { 16 | return c.args 17 | } 18 | func (c *mockContext) Bool(name string) bool { 19 | return c.bools[name] 20 | } 21 | 22 | func TestGit2semver(t *testing.T) { 23 | tests := map[string]string{ 24 | "v1.10.0-123-g0239788": "1.10.0+123.g0239788", 25 | "1.10.0-123-g0239788": "1.10.0+123.g0239788", 26 | } 27 | 28 | for in, expect := range tests { 29 | out, err := git2semver(in) 30 | if err != nil { 31 | t.Errorf("Failed to parse %s: %s", in, err) 32 | continue 33 | } 34 | if out.String() != expect { 35 | t.Errorf("Expected %q, got %q", expect, out.String()) 36 | } 37 | } 38 | 39 | // And test a failure 40 | in := "fatal: No names found, cannot describe anything." 41 | out, err := git2semver(in) 42 | if err == nil { 43 | t.Errorf("Expected version parse to fail for %q", in) 44 | } 45 | if out != nil { 46 | t.Errorf("Expected version to be nil for %q", in) 47 | } 48 | } 49 | 50 | func TestRun(t *testing.T) { 51 | var b bytes.Buffer 52 | 53 | c := &mockContext{ 54 | args: cli.Args{">=1.0.0", "1.0.0", "1.1.1", "1.2.3", "1.0.1", "0.9.0"}, 55 | bools: map[string]bool{ 56 | "failed": false, 57 | "sort": true, 58 | }, 59 | } 60 | 61 | // Set the package defaults 62 | stdout = &b 63 | stderr = &b 64 | 65 | tests := []struct { 66 | args cli.Args 67 | bools map[string]bool 68 | out string 69 | code int 70 | }{ 71 | // Base case. 72 | { 73 | args: cli.Args{"v1.0.0", "1.0.0"}, 74 | bools: map[string]bool{"failed": false, "sort": false}, 75 | code: 0, 76 | out: "1.0.0\n", 77 | }, 78 | // One failure, four passes, sorted. 79 | { 80 | args: cli.Args{">=1.0.0", "1.0.0", "1.1.1", "1.2.3", "1.0.1", "0.9.0"}, 81 | bools: map[string]bool{"failed": false, "sort": true}, 82 | code: 1, 83 | out: "1.0.0\n1.0.1\n1.1.1\n1.2.3\n", 84 | }, 85 | // One failure, four passes, unsorted. 86 | { 87 | args: cli.Args{">=1.0.0", "1.0.0", "1.1.1", "1.2.3", "1.0.1", "0.9.0"}, 88 | bools: map[string]bool{"failed": false, "sort": false}, 89 | code: 1, 90 | out: "1.0.0\n1.1.1\n1.2.3\n1.0.1\n", 91 | }, 92 | // One failure, print failures. 93 | { 94 | args: cli.Args{">=1.0.0", "1.0.0", "1.1.1", "1.2.3", "1.0.1", "0.9.0"}, 95 | bools: map[string]bool{"failed": true, "sort": true}, 96 | code: 1, 97 | out: "0.9.0\n", 98 | }, 99 | // Two failures, sorted. 100 | { 101 | args: cli.Args{">=1.0.0", "0.1", "v0.9.0"}, 102 | bools: map[string]bool{"failed": true, "sort": true}, 103 | code: 2, 104 | out: "0.1.0\n0.9.0\n", 105 | }, 106 | // Convert git tag 107 | { 108 | args: cli.Args{">1", "v1.10.0-123-g0239788"}, 109 | bools: map[string]bool{"git": true}, 110 | code: 0, 111 | out: "1.10.0+123.g0239788\n", 112 | }, 113 | } 114 | 115 | for _, tt := range tests { 116 | c.args = tt.args 117 | c.bools = tt.bools 118 | res := run(c) 119 | if res != tt.code { 120 | t.Errorf("Expected code %d, got %d", tt.code, res) 121 | } 122 | if b.String() != tt.out { 123 | t.Errorf("Expected:%s\nGot:%s", tt.out, b.String()) 124 | } 125 | b.Reset() 126 | } 127 | 128 | } 129 | --------------------------------------------------------------------------------