Skip to content

Commit d242a1e

Browse files
committed
Fix cli output to remove control chars
1 parent ee9c404 commit d242a1e

17 files changed

Lines changed: 176 additions & 38 deletions

cmd/openrun/app_cmds.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import (
1616
"github.com/urfave/cli/v2"
1717
)
1818

19+
var DRY_RUN_MESSAGE = "\n" + YELLOW + "*** dry-run mode, changes have NOT been committed. ***" + RESET + "\n"
20+
1921
const (
20-
DRY_RUN_FLAG = "dry-run"
21-
DRY_RUN_ARG = "dryRun"
22-
DRY_RUN_MESSAGE = "\n" + YELLOW + "*** dry-run mode, changes have NOT been committed. ***" + RESET + "\n"
23-
PATH_SPEC_HELP = `The (optional) domain and path are separated by a ":". appPathGlob supports a glob pattern.
22+
DRY_RUN_FLAG = "dry-run"
23+
DRY_RUN_ARG = "dryRun"
24+
PATH_SPEC_HELP = `The (optional) domain and path are separated by a ":". appPathGlob supports a glob pattern.
2425
In the glob, * matches any number of characters, ** matches any number of characters including /.
2526
all is a shortcut for "*:**", which matches all apps across all domains, including no domain.
2627
To prevent shell expansion for *, placing the path in quotes is recommended.
@@ -259,7 +260,7 @@ func appListCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig) *c
259260
flags := make([]cli.Flag, 0, len(commonFlags)+2)
260261
flags = append(flags, commonFlags...)
261262
flags = append(flags, newBoolFlag("internal", "i", "Include internal apps", false))
262-
flags = append(flags, newStringFlag("format", "f", "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty", ""))
263+
flags = append(flags, newFormatFlag())
263264

264265
return &cli.Command{
265266
Name: "list",

cmd/openrun/bindings.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ Examples:
225225
func bindingGetCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig) *cli.Command {
226226
flags := make([]cli.Flag, 0, len(commonFlags)+1)
227227
flags = append(flags, commonFlags...)
228-
flags = append(flags, newStringFlag("format", "f", "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty", ""))
228+
flags = append(flags, newFormatFlag())
229229

230230
return &cli.Command{
231231
Name: "get",
@@ -306,7 +306,7 @@ func bindingListCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig
306306
flags := make([]cli.Flag, 0, len(commonFlags)+2)
307307
flags = append(flags, commonFlags...)
308308
flags = append(flags, newStringFlag(SOURCE_FLAG, "s", "Filter bindings by source", ""))
309-
flags = append(flags, newStringFlag("format", "f", "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty", ""))
309+
flags = append(flags, newFormatFlag())
310310

311311
return &cli.Command{
312312
Name: "list",

cmd/openrun/color_unix.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) ClaceIO, LLC
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build !windows
5+
6+
package main
7+
8+
// enableVirtualTerminal is a no-op on non-Windows platforms, the terminal
9+
// handles ANSI escape sequences when the output is a tty
10+
func enableVirtualTerminal() bool {
11+
return true
12+
}

cmd/openrun/color_windows.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) ClaceIO, LLC
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//go:build windows
5+
6+
package main
7+
8+
import (
9+
"os"
10+
11+
"golang.org/x/sys/windows"
12+
)
13+
14+
// enableVirtualTerminal enables ANSI escape sequence processing on the
15+
// consoles attached to stdout and stderr. Returns false if the console does
16+
// not support it, in which case colored output is disabled
17+
func enableVirtualTerminal() bool {
18+
for _, f := range []*os.File{os.Stdout, os.Stderr} {
19+
handle := windows.Handle(f.Fd())
20+
var mode uint32
21+
if err := windows.GetConsoleMode(handle, &mode); err != nil {
22+
return false
23+
}
24+
if mode&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
25+
continue
26+
}
27+
if err := windows.SetConsoleMode(handle, mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil {
28+
return false
29+
}
30+
}
31+
return true
32+
}

cmd/openrun/flags.go

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"os"
99
"path/filepath"
10+
"slices"
1011
"strings"
1112

1213
"github.com/openrundev/openrun/internal/system"
@@ -22,13 +23,54 @@ const (
2223
FORMAT_JSONL_PRETTY = "jsonl_pretty"
2324
FORMAT_CSV = "csv"
2425
)
25-
const (
26-
//Terminal colors
27-
RESET = "\033[0m"
28-
RED = "\033[31m"
29-
GREEN = "\033[32m"
30-
YELLOW = "\033[33m"
31-
)
26+
27+
var validFormats = []string{FORMAT_TABLE, FORMAT_BASIC, FORMAT_CSV, FORMAT_JSON, FORMAT_JSONL, FORMAT_JSONL_PRETTY}
28+
29+
// newFormatFlag creates the output format flag, validating the value at parse
30+
// time so an invalid format is reported as an error instead of a panic
31+
func newFormatFlag() *cli.StringFlag {
32+
return &cli.StringFlag{
33+
Name: "format",
34+
Aliases: []string{"f"},
35+
Usage: "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty",
36+
Action: func(_ *cli.Context, value string) error {
37+
if !slices.Contains(validFormats, value) {
38+
return fmt.Errorf("invalid format %q: valid options are %s", value, strings.Join(validFormats, ", "))
39+
}
40+
return nil
41+
},
42+
}
43+
}
44+
// Terminal colors, empty strings when the terminal does not support ANSI escape sequences
45+
var RESET, RED, GREEN, YELLOW = initColors()
46+
47+
func initColors() (string, string, string, string) {
48+
if colorsSupported() {
49+
return "\033[0m", "\033[31m", "\033[32m", "\033[33m"
50+
}
51+
return "", "", "", ""
52+
}
53+
54+
// colorsSupported reports whether colored output should be used: disabled if
55+
// NO_COLOR is set, TERM is dumb, stdout/stderr are not terminals, or the
56+
// platform cannot process ANSI escape sequences
57+
func colorsSupported() bool {
58+
if _, ok := os.LookupEnv("NO_COLOR"); ok {
59+
return false
60+
}
61+
if os.Getenv("TERM") == "dumb" {
62+
return false
63+
}
64+
return isTerminal(os.Stdout) && isTerminal(os.Stderr) && enableVirtualTerminal()
65+
}
66+
67+
func isTerminal(f *os.File) bool {
68+
info, err := f.Stat()
69+
if err != nil {
70+
return false
71+
}
72+
return info.Mode()&os.ModeCharDevice != 0
73+
}
3274

3375
func newStringFlag(name, alias, usage, value string) *cli.StringFlag {
3476
var aliases []string

cmd/openrun/main.go

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ package main
55

66
import (
77
"fmt"
8-
"log"
98
"os"
109
"path"
1110
"path/filepath"
1211
"runtime"
12+
"slices"
1313
"strings"
1414

1515
"github.com/urfave/cli/v2"
@@ -199,13 +199,50 @@ func parseConfig(cCtx *cli.Context, globalConfig *types.GlobalConfig, clientConf
199199
if err := system.LoadClientConfig(string(buf), clientConfig); err != nil {
200200
return err
201201
}
202+
if !slices.Contains(validFormats, clientConfig.Client.DefaultFormat) {
203+
return fmt.Errorf("invalid client.default_format %q in config: valid options are %s",
204+
clientConfig.Client.DefaultFormat, strings.Join(validFormats, ", "))
205+
}
202206
if err := system.LoadServerConfig(string(buf), serverConfig); err != nil {
203207
return err
204208
}
205209

206210
return nil
207211
}
208212

213+
// fatalError prints the error to stderr and exits
214+
func fatalError(err error) {
215+
fmt.Fprintf(os.Stderr, RED+"error: %s"+RESET+"\n", err) //nolint:errcheck
216+
system.NotifyServiceFailed(1)
217+
os.Exit(1)
218+
}
219+
220+
// setUsageErrorHandlers routes flag parsing errors through the app
221+
// ExitErrHandler so they are printed to stderr instead of the urfave/cli
222+
// default of printing usage errors on stdout
223+
func setUsageErrorHandlers(commands []*cli.Command, helpPath string) {
224+
for _, command := range commands {
225+
commandPath := helpPath + " " + command.Name
226+
if command.OnUsageError == nil {
227+
command.OnUsageError = func(_ *cli.Context, err error, _ bool) error {
228+
return usageError(command.Flags, commandPath, err)
229+
}
230+
}
231+
setUsageErrorHandlers(command.Subcommands, commandPath)
232+
}
233+
}
234+
235+
// usageError adds a did-you-mean suggestion for unknown flags and a --help
236+
// hint to flag parsing errors
237+
func usageError(flags []cli.Flag, helpPath string, err error) error {
238+
if flagName, ok := strings.CutPrefix(err.Error(), "flag provided but not defined: -"); ok && cli.SuggestFlag != nil {
239+
if suggestion := cli.SuggestFlag(flags, flagName, false); suggestion != "" {
240+
return fmt.Errorf("%w (did you mean %q?)\nrun '%s --help' for usage", err, suggestion, helpPath)
241+
}
242+
}
243+
return fmt.Errorf("%w\nrun '%s --help' for usage", err, helpPath)
244+
}
245+
209246
func main() {
210247
// Start the OS service control handler if launched by the Windows
211248
// service control manager. Must run before any long initialization so
@@ -214,16 +251,17 @@ func main() {
214251

215252
globalConfig, clientConfig, serverConfig, err := system.GetDefaultConfigs()
216253
if err != nil {
217-
log.Fatal(err)
254+
fatalError(err)
218255
}
219256
globalFlags, err := globalFlags(globalConfig, clientConfig)
220257
if err != nil {
221-
log.Fatal(err)
258+
fatalError(err)
222259
}
223260
allCommands, err := getAllCommands(clientConfig, serverConfig)
224261
if err != nil {
225-
log.Fatal(err)
262+
fatalError(err)
226263
}
264+
setUsageErrorHandlers(allCommands, "openrun")
227265

228266
app := &cli.App{
229267
Name: "openrun",
@@ -249,9 +287,7 @@ func main() {
249287
},
250288
ExitErrHandler: func(c *cli.Context, err error) {
251289
if err != nil {
252-
fmt.Fprintf(cli.ErrWriter, RED+"error: %s\n"+RESET, err) //nolint:errcheck
253-
system.NotifyServiceFailed(1)
254-
os.Exit(1)
290+
fatalError(err)
255291
}
256292
},
257293
Commands: allCommands,
@@ -262,14 +298,26 @@ func main() {
262298
os.Exit(0)
263299
return nil
264300
}
301+
if ctx.Args().Present() {
302+
// An unknown command should fail instead of showing the help and exiting 0
303+
arg := ctx.Args().First()
304+
if cli.SuggestCommand != nil {
305+
if suggestion := cli.SuggestCommand(ctx.App.Commands, arg); suggestion != "" {
306+
return fmt.Errorf("unknown command %q. %s\nrun 'openrun --help' for usage", arg, suggestion)
307+
}
308+
}
309+
return fmt.Errorf("unknown command %q\nrun 'openrun --help' for usage", arg)
310+
}
265311
return cli.ShowAppHelp(ctx)
266312
},
267313
}
268314

315+
app.OnUsageError = func(_ *cli.Context, err error, _ bool) error {
316+
return usageError(app.Flags, "openrun", err)
317+
}
318+
269319
if err := app.Run(normalizeInterspersedFlags(app, os.Args)); err != nil {
270-
fmt.Fprintf(os.Stderr, "error: %s", err) //nolint:errcheck
271-
system.NotifyServiceFailed(1)
272-
os.Exit(1)
320+
fatalError(err)
273321
}
274322
}
275323

cmd/openrun/misc_cmds.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,19 @@ func bootstrapConfigFile(clHome, configFile string) error {
101101
return nil
102102
}
103103

104+
// promptPassword prompts on stderr so the command output can be redirected
104105
func promptPassword(prompt string) (string, error) {
105-
fmt.Print(prompt)
106+
fmt.Fprint(os.Stderr, prompt)
106107
password, err := readPassword()
107108
if err != nil {
108109
return "", err
109110
}
110-
fmt.Print("\nConfirm password: ")
111+
fmt.Fprint(os.Stderr, "\nConfirm password: ")
111112
confirmPassword, err := readPassword()
112113
if err != nil {
113114
return "", err
114115
}
116+
fmt.Fprintln(os.Stderr)
115117
if password != confirmPassword {
116118
return "", fmt.Errorf("passwords do not match")
117119
}

cmd/openrun/provider_cmds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func providerUninstallCommand(commonFlags []cli.Flag, clientConfig *types.Client
119119
func providerListCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig) *cli.Command {
120120
flags := make([]cli.Flag, 0, len(commonFlags)+1)
121121
flags = append(flags, commonFlags...)
122-
flags = append(flags, newStringFlag("format", "f", "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty", ""))
122+
flags = append(flags, newFormatFlag())
123123

124124
return &cli.Command{
125125
Name: "list",

cmd/openrun/replication_cmds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ func initReplicationCommand(commonFlags []cli.Flag, clientConfig *types.ClientCo
2828
func replicationStatusCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig) *cli.Command {
2929
flags := make([]cli.Flag, 0, len(commonFlags)+1)
3030
flags = append(flags, commonFlags...)
31-
flags = append(flags, newStringFlag("format", "f", "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty", ""))
31+
flags = append(flags, newFormatFlag())
3232

3333
return &cli.Command{
3434
Name: "status",

cmd/openrun/secret_cmds.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ func secretListCommand(commonFlags []cli.Flag, clientConfig *types.ClientConfig)
166166
flags := make([]cli.Flag, 0, len(commonFlags)+2)
167167
flags = append(flags, commonFlags...)
168168
flags = append(flags, secretProviderFlag())
169-
flags = append(flags, newStringFlag("format", "f", "The display format. Valid options are table, basic, csv, json, jsonl and jsonl_pretty", ""))
169+
flags = append(flags, newFormatFlag())
170170

171171
return &cli.Command{
172172
Name: "list",

0 commit comments

Comments
 (0)