package oscommands import ( "bytes" "io" "os" "os/exec" "path/filepath" "strings" "github.com/go-errors/errors" "github.com/samber/lo" "github.com/atotto/clipboard" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/utils" ) // OSCommand holds all the os commands type OSCommand struct { *common.Common Platform *Platform getenvFn func(string) string guiIO *guiIO removeFileFn func(string) error isDirEmptyFn func(string) (bool, error) removeDirFn func(string) error Cmd *CmdObjBuilder tempDir string } // Platform stores the os state type Platform struct { OS string Shell string ShellArg string PrefixForShellFunctionsFile string OpenCommand string OpenLinkCommand string } // NewOSCommand os command runner func NewOSCommand(common *common.Common, config config.AppConfigurer, platform *Platform, guiIO *guiIO) *OSCommand { c := &OSCommand{ Common: common, Platform: platform, getenvFn: os.Getenv, removeFileFn: os.RemoveAll, isDirEmptyFn: isDirEmpty, removeDirFn: os.Remove, guiIO: guiIO, tempDir: config.GetTempDir(), } runner := &cmdObjRunner{log: common.Log, guiIO: guiIO} c.Cmd = &CmdObjBuilder{runner: runner, platform: platform} return c } func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) { c.Log.WithField("command", cmdStr).Info("RunCommand") c.guiIO.logCommandFn(cmdStr, commandLine) } // FileType tells us if the file is a file, directory or other func FileType(path string) string { fileInfo, err := os.Stat(path) if err != nil { return "other" } if fileInfo.IsDir() { return "directory" } return "file" } func (c *OSCommand) OpenFile(filename string) error { commandTemplate := c.UserConfig().OS.Open if commandTemplate == "" { commandTemplate = config.GetPlatformDefaultConfig().Open } templateValues := map[string]string{ "filename": c.Quote(filename), } command := utils.ResolvePlaceholderString(commandTemplate, templateValues) return c.Cmd.NewShell(command, c.UserConfig().OS.ShellFunctionsFile).Run() } func (c *OSCommand) OpenLink(link string) error { commandTemplate := c.UserConfig().OS.OpenLink if commandTemplate == "" { commandTemplate = config.GetPlatformDefaultConfig().OpenLink } templateValues := map[string]string{ "link": c.Quote(link), } command := utils.ResolvePlaceholderString(commandTemplate, templateValues) return c.Cmd.NewShell(command, c.UserConfig().OS.ShellFunctionsFile).Run() } // Quote wraps a message in platform-specific quotation marks func (c *OSCommand) Quote(message string) string { return c.Cmd.Quote(message) } // AppendLineToFile adds a new line in file func (c *OSCommand) AppendLineToFile(filename, line string) error { msg := utils.ResolvePlaceholderString( c.Tr.Log.AppendingLineToFile, map[string]string{ "line": line, "filename": filename, }, ) c.LogCommand(msg, false) f, err := os.OpenFile(filename, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0o600) if err != nil { return utils.WrapError(err) } defer f.Close() info, err := os.Stat(filename) if err != nil { return utils.WrapError(err) } if info.Size() > 0 { // read last char buf := make([]byte, 1) if _, err := f.ReadAt(buf, info.Size()-1); err != nil { return utils.WrapError(err) } // if the last byte of the file is not a newline, add it if []byte("\n")[0] != buf[0] { _, err = f.WriteString("\n") } } if err == nil { _, err = f.WriteString(line + "\n") } if err != nil { return utils.WrapError(err) } return nil } // CreateFileWithContent creates a file with the given content func (c *OSCommand) CreateFileWithContent(path string, content string) error { msg := utils.ResolvePlaceholderString( c.Tr.Log.CreateFileWithContent, map[string]string{ "path": path, }, ) c.LogCommand(msg, false) if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { c.Log.Error(err) return err } if err := os.WriteFile(path, []byte(content), 0o644); err != nil { c.Log.Error(err) return utils.WrapError(err) } return nil } // Remove removes a file or directory at the specified path func (c *OSCommand) Remove(filename string) error { msg := utils.ResolvePlaceholderString( c.Tr.Log.Remove, map[string]string{ "filename": filename, }, ) c.LogCommand(msg, false) err := os.RemoveAll(filename) return utils.WrapError(err) } // FileExists checks whether a file exists at the specified path func (c *OSCommand) FileExists(path string) (bool, error) { if _, err := os.Stat(path); err != nil { if os.IsNotExist(err) { return false, nil } return false, err } return true, nil } // PipeCommands runs a heap of commands and pipes their inputs/outputs together like A | B | C func (c *OSCommand) PipeCommands(cmdObjs ...*CmdObj) error { cmds := lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) *exec.Cmd { return cmdObj.GetCmd() }) logCmdStr := strings.Join( lo.Map(cmdObjs, func(cmdObj *CmdObj, _ int) string { return cmdObj.ToString() }), " | ", ) c.LogCommand(logCmdStr, true) for i := range len(cmds) - 1 { stdout, err := cmds[i].StdoutPipe() if err != nil { return err } cmds[i+1].Stdin = stdout } // keeping this here in case I adapt this code for some other purpose in the future // cmds[len(cmds)-1].Stdout = os.Stdout stderrs := make([]bytes.Buffer, len(cmds)) for i := range cmds { cmds[i].Stderr = &stderrs[i] } // Start every command before waiting for any of them: waiting for a command // closes our end of the pipe that feeds the next one, and a command that // hasn't been started by then would inherit a closed stdin. started := 0 var startErr error for _, cmd := range cmds { if err := cmd.Start(); err != nil { startErr = err break } started++ } finalErrors := []string{} if startErr != nil { c.Log.Error(startErr) finalErrors = append(finalErrors, startErr.Error()) // Without the rest of the pipeline to drain them, the commands we did // start could block forever writing to a full pipe. for _, cmd := range cmds[:started] { _ = cmd.Process.Kill() } } for i, cmd := range cmds[:started] { if err := cmd.Wait(); err != nil { c.Log.Error(err) } if stderrs[i].Len() > 0 { finalErrors = append(finalErrors, stderrs[i].String()) } } if len(finalErrors) > 0 { return errors.New(strings.Join(finalErrors, "\n")) } return nil } func (c *OSCommand) CopyToClipboard(str string) error { escaped := strings.ReplaceAll(str, "\n", "\\n") truncated := utils.TruncateWithEllipsis(escaped, 40) msg := utils.ResolvePlaceholderString( c.Tr.Log.CopyToClipboard, map[string]string{ "str": truncated, }, ) c.LogCommand(msg, false) if c.UserConfig().OS.CopyToClipboardCmd != "" { cmdStr := utils.ResolvePlaceholderString(c.UserConfig().OS.CopyToClipboardCmd, map[string]string{ "text": c.Cmd.Quote(str), }) return c.Cmd.NewShell(cmdStr, c.UserConfig().OS.ShellFunctionsFile).Run() } return clipboard.WriteAll(str) } func (c *OSCommand) PasteFromClipboard() (string, error) { var s string var err error if c.UserConfig().OS.CopyToClipboardCmd != "" { cmdStr := c.UserConfig().OS.ReadFromClipboardCmd s, err = c.Cmd.NewShell(cmdStr, c.UserConfig().OS.ShellFunctionsFile).RunWithOutput() } else { s, err = clipboard.ReadAll() } if err != nil { return "", err } return strings.ReplaceAll(s, "\r\n", "\n"), nil } func (c *OSCommand) RemoveFile(path string) error { msg := utils.ResolvePlaceholderString( c.Tr.Log.RemoveFile, map[string]string{ "path": path, }, ) c.LogCommand(msg, false) return c.removeFileFn(path) } func (c *OSCommand) IsDirEmpty(path string) (bool, error) { return c.isDirEmptyFn(path) } func (c *OSCommand) RemoveDir(path string) error { msg := utils.ResolvePlaceholderString( c.Tr.Log.RemoveEmptyDir, map[string]string{ "path": path, }, ) c.LogCommand(msg, false) return c.removeDirFn(path) } func isDirEmpty(path string) (bool, error) { f, err := os.Open(path) if err != nil { return false, err } _, err = f.Readdirnames(1) _ = f.Close() if errors.Is(err, io.EOF) { return true, nil } return false, err } func (c *OSCommand) Getenv(key string) string { return c.getenvFn(key) } func (c *OSCommand) GetTempDir() string { return c.tempDir } // GetLazygitPath returns the path of the currently executed file func GetLazygitPath() string { ex, err := os.Executable() // get the executable path for git to use if err != nil { ex = os.Args[0] // fallback to the first call argument if needed } return `"` + filepath.ToSlash(ex) + `"` }