...

Source file src/k8s.io/kubectl/pkg/cmd/util/editor/editor.go

Documentation: k8s.io/kubectl/pkg/cmd/util/editor

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

View as plain text