Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@vilmibm
Copy link
Contributor

@vilmibm vilmibm commented Jun 12, 2020

This PR augments gh pr alias set with the ability to accept -s/--shell. Shell aliases are executed through $SHELL and allow executing various commands in composition instead of just rewriting invocations of gh subcommands.

It works like this:

$ gh alias set -s checklist 'gh issue list -l$1 | sed "s/^/- [ ] /" > checklist.md'
- Adding alias for checklist: gh issue list -l$1 | sed "s/^/- [ ] /" > checklist.md
✓ Added alias.
$ gh checklist epic

Showing 2 of 2 issues in cli/cli that match your search

$ cat checklist.md
- [ ] 957       [Tracking issue] Improve GitHub authentication via GitHub CLI   enhancement, epic       about 4 days ago
- [ ] 939       Alias support phase 2   aliases, epic   about 12 days ago

Below is the initial sketch/discussion about this feature:


$ gh alias set igrep '!sh -c "gh issue list -L100 | grep $1"'
$ gh igrep alias

Showing 100 of 206 issues in cli/cli

1128    completions for aliases aliases, enhancement    about 18 minutes ago
1045    Share and consume gh aliases    enhancement     about 9 days ago
941     shell version of `gh alias set` aliases about 3 hours ago
940     Scriptability audit     aliases, tech-debt      about 9 days ago
939     Alias support phase 2   aliases, epic   about 2 hours ago

But the caveats:

! character

Enclosing the expansion in single quotes is necessary. both bash and zsh interpret ! as a
special character and it will not make it into gh, which will lead to potentially confusing errors
for users who forget to use single quotes when using ! style aliases.

Potential alternatives:

  • Use a different character. None really felt right to me and just about every special character
    means something to shells.
  • Have gh alias set respect a --external flag. This is circular because to support this we'd have
    to re-enable flag parsing on gh alias set. In other words, to support --external, this would
    no longer work: gh alias set co pr checkout.
  • Assume an external alias if the expansion does not map to a gh command. I don't love this;
    losing that bit of validation for non-external aliases seems not worth it.

my opinion: we're not going to do better than ! and requiring single quotes is acceptable.

quote madness

There are a lot of quote characters in gh alias set igrep '!sh -c "gh issue list | grep $1"'.

The sh -c in !sh -c "command goes here" is not needed in all cases. It's only necessary when you
want to compose commands.

For example, this works fine:

$ gh alias set echo '!echo'
$ gh echo hi how are you
hi how are you

But this won't do what you want:

$ gh alias set igrep '!gh issue list | grep $1'
$ gh igrep alias

# (...just runs issue list and discards subsequent |...)

My concern here is that it's cognitively confusing for people who aren't used to scripting in
shells. The sh -c is probably confusing and the need to put what they actually want to run in
the double quotes when they're already wrapping everything in single quotes could lead users to
frustration.

Potential alternative:

  • always wrapping ! aliases in sh -c "<expansion>". I've hacked this together and it seems to
    work well; the downside is that users are now stuck invoking their composed commands via sh.
    They might prefer to run things through zsh or bash to pick up their own shell aliases (i.e.
    aliases they've defined in their shells, not gh aliases). We could expose the shell for external
    aliases as a config option with a default of sh.

That could lead to something like this:

# In ZSH:
$ alias g=grep
$ gh alias set ig '!gh issue list | g $1'
$ gh ig alias
external alias failed: sh: 1: command not found: g

$ gh config set shell zsh
$ gh ig alias
# ... expected output ...

my opinion: I'm really intrigued by always wrapping in a sh -c "<...>". I like combining it with the new config setting.

Multiline input

I started experimenting with accepting an --input <filename> argument with an alias expansion in
it so that alias definitions could be written on multiple lines. This felt kind of bad; I wasn't
sure if suddenly having to worry about newlines in alias expansions was worth it.

Potential paths:

  • Continue trying to allow for file input and handling newlines appropriately when setting up
    commands.
  • Leave as-is and worry about more complex scripting in @mislav's extensions hack day proposal.

my opinion: we can punt on this for now.

If you read this far

I'd love feedback on the above three caveats as well as any other thoughts y'all have about this.

tl;dr:

  • is the ! character ok for signalling to gh that the user wants an external alias?
  • should we wrap all external aliases in sh -c "<expansion>"?
  • should we worry about multi-line input at this point?

@vilmibm vilmibm marked this pull request as draft June 12, 2020 20:43
@vilmibm vilmibm changed the title WIP: shell aliases RFC: shell aliases Jun 12, 2020
@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 12, 2020

ALSO I've tested this on windows; it works but sh is, of course, not there. I'm not sure what the recommended way of doing this in Windows is so I'll research that next week.

@probablycorey
Copy link
Contributor

Thanks for such a great writeup @vilmibm!

! character

I’m leaning more towards something like the —external flag. The main reason is it makes things very obvious when you read it.

$ gh alias set igrep '!gh issue list | grep $1' vs $ gh alias set --external igrep "gh issue list | grep $1"

The ! is handy, but it feels a little too magical to me. Naming it something like —shell advanced or —complex would also make sense to me.

quote madness

I think always wrapping the command in $SHELL -c makes a lot of sense. Especially if we can detect their default shell and put it in the config. It’s magical on the code side, but I think it matches expectations the use will have.

Multiline input

I don’t have strong opinions on this right now. Seeing some examples where it would make the aliases better or easier to use would be 👍

@mislav
Copy link
Contributor

mislav commented Jun 15, 2020

Thank you for writing this up @vilmibm!

  • is the ! character ok for signalling to gh that the user wants an external alias?

We original set out to support quote-less syntax defining aliases, e.g. gh alias set foo issue list --foo. If we introduce ! into the syntax, the quote-less approach will no longer work for shells like bash. Since we don't really advertise quote-less support, I wonder whether it makes sense to keep it?

Moreover, the ! character is special in yaml too, so if someone manually adds an alias to their aliases: config file section and doesn't properly quote or escape the ! character, their config file will suddenly trigger yaml-parsing errors.

I would really like us to avoid !. These are also ruled out due to their specialness in yaml: #, &, *, @, %. That leaves us with: ^, :, +. These characters are safer both in yaml and in shell.

There is also an option to not require any special character prefix, but to detect when shell features such as piping and redirection are used in an alias and automatically execute it though a shell interpreter. But this approach might be brittle.

  • should we wrap all external aliases in sh -c "<expansion>"?

I had expected that all "external" aliases are executed by sh so that they can support syntax such as piping and output redirection. I have understood that this was the entire point of "external" aliases.

  • should we worry about multi-line input at this point?

No, I don't think so!

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 17, 2020

That leaves us with: ^, :, +. These characters are safer both in yaml and in shell.

None of these really felt right; I like that ! at least has a vaguely consistent meaning in the CLI world of "call something."

As you point out we lose the quote-less behavior either way when a user is adding shell aliases; I think a -e/--external flag is a good place to start given that observation.

There is also an option to not require any special character prefix, but to detect when shell features such as piping and redirection are used in an alias and automatically execute it though a shell interpreter. But this approach might be brittle.

I thought about this and was also worried it would be brittle, but I wonder how often I, <, and > really appear in arguments? A middle ground might be detecting that and then suggesting --external to the user.

I had expected that all "external" aliases are executed by sh so that they can support syntax such as piping and output redirection. I have understood that this was the entire point of "external" aliases.

My baseline for external aliases is that whatever right hand side is provided is passed to exec.Command; not that we run it through sh -c. I was replicating git's example which requires explicit use of $SHELL -c "..." to support redirection and composition.

It sounds like you are in favor of passing everything through a shell implicitly, @mislav ; do you agree that having shell be configurable makes sense in that case (with a default of $SHELL)?

I propose this kind of experience which still doesn't feel perfect but I think will cover most cases:

$ gh alias set co 'pr checkout'
Added alias.

$ gh alias set igrep 'issue list | grep $1'
! It looks like you're trying to use a command external to gh. Did you want --external?
Added alias.

$ gh igrep bug
# ... shows issue list output with no composition ... 

$ gh alias set -e igrep 'issue list | grep $1'
! "issue" command not found on path
Updated alias.

$ gh igrep bug
issue: command not found

$ gh alias set -e igrep 'gh issue list | grep $1'
Updated alias.

$ gh igrep bug
# ... shows list output with grepping as expected ...

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 17, 2020

I chatted with @ampinsk about this and we decided to investigate the feasibility of letting a user not have to think about external vs. internal aliases.

We'll try and detect when a alias ought to be run through $SHELL (ie presence of certain characters, command that isn't a gh subcommand but is on $PATH) and then act accordingly. (something @mislav and I were discussing as interesting but possibly too brittle).

I'll take a shot at this so we can play with it and see how it feels in practice.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 17, 2020

@mislav @ampinsk I got implicit internal/external aliases working as discussed above. It was easier to just talk through it so I made a 6 minute video demo:

https://www.youtube.com/watch?v=lsw-tzcJmmU

If y'all like it I'll get the code cleaned up and tested, just let me know!~

@billygriffin
Copy link
Contributor

@vilmibm This is SO cool to see - I loved it all and really appreciate how much thought you and @ampinsk have put into it. 💖

The one question I had was about the warning combined with creating the alias. I wonder if a compromise there would be the warning and then a prompt with something like: "Would you like to create this alias? (Y/n)" It feels strange to me to have to go back and delete something just because of a typo, but maybe y'all already discussed that.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 18, 2020

@billygriffin I'm fine with making it a prompt! we did not explicitly discuss it.

@mislav
Copy link
Contributor

mislav commented Jun 18, 2020

My baseline for external aliases is that whatever right hand side is provided is passed to exec.Command; not that we run it through sh -c.

I see. I had understood that all git's "shell" aliases are implicitly evaluated through a shell even if sh -c '...' was not used in the alias. In fact, that's the behavior that I'm seeing:

$ git config alias.foo '!type echo && echo $SHELL || echo ERROR >&2; echo "${USER#mi}"; echo arg:'
$ git foo bar
echo is a shell builtin
/bin/bash
slav
arg: bar

Even though I didn't use sh -c or $SHELL -c, the alias seems to have been evaluated in a shell, as evident by expansions of different shell syntaxes such as redirection and chaining. The extra argument bar was appended at the end.

I was replicating git's example which requires explicit use of $SHELL -c "..." to support redirection and composition.

From what I understand, explicit usage of sh -c or $SHELL -c inside a shell alias only adds value if we are using it to inject positional arguments somewhere in the middle of the command instead of at the end:

$ git config alias.foo '!echo 2:$2 1:$1 DONE'; git foo one two
2:two 1:one DONE one two

$ git config alias.foo '!sh -c "echo 2:\\$2 1:\\$1 DONE"'; git foo one two
2:two 1:one DONE

I got implicit internal/external aliases working as discussed above. It was easier to just talk through it so I made a 6 minute video demo:

Thank you for recording this! It really communicates your goals and the potential approach well.

Since external aliases can potentially contain complex syntax, I'm wary of trying to auto-detect anything within them, such as looking at the first word and determining whether it's a gh command or an external utility somewhere in the person's PATH. I'm 4/5 on how strongly I feel that any attempt of auto-detection make ourselves vulnerable to all kinds of edge-cases which would arise when people copy-paste their aliases between different machines, from each other, and when their PATH changes between when they've recorded the alias and when they execute it.

One thing I really value about git's ! aliases is that they are explicitly marked and always guaranteed to work the same: whatever alias is configured will always be passed verbatim through a shell interpreter. Their non-magicness is a feature in it of itself.

I realize that I was the one who casually suggested auto-detection 🙈 Now that I've considered it from different angles and heard your exploration and thoughts on it, I'm much more leaning to a system where regular and "external" aliases are strongly delineated.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 18, 2020

I had understood that all git's "shell" aliases are implicitly evaluated through a shell even if sh -c '...' was not used in the alias.

Ah, I got confused; I saw the explicit use in some documentation and assumed it was required without testing it.

Your concerns with the implicit approach are valid. My big concern is the inconsistency around specifying gh; I felt very unsure about the automagic I was doing around that. It went against my usual impulses (explicitness, nonmagic) but I was pleasantly surprised by how natural actually writing the aliases felt.

I don't think I have strongly held opinions in any direction aside from now agreeing we should avoid !. After talking to Amanda I don't like -e/--external much, either, but I think -s/--shell might work?

I'll implement that and we can all make a final decision.

(cc @ampinsk )

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 19, 2020

Ok; now things work like this:

$ ghd alias set prs 'pr status'
- Adding alias for prs: pr status
✓ Added alias.

$ ghd alias set igrep '!gh issue list -L100 | grep $1'
- Adding alias for igrep: !gh issue list -L100 | grep $1
✓ Added alias.

$ ghd alias set --shell checklist 'ghd issue list --label=$1 | sed "s/^/- [ ] /" > checklist.md'
- Adding alias for checklist: ghd issue list --label=$1 | sed "s/^/- [ ] /" > checklist.md
✓ Added alias.

$ ghd alias set -s echo echo
- Adding alias for echo: echo
✓ Added alias.

$ ghd alias list
bugs:       issue list --label=bug
checklist:  !ghd issue list --label=$1 | sed "s/^/- [ ] /" > checklist.md
clone:      repo clone
co:         pr checkout
echo:       !echo
igrep:      !gh issue list -L100 | grep $1
prs:        pr status

@mislav @ampinsk if this works for y'all I'll get started on tests.

@mislav
Copy link
Contributor

mislav commented Jun 22, 2020

@vilmibm That looks great ✨

I'm noticing that alias set '!...' and alias set --shell '...' are equivalents in functionality. Would we consider only supporting one of these syntaxes for ease of documentation? My vote would be towards alias set --shell because of its explicitness.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jun 22, 2020

Would we consider only supporting one of these syntaxes for ease of documentation?

Since we're going to prepend a ! for aliases created via --shell it seemed confusing to not allow a user to prepend it themselves; that said, I'd be comfortable not documenting creating an alias with !. Does that make sense?

@mislav
Copy link
Contributor

mislav commented Jun 22, 2020

Since we're going to prepend a ! for aliases created via --shell it seemed confusing to not allow a user to prepend it themselves; that said, I'd be comfortable not documenting creating an alias with !

Ah that makes perfect sense. Thank you!

@vilmibm vilmibm changed the title RFC: shell aliases shell aliases Jun 24, 2020
@vilmibm vilmibm marked this pull request as ready for review June 24, 2020 18:17
@vilmibm vilmibm requested review from mislav and probablycorey June 24, 2020 19:10
Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! What I like about the new feature is that some old parts of the code now look even simpler, but there is more functionality ✨

The last blocking hurdle we need to cross is figuring out how to safely append extra positional arguments in shell mode.

cmd/gh/main.go Outdated
Comment on lines 49 to 57
if expandedArgs == nil && err == nil {
os.Exit(0)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to leave a comment about which case does this cover. It's not apparent to me from the code.

command/alias.go Outdated
Comment on lines 43 to 44
If --shell is specified, the alias will be run through a shell. This allows you to compose
commands with | or redirect output with >.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we quote bits that are not prose?

Suggested change
If --shell is specified, the alias will be run through a shell. This allows you to compose
commands with | or redirect output with >.
If '--shell' is specified, the alias will be run through a shell. This allows you to compose
commands with "|" or redirect output with ">".

command/alias.go Outdated
aliasCmd.AddCommand(aliasListCmd)
aliasCmd.AddCommand(aliasDeleteCmd)

aliasSetCmd.Flags().BoolP("shell", "s", false, "Whether the alias should be passed to a shell")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to avoid starting a description with "Whether", what do you think of something like this? “Declare an alias to be passed through a shell interpreter”

command/root.go Outdated
Comment on lines 424 to 427
if !strings.HasPrefix(arg, "-") {
expansion += fmt.Sprintf(" %q ", arg)
} else {
expansion += fmt.Sprintf(" %s ", arg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have some worries about how this appends additional arguments. First of all, arguments are appended as strings to expansion, which is risky because we then need to ensure that they are quoted or otherwise escaped. Second, I don't think it gains us anything to differentiate between arguments that start with - vs. those that don't, since both can contain spaces and other special characters.

Using %q to quote a string works only for the simplest values, but we should keep in mind that this produces Go syntax which is not necessarily compatible with shells. For instance, newlines and tab characters will be encoded as \n and \t, which in bash will result in literal \n and \t strings. Furthermore, using %q doesn't escape characters that may have special meaning in the shell; most notably $.

The safest way to pass additional arguments would be to avoid encoding them as escaped strings and instead passing them as extra arguments in exec.Command. The shell equivalent of that would be:

$ sh -c 'echo 1:$1 2:$2' -- foo bar
1:foo 2:bar

$ bash -c 'echo 1:$1 2:$2' -- foo bar
1:foo 2:bar

$ zsh -c 'echo 1:$1 2:$2' -- foo bar
1:foo 2:bar

The alias would then have to explicitly choose where to place positional parameters $1, $2, "$@", etc.

Unfortunately, extra arguments appended after -c <COMMAND> are downright ignored by fish fish-shell/fish-shell#2314

So for fish we would have to do something different or, if we go back to square one, we'd have to ensure that all arguments appended to expansion would have to be shell-escaped so that no characters within have any special meaning.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've found how git handles it: basically it always does sh -c '<command> "$@"' <args> regardless of the SHELL environment variable. https://github.com/git/git/blob/050319c2ae82f2fac00eac9d80a1d91394d99aec/run-command.c#L266-L291

I think that we could follow the same principle so that people's aliases work identically regardless of their current interactive shell?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with just using sh; I considered expanding $SHELL in case people wanted to rely on their shell-specific aliases. I feel like we can worry about that if it's requested.

As for the additional argument handling, I'm very on board. I hated the code I wrote and knew several ways it would break; i just wasn't sure how to improve on it. Mandating sh and using the -- trick (TIL) is great. I'll work on that now.

@vilmibm vilmibm requested a review from mislav July 1, 2020 18:45
@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 1, 2020

@mislav Caught up on the feedback as well as what we discussed in our call.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 6, 2020

@mislav The windows support is ready for review now.

Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking great! My main asks are to remove subprocess execution from ExpandAlias and to rethink shell aliases on Windows.

The approach you've taken with PowerShell looks good technically, but I wonder if it's the right thing from the usability perspective. We wanted our users to be able to share aliases between each other, but with the current approach, Windows users and those on other platforms will never be able to share their shell aliases. I know it's tricky to support sh on Windows, but maybe we could find a way as followup, and until then not support shell aliases executing for Windows users unless they have sh interpreter in their PATH? Somehow I'm more comfortable with the approach that ships fewer features than the approach that offers a feature that works with a different syntax across platforms. 🤔

cmd/gh/main.go Outdated
Comment on lines 49 to 56
if expandedArgs == nil && err == nil {
// It was an external alias; we ran it and are now done.
os.Exit(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added comment helped me understand why we exit here when both values are nil; thank you.

I'm feeling uneasy about a function called ExpandAlias also executing an external process if the alias starts with !, and otherwise just returning a slice of strings. I'm thinking that the function is too overloaded, since it has very different responsibilities depending on whether an alias starts with !. Would you be open to changing ExpandAlias to always return a slice of strings and a boolean value that dictates whether those arguments should be run as an external process or through Cobra?

With that, the main.go implementation will be in charge of actually spawning that external process, which I feel is a more suitable place for it since it's also in charge of dispatching Cobra arguments and handling errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I think I was just trying to keep all of my changes in one small box but ExpandAlias is definitely too monstrous now.

command/alias.go Outdated
Comment on lines 44 to 45
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“Note that extra arguments are not passed” - I find this to be misleading. Extra arguments are actually passed and available to shell aliases; it's just that they need to be explicitly placed into the expanded expression. Maybe you could change the wording to reflect that? Something in the lines of:

Note that any extra arguments following the alias will not be automatically appended to the expanded expression. To have the alias expansion receive arguments, use $1, $2, etc. for positional arguments or "$@" to place all arguments.

command/root.go Outdated
Comment on lines 394 to 396
argList = " -ArgumentList @("
for i, arg := range args[2:] {
argList += fmt.Sprintf("'%s'", arg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've successfully avoided having to shell-escape arguments to sh interpreter, but here we're back on square one with PowerShell since arguments with characters such as ' will cause syntax errors if they are inserted into the above expression.

Is there a way of passing arguments to PowerShell that doesn't require us to escape them? E.g.

exec.Command("pwsh", "-Command", pwshCommand, "-args", args[2:]...)
// if pwshCommand needs to pass arguments internally, it can maybe use `$args`?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fails (and was where I started :) ).

Passing arguments like this requires a literal comma separated list, unlike the sh family of interpreters. At a minimum I can join the arguments together with ,, but this fails for arguments with spaces. It is back where we started with sh but unlike sh I could find no equivalence to --.

I think that breaking on ' is bad but an acceptable edge case (at least for now) if we decide to ship pwsh support here.

Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, my above review was supposed to be "Request changes" 🙇

@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 7, 2020

I know it's tricky to support sh on Windows, but maybe we could find a way as followup, and until then not support shell aliases executing for Windows users unless they have sh interpreter in their PATH?

I don't like this approach. It feels almost rude to windows users and reminds me of the Cygwin days ("You can run this tool designed for unix, sure, but you need to install and manage this other runtime to do so").

Windows has decent command line tooling now and I think it's worth the extra effort and code to embrace that.

That said -- if we're requiring git to be installed, I believe that means we can rely on bash being installed. Is that a decent compromise? I'm trying to avoid a situation where Windows users do not get access to a feature until they take some manual step on their own (outside of the steps required to simply install gh).

@mislav
Copy link
Contributor

mislav commented Jul 10, 2020

I'm trying to avoid a situation where Windows users do not get access to a feature until they take some manual step on their own

Absolutely agreed. Ideally, gh should just work out of the box (provided git is installed) on every platform.

On Windows, if someone installed Git for Windows, there's a large chance that they have the bash interpreter installed. We could use bash --posix as a mechanism to execute shell aliases over.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 13, 2020

Switching to gitbash is not as simple we expected.

While we can run aliases through it and call the utilities that gitbash provides, gitbash knows nothing about gh. We can get the path to the executable via os.Executable() but, being windows, it's in C:\Users\...\gh.exe format, which gitbash cannot parse. Short of manually translating the path (which isn't just a straight substitution: drive letters are mapped to /mnt/C/... and we'd have to translate that too) I'm not sure of a reliable way to inject gh into gitbash's PATH.

This all works for git on windows since gitbash is specifically tailored to, well, run git. It's not tailored to run gh and I think forcing it to do so is a bad idea.

I spent the last part of my day attempting to make argument passing more robust when using pwsh. I have learned a tremendous amount about how powershell parses things and have a few leads on a more robust way to pass arguments but nothing solid yet; I am pessimistic that I can do any better than layers of escaping.

After my day of disappointing research I think we should give up on gitbash or any hope for having aliases share-able between windows and !windows. I'll put another few hours of research into improving argument passing for powershell but suspect that what is in 0459eaf is the best I'll be able to come up with for this first attempt.

@mislav
Copy link
Contributor

mislav commented Jul 14, 2020

I'm not sure of a reliable way to inject gh into gitbash's PATH.

I'm thinking that we might not need to change PATH at all. Both Git for Windows and GitHub CLI installers add the respective binaries to the system PATH. We can assume that this has happened and that unqualified git and gh will always be available for execution no matter what the context (cmd.exe, PowerShell, Git Bash, Cygwin, etc.).

Here is what I propose for executing shell aliases:

  1. Use sh if found in the PATH. This covers: macOS, Linux, Git Bash on Windows, Cygwin, WSL.

  2. If sh was unavailable, then try to find it in the following location: GIT_EXE_DIR\..\bin\sh, where GIT_EXE_DIR is found by locating git.exe in PATH. This will resolve to something like C:\Program Files\Git\bin\sh, where Git for Windows puts both sh and bash executables. (Surprisingly, C:\Program Files\Git\bin itself is not in PATH.)

  3. If sh is still unavailable, then we print a message to the user that shell aliases cannot be executed in their current setup.

Basically, we would do from Go the equivalent of this PowerShell invocation, which works for me with no extra setup apart from installing Git for Windows and GitHub CLI:

& "C:\Program Files\Git\bin\sh" -c "gh help | grep gist"

Do you think this would be feasible and would serve our users well?

@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 14, 2020

I might be missing something but I don't see how

& "C:\Program Files\Git\bin\sh" -c "gh help | grep gist"

could work. sh does not know how to run gh and producing an absolute path to gh for sh to use is difficult for the reasons I stated.

@mislav
Copy link
Contributor

mislav commented Jul 14, 2020

sh does not know how to run gh

I see! For me, when sh executes it somehow already has gh in its PATH, since the parent process also had it in its PATH. But to be honest, I don't know how its filesystem path translation works on the boundary from PowerShell → sh nor why you don't have the same experience. I will try to look into this deeper.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 14, 2020

I haven't dug into the why but I at least see how we're getting different results, here.

bash -c 'gh issue list => bash runs, gh not found
sh -c 'gh issue list => windows can't find sh
C:\Program Files\Git\bin\sh -c 'gh issue list' => works fine
C:\Program Files\Git\bin\bash -c 'gh issue list' => works fine

I'm guessing that when I just run bash there is some kind of shim that alters the path.

I will incorporate calling sh via an absolute path into the code and see how that works. Thanks for helping investigate this.

@vilmibm
Copy link
Contributor Author

vilmibm commented Jul 14, 2020

Based on our combined investigation, I finally have shell aliases working on Windows via gitbash.

PS C:\Users\vilmi\src\cli\cmd\gh> .\gh.exe alias set -s sigh 'gh issue list --label $1 | grep $2'
PS C:\Users\vilmi\src\cli\cmd\gh> .\gh.exe sigh epic alias                                                                                                       [C:\Program Files\Git\bin\sh.exe -c gh issue list --label $1 | grep $2 -- epic alias]                                                                                                                                                                                                                                                                                                                              Showing 2 of 2 issues in cli/cli that match your search                                                                                                                                                                                                                                                                           939     Alias support phase 2   aliases, epic   about 1 month ago                      

Copy link
Contributor

@mislav mislav left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your hard work! This works great for me on Windows.

I've added a few notes that are generally polish. Mainly, I'm thinking about aliases that exit with a nonzero status, and to improve the error messages around locating sh.exe.

@vilmibm vilmibm merged commit 619ef43 into trunk Jul 15, 2020
@mislav mislav deleted the shell-alias branch July 16, 2020 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants