~/bend-docscommunity

src/ez/target.bend source

src/ez/target.bend on the hub · documented module

# ez/target: what `ez add`, `ez tool run`, `install` and `upgrade` were asked# for, decided from the word alone. `owner/repo` is GitHub. A URL is that URL.# Anything else is a path, and a path written `./…`, `../…`, `/…` or `~/…` is# a path even when it would otherwise read as a repository. For `ez add`, a# word bend reads as a hub package's `<name>@<version>` names that package,# where it would otherwise be a path (`aim`).import Baseimport ../pkg/pkg.bend as K# where a target names, or why it names nothingtype Place is Data:  Bad{why: String}  Local{path: String}  Remote{url: String, slug: String}# a char a GitHub owner or repository may hold besides a dot: a letter, a# digit, `-` or `_`def plain.at(+ch: Char) -> Bool:  Bool.or(Char.is_digit(ch), Bool.or(Char.is_alpha(ch),    Bool.or(Char.is_eq(ch, '-'), Char.is_eq(ch, '_'))))# what one char of a target is, as far as telling a shorthand from a path# goes. A colon is asked about first, so a char that is anything but `Other`# is known not to be one: that is what lets a proof say a shorthand never# contains `://`.type Mark is Data:  Slash{}  Dot{}  Tilde{}  Word{}  Other{}def kind.word(word: Bool) -> Mark:  match word:    case True{}:      Word{}    case False{}:      Other{}def kind.tilde(tilde: Bool, word: Bool) -> Mark:  match tilde:    case True{}:      Tilde{}    case False{}:      kind.word(word)def kind.dot(dot: Bool, tilde: Bool, word: Bool) -> Mark:  match dot:    case True{}:      Dot{}    case False{}:      kind.tilde(tilde, word)def kind.slash(slash: Bool, dot: Bool, tilde: Bool, word: Bool) -> Mark:  match slash:    case True{}:      Slash{}    case False{}:      kind.dot(dot, tilde, word)def kind.colon(colon: Bool, slash: Bool, dot: Bool, tilde: Bool, word: Bool) -> Mark:  match colon:    case True{}:      Other{}    case False{}:      kind.slash(slash, dot, tilde, word)# a char's kinddef kind(+ch: Char) -> Mark:  kind.colon(Char.is_eq(ch, ':'), Char.is_eq(ch, '/'), Char.is_eq(ch, '.'),    Char.is_eq(ch, '~'), plain.at(ch))# one char of a name, with the rest of the name still to be asked about.# The rest arrives as a thunk, so a char that ends the name ends the walk.def name.keep(mark: Mark, rest: Unit -> Bool) -> Bool:  match mark:    case Word{}:      rest(Unit{})    case Dot{}:      rest(Unit{})    case Slash{}:      False{}    case Tilde{}:      False{}    case Other{}:      False{}# every char a letter, a digit, `-`, `_` or `.`def name.all(text: String) -> Bool:  match text:    case SNil{}:      True{}    case SCon{+h, t}:      name.keep(kind(h), _u => name.all(t))# after `..`: one more name char, so the segment is not `..`def seg.dots(text: String) -> Bool:  match text:    case SNil{}:      False{}    case SCon{+h, t}:      name.keep(kind(h), _u => name.all(t))# the second char after a leading dotdef seg.dot2(mark: Mark, rest: String) -> Bool:  match mark:    case Word{}:      name.all(rest)    case Dot{}:      seg.dots(rest)    case Slash{}:      False{}    case Tilde{}:      False{}    case Other{}:      False{}# after a leading `.`: more, so the segment is not `.`def seg.dot(text: String) -> Bool:  match text:    case SNil{}:      False{}    case SCon{+h, t}:      seg.dot2(kind(h), t)# the first char of a segmentdef seg.first(mark: Mark, rest: String) -> Bool:  match mark:    case Word{}:      name.all(rest)    case Dot{}:      seg.dot(rest)    case Slash{}:      False{}    case Tilde{}:      False{}    case Other{}:      False{}# one segment of a GitHub shorthand: letters, digits, `-`, `_` and `.`, at# least one of them, and not `.` or `..`, which name a directorydef seg(text: String) -> Bool:  match text:    case SNil{}:      False{}    case SCon{+h, t}:      seg.first(kind(h), t)# a word cut at its first slash, or not cut because a char before any slash# could not be in a shorthandtype Halves is Data:  Uncut{}  Two{left: String, right: String}# a char put back in front of the left halfdef halve.put(ch: Char, halves: Halves) -> Halves:  match halves:    case Uncut{}:      Uncut{}    case Two{left, right}:      Two{SCon{ch, left}, right}# one char of the first segment. A slash ends it; the rest of the word is# asked about only past a char a segment may hold.def halve.at(mark: Mark, ch: Char, +tail: String, rest: Unit -> Halves) -> Halves:  match mark:    case Slash{}:      Two{"", tail}    case Word{}:      halve.put(ch, rest(Unit{}))    case Dot{}:      halve.put(ch, rest(Unit{}))    case Tilde{}:      Uncut{}    case Other{}:      Uncut{}# the word before its first slash and the word after itdef halve(text: String) -> Halves:  match text:    case SNil{}:      Uncut{}    case SCon{+h, +t}:      halve.at(kind(h), h, t, _u => halve(t))# a word naming a Bend file. As the second of two segments it is the last# component of a path, `src/main.bend`, and not a repository.def bend.file(+text: String) -> Bool:  String.ends_with(text, ".bend")# the second segment of a shorthand: a segment that is not a Bend filedef repo.seg(+text: String) -> Bool:  Bool.and(seg(text), Bool.not(bend.file(text)))# both segments of a shorthand, where there are twodef github.halves(halves: Halves) -> Bool:  match halves:    case Uncut{}:      False{}    case Two{left, right}:      Bool.and(seg(left), repo.seg(right))# `owner/repo`: exactly two segments, each a `seg`, the second not ending in# `.bend`. A second slash leaves the right half with a slash in it, which no# segment holds.def github.ok(text: String) -> Bool:  github.halves(halve(text))# one step of a scan looking for `..`. A slug with one would leave the cache.def climb.step(here: Bool, rest: Unit -> Bool) -> Bool:  match here:    case True{}:      True{}    case False{}:      rest(Unit{})# whether any component is a climbdef climb(ps: List<&2, String>) -> Bool:  match ps:    case []:      False{}    case +h <> t:      climb.step(String.eq(h, ".."), _u => climb(t))# a slug that stays under the cache: non-empty, relative, and with no `..`def slug.ok(+text: String) -> Bool:  Bool.and(Bool.not(String.is_empty(text)),    Bool.and(Bool.not(String.starts_with(text, "/")),      Bool.not(climb(String.split(text, '/')))))# a remote whose slug can be a directory, or a refusal when it cannotdef remote(+url: String, +slug: String) -> Place:  Bool.pick(Place, slug.ok(slug), Remote{url, slug}, Bad{"not a repository"})# `s` with a suffix of `n` chars taken off when it ends in that suffix.# `Bool.pick` builds both arms, which is the same trade `pkg/path.unbend` makes.def cut.end(+text: String, +suf: String, len: Nat) -> String:  Bool.pick(String, String.ends_with(text, suf),    String.take(text, Nat.sub(String.length(text), len)), text)# a trailing slash is not part of the cache directorydef unslash(+text: String) -> String:  cut.end(text, "/", 1n)# a trailing `.git` is not part of the cache directorydef ungit(+text: String) -> String:  cut.end(text, ".git", 4n)# everything after the first colon. `https://host/path` splits into the# scheme and `//host/path`.def body(ps: List<&2, String>) -> String:  match ps:    case Nil{}:      ""    case _h <> t:      String.join(t, ":")# the `//` a scheme leaves in front of the hostdef drop.slashes(+text: String) -> String:  Bool.pick(String, String.starts_with(text, "//"), String.drop(text, 2n), text)# a URL with no host, `file:///srv/repo`, is on this host, and is cached# where `file://localhost/srv/repo` is, rather than at an absolute slugdef hostless(+text: String) -> String:  Bool.pick(String, String.starts_with(text, "/"), "localhost" ++ text, text)# `scheme://host/path` as a cache directorydef slug.url(+text: String) -> String:  ungit(unslash(hostless(drop.slashes(body(String.split(text, ':'))))))# `git@host:path` as a cache directory. The colon is the only separator.def slug.scp(+text: String) -> String:  ungit(unslash(String.join(String.split(String.drop(text, 4n), ':'), "/")))# the char after `~` or `..`: a slashdef looks.slash.at(mark: Mark) -> Bool:  match mark:    case Slash{}:      True{}    case Dot{}:      False{}    case Tilde{}:      False{}    case Word{}:      False{}    case Other{}:      False{}def looks.slash(text: String) -> Bool:  match text:    case SNil{}:      False{}    case SCon{+h, _t}:      looks.slash.at(kind(h))# the char after a leading dot: `./` or `../`def looks.dot.at(mark: Mark, rest: String) -> Bool:  match mark:    case Slash{}:      True{}    case Dot{}:      looks.slash(rest)    case Tilde{}:      False{}    case Word{}:      False{}    case Other{}:      False{}def looks.dot(text: String) -> Bool:  match text:    case SNil{}:      False{}    case SCon{+h, t}:      looks.dot.at(kind(h), t)# the first char: `/`, or the start of `./`, `../` or `~/`def looks.first(mark: Mark, rest: String) -> Bool:  match mark:    case Slash{}:      True{}    case Dot{}:      looks.dot(rest)    case Tilde{}:      looks.slash(rest)    case Word{}:      False{}    case Other{}:      False{}# a word that already says it is a path: it starts `/`, `./`, `../` or `~/`def looks.path(text: String) -> Bool:  match text:    case SNil{}:      False{}    case SCon{+h, t}:      looks.first(kind(h), t)# `git@host:path`, or a pathdef classify.scp(scp: Bool, +text: String) -> Place:  match scp:    case True{}:      remote(text, slug.scp(text))    case False{}:      Local{text}# `owner/repo`. Its slug is safe without the check a URL's gets: neither# segment is empty, `.`, `..` or holds a slash, so it cannot climb out of the# cache. A trailing `.git` is not part of the cache directory, as for a URL.def classify.hub(ok: Bool, +text: String) -> Place:  match ok:    case True{}:      Remote{"https://github.com/" ++ text, "github.com/" ++ ungit(text)}    case False{}:      classify.scp(String.starts_with(text, "git@"), text)# a path prefix, or a possible shorthanddef classify.path(local: Bool, +text: String) -> Place:  match local:    case True{}:      Local{text}    case False{}:      classify.hub(github.ok(text), text)# a URL, or notdef classify.url(url: Bool, +text: String) -> Place:  match url:    case True{}:      remote(text, slug.url(text))    case False{}:      classify.path(looks.path(text), text)# nothing was asked fordef classify.empty(empty: Bool, +text: String) -> Place:  match empty:    case True{}:      Bad{"a target is needed"}    case False{}:      classify.url(String.contains(text, "://"), text)# what a target word names. A word with `://` in it is a URL; one starting# `/`, `./`, `../` or `~/` is a path; `owner/repo` is GitHub; one starting# `git@` is a URL; anything else is a path. The shorthand is asked about# before `git@` because no shorthand holds an `@`, and in that order the# shorthand's law needs nothing said about `git@`.def classify(+text: String) -> Place:  classify.empty(String.is_empty(text), text)# what `ez add` was asked for: a hub package by its `<name>@<version>`, or a# place as `classify` reads ittype Aim is Data:  Named{nv: String}  Placed{place: Place}# a word `classify` reads as a path names a hub package when bend's NAMED rule# rules it in (`K.name.ok`); otherwise it is that pathdef aim.local(named: Bool, +text: String, +path: String) -> Aim:  match named:    case True{}:      Named{text}    case False{}:      Placed{Local{path}}# the place read, with only a path asked whether it is a name. A URL, a# `git@host:path` and an `owner/repo` are never read again, so nothing that# was a remote becomes a name.def aim.place(place: Place, +text: String) -> Aim:  match place:    case Bad{why}:      Placed{Bad{why}}    case Local{path}:      aim.local(K.name.ok(text), text, path)    case Remote{url, slug}:      Placed{Remote{url, slug}}# what an `ez add` target names. A name holds no `/` and no `:`, and starts# with a letter, so a word bend reads as a name is one `classify` reads as a# path, and only such a word is asked.def aim(+text: String) -> Aim:  aim.place(classify(text), text)# whether a target names a hub packagedef aim.named(aim: Aim) -> Bool:  match aim:    case Named{_nv}:      True{}    case Placed{_place}:      False{}# `~` and `~/…` expanded. Anything else is already a path.def expand(+home: String, +path: String) -> String:  Bool.pick(String, String.eq(path, "~"), home,    Bool.pick(String, String.starts_with(path, "~/"),      home ++ String.drop(path, 1n), path))# the entry, or `main.bend` when a ledger names nonedef entry.or(+path: String) -> String:  Bool.pick(String, String.is_empty(path), "main.bend", path)# the file that is built: `bin` when the ledger names one, otherwise the entrydef file.of(+bin: String, +entry: String) -> String:  Bool.pick(String, String.is_empty(bin), entry.or(entry), bin)# the binary's name: the package's, or `app` when the ledger names nonedef out.name(+name: String) -> String:  Bool.pick(String, String.is_empty(name), "app", name)# a place on one line, for a proof to readdef show(place: Place) -> String:  match place:    case Bad{why}:      "bad " ++ why    case Local{path}:      "local " ++ path    case Remote{url, slug}:      "remote " ++ url ++ " " ++ slug