-
Notifications
You must be signed in to change notification settings - Fork 629
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
Add shell-specific actions and use them to implement aliases #464
base: master
Are you sure you want to change the base?
Conversation
We could use the bash tests almost verbatim. The only changes needed were: - Use [[ ... ]] for guards - Use `direnv export zsh` So we can have one common test script and source it from both the bash and zsh tests
In the hook code, we do essentially `eval "$(direnv export bash)"`, while on the test code we were doing `eval $(direnv export bash)`. These are not equivalent, since spaces in the output of `direnv export bash` will break the output; so that everything that comes after gets interpreted outside of the `eval`.
Call was failing because the template was missing the .XXXXX bit.
Make `direnv export xxx` add the contents of the `DIRENV_QUOTES_XXX` to its output. Any other variable of the form `DIRENV_QUOTES_YYY` is ignored. This will be the basis for implementing shell-specific functionality in library code.
We can use `quote <shell> foo bar` to add the command `foo bar;` to the output of `direnv export <shell>`. This is the basic building block for providing shell-specific functionality from within the envrc.
So that we can easily encode/decode strings using this format from the envrc. We will use this to implement "on_unload" quotes.
The `shell_specific()` function is a higher-level version of `quote()`: it let's one send to the shell not only a command to do "on loading" the env, but also a command that can compute the diff with respect to the current env and record it, in the `DIRENV_ON_UNLOAD_XXX` variable, to be executed "on unloading". From the point of view of `direnv export xxx`, the `DIRENV_ON_UNLOAD_XXX` variables are similar to `DIRENV_DIFF`. The protocol that is implemented is: - When running `direnv export xxx`, if we are unloading, only `DIRENV_ON_UNLOAD_xxx` is considered. - `DIRENV_ON_UNLOAD_xxx` is a comma-separated list of shell-specific commands, encoded using gzenv. - When the decoded commands are output, they are separated by newlines
At the moment, fish has no equivalent of the `"$(cmd)"` substitution of bash, zsh, etc. In particular, `(cmd)` will split the output of `cmd` on newlines. That means that `eval (direnv export fish)`, will first run `direnv export fish`, then split it on newlines and finally call `eval` with one argument per line. The behaviour of `eval` is to concatenate all its arguments with *spaces* (not newlines) and then evaluate that. This is a bug waiting to happen (e.g., if the value of an exported value contains a `\n` it could be silently replaced by a space), and would also prevent us from implementing `shell_specific` for `fish`. In fish, though, `eval` is just a wrapper around `source`, which can actually take it's input from stdin. So we can simply pipe the output of `direnv export fish` to `source`.
tcsh is notoriously hard to use as a scripting language so trying to write actual shell-specific code for this shell will be "fun". In particular, back-ticks cannot be nested (there is no equivalent of `$(...)`) and they are not easy to escape.
We introduce the `use aliases` functionality in the stdlib. Once activated, the `alias` command will effectively set aliases on bash, zsh, tcsh and fish by way of shell-specific code. It is implemented by aliasing "alias" to a function that mimics the behaviour of "alias" under "bash" but also emits the corresponding shell-specific code. On each shell, a command is run to find out the current definition for the alias (in case it is being redefined) and an "on-unload" action is registered that will restore the original definition or remove the alias in case it is new. In the case of tcsh, this was tricky to test since in order to decide if, on unload, we need to execute a `unalias foo` or an `alias foo old-value`, we need to run a command that in some cases will finish with a non-zero value to be able to perform the "if". However, `tcsh -e` will fail immediately when this happens (the alternative would be to use backticks, but they can't be easily nested), etc. So we end up adding another test script for tcsh to be run with tcsh, for this type of scenarios...
thanks @jcpetruzza, I already integrated all your test improvements. Sorry but the rest of the PR changes direnv too much and I am not comfortable merging this. I can see direnv potentially managing more things than just environment variables but it would have to be done declaratively where each shell hook would be expanded to handle the diffing. |
@zimbatm Thanks for your response! I think it is fine not to merge the rest of this PR as it is. I was initially going to submit only the minimal number of changes that would allow people to implement things like aliases or shell-completion on their own (and share the implementations on the wiki), but I felt without a fully-developed example it wouldn't be clear that the idea works, etc, so I ended up adding a The main take-aways, for me, from implementing this prototype are:
Do you think this could be implemented in any other way? I'd propose then the following design for the extension:
Because this is a cleaned-up version of what is already in this PR, we have a validation that it should work. This also doesn't require many changes to direnv internals, imo. One can then play around, implement something cool like "use direnv to activate a local nix profile and have the shell-completion scripts updated", and share it on the wiki 🙂 Thoughts? |
sorry I haven't had much time to think and this is a bit of a deep subject. One of the fundamental difference between environment variables and the rest is that env vars are inherited between processes. So I can Now if I think about aliases, what should happen is that the bash hook, somehow keeps track of it's own aliases. If a special DIRENV_ALIASES environment variable is created after an eval, it would have to backup the old aliases and apply the new ones itself. And somehow on every prompt command it would have to check if DIRENV_ALIASES has changed so that it could work when invoked as a sub-shell. Here I am assuming that the aliases are simple and compatible with all of the shells. It also puts the burden on each shell implementation to implement their own aliasing mechanism. This is the reverse of your LOAD/UNLOAD actions as the responsibility is shifted from the .envrc to the specific shell for doing the backup and restore. But it means that aliases can actually be restored which I don't think your approach handles. Another reason for my reluctance is that I would prefer to merge all the DIRENV_ environment variables into one DIRENV_STATE at some point, so introducing new variables is not ideal. For that I think your gzenv utility is quite helpful. I realize that it's not directly related to the PR and I would be happy to be working with you on that first if that's alright with you. |
The point about subshells is a very good one, and I hadn't thought about that, really. You don't even need to go the bash -> zsh route, already bash -> bash and zsh -> zsh is not working in this prototype, since an alias defined by the envrc will "disappear" from the subshell.
I think I would do this differently, though. The code emitted by
Wait, this prototype is already restoring the aliases! 🙂 Look at the test for the scenario called "alias": it checks that on entering a directory two aliases If I understand correctly, what you are proposing is to make the hook more smart, but also more complex (first is has to know about aliases, and tomorrow about shell-completions, etc), while in my approach the hook remains just as simple as it is right now and users can add new shell-specific extensions without having to touch direnv internals, since they just need to use the envrc to emit the shell-specific code that will eventually run on the hook (not the most beautiful thing to write, but well...)
Moving towards a single DIRENV_STATE variable makes sense to me and I'd be happy to help with that. It would be a biggish change, though, so it's probably better to do it incrementally. Do you want to open an issue to lay out the plan and track the progress? |
a4e430c
to
6d567f3
Compare
What's the state of the PR ? I'd love to define directory specific aliases with direnv ! |
Any update on this? |
It's not very likely to happen. @jcpetruzza did a fantastic job, and I don't feel comfortable merging this at the same time. It's too much new surface of untested code for this little one-man project. If somebody were to abstract the changes record to include aliases and the shell process id, on top of environment variables, then it might be doable. |
This implements a mechanism to send shell-specific code to the host shell from the
.envrc
file, along with a simple protocol to register clean-up actions to be performed when unloading the environment. The supported shells at the moment arebash
,zsh
,tcsh
andfish
(elvish
is missing only because it doesn't have aneval
function, or I couldn't find it).Based on this, we implement the exporting of aliases from
.envrc
(#73). The same mechanism should let us support loading/unloading shell-completion scripts (#443) and, more in general, run arbitrary commands on entry/exit of the environment.The core of
direnv
was left mostly unaffected; this is a rundown of the changes:The first 2 commits add tests for
tcsh
andzsh
, so that we can then test the new functionality on all shells. For the latter, we can simply share the test script withbash
as long as we stay within bourne shell code.The next 3 commits fix minor issues in tests.
Next, we introduce a
quote
command to the stdlib. Intuitively,quote fish foo bar
will addfoo bar;
to the output ofdirenv export fish
, etc. This works by accumulating the quotes for shellxxsh
on theDIRENV_QUOTES_xxsh
environment variable (excluded fromDIRENV_DIFF
).quote
is a low-level operation: it will only send commands when the environment is loaded. The next 5 commits add the higher-levelshell_specific
command to the stdlib that allows one to also register clean up actions. If an.envrc
file were to include the following:Then loading the environment with
direnv export zsh
would have, conceptually, the following additional effect:DIRENV_ON_UNLOAD_zsh
is honoured bydirenv export zsh
when unloading the environment, which would then include the extra effect:The second argument to
shell_specific
is conceptually a function computing the clean-up action. In the above example, we useecho 'rm -f foo'
as a constant function that always returnsrm -f foo
. For a more realistic example, when implementing aliases forbash
, we can exploit thatalias foo
outputs the full definition offoo
(including thealias
keyword) and use something like:In reality,
DIRENV_ON_UNLOAD_xxsh
is a comma-separated list of gzenv-encoded quotes. In order to be able to add elements to this list from the envrc, we added adirenv gzenv [encode|decode]
private command.The last commit adds a
use_aliases
function to the stdlib, so that the alias-exposing functionality becomes opt-in. The usage is: