123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194 |
- /*
- Copyright 2015 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package editor
- import (
- "fmt"
- "io"
- "io/ioutil"
- "math/rand"
- "os"
- "os/exec"
- "path/filepath"
- "runtime"
- "strings"
- "k8s.io/klog"
- "k8s.io/kubernetes/pkg/kubectl/util/term"
- )
- const (
- // sorry, blame Git
- // TODO: on Windows rely on 'start' to launch the editor associated
- // with the given file type. If we can't because of the need of
- // blocking, use a script with 'ftype' and 'assoc' to detect it.
- defaultEditor = "vi"
- defaultShell = "/bin/bash"
- windowsEditor = "notepad"
- windowsShell = "cmd"
- )
- // Editor holds the command-line args to fire up the editor
- type Editor struct {
- Args []string
- Shell bool
- }
- // NewDefaultEditor creates a struct Editor that uses the OS environment to
- // locate the editor program, looking at EDITOR environment variable to find
- // the proper command line. If the provided editor has no spaces, or no quotes,
- // it is treated as a bare command to be loaded. Otherwise, the string will
- // be passed to the user's shell for execution.
- func NewDefaultEditor(envs []string) Editor {
- args, shell := defaultEnvEditor(envs)
- return Editor{
- Args: args,
- Shell: shell,
- }
- }
- func defaultEnvShell() []string {
- shell := os.Getenv("SHELL")
- if len(shell) == 0 {
- shell = platformize(defaultShell, windowsShell)
- }
- flag := "-c"
- if shell == windowsShell {
- flag = "/C"
- }
- return []string{shell, flag}
- }
- func defaultEnvEditor(envs []string) ([]string, bool) {
- var editor string
- for _, env := range envs {
- if len(env) > 0 {
- editor = os.Getenv(env)
- }
- if len(editor) > 0 {
- break
- }
- }
- if len(editor) == 0 {
- editor = platformize(defaultEditor, windowsEditor)
- }
- 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 (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
- }
- // Launch opens the described or returns an error. The TTY will be protected, and
- // SIGQUIT, SIGTERM, and SIGINT will all be trapped.
- 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
- klog.V(5).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
- }
- // LaunchTempFile reads the provided stream into a temporary file in the given directory
- // and file prefix, and then invokes Launch with the path of that file. It will return
- // the contents of the file after launch, any errors that occur, and the path of the
- // temporary file so the caller can clean it up as needed.
- func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) ([]byte, string, error) {
- f, err := tempFile(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 tempFile(prefix, suffix string) (f *os.File, err error) {
- dir := os.TempDir()
- for i := 0; i < 10000; i++ {
- name := filepath.Join(dir, prefix+randSeq(5)+suffix)
- f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
- if os.IsExist(err) {
- continue
- }
- break
- }
- return
- }
- var letters = []rune("abcdefghijklmnopqrstuvwxyz0123456789")
- func randSeq(n int) string {
- b := make([]rune, n)
- for i := range b {
- b[i] = letters[rand.Intn(len(letters))]
- }
- return string(b)
- }
- func platformize(linux, windows string) string {
- if runtime.GOOS == "windows" {
- return windows
- }
- return linux
- }
|