Files
kopia/internal/editor/editor.go
Jarek Kowalski e03971fc59 Upgraded linter to v1.33.0 (#734)
* linter: upgraded to 1.33, disabled some linters

* lint: fixed 'errorlint' errors

This ensures that all error comparisons use errors.Is() or errors.As().
We will be wrapping more errors going forward so it's important that
error checks are not strict everywhere.

Verified that there are no exceptions for errorlint linter which
guarantees that.

* lint: fixed or suppressed wrapcheck errors

* lint: nolintlint and misc cleanups

Co-authored-by: Julio López <julio+gh@kasten.io>
2020-12-21 22:39:22 -08:00

143 lines
3.1 KiB
Go

// Package editor encapsulates working with external text editor.
package editor
import (
"bufio"
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo/logging"
)
var log = logging.GetContextLoggerFunc("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, parse func(updated string) error) error {
tmpDir, err := ioutil.TempDir("", "kopia")
if err != nil {
return errors.Wrap(err, "unable to create temp directory")
}
tmpFile := filepath.Join(tmpDir, fname)
defer os.RemoveAll(tmpDir) //nolint:errcheck
if err := ioutil.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)
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) (string, error) {
f, err := os.Open(fname) //nolint:gosec
if err != nil {
return "", errors.Wrap(err, "error opening edited file")
}
defer f.Close() //nolint:errcheck,gosec
var result []string
s := bufio.NewScanner(f)
for s.Scan() {
l := s.Text()
l = strings.TrimSpace(strings.Split(l, "#")[0])
if l != "" {
result = append(result, l)
}
}
return strings.Join(result, "\n"), nil
}
func editFile(ctx context.Context, file string) error {
editor, editorArgs := getEditorCommand()
var args []string
args = append(args, editorArgs...)
args = append(args, file)
cmd := exec.Command(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:]
}