Add contextual help to Yay operations (#2921)

Instead of `yay -<op> --help` printing the entire help message, it will
now print a contextual help message based on the operation being
performed. The top-level help message shows available operations and
global flags as before. It no longer shows operation-specific flags and
`pacman -S --help` output. Like the pacman output, it shows that you can
get operation-specific help by running `yay -<op> --help`.

The operation-specific help message shows the operation's flags and all
pacman operation flags pass through to pacman.
This commit is contained in:
Joey Holtzman authored and GitHub committed 2026-07-31 03:04:39 +09:00
1 parent c7a6d20e62
commit 19a67d1dbd
4 files changed
+133 -26

No files matched your search

+82 -26
View File
@@ -26,6 +26,59 @@ import (
"github.com/Jguer/yay/v13/pkg/vcs"
)
type helpOption struct {
flags string
description string
}
type operationHelp struct {
short string
long string
targets string
options []helpOption
}
var yayOperationHelp = []operationHelp{
{"B", "build", "[options] [dir]", []helpOption{
{"-i --install", "Build and install a PKGBUILD"},
}},
{"G", "getpkgbuild", "[options] [package(s)]", []helpOption{
{"-f --force", "Force download for existing ABS packages"},
{"-p --print", "Print pkgbuild of packages"},
}},
{"P", "show", "[options]", []helpOption{
{"-c --complete", "Used for completions"},
{"-d --defaultconfig", "Print default yay configuration"},
{"-g --currentconfig", "Print current yay configuration"},
{"-s --stats", "Display system package statistics"},
{"-w --news", "Print arch news"},
{"-q --quiet", "Only show titles when printing news"},
}},
{"W", "web", "[options] [package(s)]", []helpOption{
{"-u --unvote", "Remove a vote from AUR package(s)"},
{"-v --vote", "Vote for AUR package(s)"},
}},
{"Y", "yay", "[options] [package(s)]", []helpOption{
{"-c --clean", "Remove unneeded dependencies (-cc to ignore optdepends)"},
{" --gendb", "Generates development package DB used for updating"},
}},
}
func (h *operationHelp) syntax() string {
return fmt.Sprintf("yay {-%s --%s}", h.short, h.long)
}
func findYayOperationHelp(operation string) *operationHelp {
for i := range yayOperationHelp {
help := &yayOperationHelp[i]
if operation == help.short || operation == help.long {
return help
}
}
return nil
}
func usage(logger *text.Logger) {
logger.Println(`Usage:
yay
@@ -41,15 +94,17 @@ operations:
yay {-R --remove} [options] <package(s)>
yay {-S --sync} [options] [package(s)]
yay {-T --deptest} [options] [package(s)]
yay {-U --upgrade} [options] <file(s)>
yay {-U --upgrade} [options] <file(s)>`)
New operations:
yay {-B --build} [options] [dir]
yay {-G --getpkgbuild} [options] [package(s)]
yay {-P --show} [options]
yay {-W --web} [options] [package(s)]
yay {-Y --yay} [options] [package(s)]
// Yay-specific operations
logger.Println("\nNew operations:")
for i := range yayOperationHelp {
help := &yayOperationHelp[i]
logger.Printf(" %-22s %s\n", help.syntax(), help.targets)
}
logger.Println("\nuse 'yay {-h --help}' with an operation for available options")
logger.Println(`
If no operation is specified 'yay -Syu' will be performed
If no operation is specified and targets are provided, -Y will be assumed
@@ -118,27 +173,29 @@ Permanent configuration options:
--sudo <file> sudo command to use
--sudoflags <flags> Pass arguments to sudo
--sudoloop Loop sudo calls in the background to avoid timeout
--sudoloop Loop sudo calls in the background to avoid timeout`)
}
show specific options (used with -P):
-c --complete Used for completions
-d --defaultconfig Print default yay configuration
-g --currentconfig Print current yay configuration
-s --stats Display system package statistics
-w --news Print arch news
func operationUsage(logger *text.Logger, operation string) {
help := findYayOperationHelp(operation)
if help == nil {
return
}
yay specific options (used with -Y):
-c --clean Remove unneeded dependencies (-cc to ignore optdepends)
--gendb Generates development package DB used for updating
getpkgbuild specific options (used with -G):
-f --force Force download for existing ABS packages
-p --print Print pkgbuild of packages`)
logger.Printf("Usage: %s %s\noptions:\n", help.syntax(), help.targets)
for _, option := range help.options {
logger.Printf(" %-21s %s\n", option.flags, option.description)
}
}
func handleCmd(ctx context.Context, run *runtime.Runtime,
cmdArgs *parser.Arguments, dbExecutor db.Executor,
) error {
if cmdArgs.Op == "V" || cmdArgs.Op == "version" {
handleVersion(run.Logger)
return nil
}
if cmdArgs.ExistsArg("h", "help") {
return handleHelp(ctx, run, cmdArgs)
}
@@ -148,9 +205,6 @@ func handleCmd(ctx context.Context, run *runtime.Runtime,
}
switch cmdArgs.Op {
case "V", "version":
handleVersion(run.Logger)
return nil
case "D", "database":
return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
cmdArgs, run.Cfg.Mode, settings.NoConfirm))
@@ -233,13 +287,15 @@ func handleQuery(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Argu
}
func handleHelp(ctx context.Context, run *runtime.Runtime, cmdArgs *parser.Arguments) error {
usage(run.Logger)
switch cmdArgs.Op {
case "Y", "yay", "G", "getpkgbuild", "P", "show", "W", "web", "B", "build":
operationUsage(run.Logger, cmdArgs.Op)
return nil
case "":
usage(run.Logger)
return nil
}
run.Logger.Println("\npacman operation specific options:")
return run.CmdBuilder.Show(run.CmdBuilder.BuildPacmanCmd(ctx,
cmdArgs, run.Cfg.Mode, settings.NoConfirm))
}
+37
View File
@@ -3,6 +3,7 @@
package main
import (
"bytes"
"context"
"fmt"
"io"
@@ -26,6 +27,42 @@ import (
"github.com/Jguer/yay/v13/pkg/vcs"
)
func TestHandleHelp(t *testing.T) {
t.Parallel()
tests := []struct {
name string
operation string
wantOutput string
wantCalls int
}{
{"top-level", "", "Usage:\n yay", 0},
{"yay operation", "Y", "Usage: yay {-Y --yay}", 0},
{"pacman operation", "S", "", 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var output bytes.Buffer
logger := text.NewLogger(&output, &output, strings.NewReader(""), false, "test")
runner := &exe.MockRunner{}
cmdArgs := parser.MakeArguments()
cmdArgs.Op = tt.operation
require.NoError(t, cmdArgs.AddArg("help"))
run := &runtime.Runtime{
Cfg: &settings.Configuration{Mode: parser.ModeAny},
CmdBuilder: &exe.CmdBuilder{PacmanBin: "pacman", PacmanConfigPath: "/etc/pacman.conf",
PacmanDBPath: t.TempDir(), Runner: runner, Log: logger},
Logger: logger,
}
require.NoError(t, handleHelp(t.Context(), run, cmdArgs))
assert.Contains(t, output.String(), tt.wantOutput)
assert.Len(t, runner.ShowCalls, tt.wantCalls)
})
}
}
func TestYogurtMenuAURDB(t *testing.T) {
t.Skip("skip until Operation service is an interface")
t.Parallel()
+4
View File
@@ -654,6 +654,10 @@ func (a *Arguments) Parse() error {
}
if a.Op == "" {
if a.ExistsArg("h", "help") {
return nil
}
if len(a.Targets) > 0 {
a.Op = "Y"
} else {
+10
View File
@@ -363,3 +363,13 @@ func TestArguments_ParseStdin_broken_pipe(t *testing.T) {
err = args.parseStdin()
assert.Error(t, err)
}
func TestArguments_ParseTopLevelHelp(t *testing.T) {
originalArgs := os.Args
t.Cleanup(func() { os.Args = originalArgs })
os.Args = []string{"yay", "--help"}
args := MakeArguments()
require.NoError(t, args.Parse())
assert.Empty(t, args.Op)
}