Files
yay/pkg/settings/parser/parser.go
Jo ac83d3fb06 Switch to JaroWinkler distance and reweight package search scores (#2840)
fix(query): use JaroWinkler distance and reweight metric components

Switch from Hamming to JaroWinkler distance for package name matching,
as JaroWinkler handles variable-length strings and gives a prefix-match
bonus better suited to package search.

Reweight the score components from sim*0.50 + simDesc*0.20 + popularity*0.30
to sim*0.35 + simDesc*0.15 + popularity*0.50 to better surface popular
packages when name similarity is moderate.

Roll back the unreleased version target from 13.0.0 to 12.6.0 to align
with the next minor release after v12.5.7.

Fix scanner.Err() check in parseStdin to surface I/O errors that were
previously silently dropped.
2026-06-07 18:16:46 +02:00

691 lines
12 KiB
Go

package parser
import (
"bufio"
"errors"
"fmt"
"maps"
"os"
"strings"
"github.com/leonelquinteros/gotext"
)
type Option struct {
Global bool
Args []string
}
func (o *Option) Add(args ...string) {
if o.Args == nil {
o.Args = args
return
}
o.Args = append(o.Args, args...)
}
func (o *Option) First() string {
if len(o.Args) == 0 {
return ""
}
return o.Args[0]
}
func (o *Option) Set(arg string) {
o.Args = []string{arg}
}
func (o *Option) String() string {
return fmt.Sprintf("Global:%v Args:%v", o.Global, o.Args)
}
// Arguments Parses command line arguments in a way we can interact with programmatically but
// also in a way that can easily be passed to pacman later on.
type Arguments struct {
Op string
Options map[string]*Option
Targets []string
}
func (a *Arguments) String() string {
return fmt.Sprintf("Op:%v Options:%+v Targets: %v", a.Op, a.Options, a.Targets)
}
func (a *Arguments) CreateOrAppendOption(option string, values ...string) {
if a.Options[option] == nil {
a.Options[option] = &Option{
Args: values,
}
} else {
a.Options[option].Add(values...)
}
}
func MakeArguments() *Arguments {
return &Arguments{
"",
make(map[string]*Option),
make([]string, 0),
}
}
func (a *Arguments) CopyGlobal() *Arguments {
cp := MakeArguments()
for k, v := range a.Options {
if v.Global {
cp.Options[k] = v
}
}
return cp
}
func (a *Arguments) Copy() (cp *Arguments) {
cp = MakeArguments()
cp.Op = a.Op
maps.Copy(cp.Options, a.Options)
cp.Targets = make([]string, len(a.Targets))
copy(cp.Targets, a.Targets)
return
}
func (a *Arguments) DelArg(options ...string) {
for _, option := range options {
delete(a.Options, option)
}
}
func (a *Arguments) NeedRoot(mode TargetMode) bool {
if a.ExistsArg("h", "help") {
return false
}
switch a.Op {
case "D", "database":
if a.ExistsArg("k", "check") {
return false
}
return true
case "F", "files":
if a.ExistsArg("y", "refresh") {
return true
}
return false
case "Q", "query":
if a.ExistsArg("k", "check") {
return true
}
return false
case "R", "remove":
if a.ExistsArg("p", "print", "print-format") {
return false
}
return true
case "S", "sync":
switch {
case a.ExistsArg("y", "refresh"):
return true
case a.ExistsArg("p", "print", "print-format"):
return false
case a.ExistsArg("s", "search"):
return false
case a.ExistsArg("l", "list"):
return false
case a.ExistsArg("g", "groups"):
return false
case a.ExistsArg("i", "info"):
return false
case a.ExistsArg("c", "clean") && mode == ModeAUR:
return false
}
return true
case "U", "upgrade":
return true
default:
return false
}
}
func (a *Arguments) addOP(op string) error {
if a.Op != "" {
return errors.New(gotext.Get("only one operation may be used at a time"))
}
a.Op = op
return nil
}
func (a *Arguments) addParam(option, arg string) error {
if !isArg(option) {
return errors.New(gotext.Get("invalid option '%s'", option))
}
if isOp(option) {
return a.addOP(option)
}
a.CreateOrAppendOption(option, strings.Split(arg, ",")...)
if isGlobal(option) {
a.Options[option].Global = true
}
return nil
}
func (a *Arguments) AddArg(options ...string) error {
for _, option := range options {
err := a.addParam(option, "")
if err != nil {
return err
}
}
return nil
}
// Multiple args acts as an OR operator.
func (a *Arguments) ExistsArg(options ...string) bool {
for _, option := range options {
if _, exists := a.Options[option]; exists {
return true
}
}
return false
}
func (a *Arguments) GetArg(options ...string) (arg string, double, exists bool) {
for _, option := range options {
value, exists := a.Options[option]
if exists {
return value.First(), len(value.Args) >= 2, len(value.Args) >= 1
}
}
return arg, false, false
}
func (a *Arguments) GetArgs(option string) (args []string) {
value, exists := a.Options[option]
if exists {
return value.Args
}
return nil
}
func (a *Arguments) AddTarget(targets ...string) {
a.Targets = append(a.Targets, targets...)
}
func (a *Arguments) ClearTargets() {
a.Targets = make([]string, 0)
}
// Multiple args acts as an OR operator.
func (a *Arguments) ExistsDouble(options ...string) bool {
for _, option := range options {
if value, exists := a.Options[option]; exists {
return len(value.Args) >= 2
}
}
return false
}
func (a *Arguments) FormatArgs() (args []string) {
if a.Op != "" {
args = append(args, formatArg(a.Op))
}
for option, arg := range a.Options {
if arg.Global || option == "--" {
continue
}
formattedOption := formatArg(option)
for _, value := range arg.Args {
args = append(args, formattedOption)
if hasParam(option) {
args = append(args, value)
}
}
}
return args
}
func (a *Arguments) FormatGlobals() (args []string) {
for option, arg := range a.Options {
if !arg.Global {
continue
}
formattedOption := formatArg(option)
for _, value := range arg.Args {
args = append(args, formattedOption)
if hasParam(option) {
args = append(args, value)
}
}
}
return args
}
func formatArg(arg string) string {
if len(arg) > 1 {
arg = "--" + arg
} else {
arg = "-" + arg
}
return arg
}
func isArg(arg string) bool {
switch arg {
case "-", "--":
case "ask":
case "D", "database":
case "Q", "query":
case "R", "remove":
case "S", "sync":
case "T", "deptest":
case "U", "upgrade":
case "F", "files":
case "V", "version":
case "h", "help":
case "Y", "yay":
case "W", "web":
case "P", "show":
case "B", "build":
case "G", "getpkgbuild":
case "b", "dbpath":
case "r", "root":
case "v", "verbose":
case "arch":
case "cachedir":
case "color":
case "config":
case "debug":
case "gpgdir":
case "hookdir":
case "logfile":
case "noconfirm":
case "confirm":
case "disable-download-timeout":
case "sysroot":
case "d", "nodeps":
case "assume-installed":
case "dbonly":
case "noprogressbar":
case "numberupgrades":
case "noscriptlet":
case "p", "print":
case "print-format":
case "asdeps":
case "asexplicit":
case "ignore":
case "ignoregroup":
case "needed":
case "overwrite":
case "f", "force":
case "c", "changelog":
case "deps":
case "e", "explicit":
case "g", "groups":
case "i", "info":
case "k", "check":
case "l", "list":
case "m", "foreign":
case "n", "native":
case "o", "owns":
case "file":
case "q", "quiet":
case "s", "search":
case "t", "unrequired":
case "u", "upgrades":
case "cascade":
case "nosave":
case "recursive":
case "unneeded":
case "clean":
case "sysupgrade":
case "w", "downloadonly":
case "y", "refresh":
case "x", "regex":
case "machinereadable":
case "disable-sandbox":
// yay options
case "aururl":
case "aurrpcurl":
case "save":
case "afterclean", "cleanafter":
case "keepsrc":
case "devel":
case "topdown":
case "bottomup":
case "completioninterval":
case "sortby":
case "searchby":
case "redownload":
case "redownloadall":
case "noredownload":
case "rebuild":
case "rebuildall":
case "rebuildtree":
case "norebuild":
case "batchinstall":
case "answerclean":
case "noanswerclean":
case "answerdiff":
case "noanswerdiff":
case "answeredit":
case "noansweredit":
case "answerupgrade":
case "noanswerupgrade":
case "gpgflags":
case "mflags":
case "gitflags":
case "builddir":
case "editor":
case "editorflags":
case "makepkg":
case "makepkgconf":
case "nomakepkgconf":
case "pacman":
case "git":
case "gpg":
case "sudo":
case "sudoflags":
case "requestsplitn":
case "sudoloop":
case "provides":
case "pgpfetch":
case "cleanmenu":
case "diffmenu":
case "editmenu":
case "useask":
case "combinedupgrade":
case "a", "aur":
case "N", "repo":
case "removemake":
case "noremovemake":
case "askremovemake":
case "askyesremovemake":
case "complete":
case "stats":
case "news":
case "gendb":
case "currentconfig":
case "defaultconfig":
case "singlelineresults":
case "doublelineresults":
case "separatesources":
default:
return false
}
return true
}
func isOp(op string) bool {
switch op {
case "V", "version":
case "D", "database":
case "F", "files":
case "Q", "query":
case "R", "remove":
case "S", "sync":
case "T", "deptest":
case "U", "upgrade":
// yay specific
case "Y", "yay":
case "W", "web":
case "B", "build":
case "P", "show":
case "G", "getpkgbuild":
default:
return false
}
return true
}
func isGlobal(op string) bool {
switch op {
case "b", "dbpath":
case "r", "root":
case "v", "verbose":
case "arch":
case "cachedir":
case "color":
case "config":
case "debug":
case "gpgdir":
case "hookdir":
case "logfile":
case "noconfirm":
case "confirm":
default:
return false
}
return true
}
func hasParam(arg string) bool {
switch arg {
case "dbpath", "b":
case "root", "r":
case "sysroot":
case "config":
case "ignore":
case "assume-installed":
case "overwrite":
case "ask":
case "cachedir":
case "hookdir":
case "logfile":
case "ignoregroup":
case "arch":
case "print-format":
case "gpgdir":
case "color":
// yay params
case "aururl":
case "aurrpcurl":
case "mflags":
case "gpgflags":
case "gitflags":
case "builddir":
case "editor":
case "editorflags":
case "makepkg":
case "makepkgconf":
case "pacman":
case "git":
case "gpg":
case "sudo":
case "sudoflags":
case "requestsplitn":
case "answerclean":
case "answerdiff":
case "answeredit":
case "answerupgrade":
case "completioninterval":
case "sortby":
case "searchby":
default:
return false
}
return true
}
// Parses short hand options such as:
// -Syu -b /some/path -.
func (a *Arguments) parseShortOption(arg, param string) (usedNext bool, err error) {
if arg == "-" {
err = a.AddArg("-")
return
}
arg = arg[1:]
for k, _char := range arg {
char := string(_char)
if hasParam(char) {
if k < len(arg)-1 {
err = a.addParam(char, arg[k+1:])
} else {
usedNext = true
err = a.addParam(char, param)
}
break
} else {
err = a.AddArg(char)
if err != nil {
return
}
}
}
return
}
// Parses full length options such as:
// --sync --refresh --sysupgrade --dbpath /some/path --.
func (a *Arguments) parseLongOption(arg, param string) (usedNext bool, err error) {
if arg == "--" {
err = a.AddArg(arg)
return
}
arg = arg[2:]
switch split := strings.SplitN(arg, "=", 2); {
case len(split) == 2:
err = a.addParam(split[0], split[1])
case hasParam(arg):
err = a.addParam(arg, param)
usedNext = true
default:
err = a.AddArg(arg)
}
return
}
func (a *Arguments) parseStdin() error {
fi, err := os.Stdin.Stat()
if err != nil {
return err
}
// Ensure data is piped
if (fi.Mode() & os.ModeCharDevice) != 0 {
return errors.New(gotext.Get("argument '-' specified without input on stdin"))
}
scanner := bufio.NewScanner(os.Stdin)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
a.AddTarget(scanner.Text())
}
if err := scanner.Err(); err != nil {
return err
}
return os.Stdin.Close()
}
func (a *Arguments) Parse() error {
args := os.Args[1:]
usedNext := false
for k, arg := range args {
var nextArg string
if usedNext {
usedNext = false
continue
}
if k+1 < len(args) {
nextArg = args[k+1]
}
var err error
switch {
case a.ExistsArg("--"):
a.AddTarget(arg)
case strings.HasPrefix(arg, "--"):
usedNext, err = a.parseLongOption(arg, nextArg)
case strings.HasPrefix(arg, "-"):
usedNext, err = a.parseShortOption(arg, nextArg)
default:
a.AddTarget(arg)
}
if err != nil {
return err
}
}
if a.Op == "" {
if len(a.Targets) > 0 {
a.Op = "Y"
} else {
// Handle args incompatible with -Syu
if a.ExistsArg("c", "clean") {
fmt.Println(gotext.Get("the clean command requires an operation to be specified"))
fmt.Println(gotext.Get("did you mean yay -Scc (clean caches) or yay -Ycc (clean orphaned packages)?"))
return errors.New(gotext.Get("invalid option"))
}
if _, err := a.parseShortOption("-Syu", ""); err != nil {
return err
}
}
}
if a.ExistsArg("-") {
if err := a.parseStdin(); err != nil {
return err
}
a.DelArg("-")
file, err := os.Open("/dev/tty")
if err != nil {
return err
}
os.Stdin = file
}
return nil
}