aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-09-19 22:51:49 +0200
committerRobin Jarry <robin@jarry.cc>2023-10-03 22:34:39 +0200
commit96ab33a5841fcb69fdd7f30661eb17b03a4d5b18 (patch)
tree5ba627a476c059aa35489bb6df4307f0c42b0dad
parentef69ca8707eb7072241c987527166b254378c1b5 (diff)
downloadaerc-96ab33a5841fcb69fdd7f30661eb17b03a4d5b18.tar.gz
aerc-96ab33a5841fcb69fdd7f30661eb17b03a4d5b18.zip
commands: pass raw command line down to template evaluation
Some commands need to invoke others and/or run shell commands. For this, we need the raw command line as entered by the user. Pass it down the call chain just before it is split to invoke the command Execute method. Remove unit tests for the template expand() test which does have any added value now that it is performed on a single string without any quote juggling. Update all code to handle a single string instead of a list of arguments. This is in preparation for using opt.ArgsToStruct to parse arguments for all aerc commands. There should be no functional change after this patch. Signed-off-by: Robin Jarry <robin@jarry.cc>
-rw-r--r--commands/choose.go3
-rw-r--r--commands/commands.go61
-rw-r--r--commands/commands_test.go114
-rw-r--r--commands/prompt.go8
-rw-r--r--lib/ipc/handler.go2
-rw-r--r--lib/ipc/receive.go8
-rw-r--r--main.go27
-rw-r--r--widgets/aerc.go24
8 files changed, 58 insertions, 189 deletions
diff --git a/commands/choose.go b/commands/choose.go
index 3b3af794..c18f0292 100644
--- a/commands/choose.go
+++ b/commands/choose.go
@@ -2,7 +2,6 @@ package commands
import (
"fmt"
- "strings"
"git.sr.ht/~rjarry/aerc/widgets"
)
@@ -34,7 +33,7 @@ func (Choose) Execute(aerc *widgets.Aerc, args []string) error {
choices = append(choices, widgets.Choice{
Key: args[i+2],
Text: args[i+3],
- Command: strings.Split(args[i+4], " "),
+ Command: args[i+4],
})
}
diff --git a/commands/commands.go b/commands/commands.go
index 9366be9c..4942f4aa 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -10,6 +10,7 @@ import (
"github.com/google/shlex"
"git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/opt"
"git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/log"
@@ -113,68 +114,48 @@ func templateData(
func (cmds *Commands) ExecuteCommand(
aerc *widgets.Aerc,
- origArgs []string,
+ cmdline string,
account *config.AccountConfig,
msg *models.MessageInfo,
) error {
- if len(origArgs) == 0 {
- return errors.New("Expected a command.")
- }
data := templateData(aerc, account, msg)
- args, err := expand(data, origArgs)
+ cmdline, err := expand(data, cmdline)
if err != nil {
return err
}
- if len(args) == 0 {
+ args, err := opt.SplitArgs(cmdline)
+ if err != nil {
+ return err
+ }
+ name, err := args.ArgSafe(0)
+ if err != nil {
return errors.New("Expected a command after template evaluation.")
}
- if cmd, ok := cmds.dict()[args[0]]; ok {
- log.Tracef("executing command %v", args)
- return cmd.Execute(aerc, args)
+ if cmd, ok := cmds.dict()[name]; ok {
+ log.Tracef("executing command %s", args.String())
+ return cmd.Execute(aerc, args.Args())
}
- return NoSuchCommand(args[0])
+ return NoSuchCommand(name)
}
-// expand expands template expressions and returns a new slice of arguments
-func expand(data models.TemplateData, origArgs []string) ([]string, error) {
- args := make([]string, len(origArgs))
- copy(args, origArgs)
-
- c := strings.Join(origArgs, "")
- isTemplate := strings.Contains(c, "{{") || strings.Contains(c, "}}")
-
- if isTemplate {
- for i := range args {
- if strings.Contains(args[i], " ") {
- q := "\""
- if strings.ContainsAny(args[i], "\"") {
- q = "'"
- }
- args[i] = q + args[i] + q
- }
- }
-
- cmdline := strings.Join(args, " ")
- log.Tracef("template data found in: %v", cmdline)
-
- t, err := templates.ParseTemplate("execute", cmdline)
+// expand expands template expressions
+func expand(data models.TemplateData, s string) (string, error) {
+ if strings.Contains(s, "{{") && strings.Contains(s, "}}") {
+ t, err := templates.ParseTemplate("execute", s)
if err != nil {
- return nil, err
+ return "", err
}
var buf bytes.Buffer
err = templates.Render(t, &buf, data)
if err != nil {
- return nil, err
+ return "", err
}
- args, err = splitCmd(buf.String())
- if err != nil {
- return nil, err
- }
+ s = buf.String()
}
- return args, nil
+ return s, nil
}
func GetTemplateCompletion(
diff --git a/commands/commands_test.go b/commands/commands_test.go
deleted file mode 100644
index 7edd0228..00000000
--- a/commands/commands_test.go
+++ /dev/null
@@ -1,114 +0,0 @@
-package commands
-
-import (
- "reflect"
- "testing"
- "time"
-
- "git.sr.ht/~rjarry/aerc/models"
- "github.com/emersion/go-message/mail"
-)
-
-func TestExecuteCommand_expand(t *testing.T) {
- tests := []struct {
- args []string
- want []string
- }{
- {
- args: []string{"prompt", "Really quit? ", "quit"},
- want: []string{"prompt", "Really quit? ", "quit"},
- },
- {
- args: []string{"{{", "print", "\"hello\"", "}}"},
- want: []string{"hello"},
- },
- {
- args: []string{"prompt", "Really quit ? ", " quit "},
- want: []string{"prompt", "Really quit ? ", " quit "},
- },
- {
- args: []string{
- "prompt", "Really quit? ", "{{",
- "print", "\"quit\"", "}}",
- },
- want: []string{"prompt", "Really quit? ", "quit"},
- },
- {
- args: []string{
- "prompt", "Really quit? ", "{{",
- "if", "1", "}}", "quit", "{{end}}",
- },
- want: []string{"prompt", "Really quit? ", "quit"},
- },
- }
-
- var data dummyData
-
- for i, test := range tests {
- got, err := expand(&data, test.args)
- if err != nil {
- t.Errorf("test %d failed with err: %v", i, err)
- } else if !reflect.DeepEqual(got, test.want) {
- t.Errorf("test %d failed: "+
- "got: %v, but want: %v", i, got, test.want)
- }
- }
-}
-
-// only for validation
-type dummyData struct{}
-
-var (
- addr1 = mail.Address{Name: "John Foo", Address: "foo@bar.org"}
- addr2 = mail.Address{Name: "John Bar", Address: "bar@foo.org"}
-)
-
-func (d *dummyData) Account() string { return "work" }
-func (d *dummyData) Folder() string { return "INBOX" }
-func (d *dummyData) To() []*mail.Address { return []*mail.Address{&addr1} }
-func (d *dummyData) Cc() []*mail.Address { return nil }
-func (d *dummyData) Bcc() []*mail.Address { return nil }
-func (d *dummyData) From() []*mail.Address { return []*mail.Address{&addr2} }
-func (d *dummyData) Peer() []*mail.Address { return d.From() }
-func (d *dummyData) ReplyTo() []*mail.Address { return nil }
-func (d *dummyData) Date() time.Time { return time.Now() }
-func (d *dummyData) DateAutoFormat(time.Time) string { return "" }
-func (d *dummyData) Header(string) string { return "" }
-func (d *dummyData) ThreadPrefix() string { return "└─>" }
-func (d *dummyData) ThreadCount() int { return 0 }
-func (d *dummyData) ThreadFolded() bool { return false }
-func (d *dummyData) ThreadContext() bool { return false }
-func (d *dummyData) Subject() string { return "Re: [PATCH] hey" }
-func (d *dummyData) SubjectBase() string { return "[PATCH] hey" }
-func (d *dummyData) Number() int { return 0 }
-func (d *dummyData) Labels() []string { return nil }
-func (d *dummyData) Flags() []string { return nil }
-func (d *dummyData) IsReplied() bool { return true }
-func (d *dummyData) HasAttachment() bool { return true }
-func (d *dummyData) Attach(string) string { return "" }
-func (d *dummyData) IsRecent() bool { return false }
-func (d *dummyData) IsUnread() bool { return false }
-func (d *dummyData) IsFlagged() bool { return false }
-func (d *dummyData) IsMarked() bool { return false }
-func (d *dummyData) MessageId() string { return "123456789@foo.org" }
-func (d *dummyData) Size() int { return 420 }
-func (d *dummyData) OriginalText() string { return "Blah blah blah" }
-func (d *dummyData) OriginalDate() time.Time { return time.Now() }
-func (d *dummyData) OriginalFrom() []*mail.Address { return d.From() }
-func (d *dummyData) OriginalMIMEType() string { return "text/plain" }
-func (d *dummyData) OriginalHeader(string) string { return "" }
-func (d *dummyData) Recent(...string) int { return 1 }
-func (d *dummyData) Unread(...string) int { return 3 }
-func (d *dummyData) Exists(...string) int { return 14 }
-func (d *dummyData) RUE(...string) string { return "1/3/14" }
-func (d *dummyData) Connected() bool { return false }
-func (d *dummyData) ConnectionInfo() string { return "" }
-func (d *dummyData) ContentInfo() string { return "" }
-func (d *dummyData) StatusInfo() string { return "" }
-func (d *dummyData) TrayInfo() string { return "" }
-func (d *dummyData) PendingKeys() string { return "" }
-func (d *dummyData) Role() string { return "inbox" }
-func (d *dummyData) Style(string, string) string { return "" }
-func (d *dummyData) StyleSwitch(string, ...models.Case) string { return "" }
-
-func (d *dummyData) StyleMap([]string, ...models.Case) []string { return []string{} }
diff --git a/commands/prompt.go b/commands/prompt.go
index a93d19a9..3d84703d 100644
--- a/commands/prompt.go
+++ b/commands/prompt.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
+ "git.sr.ht/~rjarry/aerc/lib/opt"
"git.sr.ht/~rjarry/aerc/widgets"
)
@@ -75,7 +76,10 @@ func (Prompt) Execute(aerc *widgets.Aerc, args []string) error {
}
prompt := args[1]
- cmd := args[2:]
- aerc.RegisterPrompt(prompt, cmd)
+ cmd, err := opt.QuoteArgs(args[2:]...)
+ if err != nil {
+ return err
+ }
+ aerc.RegisterPrompt(prompt, cmd.String())
return nil
}
diff --git a/lib/ipc/handler.go b/lib/ipc/handler.go
index c00acd63..10f42753 100644
--- a/lib/ipc/handler.go
+++ b/lib/ipc/handler.go
@@ -5,5 +5,5 @@ import "net/url"
type Handler interface {
Mailto(addr *url.URL) error
Mbox(source string) error
- Command(args []string) error
+ Command(cmdline string) error
}
diff --git a/lib/ipc/receive.go b/lib/ipc/receive.go
index 29ed0808..08eb909b 100644
--- a/lib/ipc/receive.go
+++ b/lib/ipc/receive.go
@@ -11,6 +11,7 @@ import (
"time"
"git.sr.ht/~rjarry/aerc/config"
+ "git.sr.ht/~rjarry/aerc/lib/opt"
"git.sr.ht/~rjarry/aerc/lib/xdg"
"git.sr.ht/~rjarry/aerc/log"
)
@@ -122,9 +123,10 @@ func (as *AercServer) handleMessage(req *Request) *Response {
Error: "command rejected: IPC is disabled",
}
}
-
- req.Arguments[0] = strings.TrimPrefix(req.Arguments[0], ":")
- err := as.handler.Command(req.Arguments)
+ args, err := opt.QuoteArgs(req.Arguments...)
+ if err == nil {
+ err = as.handler.Command(args.String())
+ }
if err != nil {
return &Response{Error: err.Error()}
}
diff --git a/main.go b/main.go
index 0f897bd0..707f0444 100644
--- a/main.go
+++ b/main.go
@@ -67,16 +67,13 @@ func getCommands(selected libui.Drawable) []*commands.Commands {
// :q --> :quit
// :ar --> :archive
// :im --> :import-mbox
-func expandAbbreviations(cmd []string, sets []*commands.Commands) []string {
- if len(cmd) == 0 {
- return cmd
- }
- name := strings.TrimLeft(cmd[0], ":")
+func expandAbbreviations(name string, sets []*commands.Commands) string {
+ name = strings.TrimLeft(name, ":")
candidate := ""
for _, set := range sets {
if set.ByName(name) != nil {
// Direct match, return it directly.
- return cmd
+ return name
}
// Check for partial matches.
for _, n := range set.Names() {
@@ -88,7 +85,7 @@ func expandAbbreviations(cmd []string, sets []*commands.Commands) []string {
// matching the input. We can't expand such an
// abbreviation, so return the command as is so
// it can raise an error later.
- return cmd
+ return name
}
// We have a partial match.
candidate = n
@@ -98,19 +95,23 @@ func expandAbbreviations(cmd []string, sets []*commands.Commands) []string {
// name in `cmd`. In that case we replace the name in `cmd` with the
// full name, otherwise we simply return `cmd` as is.
if candidate != "" {
- cmd[0] = candidate
+ name = candidate
}
- return cmd
+ return name
}
func execCommand(
- aerc *widgets.Aerc, ui *libui.UI, cmd []string,
+ aerc *widgets.Aerc, ui *libui.UI, cmdline string,
acct *config.AccountConfig, msg *models.MessageInfo,
) error {
+ name, rest, didCut := strings.Cut(cmdline, " ")
cmds := getCommands(aerc.SelectedTabContent())
- cmd = expandAbbreviations(cmd, cmds)
+ cmdline = expandAbbreviations(name, cmds)
+ if didCut {
+ cmdline += " " + rest
+ }
for i, set := range cmds {
- err := set.ExecuteCommand(aerc, cmd, acct, msg)
+ err := set.ExecuteCommand(aerc, cmdline, acct, msg)
if err != nil {
if errors.As(err, new(commands.NoSuchCommand)) {
if i == len(cmds)-1 {
@@ -243,7 +244,7 @@ func main() {
defer c.Close()
aerc = widgets.NewAerc(c, func(
- cmd []string, acct *config.AccountConfig,
+ cmd string, acct *config.AccountConfig,
msg *models.MessageInfo,
) error {
return execCommand(aerc, ui, cmd, acct, msg)
diff --git a/widgets/aerc.go b/widgets/aerc.go
index efa13194..66147e42 100644
--- a/widgets/aerc.go
+++ b/widgets/aerc.go
@@ -13,11 +13,11 @@ import (
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/emersion/go-message/mail"
"github.com/gdamore/tcell/v2"
- "github.com/google/shlex"
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/crypto"
+ "git.sr.ht/~rjarry/aerc/lib/opt"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/log"
"git.sr.ht/~rjarry/aerc/models"
@@ -26,7 +26,7 @@ import (
type Aerc struct {
accounts map[string]*AccountView
- cmd func([]string, *config.AccountConfig, *models.MessageInfo) error
+ cmd func(string, *config.AccountConfig, *models.MessageInfo) error
cmdHistory lib.History
complete func(cmd string) ([]string, string)
focused ui.Interactive
@@ -48,12 +48,12 @@ type Aerc struct {
type Choice struct {
Key string
Text string
- Command []string
+ Command string
}
func NewAerc(
crypto crypto.Provider,
- cmd func([]string, *config.AccountConfig, *models.MessageInfo) error,
+ cmd func(string, *config.AccountConfig, *models.MessageInfo) error,
complete func(cmd string) ([]string, string), cmdHistory lib.History,
deferLoop chan struct{},
) *Aerc {
@@ -597,11 +597,7 @@ func (aerc *Aerc) BeginExCommand(cmd string) {
}
}
exline := NewExLine(cmd, func(cmd string) {
- parts, err := shlex.Split(cmd)
- if err != nil {
- aerc.PushError(err.Error())
- }
- err = aerc.cmd(parts, nil, nil)
+ err := aerc.cmd(cmd, nil, nil)
if err != nil {
aerc.PushError(err.Error())
}
@@ -622,10 +618,10 @@ func (aerc *Aerc) PushPrompt(prompt *ExLine) {
aerc.prompts.Push(prompt)
}
-func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
+func (aerc *Aerc) RegisterPrompt(prompt string, cmd string) {
p := NewPrompt(prompt, func(text string) {
if text != "" {
- cmd = append(cmd, text)
+ cmd += " " + opt.QuoteArg(text)
}
err := aerc.cmd(cmd, nil, nil)
if err != nil {
@@ -638,7 +634,7 @@ func (aerc *Aerc) RegisterPrompt(prompt string, cmd []string) {
}
func (aerc *Aerc) RegisterChoices(choices []Choice) {
- cmds := make(map[string][]string)
+ cmds := make(map[string]string)
texts := []string{}
for _, c := range choices {
text := fmt.Sprintf("[%s] %s", c.Key, c.Text)
@@ -785,9 +781,9 @@ func (aerc *Aerc) Mbox(source string) error {
return nil
}
-func (aerc *Aerc) Command(args []string) error {
+func (aerc *Aerc) Command(cmd string) error {
defer ui.Invalidate()
- return aerc.cmd(args, nil, nil)
+ return aerc.cmd(cmd, nil, nil)
}
func (aerc *Aerc) CloseBackends() error {