mirror of
https://github.com/kopia/kopia.git
synced 2025-12-23 22:57:50 -05:00
156 lines
3.5 KiB
Go
156 lines
3.5 KiB
Go
// Package editor encapsulates working with external text editor.
|
|
package editor
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/kopia/kopia/repo/logging"
|
|
)
|
|
|
|
var log = logging.Module("editor")
|
|
|
|
// EditLoop launches OS-specific editor (VI, notepad.exe or another editor configured through environment variables)
|
|
// It creates a temporary file with 'initial' contents and repeatedly invokes the editor until the provided 'parse' function
|
|
// returns nil result indicating success. The 'parse' function is passed the contents of edited files without # line comments.
|
|
func EditLoop(ctx context.Context, fname, initial string, withComments bool, parse func(updated string) error) error {
|
|
tmpDir, err := os.MkdirTemp("", "kopia")
|
|
if err != nil {
|
|
return errors.Wrap(err, "unable to create temp directory")
|
|
}
|
|
|
|
tmpFile := filepath.Join(tmpDir, fname)
|
|
defer os.RemoveAll(tmpDir) //nolint:errcheck
|
|
|
|
//nolint:mnd
|
|
if err := os.WriteFile(tmpFile, []byte(initial), 0o600); err != nil {
|
|
return errors.Wrap(err, "unable to write file to edit")
|
|
}
|
|
|
|
for {
|
|
if err := EditFile(ctx, tmpFile); err != nil {
|
|
return errors.Wrap(err, "error launching editor")
|
|
}
|
|
|
|
txt, err := readAndStripComments(tmpFile, withComments)
|
|
if err != nil {
|
|
return errors.Wrap(err, "error parsing edited file")
|
|
}
|
|
|
|
err = parse(txt)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
log(ctx).Errorf("%v", err)
|
|
fmt.Print("Reopen editor to fix? (Y/n) ")
|
|
|
|
var shouldReopen string
|
|
|
|
_, _ = fmt.Scanf("%s", &shouldReopen)
|
|
|
|
if strings.HasPrefix(strings.ToLower(shouldReopen), "n") {
|
|
return errors.New("aborted")
|
|
}
|
|
}
|
|
}
|
|
|
|
func readAndStripComments(fname string, withComments bool) (string, error) {
|
|
if !withComments {
|
|
b, err := os.ReadFile(fname) //nolint:gosec
|
|
|
|
return string(b), errors.Wrap(err, "error reading file")
|
|
}
|
|
|
|
f, err := os.Open(fname) //nolint:gosec
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error opening edited file")
|
|
}
|
|
defer f.Close() //nolint:errcheck
|
|
|
|
var result []string
|
|
|
|
s := bufio.NewScanner(f)
|
|
for s.Scan() {
|
|
l := s.Text()
|
|
|
|
if strings.HasPrefix(strings.TrimSpace(l), "#") {
|
|
continue
|
|
}
|
|
|
|
if l != "" {
|
|
result = append(result, l)
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, "\n"), nil
|
|
}
|
|
|
|
// EditFile launches the OS-specific editor (VI, notepad.exe or another editor configured through environment variables)
|
|
// to edit the specified file and waits for it to complete.
|
|
//
|
|
//nolint:gochecknoglobals
|
|
var EditFile = func(ctx context.Context, file string) error {
|
|
editor, editorArgs := getEditorCommand()
|
|
|
|
var args []string
|
|
args = append(args, editorArgs...)
|
|
args = append(args, file)
|
|
|
|
cmd := exec.CommandContext(ctx, editor, args...) //nolint:gosec
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
|
|
log(ctx).Debugf("launching editor %q on file %q", editor, file)
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return errors.Wrap(err, "error running editor command")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getEditorCommand() (cmd string, args []string) {
|
|
editor := os.Getenv("VISUAL")
|
|
if editor == "" {
|
|
editor = os.Getenv("EDITOR")
|
|
}
|
|
|
|
if editor != "" {
|
|
return parseEditor(editor)
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
return "notepad.exe", nil
|
|
}
|
|
|
|
return "vi", nil
|
|
}
|
|
|
|
func parseEditor(s string) (cmd string, args []string) {
|
|
// quoted editor path
|
|
if s[0] == '"' {
|
|
p := strings.Index(s[1:], "\"")
|
|
if p == -1 {
|
|
// invalid
|
|
return s, nil
|
|
}
|
|
|
|
return s[1 : p+1], strings.Split(strings.TrimSpace(s[p+1:]), " ")
|
|
}
|
|
|
|
parts := strings.Split(s, " ")
|
|
|
|
return parts[0], parts[1:]
|
|
}
|