Files
upd/spec.go
T

224 lines
4.8 KiB
Go

package main
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
// Spec is one repository plus the options that belong to it - either a command
// line invocation or one line of the config file.
type Spec struct {
Repo string
Install string
Name string
Tag string
Asset string
Pattern string
OS string
Arch string
Forge string
Token string
VersionFlag string
Pre bool
}
func (s *Spec) set(key, val string) error {
switch key {
case "install":
s.Install = val
case "name":
s.Name = val
case "tag":
s.Tag = val
case "asset":
s.Asset = val
case "pattern":
s.Pattern = val
case "os":
s.OS = val
case "arch":
s.Arch = val
case "forge":
s.Forge = val
case "token":
s.Token = val
case "version-flag":
s.VersionFlag = val
case "pre":
s.Pre = val != "" && val != "0"
default:
return fmt.Errorf("unknown key %q (allowed: %s)", key, strings.Join(specKeys, ", "))
}
return nil
}
// Options typed on the command line win over the config file. Unlike the Perl
// version this looks at what was actually given, so a "version-flag=" in the
// config file is no longer overwritten by its own default.
func (s *Spec) applyGlobals() {
for _, k := range specKeys {
if !given[k] {
continue
}
var v string
switch k {
case "install":
v = opt.install
case "name":
v = opt.name
case "tag":
v = opt.tag
case "asset":
v = opt.asset
case "pattern":
v = opt.pattern
case "os":
v = opt.os
case "arch":
v = opt.arch
case "forge":
v = opt.forge
case "token":
v = opt.token
case "version-flag":
v = opt.versionFlag
case "pre":
if !opt.pre {
continue // --pre only ever switches on
}
v = "1"
}
s.set(k, v)
}
}
func (s *Spec) versionFlag() string {
if s.VersionFlag != "" {
return s.VersionFlag
}
return opt.versionFlag
}
func (s *Spec) label() string {
if s.Name != "" {
return parseNames(s.Name, "?")[0].out
}
r := strings.TrimRight(s.Repo, "/")
if r == "" {
return "?"
}
return filepath.Base(r)
}
// ==============================================================================
// Config file (--all)
// ==============================================================================
func configPath() string {
base := os.Getenv("XDG_CONFIG_HOME")
if base == "" {
base = filepath.Join(homeDir(), ".config")
}
return filepath.Join(base, "upd", "tools")
}
// One entry per line: <repo-url> [key=value ...] [pre]
func readConfig(path string) ([]*Spec, error) {
fh, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("cannot read config %s: %w\n"+
"Format: one line per tool, e.g.\n"+
" https://github.com/sxyazi/yazi install=~/bin name=yazi,ya", path, err)
}
defer fh.Close()
var specs []*Spec
sc := bufio.NewScanner(fh)
for ln := 1; sc.Scan(); ln++ {
line := strings.TrimSpace(sc.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
tokens := splitTokens(line)
s := &Spec{Repo: tokens[0]}
for _, t := range tokens[1:] {
key, val, ok := strings.Cut(t, "=")
if !ok {
val = "1" // a bare "pre"
}
if err := s.set(strings.ToLower(key), val); err != nil {
return nil, fmt.Errorf("%s:%d: %w", path, ln, err)
}
}
s.applyGlobals()
specs = append(specs, s)
}
if err := sc.Err(); err != nil {
return nil, fmt.Errorf("cannot read config %s: %w", path, err)
}
return specs, nil
}
// Whitespace separated, with double quotes around values that contain spaces.
func splitTokens(line string) []string {
var out []string
var cur strings.Builder
quoted, escaped, started := false, false, false
for _, r := range line {
switch {
case escaped:
cur.WriteRune(r)
escaped = false
case r == '\\' && quoted:
escaped = true
case r == '"':
quoted = !quoted
started = true
case (r == ' ' || r == '\t') && !quoted:
if started {
out = append(out, cur.String())
cur.Reset()
started = false
}
default:
cur.WriteRune(r)
started = true
}
}
if started {
out = append(out, cur.String())
}
return out
}
// ==============================================================================
// Binary names
// ==============================================================================
// name holds what to look for inside the asset (src) and what to write to disk
// (out): "yazi,ya" are two binaries, "yazi:yazi-nightly" renames one.
type name struct{ src, out string }
func parseNames(list, fallback string) []name {
var names []name
for _, part := range strings.Split(list, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
src, out, ok := strings.Cut(part, ":")
if !ok || out == "" {
out = src
}
names = append(names, name{src: src, out: out})
}
if len(names) == 0 {
names = append(names, name{src: fallback, out: fallback})
}
return names
}