235 lines
7.4 KiB
Go
235 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
// The session environment as a collection (novox/hq ADR 0203, ADR 0349 clause 13, design 57 L3, issue 795). One
|
|
// collection, `environment`, whose items are typed by attributes: an item is a variable (`kind=variable`,
|
|
// `name=<NAME>`) or a PATH entry (`kind=path`, `at=start|end`), and its template's text is the variable's value or
|
|
// the entry. The collector names the rendering, not a selector: `${collected:environment:posix}` and
|
|
// `${collected:environment:systemd}` (and the old `${environment:posix|systemd}`) are the collection `environment`
|
|
// written for a POSIX shell or for environment.d. Both renderings are the ones mesh-controller internal/catalogue
|
|
// environment_into.go writes for a collector not handed, byte for byte (its golden files are this client's).
|
|
|
|
// Environment collection and renderers.
|
|
const (
|
|
EnvironmentCollection = "environment"
|
|
EnvironmentPOSIX = "posix"
|
|
EnvironmentSystemd = "systemd"
|
|
)
|
|
|
|
// environmentName is an environment variable's name, which only this collection's `name` attribute may be.
|
|
var environmentName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
|
|
|
// attributeFits is whether an item's attribute is of the shape the collection allows.
|
|
func attributeFits(collection, k, v string) bool {
|
|
if collection == EnvironmentCollection && k == "name" {
|
|
return environmentName.MatchString(v)
|
|
}
|
|
return attributeKeyShape.MatchString(k) && attributeValueShape.MatchString(v)
|
|
}
|
|
|
|
// environmentRenderer is the rendering a need names, when it is the environment's: `${collected:environment:posix}`
|
|
// or `${environment:posix}`.
|
|
func environmentRenderer(n Need) (string, bool) {
|
|
name := n.Name
|
|
switch {
|
|
case n.Kind == "environment":
|
|
case n.Kind == CollectedKind:
|
|
rest, is := strings.CutPrefix(name, EnvironmentCollection+":")
|
|
if !is {
|
|
return "", false
|
|
}
|
|
name = rest
|
|
default:
|
|
return "", false
|
|
}
|
|
return name, name == EnvironmentPOSIX || name == EnvironmentSystemd
|
|
}
|
|
|
|
type envVariable struct{ module, name, value string }
|
|
|
|
type envEntry struct {
|
|
module, entry string
|
|
order int
|
|
item string
|
|
}
|
|
|
|
// literalProblem is why a value cannot be written, unquoted by either reader, as the same string in both formats
|
|
// (mesh-controller internal/catalogue environment_into.go).
|
|
func literalProblem(v string) string {
|
|
switch {
|
|
case strings.ContainsAny(v, `'"`):
|
|
return "holds a quote"
|
|
case strings.Contains(v, `\`):
|
|
return "holds a backslash"
|
|
case strings.ContainsAny(v, "\n\r"):
|
|
return "holds a line break"
|
|
case strings.ContainsRune(v, 0):
|
|
return "holds a NUL"
|
|
case strings.Contains(v, "$"):
|
|
return "holds a $, which the two readers expand differently"
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// environmentText is the collection's items written for one renderer: modules in name order, each module's variables
|
|
// sorted by name under a line naming it, then PATH's entries, each once where the first puts it, as the old form
|
|
// wrote them. An empty collection is empty text.
|
|
func environmentText(items map[string]itemValue, renderer string) (string, error) {
|
|
type owned struct {
|
|
id string
|
|
itemValue
|
|
}
|
|
var all []owned
|
|
for id, it := range items {
|
|
all = append(all, owned{id, it})
|
|
}
|
|
slices.SortFunc(all, func(a, b owned) int {
|
|
ma, na := splitID(a.id)
|
|
mb, nb := splitID(b.id)
|
|
return cmp.Or(cmp.Compare(ma, mb), cmp.Compare(na, nb))
|
|
})
|
|
var variables []envVariable
|
|
var start, end []envEntry
|
|
setter := map[string]string{}
|
|
placed := map[string]bool{}
|
|
// Items of one module in the order the old form declared them: by order, then name.
|
|
slices.SortStableFunc(all, func(a, b owned) int {
|
|
ma, _ := splitID(a.id)
|
|
mb, _ := splitID(b.id)
|
|
if c := cmp.Compare(ma, mb); c != 0 {
|
|
return c
|
|
}
|
|
if c := cmp.Compare(a.order(), b.order()); c != 0 {
|
|
return c
|
|
}
|
|
_, na := splitID(a.id)
|
|
_, nb := splitID(b.id)
|
|
return cmp.Compare(na, nb)
|
|
})
|
|
for _, it := range all {
|
|
module, _ := splitID(it.id)
|
|
value := strings.TrimSuffix(it.text, "\n")
|
|
if why := literalProblem(value); why != "" {
|
|
return "", fmt.Errorf("%s's item %s is %q on this machine, which %s: a value is a literal both readers "+
|
|
"take alike", module, it.id, value, why)
|
|
}
|
|
switch it.attributes["kind"] {
|
|
case "variable":
|
|
n := it.attributes["name"]
|
|
if n == "" {
|
|
return "", fmt.Errorf("%s's item %s is a variable and has no name", module, it.id)
|
|
}
|
|
if first, taken := setter[n]; taken {
|
|
return "", fmt.Errorf("%s and %s both set %s on this machine; the account has one environment, so "+
|
|
"one of them must stop setting it (novox/hq ADR 0203)", first, module, n)
|
|
}
|
|
setter[n] = module
|
|
variables = append(variables, envVariable{module, n, value})
|
|
case "path":
|
|
if strings.Contains(value, ":") {
|
|
return "", fmt.Errorf("%s puts %q on PATH on this machine, which holds a colon, PATH's own separator",
|
|
module, value)
|
|
}
|
|
if value == "" {
|
|
return "", fmt.Errorf("%s's item %s is a PATH entry and says none", module, it.id)
|
|
}
|
|
if placed[value] {
|
|
continue
|
|
}
|
|
placed[value] = true
|
|
e := envEntry{module: module, entry: value, order: it.order(), item: it.id}
|
|
switch it.attributes["at"] {
|
|
case "start":
|
|
start = append(start, e)
|
|
case "end":
|
|
end = append(end, e)
|
|
default:
|
|
return "", fmt.Errorf("%s's item %s is a PATH entry and is not at start or end", module, it.id)
|
|
}
|
|
default:
|
|
return "", fmt.Errorf("%s's item %s is of the environment and is not kind=variable or kind=path", module,
|
|
it.id)
|
|
}
|
|
}
|
|
// Variables per module, sorted by name within it.
|
|
slices.SortStableFunc(variables, func(a, b envVariable) int {
|
|
return cmp.Or(cmp.Compare(a.module, b.module), cmp.Compare(a.name, b.name))
|
|
})
|
|
if renderer == EnvironmentSystemd {
|
|
return systemdText(variables, start, end), nil
|
|
}
|
|
return posixText(variables, start, end), nil
|
|
}
|
|
|
|
func posixText(variables []envVariable, start, end []envEntry) string {
|
|
var b strings.Builder
|
|
named := ""
|
|
for _, v := range variables {
|
|
if v.module != named {
|
|
fmt.Fprintf(&b, "# %s\n", v.module)
|
|
named = v.module
|
|
}
|
|
fmt.Fprintf(&b, "export %s='%s'\n", v.name, v.value)
|
|
}
|
|
named = ""
|
|
for i := len(start) - 1; i >= 0; i-- {
|
|
p := start[i]
|
|
if p.module != named {
|
|
fmt.Fprintf(&b, "# %s\n", p.module)
|
|
named = p.module
|
|
}
|
|
fmt.Fprintf(&b, "case \":${PATH}:\" in *':%s:'*) ;; *) PATH='%s'\"${PATH:+:${PATH}}\" ;; esac\n",
|
|
p.entry, p.entry)
|
|
}
|
|
named = ""
|
|
for _, p := range end {
|
|
if p.module != named {
|
|
fmt.Fprintf(&b, "# %s\n", p.module)
|
|
named = p.module
|
|
}
|
|
fmt.Fprintf(&b, "case \":${PATH}:\" in *':%s:'*) ;; *) PATH=\"${PATH:+${PATH}:}\"'%s' ;; esac\n",
|
|
p.entry, p.entry)
|
|
}
|
|
if len(start)+len(end) > 0 {
|
|
b.WriteString("export PATH\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func systemdText(variables []envVariable, start, end []envEntry) string {
|
|
var b strings.Builder
|
|
named := ""
|
|
for _, v := range variables {
|
|
if v.module != named {
|
|
fmt.Fprintf(&b, "# %s\n", v.module)
|
|
named = v.module
|
|
}
|
|
fmt.Fprintf(&b, "%s=%s\n", v.name, v.value)
|
|
}
|
|
line := func(entries []envEntry, format string) {
|
|
if len(entries) == 0 {
|
|
return
|
|
}
|
|
var modules, paths []string
|
|
seen := map[string]bool{}
|
|
for _, p := range entries {
|
|
if !seen[p.module] {
|
|
seen[p.module] = true
|
|
modules = append(modules, p.module)
|
|
}
|
|
paths = append(paths, p.entry)
|
|
}
|
|
fmt.Fprintf(&b, "# %s\n"+format+"\n", strings.Join(modules, ", "), strings.Join(paths, ":"))
|
|
}
|
|
line(start, "PATH=%s${PATH:+:$PATH}")
|
|
line(end, "PATH=${PATH:+$PATH:}%s")
|
|
return b.String()
|
|
}
|