...

Source file src/sigs.k8s.io/release-utils/editor/editor.go

Documentation: sigs.k8s.io/release-utils/editor

     1  /*
     2  Copyright 2020 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  	"github.com/sirupsen/logrus"
    29  )
    30  
    31  const (
    32  	// sorry, blame Git
    33  	// TODO: on Windows rely on 'start' to launch the editor associated
    34  	// with the given file type. If we can't because of the need of
    35  	// blocking, use a script with 'ftype' and 'assoc' to detect it.
    36  	defaultEditor = "vi"
    37  	defaultShell  = "/bin/bash"
    38  	windowsEditor = "notepad"
    39  	windowsShell  = "cmd"
    40  )
    41  
    42  // Editor holds the command-line args to fire up the editor
    43  type Editor struct {
    44  	Args  []string
    45  	Shell bool
    46  }
    47  
    48  // NewDefaultEditor creates a struct Editor that uses the OS environment to
    49  // locate the editor program, looking at EDITOR environment variable to find
    50  // the proper command line. If the provided editor has no spaces, or no quotes,
    51  // it is treated as a bare command to be loaded. Otherwise, the string will
    52  // be passed to the user's shell for execution.
    53  func NewDefaultEditor(envs []string) Editor {
    54  	args, shell := defaultEnvEditor(envs)
    55  	return Editor{
    56  		Args:  args,
    57  		Shell: shell,
    58  	}
    59  }
    60  
    61  func defaultEnvShell() []string {
    62  	shell := os.Getenv("SHELL")
    63  	if shell == "" {
    64  		shell = platformize(defaultShell, windowsShell)
    65  	}
    66  	flag := "-c"
    67  	if shell == windowsShell {
    68  		flag = "/C"
    69  	}
    70  	return []string{shell, flag}
    71  }
    72  
    73  func defaultEnvEditor(envs []string) ([]string, bool) {
    74  	var editor string
    75  	for _, env := range envs {
    76  		if len(env) > 0 {
    77  			editor = os.Getenv(env)
    78  		}
    79  		if len(editor) > 0 {
    80  			break
    81  		}
    82  	}
    83  	if editor == "" {
    84  		editor = platformize(defaultEditor, windowsEditor)
    85  	}
    86  	if !strings.Contains(editor, " ") {
    87  		return []string{editor}, false
    88  	}
    89  	if !strings.ContainsAny(editor, "\"'\\") {
    90  		return strings.Split(editor, " "), false
    91  	}
    92  	// rather than parse the shell arguments ourselves, punt to the shell
    93  	shell := defaultEnvShell()
    94  	return append(shell, editor), true
    95  }
    96  
    97  func (e Editor) args(path string) []string {
    98  	args := make([]string, len(e.Args))
    99  	copy(args, e.Args)
   100  	if e.Shell {
   101  		last := args[len(args)-1]
   102  		args[len(args)-1] = fmt.Sprintf("%s %q", last, path)
   103  	} else {
   104  		args = append(args, path) //nolint: makezero
   105  	}
   106  	return args
   107  }
   108  
   109  // Launch opens the described or returns an error. The TTY will be protected, and
   110  // SIGQUIT, SIGTERM, and SIGINT will all be trapped.
   111  func (e Editor) Launch(path string) error {
   112  	if len(e.Args) == 0 {
   113  		return fmt.Errorf("no editor defined, can't open %s", path)
   114  	}
   115  	abs, err := filepath.Abs(path)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	args := e.args(abs)
   120  	// TODO: check to validate the args and maybe sabitize those
   121  	cmd := exec.Command(args[0], args[1:]...) //nolint: gosec
   122  	cmd.Stdout = os.Stdout
   123  	cmd.Stderr = os.Stderr
   124  	cmd.Stdin = os.Stdin
   125  	logrus.Infof("Opening file with editor %v", args)
   126  	if err := (TTY{In: os.Stdin, TryDev: true}).Safe(cmd.Run); err != nil {
   127  		if err, ok := err.(*exec.Error); ok {
   128  			if err.Err == exec.ErrNotFound {
   129  				return fmt.Errorf("unable to launch the editor %q", strings.Join(e.Args, " "))
   130  			}
   131  		}
   132  		return fmt.Errorf("there was a problem with the editor %q", strings.Join(e.Args, " "))
   133  	}
   134  	return nil
   135  }
   136  
   137  // LaunchTempFile reads the provided stream into a temporary file in the given directory
   138  // and file prefix, and then invokes Launch with the path of that file. It will return
   139  // the contents of the file after launch, any errors that occur, and the path of the
   140  // temporary file so the caller can clean it up as needed.
   141  func (e Editor) LaunchTempFile(prefix, suffix string, r io.Reader) (bytes []byte, path string, err error) {
   142  	f, err := os.CreateTemp("", fmt.Sprintf("%s*%s", prefix, suffix))
   143  	if err != nil {
   144  		return nil, "", err
   145  	}
   146  	defer f.Close()
   147  	path = f.Name()
   148  	if _, err := io.Copy(f, r); err != nil {
   149  		os.Remove(path)
   150  		return nil, path, err
   151  	}
   152  	// This file descriptor needs to close so the next process (Launch) can claim it.
   153  	f.Close()
   154  	if err := e.Launch(path); err != nil {
   155  		return nil, path, err
   156  	}
   157  	bytes, err = os.ReadFile(path)
   158  	return bytes, path, err
   159  }
   160  
   161  func platformize(linux, windows string) string {
   162  	if runtime.GOOS == "windows" {
   163  		return windows
   164  	}
   165  	return linux
   166  }
   167  

View as plain text