diff options
author | Robin Jarry <robin@jarry.cc> | 2023-09-19 22:51:49 +0200 |
---|---|---|
committer | Robin Jarry <robin@jarry.cc> | 2023-10-28 19:24:40 +0200 |
commit | b336a5c9e19adba31bec1da51e093a11e09a8ead (patch) | |
tree | 4ab356b699472e7fa9e1f2445b499f6b1d0ec039 | |
parent | a2a692e7736ef82627eae885d17024d92e33cb4a (diff) | |
download | aerc-b336a5c9e19adba31bec1da51e093a11e09a8ead.tar.gz aerc-b336a5c9e19adba31bec1da51e093a11e09a8ead.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.
Introduce a new dependency on git.sr.ht/~rjarry/go-opt to deal with
shell splitting. 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>
Reviewed-by: Koni Marti <koni.marti@gmail.com>
Tested-by: Moritz Poldrack <moritz@poldrack.dev>
Tested-by: Inwit <inwit@sindominio.net>
-rw-r--r-- | app/aerc.go | 24 | ||||
-rw-r--r-- | app/app.go | 6 | ||||
-rw-r--r-- | commands/choose.go | 3 | ||||
-rw-r--r-- | commands/commands.go | 58 | ||||
-rw-r--r-- | commands/commands_test.go | 114 | ||||
-rw-r--r-- | commands/prompt.go | 6 | ||||
-rw-r--r-- | go.mod | 3 | ||||
-rw-r--r-- | go.sum | 5 | ||||
-rw-r--r-- | lib/ipc/handler.go | 2 | ||||
-rw-r--r-- | lib/ipc/receive.go | 7 | ||||
-rw-r--r-- | main.go | 25 |
11 files changed, 60 insertions, 193 deletions
diff --git a/app/aerc.go b/app/aerc.go index dbde484f..f51aa242 100644 --- a/app/aerc.go +++ b/app/aerc.go @@ -10,10 +10,10 @@ import ( "strings" "time" + "git.sr.ht/~rjarry/go-opt" "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" @@ -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 @@ -47,12 +47,12 @@ type Aerc struct { type Choice struct { Key string Text string - Command []string + Command string } func (aerc *Aerc) Init( 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{}, ) { @@ -590,11 +590,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()) } @@ -615,10 +611,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 { @@ -631,7 +627,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) @@ -778,9 +774,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 { @@ -17,7 +17,7 @@ var aerc Aerc func Init( 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), history lib.History, deferLoop chan struct{}, ) { @@ -71,8 +71,8 @@ func PushStatus(text string, expiry time.Duration) *StatusMessage { return aerc.PushStatus(text, expiry) } -func RegisterChoices(choices []Choice) { aerc.RegisterChoices(choices) } -func RegisterPrompt(prompt string, cmd []string) { aerc.RegisterPrompt(prompt, cmd) } +func RegisterChoices(choices []Choice) { aerc.RegisterChoices(choices) } +func RegisterPrompt(prompt string, cmd string) { aerc.RegisterPrompt(prompt, cmd) } func CryptoProvider() crypto.Provider { return aerc.Crypto } func DecryptKeys(keys []openpgp.Key, symmetric bool) (b []byte, err error) { diff --git a/commands/choose.go b/commands/choose.go index 3f1410cf..6810ed1f 100644 --- a/commands/choose.go +++ b/commands/choose.go @@ -2,7 +2,6 @@ package commands import ( "fmt" - "strings" "git.sr.ht/~rjarry/aerc/app" ) @@ -34,7 +33,7 @@ func (Choose) Execute(args []string) error { choices = append(choices, app.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 73550a35..0ca8dc36 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -7,6 +7,7 @@ import ( "strings" "unicode" + "git.sr.ht/~rjarry/go-opt" "github.com/google/shlex" "git.sr.ht/~rjarry/aerc/app" @@ -111,68 +112,45 @@ func templateData( } func (cmds *Commands) ExecuteCommand( - origArgs []string, + cmdline string, account *config.AccountConfig, msg *models.MessageInfo, ) error { - if len(origArgs) == 0 { - return errors.New("Expected a command.") - } data := templateData(account, msg) - args, err := expand(data, origArgs) + cmdline, err := expand(data, cmdline) if err != nil { return err } - if len(args) == 0 { + args := opt.LexArgs(cmdline) + 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(args) + if cmd, ok := cmds.dict()[name]; ok { + log.Tracef("executing command %s", args.String()) + return cmd.Execute(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 f9f5fcc0..0d10ffa0 100644 --- a/commands/prompt.go +++ b/commands/prompt.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" + "git.sr.ht/~rjarry/go-opt" + "git.sr.ht/~rjarry/aerc/app" ) @@ -75,7 +77,7 @@ func (Prompt) Execute(args []string) error { } prompt := args[1] - cmd := args[2:] - app.RegisterPrompt(prompt, cmd) + cmd := opt.QuoteArgs(args[2:]...) + app.RegisterPrompt(prompt, cmd.String()) return nil } @@ -3,6 +3,7 @@ module git.sr.ht/~rjarry/aerc go 1.18 require ( + git.sr.ht/~rjarry/go-opt v1.2.0 git.sr.ht/~rockorager/go-jmap v0.3.0 git.sr.ht/~rockorager/tcell-term v0.8.0 git.sr.ht/~sircmpwn/getopt v1.0.0 @@ -30,7 +31,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rivo/uniseg v0.4.4 github.com/riywo/loginshell v0.0.0-20200815045211-7d26008be1ab - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.8.4 github.com/syndtr/goleveldb v1.0.0 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e golang.org/x/oauth2 v0.7.0 @@ -1,3 +1,5 @@ +git.sr.ht/~rjarry/go-opt v1.2.0 h1:/RPKvUxr/8k0TnNU30aGJhbRXuGNYWyn2wzVH+LMrCA= +git.sr.ht/~rjarry/go-opt v1.2.0/go.mod h1:oEPZUTJKGn1FVye0znaLoeskE/QTuyoJw5q+fjusdM4= git.sr.ht/~rockorager/go-jmap v0.3.0 h1:h2WuPcNyXRYFg9+W2HGf/mzIqC6ISy9EaS/BGa7Z5RY= git.sr.ht/~rockorager/go-jmap v0.3.0/go.mod h1:aOTCtwpZSINpDDSOkLGpHU0Kbbm5lcSDMcobX3ZtOjY= git.sr.ht/~rockorager/tcell-term v0.8.0 h1:jAAzWgTAzMz8uMXbOLZd5WgV7qmb6zRE0Z7HUrDdVPs= @@ -144,8 +146,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 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..47ffa89f 100644 --- a/lib/ipc/receive.go +++ b/lib/ipc/receive.go @@ -10,6 +10,8 @@ import ( "sync/atomic" "time" + "git.sr.ht/~rjarry/go-opt" + "git.sr.ht/~rjarry/aerc/config" "git.sr.ht/~rjarry/aerc/lib/xdg" "git.sr.ht/~rjarry/aerc/log" @@ -122,9 +124,8 @@ 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) + cmdline := opt.QuoteArgs(req.Arguments...) + err = as.handler.Command(cmdline.String()) if err != nil { return &Response{Error: err.Error()} } @@ -68,16 +68,13 @@ func getCommands(selected ui.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() { @@ -89,7 +86,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 @@ -99,19 +96,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( - cmd []string, + cmdline string, acct *config.AccountConfig, msg *models.MessageInfo, ) error { + name, rest, didCut := strings.Cut(cmdline, " ") cmds := getCommands(app.SelectedTabContent()) - cmd = expandAbbreviations(cmd, cmds) + cmdline = expandAbbreviations(name, cmds) + if didCut { + cmdline += " " + rest + } for i, set := range cmds { - err := set.ExecuteCommand(cmd, acct, msg) + err := set.ExecuteCommand(cmdline, acct, msg) if err != nil { if errors.As(err, new(commands.NoSuchCommand)) { if i == len(cmds)-1 { |