github.com/Ryooooooga/lazygit@v0.8.1/pkg/commands/os.go (about)

     1  package commands
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/go-errors/errors"
    13  
    14  	"github.com/jesseduffield/lazygit/pkg/config"
    15  	"github.com/jesseduffield/lazygit/pkg/utils"
    16  	"github.com/mgutz/str"
    17  	"github.com/sirupsen/logrus"
    18  	gitconfig "github.com/tcnksm/go-gitconfig"
    19  )
    20  
    21  // Platform stores the os state
    22  type Platform struct {
    23  	os                   string
    24  	shell                string
    25  	shellArg             string
    26  	escapedQuote         string
    27  	openCommand          string
    28  	openLinkCommand      string
    29  	fallbackEscapedQuote string
    30  }
    31  
    32  // OSCommand holds all the os commands
    33  type OSCommand struct {
    34  	Log                *logrus.Entry
    35  	Platform           *Platform
    36  	Config             config.AppConfigurer
    37  	command            func(string, ...string) *exec.Cmd
    38  	getGlobalGitConfig func(string) (string, error)
    39  	getenv             func(string) string
    40  }
    41  
    42  // NewOSCommand os command runner
    43  func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
    44  	return &OSCommand{
    45  		Log:                log,
    46  		Platform:           getPlatform(),
    47  		Config:             config,
    48  		command:            exec.Command,
    49  		getGlobalGitConfig: gitconfig.Global,
    50  		getenv:             os.Getenv,
    51  	}
    52  }
    53  
    54  // SetCommand sets the command function used by the struct.
    55  // To be used for testing only
    56  func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
    57  	c.command = cmd
    58  }
    59  
    60  // RunCommandWithOutput wrapper around commands returning their output and error
    61  func (c *OSCommand) RunCommandWithOutput(command string) (string, error) {
    62  	c.Log.WithField("command", command).Info("RunCommand")
    63  	cmd := c.ExecutableFromString(command)
    64  	return sanitisedCommandOutput(cmd.CombinedOutput())
    65  }
    66  
    67  // RunExecutableWithOutput runs an executable file and returns its output
    68  func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
    69  	return sanitisedCommandOutput(cmd.CombinedOutput())
    70  }
    71  
    72  // RunExecutable runs an executable file and returns an error if there was one
    73  func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
    74  	_, err := c.RunExecutableWithOutput(cmd)
    75  	return err
    76  }
    77  
    78  // ExecutableFromString takes a string like `git status` and returns an executable command for it
    79  func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
    80  	splitCmd := str.ToArgv(commandStr)
    81  	cmd := c.command(splitCmd[0], splitCmd[1:]...)
    82  	cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
    83  	return cmd
    84  }
    85  
    86  // RunCommandWithOutputLive runs RunCommandWithOutputLiveWrapper
    87  func (c *OSCommand) RunCommandWithOutputLive(command string, output func(string) string) error {
    88  	return RunCommandWithOutputLiveWrapper(c, command, output)
    89  }
    90  
    91  // DetectUnamePass detect a username / password question in a command
    92  // ask is a function that gets executen when this function detect you need to fillin a password
    93  // The ask argument will be "username" or "password" and expects the user's password or username back
    94  func (c *OSCommand) DetectUnamePass(command string, ask func(string) string) error {
    95  	ttyText := ""
    96  	errMessage := c.RunCommandWithOutputLive(command, func(word string) string {
    97  		ttyText = ttyText + " " + word
    98  
    99  		prompts := map[string]string{
   100  			"password": `Password\s*for\s*'.+':`,
   101  			"username": `Username\s*for\s*'.+':`,
   102  		}
   103  
   104  		for askFor, pattern := range prompts {
   105  			if match, _ := regexp.MatchString(pattern, ttyText); match {
   106  				ttyText = ""
   107  				return ask(askFor)
   108  			}
   109  		}
   110  
   111  		return ""
   112  	})
   113  	return errMessage
   114  }
   115  
   116  // RunCommand runs a command and just returns the error
   117  func (c *OSCommand) RunCommand(command string) error {
   118  	_, err := c.RunCommandWithOutput(command)
   119  	return err
   120  }
   121  
   122  // FileType tells us if the file is a file, directory or other
   123  func (c *OSCommand) FileType(path string) string {
   124  	fileInfo, err := os.Stat(path)
   125  	if err != nil {
   126  		return "other"
   127  	}
   128  	if fileInfo.IsDir() {
   129  		return "directory"
   130  	}
   131  	return "file"
   132  }
   133  
   134  // RunDirectCommand wrapper around direct commands
   135  func (c *OSCommand) RunDirectCommand(command string) (string, error) {
   136  	c.Log.WithField("command", command).Info("RunDirectCommand")
   137  
   138  	return sanitisedCommandOutput(
   139  		c.command(c.Platform.shell, c.Platform.shellArg, command).
   140  			CombinedOutput(),
   141  	)
   142  }
   143  
   144  func sanitisedCommandOutput(output []byte, err error) (string, error) {
   145  	outputString := string(output)
   146  	if err != nil {
   147  		// errors like 'exit status 1' are not very useful so we'll create an error
   148  		// from the combined output
   149  		if outputString == "" {
   150  			return "", WrapError(err)
   151  		}
   152  		return outputString, errors.New(outputString)
   153  	}
   154  	return outputString, nil
   155  }
   156  
   157  // OpenFile opens a file with the given
   158  func (c *OSCommand) OpenFile(filename string) error {
   159  	commandTemplate := c.Config.GetUserConfig().GetString("os.openCommand")
   160  	templateValues := map[string]string{
   161  		"filename": c.Quote(filename),
   162  	}
   163  
   164  	command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
   165  	err := c.RunCommand(command)
   166  	return err
   167  }
   168  
   169  // OpenLink opens a file with the given
   170  func (c *OSCommand) OpenLink(link string) error {
   171  	commandTemplate := c.Config.GetUserConfig().GetString("os.openLinkCommand")
   172  	templateValues := map[string]string{
   173  		"link": c.Quote(link),
   174  	}
   175  
   176  	command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
   177  	err := c.RunCommand(command)
   178  	return err
   179  }
   180  
   181  // EditFile opens a file in a subprocess using whatever editor is available,
   182  // falling back to core.editor, VISUAL, EDITOR, then vi
   183  func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) {
   184  	editor, _ := c.getGlobalGitConfig("core.editor")
   185  
   186  	if editor == "" {
   187  		editor = c.getenv("VISUAL")
   188  	}
   189  	if editor == "" {
   190  		editor = c.getenv("EDITOR")
   191  	}
   192  	if editor == "" {
   193  		if err := c.RunCommand("which vi"); err == nil {
   194  			editor = "vi"
   195  		}
   196  	}
   197  	if editor == "" {
   198  		return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config")
   199  	}
   200  
   201  	return c.PrepareSubProcess(editor, filename), nil
   202  }
   203  
   204  // PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
   205  // TODO: see if this needs to exist, given that ExecutableFromString does the same things
   206  func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
   207  	cmd := c.command(cmdName, commandArgs...)
   208  	if cmd != nil {
   209  		cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
   210  	}
   211  	return cmd
   212  }
   213  
   214  // Quote wraps a message in platform-specific quotation marks
   215  func (c *OSCommand) Quote(message string) string {
   216  	message = strings.Replace(message, "`", "\\`", -1)
   217  	escapedQuote := c.Platform.escapedQuote
   218  	if strings.Contains(message, c.Platform.escapedQuote) {
   219  		escapedQuote = c.Platform.fallbackEscapedQuote
   220  	}
   221  	return escapedQuote + message + escapedQuote
   222  }
   223  
   224  // Unquote removes wrapping quotations marks if they are present
   225  // this is needed for removing quotes from staged filenames with spaces
   226  func (c *OSCommand) Unquote(message string) string {
   227  	return strings.Replace(message, `"`, "", -1)
   228  }
   229  
   230  // AppendLineToFile adds a new line in file
   231  func (c *OSCommand) AppendLineToFile(filename, line string) error {
   232  	f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
   233  	if err != nil {
   234  		return WrapError(err)
   235  	}
   236  	defer f.Close()
   237  
   238  	_, err = f.WriteString("\n" + line)
   239  	if err != nil {
   240  		return WrapError(err)
   241  	}
   242  	return nil
   243  }
   244  
   245  // CreateTempFile writes a string to a new temp file and returns the file's name
   246  func (c *OSCommand) CreateTempFile(filename, content string) (string, error) {
   247  	tmpfile, err := ioutil.TempFile("", filename)
   248  	if err != nil {
   249  		c.Log.Error(err)
   250  		return "", WrapError(err)
   251  	}
   252  
   253  	if _, err := tmpfile.WriteString(content); err != nil {
   254  		c.Log.Error(err)
   255  		return "", WrapError(err)
   256  	}
   257  	if err := tmpfile.Close(); err != nil {
   258  		c.Log.Error(err)
   259  		return "", WrapError(err)
   260  	}
   261  
   262  	return tmpfile.Name(), nil
   263  }
   264  
   265  // Remove removes a file or directory at the specified path
   266  func (c *OSCommand) Remove(filename string) error {
   267  	err := os.RemoveAll(filename)
   268  	return WrapError(err)
   269  }
   270  
   271  // FileExists checks whether a file exists at the specified path
   272  func (c *OSCommand) FileExists(path string) (bool, error) {
   273  	if _, err := os.Stat(path); err != nil {
   274  		if os.IsNotExist(err) {
   275  			return false, nil
   276  		}
   277  		return false, err
   278  	}
   279  	return true, nil
   280  }
   281  
   282  // RunPreparedCommand takes a pointer to an exec.Cmd and runs it
   283  // this is useful if you need to give your command some environment variables
   284  // before running it
   285  func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
   286  	out, err := cmd.CombinedOutput()
   287  	outString := string(out)
   288  	c.Log.Info(outString)
   289  	if err != nil {
   290  		if len(outString) == 0 {
   291  			return err
   292  		}
   293  		return errors.New(outString)
   294  	}
   295  	return nil
   296  }
   297  
   298  // GetLazygitPath returns the path of the currently executed file
   299  func (c *OSCommand) GetLazygitPath() string {
   300  	ex, err := os.Executable() // get the executable path for git to use
   301  	if err != nil {
   302  		ex = os.Args[0] // fallback to the first call argument if needed
   303  	}
   304  	return filepath.ToSlash(ex)
   305  }
   306  
   307  // RunCustomCommand returns the pointer to a custom command
   308  func (c *OSCommand) RunCustomCommand(command string) *exec.Cmd {
   309  	return c.PrepareSubProcess(c.Platform.shell, c.Platform.shellArg, command)
   310  }
   311  
   312  // PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C
   313  func (c *OSCommand) PipeCommands(commandStrings ...string) error {
   314  
   315  	cmds := make([]*exec.Cmd, len(commandStrings))
   316  
   317  	for i, str := range commandStrings {
   318  		cmds[i] = c.ExecutableFromString(str)
   319  	}
   320  
   321  	for i := 0; i < len(cmds)-1; i++ {
   322  		stdout, err := cmds[i].StdoutPipe()
   323  		if err != nil {
   324  			return err
   325  		}
   326  
   327  		cmds[i+1].Stdin = stdout
   328  	}
   329  
   330  	// keeping this here in case I adapt this code for some other purpose in the future
   331  	// cmds[len(cmds)-1].Stdout = os.Stdout
   332  
   333  	finalErrors := []string{}
   334  
   335  	wg := sync.WaitGroup{}
   336  	wg.Add(len(cmds))
   337  
   338  	for _, cmd := range cmds {
   339  		currentCmd := cmd
   340  		go func() {
   341  			stderr, err := currentCmd.StderrPipe()
   342  			if err != nil {
   343  				c.Log.Error(err)
   344  			}
   345  
   346  			if err := currentCmd.Start(); err != nil {
   347  				c.Log.Error(err)
   348  			}
   349  
   350  			if b, err := ioutil.ReadAll(stderr); err == nil {
   351  				if len(b) > 0 {
   352  					finalErrors = append(finalErrors, string(b))
   353  				}
   354  			}
   355  
   356  			if err := currentCmd.Wait(); err != nil {
   357  				c.Log.Error(err)
   358  			}
   359  
   360  			wg.Done()
   361  		}()
   362  	}
   363  
   364  	wg.Wait()
   365  
   366  	if len(finalErrors) > 0 {
   367  		return errors.New(strings.Join(finalErrors, "\n"))
   368  	}
   369  	return nil
   370  }