-
Notifications
You must be signed in to change notification settings - Fork 7.3k
shell aliases #1191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
shell aliases #1191
Conversation
|
ALSO I've tested this on windows; it works but |
|
Thanks for such a great writeup @vilmibm! ! character I’m leaning more towards something like the
The quote madness I think always wrapping the command in 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 👍 |
|
Thank you for writing this up @vilmibm!
We original set out to support quote-less syntax defining aliases, e.g. Moreover, the I would really like us to avoid 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 had expected that all "external" aliases are executed by
No, I don't think so! |
None of these really felt right; I like that As you point out we lose the quote-less behavior either way when a user is adding shell aliases; I think a
I thought about this and was also worried it would be brittle, but I wonder how often
My baseline for external aliases is that whatever right hand side is provided is passed to 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 I propose this kind of experience which still doesn't feel perfect but I think will cover most cases: |
|
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 I'll take a shot at this so we can play with it and see how it feels in practice. |
|
@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!~ |
|
@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. |
|
@billygriffin I'm fine with making it a prompt! we did not explicitly discuss it. |
I see. I had understood that all git's "shell" aliases are implicitly evaluated through a shell even if Even though I didn't use
From what I understand, explicit usage of
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 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. |
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 I don't think I have strongly held opinions in any direction aside from now agreeing we should avoid I'll implement that and we can all make a final decision. (cc @ampinsk ) |
|
Ok; now things work like this: @mislav @ampinsk if this works for y'all I'll get started on tests. |
|
@vilmibm That looks great ✨ I'm noticing that |
Since we're going to prepend a |
Ah that makes perfect sense. Thank you! |
There was a problem hiding this 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
| if expandedArgs == nil && err == nil { | ||
| os.Exit(0) | ||
| } |
There was a problem hiding this comment.
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
| If --shell is specified, the alias will be run through a shell. This allows you to compose | ||
| commands with | or redirect output with >. |
There was a problem hiding this comment.
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?
| 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") |
There was a problem hiding this comment.
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
| if !strings.HasPrefix(arg, "-") { | ||
| expansion += fmt.Sprintf(" %q ", arg) | ||
| } else { | ||
| expansion += fmt.Sprintf(" %s ", arg) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
|
@mislav Caught up on the feedback as well as what we discussed in our call. |
|
@mislav The windows support is ready for review now. |
There was a problem hiding this 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
| if expandedArgs == nil && err == nil { | ||
| // It was an external alias; we ran it and are now done. | ||
| os.Exit(0) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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
| argList = " -ArgumentList @(" | ||
| for i, arg := range args[2:] { | ||
| argList += fmt.Sprintf("'%s'", arg) |
There was a problem hiding this comment.
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`?There was a problem hiding this comment.
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.
There was a problem hiding this 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" 🙇
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 |
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 |
|
Switching to While we can run aliases through it and call the utilities that This all works for I spent the last part of my day attempting to make argument passing more robust when using After my day of disappointing research I think we should give up on |
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 Here is what I propose for executing shell aliases:
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? |
|
I might be missing something but I don't see how
could work. |
I see! For me, when |
|
I haven't dug into the why but I at least see how we're getting different results, here.
I'm guessing that when I just run I will incorporate calling |
|
Based on our combined investigation, I finally have shell aliases working on Windows via |
This command adds --shell to `gh alias set`, allowing specified aliases to be run through a shell interpreter.
There was a problem hiding this 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.
This PR augments
gh pr alias setwith the ability to accept-s/--shell. Shell aliases are executed through$SHELLand allow executing various commands in composition instead of just rewriting invocations of gh subcommands.It works like this:
Below is the initial sketch/discussion about this feature:
But the caveats:
! character
Enclosing the expansion in single quotes is necessary. both
bashandzshinterpret!as aspecial character and it will not make it into
gh, which will lead to potentially confusing errorsfor users who forget to use single quotes when using
!style aliases.Potential alternatives:
means something to shells.
gh alias setrespect a--externalflag. This is circular because to support this we'd haveto re-enable flag parsing on
gh alias set. In other words, to support--external, this wouldno longer work:
gh alias set co pr checkout.ghcommand. 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 -cin!sh -c "command goes here"is not needed in all cases. It's only necessary when youwant to compose commands.
For example, this works fine:
But this won't do what you want:
My concern here is that it's cognitively confusing for people who aren't used to scripting in
shells. The
sh -cis probably confusing and the need to put what they actually want to run inthe double quotes when they're already wrapping everything in single quotes could lead users to
frustration.
Potential alternative:
!aliases insh -c "<expansion>". I've hacked this together and it seems towork well; the downside is that users are now stuck invoking their composed commands via
sh.They might prefer to run things through
zshorbashto 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:
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 init 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:
commands.
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:
!character ok for signalling toghthat the user wants an external alias?sh -c "<expansion>"?