mirror of
https://github.com/Jguer/yay.git
synced 2024-11-07 09:37:22 +01:00
8948278568
fix(parser): ensure data is piped when using '-' argument Fixes #1626. Check to see if data was actually piped to yay, otherwise it will hang forever. This error will give users better feedback of how to use the '-' argument (it is also the same exact error pacman throws for invalid piped data). Two tests were added. However, I didn't know how to add a test for the actual part that detects whether data was piped or not (which is essentially what this commit adds).
693 lines
12 KiB
Go
693 lines
12 KiB
Go
package parser
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"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 o.Args == nil || 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
|
|
|
|
for k, v := range a.Options {
|
|
cp.Options[k] = v
|
|
}
|
|
|
|
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":
|
|
// yay options
|
|
case "aururl":
|
|
case "aurrpcurl":
|
|
case "save":
|
|
case "afterclean", "cleanafter":
|
|
case "noafterclean", "nocleanafter":
|
|
case "devel":
|
|
case "nodevel":
|
|
case "timeupdate":
|
|
case "notimeupdate":
|
|
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 "nobatchinstall":
|
|
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 "nosudoloop":
|
|
case "provides":
|
|
case "noprovides":
|
|
case "pgpfetch":
|
|
case "nopgpfetch":
|
|
case "upgrademenu":
|
|
case "noupgrademenu":
|
|
case "cleanmenu":
|
|
case "nocleanmenu":
|
|
case "diffmenu":
|
|
case "nodiffmenu":
|
|
case "editmenu":
|
|
case "noeditmenu":
|
|
case "useask":
|
|
case "nouseask":
|
|
case "combinedupgrade":
|
|
case "nocombinedupgrade":
|
|
case "a", "aur":
|
|
case "repo":
|
|
case "removemake":
|
|
case "noremovemake":
|
|
case "askremovemake":
|
|
case "complete":
|
|
case "stats":
|
|
case "news":
|
|
case "gendb":
|
|
case "currentconfig":
|
|
case "defaultconfig":
|
|
case "singlelineresults":
|
|
case "doublelineresults":
|
|
case "separatesources", "noseparatesources":
|
|
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())
|
|
}
|
|
|
|
return os.Stdin.Close()
|
|
}
|
|
|
|
func (a *Arguments) Parse() error {
|
|
args := os.Args[1:]
|
|
usedNext := false
|
|
|
|
if len(args) < 1 {
|
|
if _, err := a.parseShortOption("-Syu", ""); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
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 == "" {
|
|
a.Op = "Y"
|
|
}
|
|
|
|
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
|
|
}
|