diff --git a/internal/cli/rules.go b/internal/cli/rules.go index 6f03c0ddd..fac10b1fd 100644 --- a/internal/cli/rules.go +++ b/internal/cli/rules.go @@ -67,10 +67,10 @@ func rulesCmd(cli *cli) *cobra.Command { func listRulesCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ - Use: "list", + Use: "list", Aliases: []string{"ls"}, - Short: "List your rules", - Long: `List the rules in your current tenant.`, + Short: "List your rules", + Long: `List the rules in your current tenant.`, Example: `auth0 rules list auth0 rules ls`, RunE: func(cmd *cobra.Command, args []string) error { @@ -106,9 +106,9 @@ func createRuleCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "Create a new rule", - Long: `Create a new rule:`, - Example: `auth0 rules create -auth0 rules create --name "My Rule" + Long: `Create a new rule:`, + Example: `auth0 rules create +auth0 rules create --name "My Rule" auth0 rules create -n "My Rule" --template "Empty rule" auth0 rules create -n "My Rule" -t "Empty rule" --enabled=false`, PreRun: func(cmd *cobra.Command, args []string) { @@ -129,6 +129,9 @@ auth0 rules create -n "My Rule" -t "Empty rule" --enabled=false`, script, err := prompt.CaptureInputViaEditor( ruleTemplateOptions.getValue(inputs.Template), inputs.Name+".*.js", + func() { + cli.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) + }, ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) @@ -169,7 +172,7 @@ func showRuleCmd(cli *cli) *cobra.Command { Use: "show", Args: cobra.MaximumNArgs(1), Short: "Show a rule", - Long: `Show a rule:`, + Long: `Show a rule:`, Example: `auth0 rules show auth0 rules show `, PreRun: func(cmd *cobra.Command, args []string) { @@ -213,7 +216,7 @@ func deleteRuleCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "delete", Short: "Delete a rule", - Long: `Delete a rule`, + Long: `Delete a rule`, Example: `auth0 rules delete auth0 rules delete `, PreRun: func(cmd *cobra.Command, args []string) { @@ -254,7 +257,7 @@ func updateRuleCmd(cli *cli) *cobra.Command { cmd := &cobra.Command{ Use: "update", Short: "Update a rule", - Long: `Update a rule`, + Long: `Update a rule`, Example: `auth0 rules update auth0 rules update --name "My Updated Rule" auth0 rules update -n "My Updated Rule" --enabled=false`, @@ -291,6 +294,9 @@ auth0 rules update -n "My Updated Rule" --enabled=false`, script, err := prompt.CaptureInputViaEditor( rule.GetScript(), rule.GetName()+".*.js", + func() { + cli.renderer.Infof("%s once you close the editor, the rule will be saved. To cancel, CTRL+C.", ansi.Faint("Hint:")) + }, ) if err != nil { return fmt.Errorf("Failed to capture input from the editor: %w", err) diff --git a/internal/prompt/editor.go b/internal/prompt/editor.go index a063c0969..709e7383b 100644 --- a/internal/prompt/editor.go +++ b/internal/prompt/editor.go @@ -1,68 +1,70 @@ package prompt import ( + "bytes" "fmt" "os" "os/exec" - "strings" + "runtime" + + "github.com/kballard/go-shellquote" ) -const defaultEditor = "vim" +const ( + defaultEditor = "vim" +) -var defaultEditorPrompt = &editorPrompt{defaultEditor: defaultEditor} +var ( + bom = []byte{0xef, 0xbb, 0xbf} + cliEditors = []string{"emacs", "micro", "nano", "nvim", "vi", "vim", "nvim"} +) + +var defaultEditorPrompt = &editorPrompt{cmd: getDefaultEditor()} // CaptureInputViaEditor is the high level function to use in this package in // order to capture input from an editor. // // The arguments have been tailored for our use of strings mostly in the rest // of the CLI even though internally we're using []byte. -func CaptureInputViaEditor(contents, pattern string) (result string, err error) { - v, err := defaultEditorPrompt.captureInput([]byte(contents), pattern) +func CaptureInputViaEditor(contents, pattern string, infoFn func()) (result string, err error) { + v, err := defaultEditorPrompt.captureInput([]byte(contents), pattern, infoFn) return string(v), err } type editorPrompt struct { - defaultEditor string + cmd string } -// GetPreferredEditorFromEnvironment returns the user's editor as defined by the -// `$EDITOR` environment variable, or the `defaultEditor` if it is not set. -func (p *editorPrompt) getPreferredEditor() string { - editor := os.Getenv("EDITOR") - - if editor == "" { - return p.defaultEditor +// openFile opens filename in the preferred text editor, resolving the +// arguments with editor specific logic. +func (p *editorPrompt) openFile(filename string, infoFn func()) error { + args, err := shellquote.Split(p.cmd) + if err != nil { + return err } + args = append(args, filename) - return editor -} - -func (p *editorPrompt) resolveEditorArguments(executable string, filename string) []string { - args := []string{filename} - - if strings.Contains(executable, "Visual Studio Code.app") { - args = append([]string{"--wait"}, args...) + isCLIEditor := false + for _, e := range cliEditors { + if e == args[0] { + isCLIEditor = true + } } - // TODO(cyx): add other common editors - - return args -} - -// openFile opens filename in the preferred text editor, resolving the -// arguments with editor specific logic. -func (p *editorPrompt) openFile(filename string) error { - // Get the full executable path for the editor. - executable, err := exec.LookPath(p.getPreferredEditor()) + editorExe, err := exec.LookPath(args[0]) if err != nil { return err } - cmd := exec.Command(executable, p.resolveEditorArguments(executable, filename)...) + cmd := exec.Command(editorExe, args[1:]...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr + if !isCLIEditor && infoFn != nil { + infoFn() + } + return cmd.Run() } @@ -72,7 +74,7 @@ func (p *editorPrompt) openFile(filename string) error { // // If given default contents, it will write that to the file before popping // open the editor. -func (p *editorPrompt) captureInput(contents []byte, pattern string) ([]byte, error) { +func (p *editorPrompt) captureInput(contents []byte, pattern string, infoFn func()) ([]byte, error) { file, err := os.CreateTemp(os.TempDir(), pattern) if err != nil { return []byte{}, err @@ -80,27 +82,54 @@ func (p *editorPrompt) captureInput(contents []byte, pattern string) ([]byte, er filename := file.Name() + // Defer removal of the temporary file in case any of the next steps fail. + defer os.Remove(filename) + + // write utf8 BOM header + // The reason why we do this is because notepad.exe on Windows determines the + // encoding of an "empty" text file by the locale, for example, GBK in China, + // while golang string only handles utf8 well. However, a text file with utf8 + // BOM header is not considered "empty" on Windows, and the encoding will then + // be determined utf8 by notepad.exe, instead of GBK or other encodings. + if _, err := file.Write(bom); err != nil { + return nil, err + } + if len(contents) > 0 { - if err := os.WriteFile(filename, contents, 0644); err != nil { + if _, err := file.Write(contents); err != nil { return nil, fmt.Errorf("Failed to write to file: %w", err) } } - // Defer removal of the temporary file in case any of the next steps fail. - defer os.Remove(filename) - if err = file.Close(); err != nil { return nil, err } - if err = p.openFile(filename); err != nil { + if err = p.openFile(filename, infoFn); err != nil { return nil, err } - bytes, err := os.ReadFile(filename) + raw, err := os.ReadFile(filename) if err != nil { return []byte{}, err } - return bytes, nil + // strip BOM header + return bytes.TrimPrefix(raw, bom), nil +} + +// getDefaultEditor is taken from https://github.com/cli/cli/blob/trunk/pkg/surveyext/editor_manual.go +// and tries to infer the editor from different heuristics. +func getDefaultEditor() string { + if runtime.GOOS == "windows" { + return "notepad" + } else if g := os.Getenv("GIT_EDITOR"); g != "" { + return g + } else if v := os.Getenv("VISUAL"); v != "" { + return v + } else if e := os.Getenv("EDITOR"); e != "" { + return e + } + + return defaultEditor }