From d77b89531902b08d5f44c4d529c5955cee08610f Mon Sep 17 00:00:00 2001 From: Gustavo Andrioli Date: Sat, 15 Oct 2022 16:29:16 -0300 Subject: [PATCH] Wrap custom commands on shell --- pkg/commands/os.go | 62 +++++++++++++++--------- pkg/commands/os_default_platform.go | 12 ++--- pkg/commands/os_test.go | 75 ++++++++++++++++++++++++++--- pkg/commands/os_windows.go | 8 ++- pkg/config/app_config.go | 4 ++ pkg/gui/custom_commands.go | 8 ++- 6 files changed, 127 insertions(+), 42 deletions(-) diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 79b7dea3..ae229291 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -22,13 +22,11 @@ import ( // Platform stores the os state type Platform struct { - os string - shell string - shellArg string - escapedQuote string - openCommand string - openLinkCommand string - fallbackEscapedQuote string + os string + shell string + shellArg string + openCommand string + openLinkCommand string } // OSCommand holds all the os commands @@ -98,6 +96,25 @@ func (c *OSCommand) ExecutableFromStringContext(ctx context.Context, commandStr return exec.CommandContext(ctx, splitCmd[0], splitCmd[1:]...) } +func (c *OSCommand) NewCommandStringWithShell(commandStr string) string { + quotedCommand := "" + // Windows does not seem to like quotes around the command + if c.Platform.os == "windows" { + quotedCommand = strings.NewReplacer( + "^", "^^", + "&", "^&", + "|", "^|", + "<", "^<", + ">", "^>", + "%", "^%", + ).Replace(commandStr) + } else { + quotedCommand = c.Quote(commandStr) + } + + return fmt.Sprintf("%s %s %s", c.Platform.shell, c.Platform.shellArg, quotedCommand) +} + // RunCommand runs a command and just returns the error func (c *OSCommand) RunCommand(command string) error { _, err := c.RunCommandWithOutput(command) @@ -116,16 +133,6 @@ func (c *OSCommand) FileType(path string) string { return "file" } -// RunDirectCommand wrapper around direct commands -func (c *OSCommand) RunDirectCommand(command string) (string, error) { - c.Log.WithField("command", command).Info("RunDirectCommand") - - return sanitisedCommandOutput( - c.command(c.Platform.shell, c.Platform.shellArg, command). - CombinedOutput(), - ) -} - func sanitisedCommandOutput(output []byte, err error) (string, error) { outputString := string(output) if err != nil { @@ -190,12 +197,23 @@ func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *ex // Quote wraps a message in platform-specific quotation marks func (c *OSCommand) Quote(message string) string { - message = strings.Replace(message, "`", "\\`", -1) - escapedQuote := c.Platform.escapedQuote - if strings.Contains(message, c.Platform.escapedQuote) { - escapedQuote = c.Platform.fallbackEscapedQuote + var quote string + if c.Platform.os == "windows" { + quote = `\"` + message = strings.NewReplacer( + `"`, `"'"'"`, + `\"`, `\\"`, + ).Replace(message) + } else { + quote = `"` + message = strings.NewReplacer( + `\`, `\\`, + `"`, `\"`, + `$`, `\$`, + "`", "\\`", + ).Replace(message) } - return escapedQuote + message + escapedQuote + return quote + message + quote } // Unquote removes wrapping quotations marks if they are present diff --git a/pkg/commands/os_default_platform.go b/pkg/commands/os_default_platform.go index bbc3935a..03a5d965 100644 --- a/pkg/commands/os_default_platform.go +++ b/pkg/commands/os_default_platform.go @@ -9,12 +9,10 @@ import ( func getPlatform() *Platform { return &Platform{ - os: runtime.GOOS, - shell: "bash", - shellArg: "-c", - escapedQuote: "'", - openCommand: "open {{filename}}", - openLinkCommand: "open {{link}}", - fallbackEscapedQuote: "\"", + os: runtime.GOOS, + shell: "bash", + shellArg: "-c", + openCommand: "open {{filename}}", + openLinkCommand: "open {{link}}", } } diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go index e1a7f789..978844d9 100644 --- a/pkg/commands/os_test.go +++ b/pkg/commands/os_test.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "io/ioutil" "os" "os/exec" @@ -153,13 +154,14 @@ func TestOSCommandEditFile(t *testing.T) { } } -// TestOSCommandQuote is a function. func TestOSCommandQuote(t *testing.T) { osCommand := NewDummyOSCommand() + osCommand.Platform.os = "linux" + actual := osCommand.Quote("hello `test`") - expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote + expected := "\"hello \\`test\\`\"" assert.EqualValues(t, expected, actual) } @@ -169,12 +171,10 @@ func TestOSCommandQuoteSingleQuote(t *testing.T) { osCommand := NewDummyOSCommand() osCommand.Platform.os = "linux" - osCommand.Platform.fallbackEscapedQuote = "\"" - osCommand.Platform.escapedQuote = "'" actual := osCommand.Quote("hello 'test'") - expected := osCommand.Platform.fallbackEscapedQuote + "hello 'test'" + osCommand.Platform.fallbackEscapedQuote + expected := `"hello 'test'"` assert.EqualValues(t, expected, actual) } @@ -187,7 +187,20 @@ func TestOSCommandQuoteDoubleQuote(t *testing.T) { actual := osCommand.Quote(`hello "test"`) - expected := osCommand.Platform.escapedQuote + "hello \"test\"" + osCommand.Platform.escapedQuote + expected := `"hello \"test\""` + + assert.EqualValues(t, expected, actual) +} + +// TestOSCommandQuoteWindows tests the quote function for Windows +func TestOSCommandQuoteWindows(t *testing.T) { + osCommand := NewDummyOSCommand() + + osCommand.Platform.os = "windows" + + actual := osCommand.Quote(`hello "test" 'test2'`) + + expected := `\"hello "'"'"test"'"'" 'test2'\"` assert.EqualValues(t, expected, actual) } @@ -291,3 +304,53 @@ func TestOSCommandCreateTempFile(t *testing.T) { }) } } + +func TestOSCommandExecutableFromStringWithShellLinux(t *testing.T) { + osCommand := NewDummyOSCommand() + + osCommand.Platform.os = "linux" + + tests := []struct { + name string + commandStr string + want string + }{ + { + "success", + "pwd", + fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "\"pwd\""), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := osCommand.NewCommandStringWithShell(tt.commandStr) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestOSCommandNewCommandStringWithShellWindows(t *testing.T) { + osCommand := NewDummyOSCommand() + + osCommand.Platform.os = "windows" + + tests := []struct { + name string + commandStr string + want string + }{ + { + "success", + "pwd", + fmt.Sprintf("%v %v %v", osCommand.Platform.shell, osCommand.Platform.shellArg, "pwd"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := osCommand.NewCommandStringWithShell(tt.commandStr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/commands/os_windows.go b/pkg/commands/os_windows.go index 8fa9ce1c..f219993e 100644 --- a/pkg/commands/os_windows.go +++ b/pkg/commands/os_windows.go @@ -2,10 +2,8 @@ package commands func getPlatform() *Platform { return &Platform{ - os: "windows", - shell: "cmd", - shellArg: "/c", - escapedQuote: `\"`, - fallbackEscapedQuote: "\\'", + os: "windows", + shell: "cmd", + shellArg: "/c", } } diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 13815331..87bc4f2c 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -306,6 +306,10 @@ type CustomCommand struct { // option where the output plays in the main panel. Attach bool `yaml:"attach"` + // Shell indicates whether to invoke the Command on a shell or not. + // Example of a bash invoked command: `/bin/bash -c "{Command}". + Shell bool `yaml:"shell"` + // Command is the command we want to run. We can use the go templates here as // well. One example might be `{{ .DockerCompose }} exec {{ .Service.Name }} // /bin/sh` diff --git a/pkg/gui/custom_commands.go b/pkg/gui/custom_commands.go index 4580abd5..0c36b876 100644 --- a/pkg/gui/custom_commands.go +++ b/pkg/gui/custom_commands.go @@ -18,10 +18,14 @@ func (gui *Gui) createCommandMenu(customCommands []config.CustomCommand, command return command.InternalFunction() } + resolvedCommand := command.Command + if command.Shell { + resolvedCommand = gui.OSCommand.NewCommandStringWithShell(command.Command) + } + // if we have a command for attaching, we attach and return the subprocess error if command.Attach { - cmd := gui.OSCommand.ExecutableFromString(resolvedCommand) - return gui.runSubprocess(cmd) + return gui.runSubprocess(gui.OSCommand.ExecutableFromString(resolvedCommand)) } return gui.WithWaitingStatus(waitingStatus, func() error {