From dc48cf963a107904384d0b944224699d0bf515d2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 9 Mar 2025 18:15:48 +0100 Subject: [PATCH] Add config os.shellFunctionsFile --- docs/Config.md | 19 +++++++++++++ pkg/commands/git_cmd_obj_builder.go | 4 +-- pkg/commands/oscommands/cmd_obj_builder.go | 9 ++++-- pkg/commands/oscommands/os.go | 21 +++++++------- .../oscommands/os_default_platform.go | 28 +++++++++++++++---- pkg/config/user_config.go | 4 +++ pkg/gui/controllers/helpers/files_helper.go | 4 +-- pkg/gui/controllers/shell_command_action.go | 2 +- .../custom_commands/handler_creator.go | 4 +-- schema/config.json | 4 +++ 10 files changed, 75 insertions(+), 24 deletions(-) diff --git a/docs/Config.md b/docs/Config.md index 0a8c9d1e7..b7fcadc7a 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -442,6 +442,10 @@ os: # See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard readFromClipboardCmd: "" + # A shell startup file containing shell aliases or shell functions. This will be sourced before running any shell commands, so that shell functions are available in the `:` command prompt or even in custom commands. + # See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands + shellFunctionsFile: "" + # If true, don't display introductory popups upon opening Lazygit. disableStartupPopups: false @@ -740,6 +744,21 @@ The `editInTerminal` option is used to decide whether lazygit needs to suspend i Contributions of new editor presets are welcome; see the `getPreset` function in [`editor_presets.go`](https://github.com/jesseduffield/lazygit/blob/master/pkg/config/editor_presets.go). +## Using aliases or functions in shell commands + +Lazygit has a command prompt (`:`) for quickly executing shell commands without having to quit lazygit or switch to a different terminal. Most people find it convenient to have their usual shell aliases or shell functions available at this prompt. To achieve this, put your alias definitions in a separate shell startup file (which you source from your normal startup file, i.e. from `.bashrc` or `.zshrc`), and then tell lazygit about this file like so: + +```yml +os: + shellFunctionsFile: ~/.my_aliases.sh +``` + +For many people it might work well enough to use their entire shell config file (`~/.bashrc` or `~/.zshrc`) as the `shellFunctionsFile`, but these config files typically do a lot more than defining aliases (e.g. initialize the completion system, start an ssh-agent, etc.) and this may unnecessarily delay execution of shell commands. + +When using zsh, aliases can't be used here, but functions can. It is easy to convert your existing aliases into functions, just change `alias l="ls -la"` to `l() ls -la`, for example. This way it will work as before both in the shell and in lazygit. + +Note that the shell aliases file is not only used when executing shell commands, but also for [custom commands](Custom_Command_Keybindings.md), and when opening a file in the editor. + ## Overriding default config file location To override the default config directory, use `CONFIG_DIR="$HOME/.config/lazygit"`. This directory contains the config file in addition to some other files lazygit uses to keep track of state across sessions. diff --git a/pkg/commands/git_cmd_obj_builder.go b/pkg/commands/git_cmd_obj_builder.go index 21f300bb3..b2877a5a1 100644 --- a/pkg/commands/git_cmd_obj_builder.go +++ b/pkg/commands/git_cmd_obj_builder.go @@ -34,8 +34,8 @@ func (self *gitCmdObjBuilder) New(args []string) oscommands.ICmdObj { return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar) } -func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj { - return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar) +func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) oscommands.ICmdObj { + return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(defaultEnvVar) } func (self *gitCmdObjBuilder) Quote(str string) string { diff --git a/pkg/commands/oscommands/cmd_obj_builder.go b/pkg/commands/oscommands/cmd_obj_builder.go index c72f3c187..2ab5c1071 100644 --- a/pkg/commands/oscommands/cmd_obj_builder.go +++ b/pkg/commands/oscommands/cmd_obj_builder.go @@ -13,7 +13,9 @@ type ICmdObjBuilder interface { // NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. 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(commandStr string) ICmdObj + // shellFunctionsFile is an optional file path that will be sourced before executing the command. Callers should pass + // the value of UserConfig.OS.ShellFunctionsFile. + NewShell(commandStr string, shellFunctionsFile 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(str string) string } @@ -42,7 +44,10 @@ func (self *CmdObjBuilder) NewWithEnviron(args []string, env []string) ICmdObj { } } -func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj { +func (self *CmdObjBuilder) NewShell(commandStr string, shellFunctionsFile string) ICmdObj { + if len(shellFunctionsFile) > 0 { + commandStr = fmt.Sprintf("%ssource %s\n%s", self.platform.PrefixForShellFunctionsFile, shellFunctionsFile, commandStr) + } quotedCommand := self.quotedCommandString(commandStr) cmdArgs := str.ToArgv(fmt.Sprintf("%s %s %s", self.platform.Shell, self.platform.ShellArg, quotedCommand)) diff --git a/pkg/commands/oscommands/os.go b/pkg/commands/oscommands/os.go index dc5ef222c..108e9b2ca 100644 --- a/pkg/commands/oscommands/os.go +++ b/pkg/commands/oscommands/os.go @@ -35,11 +35,12 @@ type OSCommand struct { // Platform stores the os state type Platform struct { - OS string - Shell string - ShellArg string - OpenCommand string - OpenLinkCommand string + OS string + Shell string + ShellArg string + PrefixForShellFunctionsFile string + OpenCommand string + OpenLinkCommand string } // NewOSCommand os command runner @@ -90,7 +91,7 @@ func (c *OSCommand) OpenFile(filename string) error { "filename": c.Quote(filename), } command := utils.ResolvePlaceholderString(commandTemplate, templateValues) - return c.Cmd.NewShell(command).Run() + return c.Cmd.NewShell(command, c.UserConfig().OS.ShellFunctionsFile).Run() } func (c *OSCommand) OpenLink(link string) error { @@ -107,7 +108,7 @@ func (c *OSCommand) OpenLink(link string) error { } command := utils.ResolvePlaceholderString(commandTemplate, templateValues) - return c.Cmd.NewShell(command).Run() + return c.Cmd.NewShell(command, c.UserConfig().OS.ShellFunctionsFile).Run() } // Quote wraps a message in platform-specific quotation marks @@ -296,7 +297,7 @@ func (c *OSCommand) CopyToClipboard(str string) error { cmdStr := utils.ResolvePlaceholderString(c.UserConfig().OS.CopyToClipboardCmd, map[string]string{ "text": c.Cmd.Quote(str), }) - return c.Cmd.NewShell(cmdStr).Run() + return c.Cmd.NewShell(cmdStr, c.UserConfig().OS.ShellFunctionsFile).Run() } return clipboard.WriteAll(str) @@ -307,7 +308,7 @@ func (c *OSCommand) PasteFromClipboard() (string, error) { var err error if c.UserConfig().OS.CopyToClipboardCmd != "" { cmdStr := c.UserConfig().OS.ReadFromClipboardCmd - s, err = c.Cmd.NewShell(cmdStr).RunWithOutput() + s, err = c.Cmd.NewShell(cmdStr, c.UserConfig().OS.ShellFunctionsFile).RunWithOutput() } else { s, err = clipboard.ReadAll() } @@ -357,5 +358,5 @@ func (c *OSCommand) UpdateWindowTitle() error { return getWdErr } argString := fmt.Sprint("title ", filepath.Base(path), " - Lazygit") - return c.Cmd.NewShell(argString).Run() + return c.Cmd.NewShell(argString, c.UserConfig().OS.ShellFunctionsFile).Run() } diff --git a/pkg/commands/oscommands/os_default_platform.go b/pkg/commands/oscommands/os_default_platform.go index fd4967d95..11e1dc9aa 100644 --- a/pkg/commands/oscommands/os_default_platform.go +++ b/pkg/commands/oscommands/os_default_platform.go @@ -4,15 +4,33 @@ package oscommands import ( + "os" "runtime" + "strings" ) func GetPlatform() *Platform { + shell := getUserShell() + + prefixForShellFunctionsFile := "" + if strings.HasSuffix(shell, "bash") { + prefixForShellFunctionsFile = "shopt -s expand_aliases\n" + } + return &Platform{ - OS: runtime.GOOS, - Shell: "bash", - ShellArg: "-c", - OpenCommand: "open {{filename}}", - OpenLinkCommand: "open {{link}}", + OS: runtime.GOOS, + Shell: shell, + ShellArg: "-c", + PrefixForShellFunctionsFile: prefixForShellFunctionsFile, + OpenCommand: "open {{filename}}", + OpenLinkCommand: "open {{link}}", } } + +func getUserShell() string { + if shell := os.Getenv("SHELL"); shell != "" { + return shell + } + + return "bash" +} diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index 2ef595a86..fd682a9cb 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -589,6 +589,10 @@ type OSConfig struct { // See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard ReadFromClipboardCmd string `yaml:"readFromClipboardCmd,omitempty"` + // A shell startup file containing shell aliases or shell functions. This will be sourced before running any shell commands, so that shell functions are available in the `:` command prompt or even in custom commands. + // See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands + ShellFunctionsFile string `yaml:"shellFunctionsFile"` + // -------- // The following configs are all deprecated and kept for backward diff --git a/pkg/gui/controllers/helpers/files_helper.go b/pkg/gui/controllers/helpers/files_helper.go index 328d0f889..24789ec0c 100644 --- a/pkg/gui/controllers/helpers/files_helper.go +++ b/pkg/gui/controllers/helpers/files_helper.go @@ -71,11 +71,11 @@ func (self *FilesHelper) OpenDirInEditor(path string) error { func (self *FilesHelper) callEditor(cmdStr string, suspend bool) error { if suspend { return self.c.RunSubprocessAndRefresh( - self.c.OS().Cmd.NewShell(cmdStr), + self.c.OS().Cmd.NewShell(cmdStr, self.c.UserConfig().OS.ShellFunctionsFile), ) } - return self.c.OS().Cmd.NewShell(cmdStr).Run() + return self.c.OS().Cmd.NewShell(cmdStr, self.c.UserConfig().OS.ShellFunctionsFile).Run() } func (self *FilesHelper) OpenFile(filename string) error { diff --git a/pkg/gui/controllers/shell_command_action.go b/pkg/gui/controllers/shell_command_action.go index 547ab012d..185f74893 100644 --- a/pkg/gui/controllers/shell_command_action.go +++ b/pkg/gui/controllers/shell_command_action.go @@ -31,7 +31,7 @@ func (self *ShellCommandAction) Call() error { self.c.LogAction(self.c.Tr.Actions.CustomCommand) return self.c.RunSubprocessAndRefresh( - self.c.OS().Cmd.NewShell(command), + self.c.OS().Cmd.NewShell(command, self.c.UserConfig().OS.ShellFunctionsFile), ) }, HandleDeleteSuggestion: func(index int) error { diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go index 9e17ca9fa..5f647606b 100644 --- a/pkg/gui/services/custom_commands/handler_creator.go +++ b/pkg/gui/services/custom_commands/handler_creator.go @@ -148,7 +148,7 @@ func (self *HandlerCreator) generateFindSuggestionsFunc(prompt *config.CustomCom func (self *HandlerCreator) getCommandSuggestionsFn(command string) (func(string) []*types.Suggestion, error) { lines := []*types.Suggestion{} - err := self.c.OS().Cmd.NewShell(command).RunAndProcessLines(func(line string) (bool, error) { + err := self.c.OS().Cmd.NewShell(command, self.c.UserConfig().OS.ShellFunctionsFile).RunAndProcessLines(func(line string) (bool, error) { lines = append(lines, &types.Suggestion{Value: line, Label: line}) return false, nil }) @@ -259,7 +259,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses return err } - cmdObj := self.c.OS().Cmd.NewShell(cmdStr) + cmdObj := self.c.OS().Cmd.NewShell(cmdStr, self.c.UserConfig().OS.ShellFunctionsFile) if customCommand.Subprocess != nil && *customCommand.Subprocess { return self.c.RunSubprocessAndRefresh(cmdObj) diff --git a/schema/config.json b/schema/config.json index 4ce777b11..7c642af96 100644 --- a/schema/config.json +++ b/schema/config.json @@ -1559,6 +1559,10 @@ "type": "string", "description": "ReadFromClipboardCmd is the command for reading the clipboard.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#custom-command-for-copying-to-and-pasting-from-clipboard" }, + "shellFunctionsFile": { + "type": "string", + "description": "A shell startup file containing shell aliases or shell functions. This will be sourced before running any shell commands, so that shell functions are available in the `:` command prompt or even in custom commands.\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#using-aliases-or-functions-in-shell-commands" + }, "editCommand": { "type": "string", "description": "EditCommand is the command for editing a file.\nDeprecated: use Edit instead. Note that semantics are different:\nEditCommand is just the command itself, whereas Edit contains a\n\"{{filename}}\" variable."