Skip to content

Commit db30342

Browse files
authored
Minor usability tweaks (#51)
* Use mvdan/sh for parsing and removing comments * Update readline dep * Update carapace dependency library * Update readline dependency * Ensure the completion function is initialized * Update completer.go * Update carapace to latest * Update deps, usability tweaks
1 parent af61a0f commit db30342

File tree

7 files changed

+149
-27
lines changed

7 files changed

+149
-27
lines changed

completer.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ func splitCompWords(input string) (words []string, remainder string, err error)
262262

263263
var word string
264264
word, input, err = splitCompWord(input, &buf)
265-
266265
if err != nil {
267266
return words, word + input, err
268267
}

errors.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package console
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
type (
9+
// ErrorHandler is a function that handles errors.
10+
//
11+
// The handler can choose not to bubble up the error by returning nil.
12+
ErrorHandler func(err error) error
13+
14+
// Err is the Console base error type.
15+
//
16+
// All errors that bubble up to the error handler should be
17+
// wrapped in this error type.
18+
//
19+
// There are more concrete error types that wrap this one defined below
20+
// this allow for easy use of errors.As.
21+
Err struct {
22+
err error
23+
message string
24+
}
25+
26+
// PreReadError is an error that occurs during the pre-read phase.
27+
PreReadError struct{ Err }
28+
29+
// ParseError is an error that occurs during the parsing phase.
30+
ParseError struct{ Err }
31+
32+
// LineHookError is an error that occurs during the line hook phase.
33+
LineHookError struct{ Err }
34+
35+
// ExecutionError is an error that occurs during the execution phase.
36+
ExecutionError struct{ Err }
37+
)
38+
39+
func defaultErrorHandler(err error) error {
40+
fmt.Fprintf(os.Stderr, "Error: %s\n", err)
41+
42+
return nil
43+
}
44+
45+
// newError creates a new Err.
46+
func newError(err error, message string) Err {
47+
return Err{
48+
err: err,
49+
message: message,
50+
}
51+
}
52+
53+
// Error returns the error message with an optional
54+
// message prefix.
55+
func (e Err) Error() string {
56+
if len(e.message) > 0 {
57+
return fmt.Sprintf("%s: %s", e.message, e.err.Error())
58+
}
59+
return e.err.Error()
60+
}
61+
62+
// Unwrap implements the errors Unwrap interface.
63+
func (e Err) Unwrap() error {
64+
return e.err
65+
}

example/main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ func main() {
2222

2323
app.SetPrintLogo(func(_ *console.Console) {
2424
fmt.Print(`
25-
_____ __ _ _ _ _____ _
26-
| __ \ / _| | | | (_) / ____| | |
27-
| |__) |___ ___| |_| | ___ ___| |_ ___ _____ | | ___ _ __ ___ ___ | | ___
25+
_____ __ _ _ _ _____ _
26+
| __ \ / _| | | | (_) / ____| | |
27+
| |__) |___ ___| |_| | ___ ___| |_ ___ _____ | | ___ _ __ ___ ___ | | ___
2828
| _ // _ \/ _ \ _| |/ _ \/ __| __| \ \ / / _ \ | | / _ \| '_ \/ __|/ _ \| |/ _ \
2929
| | \ \ __/ __/ | | | __/ (__| |_| |\ V / __/ | |___| (_) | | | \__ \ (_) | | __/
3030
|_| \_\___|\___|_| |_|\___|\___|\__|_| \_/ \___| \_____\___/|_| |_|___/\___/|_|\___|

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ go 1.21
44

55
require (
66
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
7-
github.com/reeflective/readline v1.0.13
7+
github.com/reeflective/readline v1.0.15
88
github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69
99
github.com/spf13/cobra v1.8.0
1010
github.com/spf13/pflag v1.0.5
11-
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
11+
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
1212
mvdan.cc/sh/v3 v3.7.0
1313
)
1414

1515
require (
1616
github.com/inconshreveable/mousetrap v1.1.0 // indirect
17-
github.com/rivo/uniseg v0.4.4 // indirect
17+
github.com/rivo/uniseg v0.4.7 // indirect
1818
github.com/rsteube/carapace-shlex v0.1.1 // indirect
19-
golang.org/x/sys v0.15.0 // indirect
20-
golang.org/x/term v0.15.0 // indirect
19+
golang.org/x/sys v0.24.0 // indirect
20+
golang.org/x/term v0.23.0 // indirect
2121
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
2222
gopkg.in/yaml.v3 v3.0.1 // indirect
2323
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1616
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1717
github.com/reeflective/readline v1.0.13 h1:TeJmYw9B7VRPZWfNExr9QHxL1m0iSicyqBSQIRn39Ss=
1818
github.com/reeflective/readline v1.0.13/go.mod h1:3iOe/qyb2jEy0KqLrNlb/CojBVqxga9ACqz/VU22H6A=
19+
github.com/reeflective/readline v1.0.15 h1:uB/M1sAc2yZGO14Ujgr/imLwQXqGdOhDDWAEHF+MBaE=
20+
github.com/reeflective/readline v1.0.15/go.mod h1:3iOe/qyb2jEy0KqLrNlb/CojBVqxga9ACqz/VU22H6A=
1921
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
2022
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
23+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
24+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
2125
github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY=
2226
github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
2327
github.com/rsteube/carapace v0.46.3-0.20231214181515-27e49f3c3b69 h1:ctOUuKn5PO6VtwtaS7unNrm6u20YXESPtnKEie/u304=
@@ -31,10 +35,16 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
3135
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
3236
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
3337
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
38+
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
39+
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
3440
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
3541
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
42+
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
43+
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3644
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
3745
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
46+
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
47+
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
3848
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3949
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
4050
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

menu.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type Menu struct {
2626
// Maps interrupt signals (CtrlC/IOF, etc) to specific error handlers.
2727
interruptHandlers map[error]func(c *Console)
2828

29+
// ErrorHandler is called when an error is encountered.
30+
//
31+
// If not set, the error is printed to the console on os.Stderr.
32+
ErrorHandler ErrorHandler
33+
2934
// Input/output channels
3035
out *bytes.Buffer
3136

@@ -60,6 +65,7 @@ func newMenu(name string, console *Console) *Menu {
6065
interruptHandlers: make(map[error]func(c *Console)),
6166
histories: make(map[string]readline.History),
6267
mutex: &sync.RWMutex{},
68+
ErrorHandler: defaultErrorHandler,
6369
}
6470

6571
// Add a default in memory history to each menu

run.go

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ import (
1010

1111
"github.com/kballard/go-shellquote"
1212
"github.com/spf13/cobra"
13+
"github.com/spf13/pflag"
1314
)
1415

1516
// Start - Start the console application (readline loop). Blocking.
1617
// The error returned will always be an error that the console
1718
// application does not understand or cannot handle.
1819
func (c *Console) Start() error {
20+
return c.StartContext(context.Background())
21+
}
22+
23+
// StartContext is like console.Start(). with a user-provided context.
24+
func (c *Console) StartContext(ctx context.Context) error {
1925
c.loadActiveHistories()
2026

2127
// Print the console logo
@@ -51,7 +57,8 @@ func (c *Console) Start() error {
5157
c.printed = false
5258

5359
if err := c.runAllE(c.PreReadlineHooks); err != nil {
54-
fmt.Printf("Pre-read error: %s\n", err.Error())
60+
menu.ErrorHandler(PreReadError{newError(err, "Pre-read error")})
61+
5562
continue
5663
}
5764

@@ -81,8 +88,7 @@ func (c *Console) Start() error {
8188
// Parse the line with bash-syntax, removing comments.
8289
args, err := c.parse(line)
8390
if err != nil {
84-
fmt.Printf("Parsing error: %s\n", err.Error())
85-
lastLine = line
91+
menu.ErrorHandler(ParseError{newError(err, "Parsing error")})
8692
continue
8793
}
8894

@@ -95,8 +101,7 @@ func (c *Console) Start() error {
95101
// which may modify the input line args.
96102
args, err = c.runLineHooks(args)
97103
if err != nil {
98-
fmt.Printf("Line error: %s\n", err.Error())
99-
lastLine = line
104+
menu.ErrorHandler(LineHookError{newError(err, "Line error")})
100105
continue
101106
}
102107

@@ -105,8 +110,8 @@ func (c *Console) Start() error {
105110
// the library user is responsible for setting
106111
// the cobra behavior.
107112
// If it's an interrupt, we take care of it.
108-
if err = c.execute(menu, args, false); err != nil {
109-
fmt.Println(err)
113+
if err := c.execute(ctx, menu, args, false); err != nil {
114+
menu.ErrorHandler(ExecutionError{newError(err, "")})
110115
}
111116
lastLine = line
112117
}
@@ -118,19 +123,19 @@ func (c *Console) Start() error {
118123
// workflow.
119124
// Although state segregation is a priority for this library to be ensured as much
120125
// as possible, you should be cautious when using this function to run commands.
121-
func (m *Menu) RunCommandArgs(args []string) (err error) {
126+
func (m *Menu) RunCommandArgs(ctx context.Context, args []string) (err error) {
122127
// The menu used and reset is the active menu.
123128
// Prepare its output buffer for the command.
124129
m.resetPreRun()
125130

126131
// Run the command and associated helpers.
127-
return m.console.execute(m, args, !m.console.isExecuting)
132+
return m.console.execute(ctx, m, args, !m.console.isExecuting)
128133
}
129134

130135
// RunCommandLine is the equivalent of menu.RunCommandArgs(), but accepts
131136
// an unsplit command line to execute. This line is split and processed in
132137
// *sh-compliant form, identically to how lines are in normal console usage.
133-
func (m *Menu) RunCommandLine(line string) (err error) {
138+
func (m *Menu) RunCommandLine(ctx context.Context, line string) (err error) {
134139
if len(line) == 0 {
135140
return
136141
}
@@ -141,7 +146,7 @@ func (m *Menu) RunCommandLine(line string) (err error) {
141146
return fmt.Errorf("line error: %w", err)
142147
}
143148

144-
return m.RunCommandArgs(args)
149+
return m.RunCommandArgs(ctx, args)
145150
}
146151

147152
// execute - The user has entered a command input line, the arguments have been processed:
@@ -150,7 +155,7 @@ func (m *Menu) RunCommandLine(line string) (err error) {
150155
// Our main object of interest is the menu's root command, and we explicitly use this reference
151156
// instead of the menu itself, because if RunCommand() is asynchronously triggered while another
152157
// command is running, the menu's root command will be overwritten.
153-
func (c *Console) execute(menu *Menu, args []string, async bool) (err error) {
158+
func (c *Console) execute(ctx context.Context, menu *Menu, args []string, async bool) error {
154159
if !async {
155160
c.mutex.RLock()
156161
c.isExecuting = true
@@ -169,12 +174,45 @@ func (c *Console) execute(menu *Menu, args []string, async bool) (err error) {
169174
// Find the target command: if this command is filtered, don't run it.
170175
target, _, _ := cmd.Find(args)
171176

172-
if err = menu.CheckIsAvailable(target); err != nil {
177+
if err := menu.CheckIsAvailable(target); err != nil {
173178
return err
174179
}
175180

181+
// Reset all flags to their default values.
182+
//
183+
// Slice flags accumulate per execution (and do not reset),
184+
// so we must reset them manually.
185+
//
186+
// Example:
187+
//
188+
// Given cmd.Flags().StringSlice("comment", nil, "")
189+
// If you run a command with --comment "a" --comment "b" you will get
190+
// the expected [a, b] slice.
191+
//
192+
// If you run a command again with no --comment flags, you will get
193+
// [a, b] again instead of an empty slice.
194+
//
195+
// If you run the command again with --comment "c" --comment "d" flags,
196+
// you will get [a, b, c, d] instead of just [c, d].
197+
target.Flags().VisitAll(func(flag *pflag.Flag) {
198+
flag.Changed = false
199+
switch value := flag.Value.(type) {
200+
case pflag.SliceValue:
201+
var res []string
202+
203+
if len(flag.DefValue) > 0 && flag.DefValue != "[]" {
204+
res = append(res, flag.DefValue)
205+
}
206+
207+
value.Replace(res)
208+
209+
default:
210+
flag.Value.Set(flag.DefValue)
211+
}
212+
})
213+
176214
// Console-wide pre-run hooks, cannot.
177-
if err = c.runAllE(c.PreCmdRunHooks); err != nil {
215+
if err := c.runAllE(c.PreCmdRunHooks); err != nil {
178216
return fmt.Errorf("pre-run error: %s", err.Error())
179217
}
180218

@@ -183,7 +221,7 @@ func (c *Console) execute(menu *Menu, args []string, async bool) (err error) {
183221

184222
// The command execution should happen in a separate goroutine,
185223
// and should notify the main goroutine when it is done.
186-
ctx, cancel := context.WithCancelCause(context.Background())
224+
ctx, cancel := context.WithCancelCause(ctx)
187225

188226
cmd.SetContext(ctx)
189227

@@ -196,22 +234,26 @@ func (c *Console) execute(menu *Menu, args []string, async bool) (err error) {
196234
// Wait for the command to finish, or for an OS signal to be caught.
197235
select {
198236
case <-ctx.Done():
199-
if !errors.Is(ctx.Err(), context.Canceled) {
200-
err = ctx.Err()
237+
cause := context.Cause(ctx)
238+
239+
if !errors.Is(cause, context.Canceled) {
240+
return cause
201241
}
202242

203243
case signal := <-sigchan:
204244
cancel(errors.New(signal.String()))
245+
205246
menu.handleInterrupt(errors.New(signal.String()))
206247
}
207248

208-
return err
249+
return nil
209250
}
210251

211252
// Run the command in a separate goroutine, and cancel the context when done.
212253
func (c *Console) executeCommand(cmd *cobra.Command, cancel context.CancelCauseFunc) {
213254
if err := cmd.Execute(); err != nil {
214255
cancel(err)
256+
215257
return
216258
}
217259

0 commit comments

Comments
 (0)