From 3a9167cfe40cf3531d14db1840417433519dc144 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Fri, 12 Jun 2020 14:24:25 -0500 Subject: [PATCH 01/10] Implement shell aliases This command adds --shell to `gh alias set`, allowing specified aliases to be run through a shell interpreter. --- cmd/gh/main.go | 6 +++ command/alias.go | 85 +++++++++++++++++++---------------- command/alias_test.go | 101 ++++++++++++++++++++++++++++++++++-------- command/root.go | 34 +++++++++++--- 4 files changed, 162 insertions(+), 64 deletions(-) diff --git a/cmd/gh/main.go b/cmd/gh/main.go index bac1659957c..a52d23f217d 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -45,6 +45,12 @@ func main() { fmt.Fprintf(stderr, "failed to process aliases: %s\n", err) os.Exit(2) } + + if expandedArgs == nil && err == nil { + // It was an external alias; we ran it and are now done. + os.Exit(0) + } + if hasDebug { fmt.Fprintf(stderr, "%v -> %v\n", originalArgs, expandedArgs) } diff --git a/command/alias.go b/command/alias.go index 1b21fcc6198..b4df2411d81 100644 --- a/command/alias.go +++ b/command/alias.go @@ -16,21 +16,35 @@ func init() { aliasCmd.AddCommand(aliasSetCmd) aliasCmd.AddCommand(aliasListCmd) aliasCmd.AddCommand(aliasDeleteCmd) + + aliasSetCmd.Flags().BoolP("shell", "s", false, "Declare an alias to be passed through a shell interpreter") } var aliasCmd = &cobra.Command{ Use: "alias", - Short: "Create shortcuts for gh commands", + Short: "Create command shortcuts", + Long: heredoc.Doc(` + Aliases can be used to make shortcuts for gh commands or to compose multiple commands. + + Run "gh help alias set" to learn more. + `), } var aliasSetCmd = &cobra.Command{ Use: "set ", Short: "Create a shortcut for a gh command", - Long: `Declare a word as a command alias that will expand to the specified command. + Long: heredoc.Doc(` + Declare a word as a command alias that will expand to the specified command(s). -The expansion may specify additional arguments and flags. If the expansion -includes positional placeholders such as '$1', '$2', etc., any extra arguments -that follow the invocation of an alias will be inserted appropriately.`, + The expansion may specify additional arguments and flags. If the expansion + includes positional placeholders such as '$1', '$2', etc., any extra arguments + that follow the invocation of an alias will be inserted appropriately. + + If '--shell' is specified, the alias will be run through a shell interpreter (sh). This allows you + to compose commands with "|" or redirect output with ">". Note that extra arguments are not passed + to shell-interpreted aliases; only placeholders ("$1", "$2", etc) are supported. + + Quotes must always be used when defining a command as in the examples.`), Example: heredoc.Doc(` $ gh alias set pv 'pr view' $ gh pv -w 123 @@ -41,13 +55,12 @@ that follow the invocation of an alias will be inserted appropriately.`, $ gh alias set epicsBy 'issue list --author="$1" --label="epic"' $ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic" - `), - Args: cobra.MinimumNArgs(2), - RunE: aliasSet, - // NB: this allows a user to eschew quotes when specifying an alias expansion. We'll have to - // revisit it if we ever want to add flags to alias set but we have no current plans for that. - DisableFlagParsing: true, + $ gh alias set --shell igrep 'gh issue list --label="$1" | grep $2' + $ gh igrep epic foo + #=> gh issue list --label="epic" | grep "foo"`), + Args: cobra.ExactArgs(2), + RunE: aliasSet, } func aliasSet(cmd *cobra.Command, args []string) error { @@ -63,19 +76,26 @@ func aliasSet(cmd *cobra.Command, args []string) error { } alias := args[0] - expansion := processArgs(args[1:]) - - expansionStr := strings.Join(expansion, " ") + expansion := args[1] out := colorableOut(cmd) - fmt.Fprintf(out, "- Adding alias for %s: %s\n", utils.Bold(alias), utils.Bold(expansionStr)) + fmt.Fprintf(out, "- Adding alias for %s: %s\n", utils.Bold(alias), utils.Bold(expansion)) - if validCommand([]string{alias}) { + shell, err := cmd.Flags().GetBool("shell") + if err != nil { + return err + } + if shell && !strings.HasPrefix(expansion, "!") { + expansion = "!" + expansion + } + isExternal := strings.HasPrefix(expansion, "!") + + if validCommand(alias) { return fmt.Errorf("could not create alias: %q is already a gh command", alias) } - if !validCommand(expansion) { - return fmt.Errorf("could not create alias: %s does not correspond to a gh command", utils.Bold(expansionStr)) + if !isExternal && !validCommand(expansion) { + return fmt.Errorf("could not create alias: %s does not correspond to a gh command", expansion) } successMsg := fmt.Sprintf("%s Added alias.", utils.Green("✓")) @@ -86,11 +106,11 @@ func aliasSet(cmd *cobra.Command, args []string) error { utils.Green("✓"), utils.Bold(alias), utils.Bold(oldExpansion), - utils.Bold(expansionStr), + utils.Bold(expansion), ) } - err = aliasCfg.Add(alias, expansionStr) + err = aliasCfg.Add(alias, expansion) if err != nil { return fmt.Errorf("could not create alias: %s", err) } @@ -100,26 +120,13 @@ func aliasSet(cmd *cobra.Command, args []string) error { return nil } -func validCommand(expansion []string) bool { - cmd, _, err := RootCmd.Traverse(expansion) - return err == nil && cmd != RootCmd -} - -func processArgs(args []string) []string { - if len(args) == 1 { - split, _ := shlex.Split(args[0]) - return split - } - - newArgs := []string{} - for _, a := range args { - if !strings.HasPrefix(a, "-") && strings.Contains(a, " ") { - a = fmt.Sprintf("%q", a) - } - newArgs = append(newArgs, a) +func validCommand(expansion string) bool { + split, err := shlex.Split(expansion) + if err != nil { + return false } - - return newArgs + cmd, _, err := RootCmd.Traverse(split) + return err == nil && cmd != RootCmd } var aliasListCmd = &cobra.Command{ diff --git a/command/alias_test.go b/command/alias_test.go index a0d894e20df..346e40c137f 100644 --- a/command/alias_test.go +++ b/command/alias_test.go @@ -2,11 +2,13 @@ package command import ( "bytes" + "fmt" "strings" "testing" "github.com/cli/cli/internal/config" "github.com/cli/cli/test" + "github.com/stretchr/testify/assert" ) func TestAliasSet_gh_command(t *testing.T) { @@ -16,7 +18,7 @@ func TestAliasSet_gh_command(t *testing.T) { hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() - _, err := RunCommand("alias set pr pr status") + _, err := RunCommand("alias set pr 'pr status'") if err == nil { t.Fatal("expected error") } @@ -35,7 +37,7 @@ editor: vim hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() - output, err := RunCommand("alias set co pr checkout") + output, err := RunCommand("alias set co 'pr checkout'") if err != nil { t.Fatalf("unexpected error: %s", err) @@ -65,7 +67,7 @@ aliases: hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() - output, err := RunCommand("alias set co pr checkout -Rcool/repo") + output, err := RunCommand("alias set co 'pr checkout -Rcool/repo'") if err != nil { t.Fatalf("unexpected error: %s", err) @@ -81,7 +83,7 @@ func TestAliasSet_space_args(t *testing.T) { hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() - output, err := RunCommand(`alias set il issue list -l 'cool story'`) + output, err := RunCommand(`alias set il 'issue list -l "cool story"'`) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -99,23 +101,17 @@ func TestAliasSet_arg_processing(t *testing.T) { ExpectedOutputLine string ExpectedConfigLine string }{ - {"alias set co pr checkout", "- Adding alias for co: pr checkout", "co: pr checkout"}, - {`alias set il "issue list"`, "- Adding alias for il: issue list", "il: issue list"}, {`alias set iz 'issue list'`, "- Adding alias for iz: issue list", "iz: issue list"}, - {`alias set iy issue list --author=\$1 --label=\$2`, - `- Adding alias for iy: issue list --author=\$1 --label=\$2`, - `iy: issue list --author=\$1 --label=\$2`}, - {`alias set ii 'issue list --author="$1" --label="$2"'`, - `- Adding alias for ii: issue list --author=\$1 --label=\$2`, - `ii: issue list --author=\$1 --label=\$2`}, + `- Adding alias for ii: issue list --author="\$1" --label="\$2"`, + `ii: issue list --author="\$1" --label="\$2"`}, - {`alias set ix issue list --author='$1' --label='$2'`, - `- Adding alias for ix: issue list --author=\$1 --label=\$2`, - `ix: issue list --author=\$1 --label=\$2`}, + {`alias set ix "issue list --author='\$1' --label='\$2'"`, + `- Adding alias for ix: issue list --author='\$1' --label='\$2'`, + `ix: issue list --author='\$1' --label='\$2'`}, } for _, c := range cases { @@ -143,7 +139,7 @@ editor: vim hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() - output, err := RunCommand("alias set diff pr diff") + output, err := RunCommand("alias set diff 'pr diff'") if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -167,7 +163,7 @@ aliases: hostsBuf := bytes.Buffer{} defer config.StubWriteConfig(&mainBuf, &hostsBuf)() - output, err := RunCommand("alias set view pr view") + output, err := RunCommand("alias set view 'pr view'") if err != nil { t.Fatalf("unexpected error: %s", err) } @@ -181,6 +177,35 @@ aliases: } +func TestExpandAlias_external(t *testing.T) { + cfg := `--- +aliases: + co: pr checkout + il: issue list --author="$1" --label="$2" + ia: issue list --author="$1" --assignee="$1" + ig: '!gh issue list --label=$1 | grep' +` + initBlankContext(cfg, "OWNER/REPO", "trunk") + + cs, teardown := test.InitCmdStubber() + defer teardown() + cs.Stub("") + + _, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + assert.Equal(t, 1, len(cs.Calls)) + + fmt.Printf("DEBUG %#v\n", cs.Calls[0]) + + expected := []string{"sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} + + assert.Equal(t, expected, cs.Calls[0].Args) +} + func TestExpandAlias(t *testing.T) { cfg := `--- aliases: @@ -230,7 +255,7 @@ aliases: func TestAliasSet_invalid_command(t *testing.T) { initBlankContext("", "OWNER/REPO", "trunk") - _, err := RunCommand("alias set co pe checkout") + _, err := RunCommand("alias set co 'pe checkout'") if err == nil { t.Fatal("expected error") } @@ -319,3 +344,43 @@ aliases: eq(t, mainBuf.String(), expected) } + +func TestShellAlias_flag(t *testing.T) { + initBlankContext("", "OWNER/REPO", "trunk") + + mainBuf := bytes.Buffer{} + hostsBuf := bytes.Buffer{} + defer config.StubWriteConfig(&mainBuf, &hostsBuf)() + + output, err := RunCommand("alias set --shell igrep 'gh issue list | grep'") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + test.ExpectLines(t, output.String(), "Adding alias for igrep") + expected := `aliases: + igrep: '!gh issue list | grep' +` + + eq(t, mainBuf.String(), expected) +} + +func TestShellAlias_bang(t *testing.T) { + initBlankContext("", "OWNER/REPO", "trunk") + + mainBuf := bytes.Buffer{} + hostsBuf := bytes.Buffer{} + defer config.StubWriteConfig(&mainBuf, &hostsBuf)() + + output, err := RunCommand("alias set igrep '!gh issue list | grep'") + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + test.ExpectLines(t, output.String(), "Adding alias for igrep") + expected := `aliases: + igrep: '!gh issue list | grep' +` + + eq(t, mainBuf.String(), expected) +} diff --git a/command/root.go b/command/root.go index df607a50ddb..fa93ca89ea9 100644 --- a/command/root.go +++ b/command/root.go @@ -6,6 +6,7 @@ import ( "io" "net/http" "os" + "os/exec" "regexp" "runtime/debug" "strings" @@ -15,6 +16,7 @@ import ( "github.com/cli/cli/context" "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghrepo" + "github.com/cli/cli/internal/run" apiCmd "github.com/cli/cli/pkg/cmd/api" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" @@ -364,24 +366,43 @@ func determineEditor(cmd *cobra.Command) (string, error) { } func ExpandAlias(args []string) ([]string, error) { - empty := []string{} if len(args) < 2 { // the command is lacking a subcommand - return empty, nil + return []string{}, nil } ctx := initContext() cfg, err := ctx.Config() if err != nil { - return empty, err + return nil, err } aliases, err := cfg.Aliases() if err != nil { - return empty, err + return nil, err } expansion, ok := aliases.Get(args[1]) if ok { + if strings.HasPrefix(expansion, "!") { + shellArgs := []string{"-c", expansion[1:]} + if len(args[2:]) > 0 { + shellArgs = append(shellArgs, "--") + shellArgs = append(shellArgs, args[2:]...) + } + externalCmd := exec.Command("sh", shellArgs...) + externalCmd.Stderr = os.Stderr + externalCmd.Stdout = os.Stdout + externalCmd.Stdin = os.Stdin + preparedCmd := run.PrepareCmd(externalCmd) + + err := preparedCmd.Run() + if err != nil { + return nil, fmt.Errorf("failed to run external command: %w", err) + } + + return nil, nil + } + extraArgs := []string{} for i, a := range args[2:] { if !strings.Contains(expansion, "$") { @@ -392,7 +413,7 @@ func ExpandAlias(args []string) ([]string, error) { } lingeringRE := regexp.MustCompile(`\$\d`) if lingeringRE.MatchString(expansion) { - return empty, fmt.Errorf("not enough arguments for alias: %s", expansion) + return nil, fmt.Errorf("not enough arguments for alias: %s", expansion) } newArgs, err := shlex.Split(expansion) @@ -400,9 +421,8 @@ func ExpandAlias(args []string) ([]string, error) { return nil, err } - newArgs = append(newArgs, extraArgs...) + return append(newArgs, extraArgs...), nil - return newArgs, nil } return args[1:], nil From c3a5384895879e7e86c5d15a1f3bdd31c05abf77 Mon Sep 17 00:00:00 2001 From: nate smith Date: Thu, 2 Jul 2020 20:12:46 -0500 Subject: [PATCH 02/10] add experimental powershell support for shell aliases --- command/alias.go | 15 +++++-- command/alias_test.go | 94 +++++++++++++++++++++++++++++++++++++++---- command/root.go | 27 +++++++++++-- 3 files changed, 121 insertions(+), 15 deletions(-) diff --git a/command/alias.go b/command/alias.go index b4df2411d81..680763bfc31 100644 --- a/command/alias.go +++ b/command/alias.go @@ -40,9 +40,10 @@ var aliasSetCmd = &cobra.Command{ includes positional placeholders such as '$1', '$2', etc., any extra arguments that follow the invocation of an alias will be inserted appropriately. - If '--shell' is specified, the alias will be run through a shell interpreter (sh). This allows you - to compose commands with "|" or redirect output with ">". Note that extra arguments are not passed - to shell-interpreted aliases; only placeholders ("$1", "$2", etc) are supported. + If '--shell' is specified, the alias will be run through a shell interpreter (sh or pwsh). This allows you + to compose commands with "|" or redirect output. Note that extra arguments are not passed to + shell-interpreted aliases; only placeholders ("$1", "$2" on *nix, "$args" in powershell) are + supported. Quotes must always be used when defining a command as in the examples.`), Example: heredoc.Doc(` @@ -56,9 +57,15 @@ var aliasSetCmd = &cobra.Command{ $ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic" + # On macOS and Linux: $ gh alias set --shell igrep 'gh issue list --label="$1" | grep $2' $ gh igrep epic foo - #=> gh issue list --label="epic" | grep "foo"`), + #=> gh issue list --label="epic" | grep "foo" + + # On Windows (Powershell): + $ gh alias set --shell igrep 'gh issue list --label=$args[0] | Select-String -Pattern $args[1] + $ gh igrep epic foo + #=> gh issue list --label=epic | Select-String -Pattern foo`), Args: cobra.ExactArgs(2), RunE: aliasSet, } diff --git a/command/alias_test.go b/command/alias_test.go index 346e40c137f..6f5077aa4a2 100644 --- a/command/alias_test.go +++ b/command/alias_test.go @@ -2,7 +2,7 @@ package command import ( "bytes" - "fmt" + "runtime" "strings" "testing" @@ -177,12 +177,39 @@ aliases: } -func TestExpandAlias_external(t *testing.T) { +func TestExpandAlias_shell_nix(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test on windows") + } + cfg := `--- +aliases: + ig: '!gh issue list | grep cool' +` + initBlankContext(cfg, "OWNER/REPO", "trunk") + + cs, teardown := test.InitCmdStubber() + defer teardown() + cs.Stub("") + + _, err := ExpandAlias([]string{"gh", "ig"}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + assert.Equal(t, 1, len(cs.Calls)) + + expected := []string{"sh", "-c", "gh issue list | grep cool"} + + assert.Equal(t, expected, cs.Calls[0].Args) +} + +func TestExpandAlias_shell_nix_extra_args(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping test on windows") + } cfg := `--- aliases: - co: pr checkout - il: issue list --author="$1" --label="$2" - ia: issue list --author="$1" --assignee="$1" ig: '!gh issue list --label=$1 | grep' ` initBlankContext(cfg, "OWNER/REPO", "trunk") @@ -199,13 +226,66 @@ aliases: assert.Equal(t, 1, len(cs.Calls)) - fmt.Printf("DEBUG %#v\n", cs.Calls[0]) - expected := []string{"sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} assert.Equal(t, expected, cs.Calls[0].Args) } +func TestExpandAlias_shell_windows(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("skipping test on non-windows") + } + cfg := `--- +aliases: + ig: '!gh issue list | select-string -Pattern cool' +` + initBlankContext(cfg, "OWNER/REPO", "trunk") + + cs, teardown := test.InitCmdStubber() + defer teardown() + cs.Stub("") + + _, err := ExpandAlias([]string{"gh", "ig"}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + assert.Equal(t, 1, len(cs.Calls)) + + expected := []string{"pwsh", "-Command", "Invoke-Command -ScriptBlock { gh issue list | select-string -Pattern cool } "} + + assert.Equal(t, expected, cs.Calls[0].Args) +} + +func TestExpandAlias_shell_windows_extra_args(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("skipping test on non-windows") + } + cfg := `--- +aliases: + co: pr checkout + ig: '!gh issue list --label=$args[0] | select-string -Pattern $args[1]' +` + initBlankContext(cfg, "OWNER/REPO", "trunk") + + cs, teardown := test.InitCmdStubber() + defer teardown() + cs.Stub("") + + _, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + assert.Equal(t, 1, len(cs.Calls)) + + expected := []string{"pwsh", "-Command", "Invoke-Command -ScriptBlock { gh issue list --label=$args[0] | select-string -Pattern $args[1] } -ArgumentList @('bug','foo')"} + + assert.Equal(t, expected, cs.Calls[0].Args) +} + func TestExpandAlias(t *testing.T) { cfg := `--- aliases: diff --git a/command/root.go b/command/root.go index fa93ca89ea9..d8e6db028b1 100644 --- a/command/root.go +++ b/command/root.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "regexp" + "runtime" "runtime/debug" "strings" @@ -385,11 +386,29 @@ func ExpandAlias(args []string) ([]string, error) { if ok { if strings.HasPrefix(expansion, "!") { shellArgs := []string{"-c", expansion[1:]} - if len(args[2:]) > 0 { - shellArgs = append(shellArgs, "--") - shellArgs = append(shellArgs, args[2:]...) + shellCmd := "sh" + if runtime.GOOS == "windows" { + shellCmd = "pwsh" + argList := "" + if len(args[2:]) > 0 { + argList = " -ArgumentList @(" + for i, arg := range args[2:] { + argList += fmt.Sprintf("'%s'", arg) + if i < len(args[2:])-1 { + argList += "," + } + } + argList += ")" + } + invoke := fmt.Sprintf("Invoke-Command -ScriptBlock { %s } %s", expansion[1:], argList) + shellArgs = []string{"-Command", invoke} + } else { + if len(args[2:]) > 0 { + shellArgs = append(shellArgs, "--") + shellArgs = append(shellArgs, args[2:]...) + } } - externalCmd := exec.Command("sh", shellArgs...) + externalCmd := exec.Command(shellCmd, shellArgs...) externalCmd.Stderr = os.Stderr externalCmd.Stdout = os.Stdout externalCmd.Stdin = os.Stdin From bbd756a99f9e77590ee4a5718193d50e83a550b3 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 7 Jul 2020 16:10:32 -0500 Subject: [PATCH 03/10] split shell alias execution into new function --- cmd/gh/main.go | 11 +++++-- command/alias.go | 6 ++-- command/alias_test.go | 44 +++++++++++----------------- command/root.go | 68 ++++++++++++++++++++++++++----------------- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/cmd/gh/main.go b/cmd/gh/main.go index a52d23f217d..2e9614a79df 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -40,14 +40,19 @@ func main() { cmd, _, err := command.RootCmd.Traverse(expandedArgs) if err != nil || cmd == command.RootCmd { originalArgs := expandedArgs - expandedArgs, err = command.ExpandAlias(os.Args) + expandedArgs, isShell, err := command.ExpandAlias(os.Args) if err != nil { fmt.Fprintf(stderr, "failed to process aliases: %s\n", err) os.Exit(2) } - if expandedArgs == nil && err == nil { - // It was an external alias; we ran it and are now done. + if isShell { + err = command.ExecuteShellAlias(expandedArgs) + if err != nil { + fmt.Fprintf(stderr, "failed to run alias %q: %s\n", expandedArgs, err) + os.Exit(3) + } + os.Exit(0) } diff --git a/command/alias.go b/command/alias.go index 680763bfc31..5baceff33c5 100644 --- a/command/alias.go +++ b/command/alias.go @@ -41,9 +41,9 @@ var aliasSetCmd = &cobra.Command{ that follow the invocation of an alias will be inserted appropriately. If '--shell' is specified, the alias will be run through a shell interpreter (sh or pwsh). This allows you - to compose commands with "|" or redirect output. Note that extra arguments are not passed to - shell-interpreted aliases; only placeholders ("$1", "$2" on *nix, "$args" in powershell) are - supported. + to compose commands with "|" or redirect output. Note that extra arguments following the alias + will not be automatically passed to the expanded expression. To have a shell alias receive + arguments, you must explicitly accept them using "$1", "$2", etc or "$@" to accept all of them. Quotes must always be used when defining a command as in the examples.`), Example: heredoc.Doc(` diff --git a/command/alias_test.go b/command/alias_test.go index 6f5077aa4a2..f140a13f80d 100644 --- a/command/alias_test.go +++ b/command/alias_test.go @@ -187,21 +187,17 @@ aliases: ` initBlankContext(cfg, "OWNER/REPO", "trunk") - cs, teardown := test.InitCmdStubber() - defer teardown() - cs.Stub("") + expanded, isShell, err := ExpandAlias([]string{"gh", "ig"}) - _, err := ExpandAlias([]string{"gh", "ig"}) + assert.True(t, isShell) if err != nil { t.Fatalf("unexpected error: %s", err) } - assert.Equal(t, 1, len(cs.Calls)) - expected := []string{"sh", "-c", "gh issue list | grep cool"} - assert.Equal(t, expected, cs.Calls[0].Args) + assert.Equal(t, expected, expanded) } func TestExpandAlias_shell_nix_extra_args(t *testing.T) { @@ -214,21 +210,17 @@ aliases: ` initBlankContext(cfg, "OWNER/REPO", "trunk") - cs, teardown := test.InitCmdStubber() - defer teardown() - cs.Stub("") + expanded, isShell, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) - _, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) + assert.True(t, isShell) if err != nil { t.Fatalf("unexpected error: %s", err) } - assert.Equal(t, 1, len(cs.Calls)) - expected := []string{"sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} - assert.Equal(t, expected, cs.Calls[0].Args) + assert.Equal(t, expected, expanded) } func TestExpandAlias_shell_windows(t *testing.T) { @@ -245,17 +237,17 @@ aliases: defer teardown() cs.Stub("") - _, err := ExpandAlias([]string{"gh", "ig"}) + expanded, isShell, err := ExpandAlias([]string{"gh", "ig"}) + + assert.True(t, isShell) if err != nil { t.Fatalf("unexpected error: %s", err) } - assert.Equal(t, 1, len(cs.Calls)) - expected := []string{"pwsh", "-Command", "Invoke-Command -ScriptBlock { gh issue list | select-string -Pattern cool } "} - assert.Equal(t, expected, cs.Calls[0].Args) + assert.Equal(t, expected, expanded) } func TestExpandAlias_shell_windows_extra_args(t *testing.T) { @@ -269,21 +261,17 @@ aliases: ` initBlankContext(cfg, "OWNER/REPO", "trunk") - cs, teardown := test.InitCmdStubber() - defer teardown() - cs.Stub("") + expanded, isShell, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) - _, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) + assert.True(t, isShell) if err != nil { t.Fatalf("unexpected error: %s", err) } - assert.Equal(t, 1, len(cs.Calls)) - expected := []string{"pwsh", "-Command", "Invoke-Command -ScriptBlock { gh issue list --label=$args[0] | select-string -Pattern $args[1] } -ArgumentList @('bug','foo')"} - assert.Equal(t, expected, cs.Calls[0].Args) + assert.Equal(t, expected, expanded) } func TestExpandAlias(t *testing.T) { @@ -317,7 +305,9 @@ aliases: args = strings.Split(c.Args, " ") } - out, err := ExpandAlias(args) + expanded, isShell, err := ExpandAlias(args) + + assert.False(t, isShell) if err == nil && c.Err != "" { t.Errorf("expected error %s for %s", c.Err, c.Args) @@ -329,7 +319,7 @@ aliases: continue } - eq(t, out, c.ExpectedArgs) + assert.Equal(t, c.ExpectedArgs, expanded) } } diff --git a/command/root.go b/command/root.go index d8e6db028b1..7d26cfb880f 100644 --- a/command/root.go +++ b/command/root.go @@ -366,29 +366,50 @@ func determineEditor(cmd *cobra.Command) (string, error) { return editorCommand, nil } -func ExpandAlias(args []string) ([]string, error) { +func ExecuteShellAlias(args []string) error { + externalCmd := exec.Command(args[0], args[1:]...) + externalCmd.Stderr = os.Stderr + externalCmd.Stdout = os.Stdout + externalCmd.Stdin = os.Stdin + preparedCmd := run.PrepareCmd(externalCmd) + + err := preparedCmd.Run() + if err != nil { + return fmt.Errorf("failed to run external command: %w", err) + } + + return nil +} + +// ExpandAlias processes argv to see if it should be rewritten according to a user's aliases. The +// second return value indicates whether the alias should be executed in a new shell process instead +// of running gh itself. +func ExpandAlias(args []string) (expanded []string, isShell bool, err error) { + err = nil + isShell = false + expanded = []string{} + if len(args) < 2 { // the command is lacking a subcommand - return []string{}, nil + return } ctx := initContext() cfg, err := ctx.Config() if err != nil { - return nil, err + return } aliases, err := cfg.Aliases() if err != nil { - return nil, err + return } expansion, ok := aliases.Get(args[1]) if ok { if strings.HasPrefix(expansion, "!") { - shellArgs := []string{"-c", expansion[1:]} - shellCmd := "sh" + isShell = true + expanded = []string{"sh", "-c", expansion[1:]} if runtime.GOOS == "windows" { - shellCmd = "pwsh" argList := "" if len(args[2:]) > 0 { argList = " -ArgumentList @(" @@ -401,25 +422,15 @@ func ExpandAlias(args []string) ([]string, error) { argList += ")" } invoke := fmt.Sprintf("Invoke-Command -ScriptBlock { %s } %s", expansion[1:], argList) - shellArgs = []string{"-Command", invoke} + expanded = []string{"pwsh", "-Command", invoke} } else { if len(args[2:]) > 0 { - shellArgs = append(shellArgs, "--") - shellArgs = append(shellArgs, args[2:]...) + expanded = append(expanded, "--") + expanded = append(expanded, args[2:]...) } } - externalCmd := exec.Command(shellCmd, shellArgs...) - externalCmd.Stderr = os.Stderr - externalCmd.Stdout = os.Stdout - externalCmd.Stdin = os.Stdin - preparedCmd := run.PrepareCmd(externalCmd) - - err := preparedCmd.Run() - if err != nil { - return nil, fmt.Errorf("failed to run external command: %w", err) - } - return nil, nil + return } extraArgs := []string{} @@ -432,19 +443,22 @@ func ExpandAlias(args []string) ([]string, error) { } lingeringRE := regexp.MustCompile(`\$\d`) if lingeringRE.MatchString(expansion) { - return nil, fmt.Errorf("not enough arguments for alias: %s", expansion) + err = fmt.Errorf("not enough arguments for alias: %s", expansion) + return } - newArgs, err := shlex.Split(expansion) + var newArgs []string + newArgs, err = shlex.Split(expansion) if err != nil { - return nil, err + return } - return append(newArgs, extraArgs...), nil - + expanded = append(newArgs, extraArgs...) + return } - return args[1:], nil + expanded = args[1:] + return } func connectedToTerminal(cmd *cobra.Command) bool { From f99b54a7311677381df85270df2892584b7abb59 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Mon, 13 Jul 2020 16:03:28 -0500 Subject: [PATCH 04/10] WIP: experimental bash support for windows --- command/root.go | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/command/root.go b/command/root.go index 7d26cfb880f..92a93359590 100644 --- a/command/root.go +++ b/command/root.go @@ -410,19 +410,27 @@ func ExpandAlias(args []string) (expanded []string, isShell bool, err error) { isShell = true expanded = []string{"sh", "-c", expansion[1:]} if runtime.GOOS == "windows" { - argList := "" - if len(args[2:]) > 0 { - argList = " -ArgumentList @(" - for i, arg := range args[2:] { - argList += fmt.Sprintf("'%s'", arg) - if i < len(args[2:])-1 { - argList += "," - } - } - argList += ")" + //argList := "" + //if len(args[2:]) > 0 { + // argList = " -ArgumentList @(" + // for i, arg := range args[2:] { + // argList += fmt.Sprintf("'%s'", arg) + // if i < len(args[2:])-1 { + // argList += "," + // } + // } + // argList += ")" + //} + //invoke := fmt.Sprintf("Invoke-Command -ScriptBlock { %s } %s", expansion[1:], argList) + //expanded = []string{"pwsh", "-Command", invoke} + executable, err := os.Executable() + if err != nil { + return } - invoke := fmt.Sprintf("Invoke-Command -ScriptBlock { %s } %s", expansion[1:], argList) - expanded = []string{"pwsh", "-Command", invoke} + invoke := fmt.Sprintf("gh () { %s \"$@\" }; %s", executable, expansion[1:]) + + expanded = []string{"bash", "--posix", "-c", invoke, "--"} + expanded = append(expanded, args[2:]...) } else { if len(args[2:]) > 0 { expanded = append(expanded, "--") From 4bd0435c38b05941ce5dad461f7038a0f3f7da78 Mon Sep 17 00:00:00 2001 From: nate smith Date: Tue, 14 Jul 2020 15:26:15 -0500 Subject: [PATCH 05/10] successfully use sh for windows aliases --- command/alias_test.go | 8 ++++---- command/root.go | 38 +++++++++++++++----------------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/command/alias_test.go b/command/alias_test.go index f140a13f80d..753e9405593 100644 --- a/command/alias_test.go +++ b/command/alias_test.go @@ -229,7 +229,7 @@ func TestExpandAlias_shell_windows(t *testing.T) { } cfg := `--- aliases: - ig: '!gh issue list | select-string -Pattern cool' + ig: '!gh issue list | grep cool' ` initBlankContext(cfg, "OWNER/REPO", "trunk") @@ -245,7 +245,7 @@ aliases: t.Fatalf("unexpected error: %s", err) } - expected := []string{"pwsh", "-Command", "Invoke-Command -ScriptBlock { gh issue list | select-string -Pattern cool } "} + expected := []string{"C:\\Program Files\\Git\\bin\\sh.exe", "-c", "gh issue list | grep cool"} assert.Equal(t, expected, expanded) } @@ -257,7 +257,7 @@ func TestExpandAlias_shell_windows_extra_args(t *testing.T) { cfg := `--- aliases: co: pr checkout - ig: '!gh issue list --label=$args[0] | select-string -Pattern $args[1]' + ig: '!gh issue list --label=$1 | grep $2' ` initBlankContext(cfg, "OWNER/REPO", "trunk") @@ -269,7 +269,7 @@ aliases: t.Fatalf("unexpected error: %s", err) } - expected := []string{"pwsh", "-Command", "Invoke-Command -ScriptBlock { gh issue list --label=$args[0] | select-string -Pattern $args[1] } -ArgumentList @('bug','foo')"} + expected := []string{"C:\\Program Files\\Git\\bin\\sh.exe", "-c", "gh issue list --label=$1 | grep $2", "--", "bug", "foo"} assert.Equal(t, expected, expanded) } diff --git a/command/root.go b/command/root.go index 92a93359590..71de365890d 100644 --- a/command/root.go +++ b/command/root.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "os/exec" + "path/filepath" "regexp" "runtime" "runtime/debug" @@ -410,32 +411,23 @@ func ExpandAlias(args []string) (expanded []string, isShell bool, err error) { isShell = true expanded = []string{"sh", "-c", expansion[1:]} if runtime.GOOS == "windows" { - //argList := "" - //if len(args[2:]) > 0 { - // argList = " -ArgumentList @(" - // for i, arg := range args[2:] { - // argList += fmt.Sprintf("'%s'", arg) - // if i < len(args[2:])-1 { - // argList += "," - // } - // } - // argList += ")" - //} - //invoke := fmt.Sprintf("Invoke-Command -ScriptBlock { %s } %s", expansion[1:], argList) - //expanded = []string{"pwsh", "-Command", invoke} - executable, err := os.Executable() - if err != nil { - return + // Need to use absolute path for sh on windows + shPath, lookErr := exec.LookPath("sh") + if lookErr != nil { + gitPath, lookErr := exec.LookPath("git") + if lookErr != nil { + // TODO this error could be better probably + err = fmt.Errorf("unable to find sh. you will not be able to use shell aliases on this platform.") + return + } + shPath = filepath.Join(filepath.Dir(gitPath), "..", "bin", "sh.exe") } - invoke := fmt.Sprintf("gh () { %s \"$@\" }; %s", executable, expansion[1:]) + expanded[0] = shPath + } - expanded = []string{"bash", "--posix", "-c", invoke, "--"} + if len(args[2:]) > 0 { + expanded = append(expanded, "--") expanded = append(expanded, args[2:]...) - } else { - if len(args[2:]) > 0 { - expanded = append(expanded, "--") - expanded = append(expanded, args[2:]...) - } } return From a9d93f8c57ed2d85ccefd145ab0c205ed1f8ea83 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Tue, 14 Jul 2020 19:42:05 -0500 Subject: [PATCH 06/10] correct docs --- command/alias.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/command/alias.go b/command/alias.go index 5baceff33c5..87573fad260 100644 --- a/command/alias.go +++ b/command/alias.go @@ -41,10 +41,13 @@ var aliasSetCmd = &cobra.Command{ that follow the invocation of an alias will be inserted appropriately. If '--shell' is specified, the alias will be run through a shell interpreter (sh or pwsh). This allows you - to compose commands with "|" or redirect output. Note that extra arguments following the alias + to compose commands with "|" or redirect with ">". Note that extra arguments following the alias will not be automatically passed to the expanded expression. To have a shell alias receive arguments, you must explicitly accept them using "$1", "$2", etc or "$@" to accept all of them. + Platform note: on Windows, shell aliases are executed via "sh" as installed by Git For Windows. If + you have installed git on Windows in some other way, shell aliases may not work for you. + Quotes must always be used when defining a command as in the examples.`), Example: heredoc.Doc(` $ gh alias set pv 'pr view' @@ -57,15 +60,9 @@ var aliasSetCmd = &cobra.Command{ $ gh epicsBy vilmibm #=> gh issue list --author="vilmibm" --label="epic" - # On macOS and Linux: $ gh alias set --shell igrep 'gh issue list --label="$1" | grep $2' $ gh igrep epic foo - #=> gh issue list --label="epic" | grep "foo" - - # On Windows (Powershell): - $ gh alias set --shell igrep 'gh issue list --label=$args[0] | Select-String -Pattern $args[1] - $ gh igrep epic foo - #=> gh issue list --label=epic | Select-String -Pattern foo`), + #=> gh issue list --label="epic" | grep "foo"`), Args: cobra.ExactArgs(2), RunE: aliasSet, } From acaaa28fd727fea03fee1a7cf86ba757314100ff Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jul 2020 11:30:20 -0500 Subject: [PATCH 07/10] helper function for finding sh --- command/alias_test.go | 4 ++-- command/root.go | 46 ++++++++++++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/command/alias_test.go b/command/alias_test.go index 753e9405593..ca1832c166d 100644 --- a/command/alias_test.go +++ b/command/alias_test.go @@ -195,7 +195,7 @@ aliases: t.Fatalf("unexpected error: %s", err) } - expected := []string{"sh", "-c", "gh issue list | grep cool"} + expected := []string{"/usr/bin/sh", "-c", "gh issue list | grep cool"} assert.Equal(t, expected, expanded) } @@ -218,7 +218,7 @@ aliases: t.Fatalf("unexpected error: %s", err) } - expected := []string{"sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} + expected := []string{"/usr/bin/sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} assert.Equal(t, expected, expanded) } diff --git a/command/root.go b/command/root.go index 71de365890d..2b3aa6a4db3 100644 --- a/command/root.go +++ b/command/root.go @@ -382,6 +382,32 @@ func ExecuteShellAlias(args []string) error { return nil } +func findSh() (string, error) { + shPath, err := exec.LookPath("sh") + if err == nil { + return shPath, nil + } + + if runtime.GOOS == "windows" { + winNotFoundErr := errors.New("unable to locate sh to execute the shell alias with. The sh.exe interpreter is typically distributed with Git for Windows.") + // We can try and find a sh executable in a Git for Windows install + gitPath, err := exec.LookPath("git") + if err != nil { + return "", winNotFoundErr + } + + shPath = filepath.Join(filepath.Dir(gitPath), "..", "bin", "sh.exe") + _, err = os.Stat(shPath) + if err != nil { + return "", winNotFoundErr + } + + return shPath, nil + } + + return "", errors.New("unable to locate sh to execute shell alias with") +} + // ExpandAlias processes argv to see if it should be rewritten according to a user's aliases. The // second return value indicates whether the alias should be executed in a new shell process instead // of running gh itself. @@ -409,22 +435,14 @@ func ExpandAlias(args []string) (expanded []string, isShell bool, err error) { if ok { if strings.HasPrefix(expansion, "!") { isShell = true - expanded = []string{"sh", "-c", expansion[1:]} - if runtime.GOOS == "windows" { - // Need to use absolute path for sh on windows - shPath, lookErr := exec.LookPath("sh") - if lookErr != nil { - gitPath, lookErr := exec.LookPath("git") - if lookErr != nil { - // TODO this error could be better probably - err = fmt.Errorf("unable to find sh. you will not be able to use shell aliases on this platform.") - return - } - shPath = filepath.Join(filepath.Dir(gitPath), "..", "bin", "sh.exe") - } - expanded[0] = shPath + shPath, shErr := findSh() + if shErr != nil { + err = shErr + return } + expanded = []string{shPath, "-c", expansion[1:]} + if len(args[2:]) > 0 { expanded = append(expanded, "--") expanded = append(expanded, args[2:]...) From cfb8eebf306f7ec7206997b8862767e6e7825c43 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jul 2020 11:39:48 -0500 Subject: [PATCH 08/10] quietly return exit code of external command --- cmd/gh/main.go | 7 ++++++- command/root.go | 7 +------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/gh/main.go b/cmd/gh/main.go index 2e9614a79df..05c2f27bc20 100644 --- a/cmd/gh/main.go +++ b/cmd/gh/main.go @@ -6,6 +6,7 @@ import ( "io" "net" "os" + "os/exec" "path" "strings" @@ -49,7 +50,11 @@ func main() { if isShell { err = command.ExecuteShellAlias(expandedArgs) if err != nil { - fmt.Fprintf(stderr, "failed to run alias %q: %s\n", expandedArgs, err) + if ee, ok := err.(*exec.ExitError); ok { + os.Exit(ee.ExitCode()) + } + + fmt.Fprintf(stderr, "failed to run external command: %s", err) os.Exit(3) } diff --git a/command/root.go b/command/root.go index 2b3aa6a4db3..ddd4d983426 100644 --- a/command/root.go +++ b/command/root.go @@ -374,12 +374,7 @@ func ExecuteShellAlias(args []string) error { externalCmd.Stdin = os.Stdin preparedCmd := run.PrepareCmd(externalCmd) - err := preparedCmd.Run() - if err != nil { - return fmt.Errorf("failed to run external command: %w", err) - } - - return nil + return preparedCmd.Run() } func findSh() (string, error) { From cf3af450ebaaa324d0367294a449bd70a00e971f Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jul 2020 11:40:35 -0500 Subject: [PATCH 09/10] minor docs --- command/alias.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command/alias.go b/command/alias.go index 87573fad260..e5cec0b0b5d 100644 --- a/command/alias.go +++ b/command/alias.go @@ -40,10 +40,10 @@ var aliasSetCmd = &cobra.Command{ includes positional placeholders such as '$1', '$2', etc., any extra arguments that follow the invocation of an alias will be inserted appropriately. - If '--shell' is specified, the alias will be run through a shell interpreter (sh or pwsh). This allows you + If '--shell' is specified, the alias will be run through a shell interpreter (sh). This allows you to compose commands with "|" or redirect with ">". Note that extra arguments following the alias will not be automatically passed to the expanded expression. To have a shell alias receive - arguments, you must explicitly accept them using "$1", "$2", etc or "$@" to accept all of them. + arguments, you must explicitly accept them using "$1", "$2", etc., or "$@" to accept all of them. Platform note: on Windows, shell aliases are executed via "sh" as installed by Git For Windows. If you have installed git on Windows in some other way, shell aliases may not work for you. From 86912b31b224fcf57be7272c176f5adf7e6a8d68 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Wed, 15 Jul 2020 12:17:36 -0500 Subject: [PATCH 10/10] stub sh lookup --- command/alias_test.go | 78 +++++++++---------------------------------- command/root.go | 2 +- 2 files changed, 17 insertions(+), 63 deletions(-) diff --git a/command/alias_test.go b/command/alias_test.go index ca1832c166d..871a540a46f 100644 --- a/command/alias_test.go +++ b/command/alias_test.go @@ -2,7 +2,6 @@ package command import ( "bytes" - "runtime" "strings" "testing" @@ -11,6 +10,16 @@ import ( "github.com/stretchr/testify/assert" ) +func stubSh(value string) func() { + orig := findSh + findSh = func() (string, error) { + return value, nil + } + return func() { + findSh = orig + } +} + func TestAliasSet_gh_command(t *testing.T) { initBlankContext("", "OWNER/REPO", "trunk") @@ -177,10 +186,8 @@ aliases: } -func TestExpandAlias_shell_nix(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping test on windows") - } +func TestExpandAlias_shell(t *testing.T) { + defer stubSh("sh")() cfg := `--- aliases: ig: '!gh issue list | grep cool' @@ -195,15 +202,13 @@ aliases: t.Fatalf("unexpected error: %s", err) } - expected := []string{"/usr/bin/sh", "-c", "gh issue list | grep cool"} + expected := []string{"sh", "-c", "gh issue list | grep cool"} assert.Equal(t, expected, expanded) } -func TestExpandAlias_shell_nix_extra_args(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping test on windows") - } +func TestExpandAlias_shell_extra_args(t *testing.T) { + defer stubSh("sh")() cfg := `--- aliases: ig: '!gh issue list --label=$1 | grep' @@ -218,58 +223,7 @@ aliases: t.Fatalf("unexpected error: %s", err) } - expected := []string{"/usr/bin/sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} - - assert.Equal(t, expected, expanded) -} - -func TestExpandAlias_shell_windows(t *testing.T) { - if runtime.GOOS != "windows" { - t.Skip("skipping test on non-windows") - } - cfg := `--- -aliases: - ig: '!gh issue list | grep cool' -` - initBlankContext(cfg, "OWNER/REPO", "trunk") - - cs, teardown := test.InitCmdStubber() - defer teardown() - cs.Stub("") - - expanded, isShell, err := ExpandAlias([]string{"gh", "ig"}) - - assert.True(t, isShell) - - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - expected := []string{"C:\\Program Files\\Git\\bin\\sh.exe", "-c", "gh issue list | grep cool"} - - assert.Equal(t, expected, expanded) -} - -func TestExpandAlias_shell_windows_extra_args(t *testing.T) { - if runtime.GOOS != "windows" { - t.Skip("skipping test on non-windows") - } - cfg := `--- -aliases: - co: pr checkout - ig: '!gh issue list --label=$1 | grep $2' -` - initBlankContext(cfg, "OWNER/REPO", "trunk") - - expanded, isShell, err := ExpandAlias([]string{"gh", "ig", "bug", "foo"}) - - assert.True(t, isShell) - - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - expected := []string{"C:\\Program Files\\Git\\bin\\sh.exe", "-c", "gh issue list --label=$1 | grep $2", "--", "bug", "foo"} + expected := []string{"sh", "-c", "gh issue list --label=$1 | grep", "--", "bug", "foo"} assert.Equal(t, expected, expanded) } diff --git a/command/root.go b/command/root.go index ddd4d983426..f710193521d 100644 --- a/command/root.go +++ b/command/root.go @@ -377,7 +377,7 @@ func ExecuteShellAlias(args []string) error { return preparedCmd.Run() } -func findSh() (string, error) { +var findSh = func() (string, error) { shPath, err := exec.LookPath("sh") if err == nil { return shPath, nil