///|
/// The moonsver command line tool.
///
/// Parse, compare and sort versions, apply bumps, expand and test ranges.
/// Built for moonctl and the website's version automation to call directly.
///|
let usage_text : String =
#|moonsver — a SemVer 2.0.0 toolkit
#|
#|Usage:
#| moonsver parse ... parse and print the canonical form
#| moonsver compare compare precedence, printing -1 / 0 / 1
#| moonsver sort ... sort ascending
#| moonsver bump apply a bump
#| moonsver range print the expanded range
#| moonsver satisfies does the version satisfy the range?
#| moonsver max ... highest satisfying version
#| moonsver min ... lowest satisfying version
#| moonsver syntax list the grammar presets
#|
#|Bump kinds (release):
#| major minor patch premajor preminor prepatch prerelease stable
#|
#|Options:
#| --syntax grammar to use (npm by default)
#| --json print JSON
#| -h, --help show this help
#|
#|Exit codes:
#| 0 the command ran (satisfies false, and max/min with no match, are still
#| 0 — read stdout for the answer)
#| other bad arguments or a parse failure, with details on stderr
///|
fn[T] die(message : String) -> T {
eprintln(message)
abort("")
}
///|
fn syntax_of(name : String) -> @moonsver.Syntax {
match @moonsver.Syntax::from_name(name) {
Some(s) => s
None => die("unknown grammar: \{name} (try npm, cargo, ruby or strict)")
}
}
///|
struct Options {
syntax : @moonsver.Syntax
json : Bool
positional : Array[String]
}
///|
fn parse_args(argv : Array[String]) -> Options {
let mut syntax = @moonsver.Syntax::npm()
let mut json = false
let positional : Array[String] = []
let mut i = 1
while i < argv.length() {
let a = argv[i]
if a == "--json" {
json = true
} else if a == "--syntax" {
i = i + 1
if i >= argv.length() {
die("--syntax needs a value")
}
syntax = syntax_of(argv[i])
} else if a.has_prefix("--syntax=") {
syntax = syntax_of(a[9:].to_owned())
} else if a == "-h" || a == "--help" || a == "help" {
positional.push("help")
} else {
positional.push(a)
}
i = i + 1
}
{ syntax, json, positional, }
}
///|
fn version_json(input : String, v : @moonsver.Version) -> Json {
let obj : Map[String, Json] = Map([])
obj["input"] = Json::string(input)
obj["normalised"] = Json::string(v.to_string())
obj["major"] = Json::string(v.major().to_string())
obj["minor"] = Json::string(v.minor().to_string())
obj["patch"] = Json::string(v.patch().to_string())
let pre : Array[Json] = []
for id in v.prerelease() {
pre.push(Json::string(id.to_string()))
}
obj["prerelease"] = Json::array(pre)
let build : Array[Json] = []
for id in v.build() {
build.push(Json::string(id))
}
obj["build"] = Json::array(build)
obj["is_prerelease"] = Json::boolean(v.is_prerelease())
Json::object(obj)
}
///|
fn parse_one(input : String, syntax : @moonsver.Syntax) -> @moonsver.Version {
@moonsver.Version::parse(input, syntax~) catch {
e => die("could not parse \{input}: \{e}")
}
}
///|
fn range_of(input : String, syntax : @moonsver.Syntax) -> @moonsver.Range {
@moonsver.Range::parse(input, syntax~) catch {
e => die("could not parse the range \{input}: \{e}")
}
}
///|
fn cmd_parse(args : ArrayView[String], opts : Options) -> Unit {
if args.is_empty() {
die("parse needs at least one version")
}
let results : Array[Json] = []
for input in args {
let v = parse_one(input, opts.syntax)
if opts.json {
results.push(version_json(input, v))
} else {
println(v.to_string())
}
}
if opts.json {
println(Json::stringify(Json::array(results), indent=2))
}
}
///|
fn cmd_compare(args : ArrayView[String], _opts : Options) -> Unit {
if args.length() != 2 {
die("compare needs two versions")
}
let a = parse_one(args[0], @moonsver.Syntax::npm())
let b = parse_one(args[1], @moonsver.Syntax::npm())
let d = a.precedence_cmp(b)
println(d.to_string())
if d == 0 && a != b {
eprintln("same precedence, but the versions differ (build metadata)")
}
}
///|
fn cmd_sort(args : ArrayView[String], opts : Options) -> Unit {
if args.is_empty() {
die("sort needs at least one version")
}
let versions : Array[@moonsver.Version] = []
for input in args {
versions.push(parse_one(input, opts.syntax))
}
versions.sort()
for v in versions {
println(v.to_string())
}
}
///|
fn cmd_bump(args : ArrayView[String], opts : Options) -> Unit {
if args.length() != 2 {
die("bump needs a bump kind and a version")
}
let release = match @moonsver.Release::parse(args[0]) {
Some(r) => r
None => die("unknown bump kind: \{args[0]}")
}
let v = parse_one(args[1], opts.syntax)
println(v.inc(release).to_string())
}
///|
fn cmd_range(args : ArrayView[String], opts : Options) -> Unit {
if args.length() != 1 {
die("range needs one range")
}
let r = range_of(args[0], opts.syntax)
if opts.json {
println(Json::stringify(Json::string(r.to_string())))
} else {
println(r.to_string())
}
}
///|
fn cmd_satisfies(args : ArrayView[String], opts : Options) -> Unit {
if args.length() != 2 {
die("satisfies needs a version and a range")
}
let v = parse_one(args[0], opts.syntax)
let r = range_of(args[1], opts.syntax)
let ok = r.satisfies(v)
if opts.json {
println(Json::stringify(Json::boolean(ok)))
} else {
println(ok.to_string())
}
}
///|
fn cmd_extreme(
args : ArrayView[String],
opts : Options,
want_max : Bool,
) -> Unit {
if args.length() < 2 {
die("needs a range and one or more versions")
}
let r = range_of(args[0], opts.syntax)
let versions : Array[@moonsver.Version] = []
for i in 1.. println(v.to_string())
None => ()
}
}
///|
fn cmd_syntax() -> Unit {
for name in ["npm", "cargo", "ruby", "strict"] {
let s = syntax_of(name)
println("\{name}: \{s}")
}
}
///|
fn main {
let opts = parse_args(@env.args())
if opts.positional.is_empty() {
println(usage_text)
return
}
let cmd = opts.positional[0]
let rest = opts.positional[1:]
match cmd {
"help" => println(usage_text)
"parse" => cmd_parse(rest, opts)
"compare" => cmd_compare(rest, opts)
"sort" => cmd_sort(rest, opts)
"bump" => cmd_bump(rest, opts)
"range" => cmd_range(rest, opts)
"satisfies" => cmd_satisfies(rest, opts)
"max" => cmd_extreme(rest, opts, true)
"min" => cmd_extreme(rest, opts, false)
"syntax" => cmd_syntax()
_ => {
eprintln("unknown command: \{cmd}")
println(usage_text)
abort("")
}
}
}