Implement shell aliases
This command adds --shell to `gh alias set`, allowing specified aliases to be run through a shell interpreter.
This commit is contained in:
parent
2b96d2ce1f
commit
3a9167cfe4
4 changed files with 162 additions and 64 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <alias> <expansion>",
|
||||
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,28 +120,15 @@ func aliasSet(cmd *cobra.Command, args []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func validCommand(expansion []string) bool {
|
||||
cmd, _, err := RootCmd.Traverse(expansion)
|
||||
func validCommand(expansion string) bool {
|
||||
split, err := shlex.Split(expansion)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
cmd, _, err := RootCmd.Traverse(split)
|
||||
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)
|
||||
}
|
||||
|
||||
return newArgs
|
||||
}
|
||||
|
||||
var aliasListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List your aliases",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue