需求:通过go打开一个编辑器,编辑完成后需要处理修改后的文件

流程分析:

  1. 调用command打开编辑器,并传入待编辑的文件;
  2. 等待编辑器退出,获取编辑后的文件;

首先定义一个常量和数据结构:

 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
const (
	defaultEditor = "vim"
	defaultShell  = "/bin/bash"
)

type Editor struct {
	Args  []string
	Shell bool
}

// 默认shell执行器
func defaultEnvShell() []string {
	shell := os.Getenv("SHELL")
	if len(shell) == 0 {
		shell = defaultShell
	}
	flag := "-c"
	return []string{shell, flag}
}

// 默认编辑器
func defaultEnvEditor() ([]string, bool) {
	editor := defaultEditor

	if !strings.Contains(editor, " ") {
		return []string{editor}, false
	}
	if !strings.ContainsAny(editor, "\"'\\") {
		return strings.Split(editor, " "), false
	}
	// rather than parse the shell arguments ourselves, punt to the shell
	shell := defaultEnvShell()
	return append(shell, editor), true
}

// 默认编辑器结构体
func NewDefaultEditor() Editor {
	args, shell := defaultEnvEditor()
	return Editor{
		Args:  args,
		Shell: shell,
	}
}

func (e Editor) args(path string) []string {
	args := make([]string, len(e.Args))
	copy(args, e.Args)
	if e.Shell {
		last := args[len(args)-1]
		args[len(args)-1] = fmt.Sprintf("%s %q", last, path)
	} else {
		args = append(args, path)
	}
	return args
}

定义如何启动一个编辑器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func (e Editor) Launch(path string) error {
	if len(e.Args) == 0 {
		return fmt.Errorf("no editor defined, can't open %s", path)
	}
	abs, err := filepath.Abs(path)
	if err != nil {
		return err
	}
	args := e.args(abs)
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Stdin = os.Stdin
	logger.Infof("Opening file with editor %v", args)
	if err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil {
		if err, ok := err.(*exec.Error); ok {
			if err.Err == exec.ErrNotFound {
				return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " "))
			}
		}
		return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " "))
	}
	return nil
}

最重要的是需要使用term.TTY:

TTY helps invoke a function and preserve the state of the terminal, even if the process is terminated during execution. It also provides support for terminal resizing for remote command execution/attachment.

Launch函数会直接编辑文件,但是一般情况了,程序需要编辑tmp文件:

 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
func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) {
	f, err := os.CreateTemp("", prefix+"*"+suffix)
	if err != nil {
		return nil, "", err
	}
	defer f.Close()
	path := f.Name()
	if _, err := io.Copy(f, r); err != nil {
		os.Remove(path)
		return nil, path, err
	}
	// This file descriptor needs to close so the next process (Launch) can claim it.
	f.Close()
	if err := e.Launch(path); err != nil {
		return nil, path, err
	}
	bytes, err := ioutil.ReadFile(path)
	return bytes, path, err
}

func (e Editor) LaunchTempFileWithJSON(v interface{}) ([]byte, bool, error) {
	if data, err := json.Marshal(v); err == nil {
		var prettyJSON bytes.Buffer
		err = json.Indent(&prettyJSON, data, "", "  ")
		if err != nil {
			logger.Errorf("JSON parse error: %v", err)
			return nil, false, err
		}
		buf := &bytes.Buffer{}
		var w io.Writer = buf
		_, err = fmt.Fprintln(w, string(prettyJSON.Bytes()))
		if err != nil {
			return nil, false, err
		}
		edited, file, err := e.LaunchTempFile(fmt.Sprintf("%s-tmp-", filepath.Base(os.Args[0])), fmt.Sprintf("%d", time.Now().UnixNano()), buf)
		if err != nil {
			return nil, false, err
		}
		os.Remove(file)
		if string(prettyJSON.Bytes()) == string(edited) {
			logger.Warnf("content not changed")
			return nil, false, nil
		}
		return edited, true, nil
	} else {
		return nil, false, err
	}
}

如何使用:

1
2
3
4
5
needToEdit:=interface{} // change to some interface
edit := editor.NewDefaultEditor()
edited, changed, err := edit.LaunchTempFileWithJSON(status)
// edited is the content read from tmp file
// changed == true then the file have been edited