editor.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. /*
  2. Copyright 2015 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package editor
  14. import (
  15. "fmt"
  16. "io"
  17. "io/ioutil"
  18. "math/rand"
  19. "os"
  20. "os/exec"
  21. "path/filepath"
  22. "runtime"
  23. "strings"
  24. "k8s.io/klog"
  25. "k8s.io/kubernetes/pkg/kubectl/util/term"
  26. )
  27. const (
  28. // sorry, blame Git
  29. // TODO: on Windows rely on 'start' to launch the editor associated
  30. // with the given file type. If we can't because of the need of
  31. // blocking, use a script with 'ftype' and 'assoc' to detect it.
  32. defaultEditor = "vi"
  33. defaultShell = "/bin/bash"
  34. windowsEditor = "notepad"
  35. windowsShell = "cmd"
  36. )
  37. // Editor holds the command-line args to fire up the editor
  38. type Editor struct {
  39. Args []string
  40. Shell bool
  41. }
  42. // NewDefaultEditor creates a struct Editor that uses the OS environment to
  43. // locate the editor program, looking at EDITOR environment variable to find
  44. // the proper command line. If the provided editor has no spaces, or no quotes,
  45. // it is treated as a bare command to be loaded. Otherwise, the string will
  46. // be passed to the user's shell for execution.
  47. func NewDefaultEditor(envs []string) Editor {
  48. args, shell := defaultEnvEditor(envs)
  49. return Editor{
  50. Args: args,
  51. Shell: shell,
  52. }
  53. }
  54. func defaultEnvShell() []string {
  55. shell := os.Getenv("SHELL")
  56. if len(shell) == 0 {
  57. shell = platformize(defaultShell, windowsShell)
  58. }
  59. flag := "-c"
  60. if shell == windowsShell {
  61. flag = "/C"
  62. }
  63. return []string{shell, flag}
  64. }
  65. func defaultEnvEditor(envs []string) ([]string, bool) {
  66. var editor string
  67. for _, env := range envs {
  68. if len(env) > 0 {
  69. editor = os.Getenv(env)
  70. }
  71. if len(editor) > 0 {
  72. break
  73. }
  74. }
  75. if len(editor) == 0 {
  76. editor = platformize(defaultEditor, windowsEditor)
  77. }
  78. if !strings.Contains(editor, " ") {
  79. return []string{editor}, false
  80. }
  81. if !strings.ContainsAny(editor, "\"'\\") {
  82. return strings.Split(editor, " "), false
  83. }
  84. // rather than parse the shell arguments ourselves, punt to the shell
  85. shell := defaultEnvShell()
  86. return append(shell, editor), true
  87. }
  88. func (e Editor) args(path string) []string {
  89. args := make([]string, len(e.Args))
  90. copy(args, e.Args)
  91. if e.Shell {
  92. last := args[len(args)-1]
  93. args[len(args)-1] = fmt.Sprintf("%s %q", last, path)
  94. } else {
  95. args = append(args, path)
  96. }
  97. return args
  98. }
  99. // Launch opens the described or returns an error. The TTY will be protected, and
  100. // SIGQUIT, SIGTERM, and SIGINT will all be trapped.
  101. func (e Editor) Launch(path string) error {
  102. if len(e.Args) == 0 {
  103. return fmt.Errorf("no editor defined, can't open %s", path)
  104. }
  105. abs, err := filepath.Abs(path)
  106. if err != nil {
  107. return err
  108. }
  109. args := e.args(abs)
  110. cmd := exec.Command(args[0], args[1:]...)
  111. cmd.Stdout = os.Stdout
  112. cmd.Stderr = os.Stderr
  113. cmd.Stdin = os.Stdin
  114. klog.V(5).Infof("Opening file with editor %v", args)
  115. if err := (term.TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil {
  116. if err, ok := err.(*exec.Error); ok {
  117. if err.Err == exec.ErrNotFound {
  118. return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " "))
  119. }
  120. }
  121. return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " "))
  122. }
  123. return nil
  124. }
  125. // LaunchTempFile reads the provided stream into a temporary file in the given directory
  126. // and file prefix, and then invokes Launch with the path of that file. It will return
  127. // the contents of the file after launch, any errors that occur, and the path of the
  128. // temporary file so the caller can clean it up as needed.
  129. func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) {
  130. f, err := tempFile(prefix, suffix)
  131. if err != nil {
  132. return nil, "", err
  133. }
  134. defer f.Close()
  135. path := f.Name()
  136. if _, err := io.Copy(f, r); err != nil {
  137. os.Remove(path)
  138. return nil, path, err
  139. }
  140. // This file descriptor needs to close so the next process (Launch) can claim it.
  141. f.Close()
  142. if err := e.Launch(path); err != nil {
  143. return nil, path, err
  144. }
  145. bytes, err := ioutil.ReadFile(path)
  146. return bytes, path, err
  147. }
  148. func tempFile(prefix, suffix string) (f *os.File, err error) {
  149. dir := os.TempDir()
  150. for i := 0; i < 10000; i++ {
  151. name := filepath.Join(dir, prefix+randSeq(5)+suffix)
  152. f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
  153. if os.IsExist(err) {
  154. continue
  155. }
  156. break
  157. }
  158. return
  159. }
  160. var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
  161. func randSeq(n int) string {
  162. b := make([]rune, n)
  163. for i := range b {
  164. b[i] = letters[rand.Intn(len(letters))]
  165. }
  166. return string(b)
  167. }
  168. func platformize(linux, windows string) string {
  169. if runtime.GOOS == "windows" {
  170. return windows
  171. }
  172. return linux
  173. }