Skip to content

Commit

Permalink
Merge pull request #387 from gusandrioli/wrap-custom-cmds-on-shell
Browse files Browse the repository at this point in the history
  • Loading branch information
jesseduffield authored Dec 13, 2022
2 parents 553b985 + d77b895 commit 6025912
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 42 deletions.
62 changes: 40 additions & 22 deletions pkg/commands/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
12 changes: 5 additions & 7 deletions pkg/commands/os_default_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}",
}
}
75 changes: 69 additions & 6 deletions pkg/commands/os_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package commands

import (
"fmt"
"io/ioutil"
"os"
"os/exec"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
})
}
}
8 changes: 3 additions & 5 deletions pkg/commands/os_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
}
4 changes: 4 additions & 0 deletions pkg/config/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
8 changes: 6 additions & 2 deletions pkg/gui/custom_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 6025912

Please sign in to comment.