1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-08-08 22:36:49 +02:00

Allow using shell aliases in interactive custom commands (#3793)

- **PR Description**

When executing an interactive custom command, use the user's shell
rather than "bash", and pass the -i flag. This makes it possible to use
shell aliases or shell functions which are not available in
non-interactive shells.

In previous attempts to solve this, concerns were brought up: [this
one](https://github.com/jesseduffield/lazygit/pull/2096#issuecomment-1257072541)
is addressed by using the interactive shell only for custom commands but
not anything else. [This
one](https://github.com/jesseduffield/lazygit/pull/2096#issuecomment-1343341795)
is a little dubious and unconfirmed, so I'm not very worried about it.

Supersedes #2096 and #3299. Fixes #770, #899, and #1642.
This commit is contained in:
Stefan Haller
2024-08-17 10:34:40 +02:00
committed by GitHub
7 changed files with 58 additions and 26 deletions

View File

@ -38,6 +38,10 @@ func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar) return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar)
} }
func (self *gitCmdObjBuilder) NewInteractiveShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewInteractiveShell(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) Quote(str string) string { func (self *gitCmdObjBuilder) Quote(str string) string {
return self.innerBuilder.Quote(str) return self.innerBuilder.Quote(str)
} }

View File

@ -14,6 +14,8 @@ type ICmdObjBuilder interface {
New(args []string) ICmdObj New(args []string) ICmdObj
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'` // NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
NewShell(commandStr string) ICmdObj NewShell(commandStr string) ICmdObj
// Like NewShell, but uses the user's shell rather than "bash", and passes -i to it
NewInteractiveShell(commandStr string) ICmdObj
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects. // Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
Quote(str string) string Quote(str string) string
} }
@ -43,10 +45,23 @@ func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj {
} }
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj { func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
var quotedCommand string quotedCommand := self.quotedCommandString(commandStr)
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand))
return self.New(cmdArgs)
}
func (self *CmdObjBuilder) NewInteractiveShell(commandStr string) ICmdObj {
quotedCommand := self.quotedCommandString(commandStr)
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s %s", self.platform.InteractiveShell, self.platform.InteractiveShellArg, self.platform.ShellArg, quotedCommand))
return self.New(cmdArgs)
}
func (self *CmdObjBuilder) quotedCommandString(commandStr string) string {
// Windows does not seem to like quotes around the command // Windows does not seem to like quotes around the command
if self.platform.OS == "windows" { if self.platform.OS == "windows" {
quotedCommand = strings.NewReplacer( return strings.NewReplacer(
"^", "^^", "^", "^^",
"&", "^&", "&", "^&",
"|", "^|", "|", "^|",
@ -54,13 +69,9 @@ func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
">", "^>", ">", "^>",
"%", "^%", "%", "^%",
).Replace(commandStr) ).Replace(commandStr)
} else {
quotedCommand = self.Quote(commandStr)
} }
cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand)) return self.Quote(commandStr)
return self.New(cmdArgs)
} }
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder { func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {

View File

@ -51,11 +51,13 @@ func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
} }
var dummyPlatform = &Platform{ var dummyPlatform = &Platform{
OS: "darwin", OS: "darwin",
Shell: "bash", Shell: "bash",
ShellArg: "-c", InteractiveShell: "bash",
OpenCommand: "open {{filename}}", ShellArg: "-c",
OpenLinkCommand: "open {{link}}", InteractiveShellArg: "-i",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
} }
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand { func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {

View File

@ -35,11 +35,13 @@ type OSCommand struct {
// Platform stores the os state // Platform stores the os state
type Platform struct { type Platform struct {
OS string OS string
Shell string Shell string
ShellArg string InteractiveShell string
OpenCommand string ShellArg string
OpenLinkCommand string InteractiveShellArg string
OpenCommand string
OpenLinkCommand string
} }
// NewOSCommand os command runner // NewOSCommand os command runner

View File

@ -4,15 +4,26 @@
package oscommands package oscommands
import ( import (
"os"
"runtime" "runtime"
) )
func GetPlatform() *Platform { func GetPlatform() *Platform {
return &Platform{ return &Platform{
OS: runtime.GOOS, OS: runtime.GOOS,
Shell: "bash", Shell: "bash",
ShellArg: "-c", InteractiveShell: getUserShell(),
OpenCommand: "open {{filename}}", ShellArg: "-c",
OpenLinkCommand: "open {{link}}", InteractiveShellArg: "-i",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
} }
} }
func getUserShell() string {
if shell := os.Getenv("SHELL"); shell != "" {
return shell
}
return "bash"
}

View File

@ -2,8 +2,10 @@ package oscommands
func GetPlatform() *Platform { func GetPlatform() *Platform {
return &Platform{ return &Platform{
OS: "windows", OS: "windows",
Shell: "cmd", Shell: "cmd",
ShellArg: "/c", InteractiveShell: "cmd",
ShellArg: "/c",
InteractiveShellArg: "",
} }
} }

View File

@ -31,7 +31,7 @@ func (self *CustomCommandAction) Call() error {
self.c.LogAction(self.c.Tr.Actions.CustomCommand) self.c.LogAction(self.c.Tr.Actions.CustomCommand)
return self.c.RunSubprocessAndRefresh( return self.c.RunSubprocessAndRefresh(
self.c.OS().Cmd.NewShell(command), self.c.OS().Cmd.NewInteractiveShell(command),
) )
}, },
HandleDeleteSuggestion: func(index int) error { HandleDeleteSuggestion: func(index int) error {