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 }