get.porter.sh/porter@v1.3.0/pkg/editor/editor.go (about)

     1  package editor
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"get.porter.sh/porter/pkg"
    11  	"get.porter.sh/porter/pkg/portercontext"
    12  )
    13  
    14  // Editor displays content to a user using an external text editor, like vi or notepad.
    15  // The content is captured and returned.
    16  //
    17  // The `EDITOR` environment variable is checked to find an editor.
    18  // Failing that, use some sensible default depending on the operating system.
    19  //
    20  // This is useful for editing things like configuration files, especially those
    21  // that might be stored on a remote server. For example: the content could be retrieved
    22  // from the remote store, edited locally, then saved back.
    23  type Editor struct {
    24  	*portercontext.Context
    25  	contents     []byte
    26  	tempFilename string
    27  }
    28  
    29  // New returns a new Editor with the temp filename and contents provided.
    30  func New(context *portercontext.Context, tempFilename string, contents []byte) *Editor {
    31  	return &Editor{
    32  		Context:      context,
    33  		tempFilename: tempFilename,
    34  		contents:     contents,
    35  	}
    36  }
    37  
    38  func (e *Editor) editorArgs(filename string) []string {
    39  	shell := e.Getenv("SHELL")
    40  	if shell == "" {
    41  		shell = defaultShell
    42  	}
    43  	editor := e.Getenv("EDITOR")
    44  	if editor == "" {
    45  		editor = defaultEditor
    46  	}
    47  
    48  	// Example of what will be run:
    49  	// on *nix: sh -c "vi /tmp/test.txt"
    50  	// on windows: cmd /C "C:\Program Files\Visual Studio Code\Code.exe --wait C:\somefile.txt"
    51  	//
    52  	// Pass the editor command to the shell so we don't have to parse the command ourselves.
    53  	// Passing the editor command that could possibly have an argument (e.g. --wait for VSCode) to the
    54  	// shell means we don't have to parse this ourselves, like splitting on spaces.
    55  	return []string{shell, shellCommandFlag, fmt.Sprintf("%s %s", editor, filename)}
    56  }
    57  
    58  // Run opens the editor, displaying the contents through a temporary file.
    59  // The content is returned once the editor closes.
    60  func (e *Editor) Run(ctx context.Context) ([]byte, error) {
    61  	tempFile, err := e.FileSystem.OpenFile(filepath.Join(os.TempDir(), e.tempFilename), os.O_RDWR|os.O_CREATE|os.O_EXCL, pkg.FileModeWritable)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	defer func() {
    66  		err = errors.Join(err, e.FileSystem.Remove(tempFile.Name()))
    67  	}()
    68  	_, err = tempFile.Write(e.contents)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	// close here without defer so cmd can grab the file
    74  	tempFile.Close()
    75  
    76  	args := e.editorArgs(tempFile.Name())
    77  	cmd := e.NewCommand(ctx, args[0], args[1:]...)
    78  	cmd.Stdout = e.Out
    79  	cmd.Stderr = e.Err
    80  	cmd.Stdin = e.In
    81  	err = cmd.Run()
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	contents, err := e.FileSystem.ReadFile(tempFile.Name())
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	return contents, err
    92  }