224 lines
4.8 KiB
Go
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
|
|
}
|