Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for EDITOR with args to commands #1174

Merged
merged 8 commits into from
Oct 29, 2024
15 changes: 2 additions & 13 deletions cli/consumer_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"math"
"math/rand"
"os"
"os/exec"
"os/signal"
"regexp"
"sort"
Expand Down Expand Up @@ -561,11 +560,6 @@ func (c *consumerCmd) leaderStandDownAction(_ *fisk.ParseContext) error {
}

func (c *consumerCmd) interactiveEdit(cfg api.ConsumerConfig) (*api.ConsumerConfig, error) {
editor := os.Getenv("EDITOR")
if editor == "" {
return &api.ConsumerConfig{}, fmt.Errorf("set EDITOR environment variable to your chosen editor")
}

cj, err := decoratedYamlMarshal(cfg)
if err != nil {
return &api.ConsumerConfig{}, fmt.Errorf("could not create temporary file: %s", err)
Expand All @@ -584,14 +578,9 @@ func (c *consumerCmd) interactiveEdit(cfg api.ConsumerConfig) (*api.ConsumerConf

tfile.Close()

cmd := exec.Command(editor, tfile.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
err = iu.EditFile(tfile.Name())
if err != nil {
return &api.ConsumerConfig{}, fmt.Errorf("could not create temporary file: %s", err)
return &api.ConsumerConfig{}, err
}

nb, err := os.ReadFile(tfile.Name())
Expand Down
23 changes: 6 additions & 17 deletions cli/context_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"os/user"
"sort"
"strings"
Expand Down Expand Up @@ -234,11 +233,6 @@ socks_proxy: {{ .SocksProxy | t }}
`

func (c *ctxCommand) editCommand(pc *fisk.ParseContext) error {
editor := os.Getenv("EDITOR")
if editor == "" {
return fmt.Errorf("set EDITOR environment variable to your chosen editor")
}

if !natscontext.IsKnown(c.name) {
return fmt.Errorf("unknown context %q", c.name)
}
Expand All @@ -247,7 +241,7 @@ func (c *ctxCommand) editCommand(pc *fisk.ParseContext) error {
if err != nil {
return err
}
editFile := path
editFp := path

var ctx *natscontext.Context

Expand Down Expand Up @@ -286,21 +280,16 @@ func (c *ctxCommand) editCommand(pc *fisk.ParseContext) error {
}
f.Close()

editFile = f.Name()
editFp = f.Name()
}

cmd := exec.Command(editor, editFile)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
err = iu.EditFile(editFp)
if err != nil {
return err
}

if path != editFile {
yctx, err := os.ReadFile(editFile)
if path != editFp {
yctx, err := os.ReadFile(editFp)
if err != nil {
return fmt.Errorf("could not read temporary copy: %w", err)
}
Expand Down Expand Up @@ -332,7 +321,7 @@ func (c *ctxCommand) editCommand(pc *fisk.ParseContext) error {
err = c.showCommand(pc)
if err != nil {
// but not if the file was already corrupt and we are editing the json directly
if path == editFile {
if path == editFp {
return err
}

Expand Down
17 changes: 5 additions & 12 deletions cli/errors_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
Expand All @@ -28,6 +27,7 @@ import (
"github.com/fatih/color"
"github.com/nats-io/jsm.go/schemas"
"github.com/nats-io/nats-server/v2/server"
iu "github.com/nats-io/natscli/internal/util"
)

type errCmd struct {
Expand Down Expand Up @@ -122,10 +122,6 @@ func (c *errCmd) listAction(_ *fisk.ParseContext) error {
}

func (c *errCmd) editAction(pc *fisk.ParseContext) error {
if os.Getenv("EDITOR") == "" {
return fmt.Errorf("EDITOR variable is not set")
}

errs, err := c.loadErrors(nil)
if err != nil {
return err
Expand Down Expand Up @@ -162,15 +158,12 @@ func (c *errCmd) editAction(pc *fisk.ParseContext) error {
tfile.Write(fj)
tfile.Close()

for {
cmd := exec.Command(os.Getenv("EDITOR"), tfile.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
fp := tfile.Name()

err = cmd.Run()
for {
err = iu.EditFile(fp)
if err != nil {
return fmt.Errorf("could not edit error: %s", err)
return err
}

eb, err := os.ReadFile(tfile.Name())
Expand Down
15 changes: 2 additions & 13 deletions cli/stream_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"io"
"math"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sort"
Expand Down Expand Up @@ -1793,11 +1792,6 @@ func (c *streamCmd) copyAndEditStream(cfg api.StreamConfig, pc *fisk.ParseContex
}

func (c *streamCmd) interactiveEdit(cfg api.StreamConfig) (api.StreamConfig, error) {
editor := os.Getenv("EDITOR")
if editor == "" {
return api.StreamConfig{}, fmt.Errorf("set EDITOR environment variable to your chosen editor")
}

cj, err := decoratedYamlMarshal(cfg)
if err != nil {
return api.StreamConfig{}, fmt.Errorf("could not create temporary file: %s", err)
Expand All @@ -1816,14 +1810,9 @@ func (c *streamCmd) interactiveEdit(cfg api.StreamConfig) (api.StreamConfig, err

tfile.Close()

cmd := exec.Command(editor, tfile.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
err = iu.EditFile(tfile.Name())
if err != nil {
return api.StreamConfig{}, fmt.Errorf("could not create temporary file: %s", err)
return api.StreamConfig{}, err
}

nb, err := os.ReadFile(tfile.Name())
Expand Down
40 changes: 40 additions & 0 deletions internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"math"
"os"
"os/exec"
"reflect"
"regexp"
"sort"
Expand All @@ -29,6 +30,7 @@ import (

"github.com/AlecAivazis/survey/v2"
"github.com/dustin/go-humanize"
"github.com/google/shlex"
"github.com/guptarohit/asciigraph"
"github.com/nats-io/jsm.go"
"github.com/nats-io/nats.go"
Expand Down Expand Up @@ -430,3 +432,41 @@ func ProgressWidth() int {
func JSONString(s string) string {
return "\"" + s + "\""
}

// Split the string into a command and its arguments.
func SplitCommand(s string) (string, []string, error) {
cmdAndArgs, err := shlex.Split(s)
if err != nil {
return "", nil, err
}

cmd := cmdAndArgs[0]
args := cmdAndArgs[1:]
return cmd, args, nil
}

// Edit the file at filepath f using the environment variable EDITOR command.
func EditFile(f string) error {
rawEditor := os.Getenv("EDITOR")
if rawEditor == "" {
return fmt.Errorf("set EDITOR environment variable to your chosen editor")
}

editor, args, err := SplitCommand(rawEditor)
if err != nil {
return fmt.Errorf("could not parse EDITOR: %v", rawEditor)
}

args = append(args, f)
cmd := exec.Command(editor, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Run()
if err != nil {
return fmt.Errorf("could not edit file %v: %s", f, err)
}

return nil
}
84 changes: 84 additions & 0 deletions internal/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
package util

import (
"io"
"os"
"slices"
"strings"
"testing"
)

Expand All @@ -34,3 +38,83 @@ func TestMultipleSort(t *testing.T) {
t.Fatalf("expected true")
}
}

func TestSplitCommand(t *testing.T) {
cmd, args, err := SplitCommand("vim")
if err != nil {
t.Fatalf("Expected err to be nil, got %v", err)
}
if cmd != "vim" && len(args) != 0 {
t.Fatalf("Expected vim and [], got %v and %v", cmd, args)
}

cmd, args, err = SplitCommand("code --wait")
if err != nil {
t.Fatalf("Expected err to be nil, got %v", err)
}
if cmd != "code" && !slices.Equal(args, []string{"--wait"}) {
t.Fatalf("Expected code and [\"--wait\"], got %v and %v", cmd, args)
}

cmd, args, err = SplitCommand("code --wait --new-window")
if err != nil {
t.Fatalf("Expected err to be nil, got %v", err)
}
if cmd != "code" && !slices.Equal(args, []string{"--wait", "--new-window"}) {
t.Fatalf("Expected code and [\"--wait\", \"--new-window\"], got %v and %v", cmd, args)
}

// EOF found when expecting closing quote
_, _, err = SplitCommand("foo --bar 'hello")
if err == nil {
t.Fatal("Expected err to not be nil, got nil")
}
}

func TestEditFile(t *testing.T) {
r, w, _ := os.Pipe()
os.Stdout = w
defer r.Close()
defer w.Close()

f, err := os.CreateTemp("", "test_edit_file")
if err != nil {
t.Fatalf("Expected err to be nil, got %v", err)
}
defer f.Close()
defer os.Remove(f.Name())

t.Run("EDITOR unset", func(t *testing.T) {
os.Unsetenv("EDITOR")
err := EditFile("")
if err == nil {
t.Fatal("Expected err to not be nil, got nil")
}
})

t.Run("EDITOR set", func(t *testing.T) {
os.Setenv("EDITOR", "echo")
err := EditFile(f.Name())
if err != nil {
t.Fatalf("Expected err to be nil, got %v", err)
}

w.Close()
stdout, err := io.ReadAll(r)
if err != nil {
t.Fatalf("Expected err to be nil, got %v", err)
}
r.Close()

actual := string(stdout)
lines := strings.Split(actual, "\n")

if len(lines) != 2 || lines[1] != "" {
t.Fatalf("Expected one line of output, got %v", actual)
}

if !strings.Contains(lines[0], "test_edit_file") {
t.Fatalf("Expected echo output, got %v", actual)
}
})
}