Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,10 @@ Select **y** to proceed with your default tenant, or **N** to choose a different

## Customization

To change the text editor used for editing templates, rules, and actions, set the environment variable `EDITOR` to your
preferred editor. If choosing a non-terminal editor, ensure that the command starts the editor and waits for the files
to be closed before returning.
The default text editor is `vim` on Linux/macOS and `notepad` on Windows. To change that for editing templates, rules, and actions, set the environment variable `EDITOR` to your
preferred editor. If choosing a non-terminal editor, ensure that the command starts the editor and waits for the files to be closed before returning.

Examples:
### Linux / macOS

```shell
# Uses vscode with the --wait flag.
Expand All @@ -208,6 +207,26 @@ export EDITOR="nano"
export EDITOR="vim"
```

### Windows

```powershell
# PowerShell (current session).
$env:EDITOR = "code --wait"
$env:EDITOR = '"C:\Path To\bin\code" --wait'
$env:EDITOR = '"C:\Path To\notepad++.exe" --wait'

# PowerShell (persistent, across sessions).
[System.Environment]::SetEnvironmentVariable("EDITOR", "code --wait", "User")
```

```cmd
REM Command Prompt (current session).
set EDITOR=code --wait

REM Command Prompt (persistent, across sessions).
setx EDITOR "code --wait"
```

## Anonymized Analytics Disclosure

Anonymized data points are collected during the use of this CLI. This data includes the CLI version, operating system, timestamp, and other technical details that do not personally identify you.
Expand Down
4 changes: 1 addition & 3 deletions internal/prompt/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import (
"os/exec"
"runtime"

"github.com/kballard/go-shellquote"

"github.com/auth0/auth0-cli/internal/iostream"
)

Expand Down Expand Up @@ -40,7 +38,7 @@ type editorPrompt struct {
// 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)
args, err := parseEditorArgs(p.cmd)
if err != nil {
return err
}
Expand Down
14 changes: 14 additions & 0 deletions internal/prompt/editor_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//go:build !windows
// +build !windows

package prompt

import (
"github.com/kballard/go-shellquote"
)

// parseEditorArgs parses POSIX shell-style command line arguments
// into a slice of strings suitable for exec.Command.
func parseEditorArgs(cmd string) ([]string, error) {
return shellquote.Split(cmd)
}
73 changes: 73 additions & 0 deletions internal/prompt/editor_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//go:build !windows
// +build !windows

package prompt

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseEditorArgs(t *testing.T) {
tests := []struct {
name string
cmd string
expected []string
wantErr bool
}{
{
name: "simple editor",
cmd: "vim",
expected: []string{"vim"},
},
{
name: "editor with flag",
cmd: "code --wait",
expected: []string{"code", "--wait"},
},
{
name: "editor with multiple flags",
cmd: "subl --wait --new-window",
expected: []string{"subl", "--wait", "--new-window"},
},
{
name: "path with spaces in double quotes",
cmd: `"/usr/local/bin/my editor" --wait`,
expected: []string{"/usr/local/bin/my editor", "--wait"},
},
{
name: "path with spaces in single quotes",
cmd: `'/usr/local/bin/my editor' --wait`,
expected: []string{"/usr/local/bin/my editor", "--wait"},
},
{
name: "path without spoaces or quotes",
cmd: `/usr/local/bin/myeditor --wait`,
expected: []string{"/usr/local/bin/myeditor", "--wait"},
},
{
name: "empty command",
cmd: ``,
expected: []string{},
},
{
name: "unterminated quote",
cmd: `"unterminated`,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args, err := parseEditorArgs(tt.cmd)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, args)
})
}
}
44 changes: 44 additions & 0 deletions internal/prompt/editor_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//go:build windows
// +build windows

package prompt

import (
"fmt"
"syscall"
"unsafe"
)

// parseEditorArgs parses Windows-style command line arguments
// into a slice of strings suitable for exec.Command.
// Uses the Windows CommandLineToArgvW API to correctly handle
// backslash path separators and quoted paths with spaces.
func parseEditorArgs(cmd string) ([]string, error) {
if cmd == "" {
return []string{}, nil
}

// Convert the Go string (UTF-8) to a UTF-16 pointer, as required by Windows APIs.
utf16Cmd, err := syscall.UTF16PtrFromString(cmd)
if err != nil {
return nil, fmt.Errorf("failed to encode editor command: %w", err)
}

// Use the Windows CommandLineToArgvW API to split the command string into arguments.
// This correctly handles Windows path separators (\) and quoted paths with spaces.
var argc int32
argv, err := syscall.CommandLineToArgv(utf16Cmd, &argc)
if err != nil {
return nil, fmt.Errorf("failed to parse editor command %q: %w", cmd, err)
}
// argv is allocated by the Windows API and must be freed with LocalFree.
defer syscall.LocalFree(syscall.Handle(unsafe.Pointer(argv)))

// Convert each UTF-16 encoded argument back to a Go string.
args := make([]string, argc)
for i := range args {
args[i] = syscall.UTF16ToString((*argv[i])[:])
}

return args, nil
}
68 changes: 68 additions & 0 deletions internal/prompt/editor_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//go:build windows
// +build windows

package prompt

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseEditorArgs(t *testing.T) {
tests := []struct {
name string
cmd string
expected []string
wantErr bool
}{
{
name: "simple editor",
cmd: "notepad",
expected: []string{"notepad"},
},
{
name: "editor with flag",
cmd: "code --wait",
expected: []string{"code", "--wait"},
},
{
name: "windows path with backslashes",
cmd: `C:\Windows\notepad.exe`,
expected: []string{`C:\Windows\notepad.exe`},
},
{
name: "quoted path with spaces",
cmd: `"C:\Program Files\Notepad++\notepad++.exe" --wait`,
expected: []string{`C:\Program Files\Notepad++\notepad++.exe`, "--wait"},
},
{
name: "quoted path with spaces and multiple flags",
cmd: `"C:\Program Files\Microsoft VS Code\code.exe" --wait --new-window`,
expected: []string{`C:\Program Files\Microsoft VS Code\code.exe`, "--wait", "--new-window"},
},
{
name: "path without spaces or quotes",
cmd: `C:\tools\vim.exe -u C:\Users\me\.vimrc`,
expected: []string{`C:\tools\vim.exe`, "-u", `C:\Users\me\.vimrc`},
},
{
name: "empty command",
cmd: ``,
expected: []string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args, err := parseEditorArgs(tt.cmd)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.expected, args)
})
}
}
Loading