diff --git a/command/alias.go b/command/alias.go index b4df2411d..680763bfc 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 346e40c13..6f5077aa4 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 fa93ca89e..d8e6db028 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