-
-
Notifications
You must be signed in to change notification settings - Fork 476
/
editor.go
177 lines (141 loc) · 4.29 KB
/
editor.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package editor
import (
"bytes"
"context"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/fatih/color"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/tempfile"
shellquote "github.com/kballard/go-shellquote"
)
var (
// Stdin is exported for tests.
Stdin io.Reader = os.Stdin
// Stdout is exported for tests.
Stdout io.Writer = os.Stdout
// Stderr is exported for tests.
Stderr io.Writer = os.Stderr
)
// Invoke will start the given editor and return the content.
func Invoke(ctx context.Context, editor string, content []byte) ([]byte, error) {
if !ctxutil.IsTerminal(ctx) {
return nil, fmt.Errorf("need terminal")
}
tmpfile, err := tempfile.New(ctx, "gopass-edit")
if err != nil {
return []byte{}, fmt.Errorf("failed to create tmpfile %s: %w", editor, err)
}
defer func() {
if err := tmpfile.Remove(ctx); err != nil {
color.Red("Failed to remove tempfile at %s: %s", tmpfile.Name(), err)
}
}()
if _, err := tmpfile.Write(content); err != nil {
return []byte{}, fmt.Errorf("failed to write tmpfile to start with %s %v: %w", editor, tmpfile.Name(), err)
}
if err := tmpfile.Close(); err != nil {
return []byte{}, fmt.Errorf("failed to close tmpfile to start with %s %v: %w", editor, tmpfile.Name(), err)
}
args := make([]string, 0, 4)
if runtime.GOOS != "windows" {
cmdArgs, err := shellquote.Split(editor)
if err != nil {
return []byte{}, fmt.Errorf("failed to parse EDITOR command `%s`", editor)
}
editor = cmdArgs[0]
args = append(args, cmdArgs[1:]...)
args = append(args, vimOptions(resolveEditor(editor))...)
}
args = append(args, tmpfile.Name())
cmd := exec.Command(editor, args...)
cmd.Stdin = Stdin
cmd.Stdout = Stdout
cmd.Stderr = Stderr
if err := cmd.Run(); err != nil {
debug.Log("cmd: %s %+v - error: %+v", cmd.Path, cmd.Args, err)
return []byte{}, fmt.Errorf("failed to run %s with %s file: %w", editor, tmpfile.Name(), err)
}
nContent, err := os.ReadFile(tmpfile.Name())
if err != nil {
return []byte{}, fmt.Errorf("failed to read from tmpfile: %w", err)
}
// enforce unix line endings in the password store.
nContent = bytes.ReplaceAll(nContent, []byte("\r\n"), []byte("\n"))
nContent = bytes.ReplaceAll(nContent, []byte("\r"), []byte("\n"))
return nContent, nil
}
func vimOptions(editor string) []string {
if editor != "vi" && editor != "vim" && editor != "neovim" {
debug.Log("Editor %s is not known to be vim compatible", editor)
return []string{}
}
if !isVim(editor) {
debug.Log("Editor %s is not known to be vim compatible", editor)
return []string{}
}
path := "/dev/shm/gopass*"
if runtime.GOOS == "darwin" {
path = "/private/**/gopass**"
}
viminfo := `viminfo=""`
if editor == "neovim" {
viminfo = `shada=""`
}
args := []string{
"-c",
fmt.Sprintf("autocmd BufNewFile,BufRead %s setlocal noswapfile nobackup noundofile %s", path, viminfo),
}
args = append(args, "-i", "NONE") // disable viminfo
args = append(args, "-n") // disable swap
return args
}
// isVim tries to identify the vi variant as vim compatible or not.
func isVim(editor string) bool {
if editor == "neovim" {
return true
}
cmd := exec.Command(editor, "--version")
out, err := cmd.CombinedOutput()
if err != nil {
debug.Log("failed to check %s --version: %s", cmd.Path, err)
return false
}
debug.Log("%s --version: %s", cmd.Path, string(out))
return strings.Contains(string(out), "VIM - Vi IMproved")
}
// resolveEditor tries to resolve the final link destination of the editor name given
// and then extract the binary file name from the path. In practice the actual editor
// is often hidden behing several layers of indirection and we want to get an idea
// which options might work.
func resolveEditor(editor string) string {
path, err := exec.LookPath(editor)
if err != nil {
debug.Log("failed to look up editor binary: %s", err)
return editor
}
for {
fi, err := os.Stat(path)
if err != nil {
debug.Log("failed to resolve %s: %s", path, err)
return editor
}
if fi.Mode()&fs.ModeSymlink != fs.ModeSymlink {
// not a symlink
break
}
path, err = os.Readlink(path)
if err != nil {
debug.Log("failed to read link %s: %s", path, err)
}
}
// return the binary name only
return filepath.Base(path)
}