Open pwaller opened 11 years ago
Hey Peter,
It sure would be handy. If you want to contribute that feature I will gladly accept a patch.
Bash can export
functions but I don't know how it works exactly. Also
target shells like fish wouldn't support that feature but it's alright.
Separately from that I don't know how to better educate the user to make him understand that the .envrc is loaded in a separate bash process who also has the direnv stdlib and ~/.direnvrc loaded. On Aug 20, 2013 2:57 PM, "Peter Waller" notifications@github.com wrote:
Just come across a case where someone was expecting functions and aliases to be picked up from the source script.
I realise what a pain this is. Any way we can reasonably make that work?
— Reply to this email directly or view it on GitHubhttps://github.com/zimbatm/direnv/issues/73 .
A few things:
I like this answer which explains how to get the bash export -f
behavior in zsh and why it may be a bad idea: http://unix.stackexchange.com/a/59431
I think it may be cool to allow function templating in an includes directory for fish and zsh, but this begs the question of whether those functions are then really part of a shell package and should be distributed some other way. The annoying thing is that (to the best of my knowledge) there is no such thing as a package manager for shell packages, let alone a shell-agnostic shell package manager.
I feel like any alias or function we would put in a .envrc file should really just be a function available to the whole system that takes configuration from variables that direnv (or anything else) could set. Is there a use case we can think of that falls outside of this?
If I'm correct, then the only directive we need to add to dotenv is the 'source' directive, which should be to our shell packages as bundler is to ruby packages, but without the update and install functionality because, again, we don't actually have a package manager or package repository.
Hey @winmillwill , I think the idea was that in the context of a project you might want to add some shortcuts as functions or aliases that are really specific. Let's say you have a ruby project, you could set r
to be an alias of bundle exec rake
or have a special filter function that sanitizes the rails logs ; I'm not sure that as global functions they would be very useful.
That being said I think that in most cases aliases and functions can be replaced by proper bash scripts that's references using the PATH_add
directive. Like you said function export is probably a hack, if the project can be kept simple it's probably better :)
Some of these tickets are also placeholders for some ideas, like here I'm not super convinced it's useful but I could be convinced otherwise. Ideas are best when simmered :)
All of that makes sense. It's just a style point for me that something like
alias r='bundle exec rake'
ought to belong to a ruby aliases package, and
then we could conditionally load that plugin based on the presence and
value of something like BUNDLE_EXEC_RAKE_IS_R. In other words, set globals,
and then indicate to direnv that you want to load the plugin, and if it has
a version for your shell, it can do so. This particular example is bit
dumb, but something more usable/useful could certainly be thought up.
I can't claim to understand the indirection you discussed about running all
of this in a subshell, so I don't know if source
and alias
would work
in bash with that strategy.
On Mon, Dec 2, 2013 at 6:45 AM, zimbatm notifications@github.com wrote:
Some of these tickets are also placeholders for some ideas, like here I'm not super convinced it's useful but I could be convinced otherwise. Ideas are best when simmered :)
— Reply to this email directly or view it on GitHubhttps://github.com/zimbatm/direnv/issues/73#issuecomment-29585608 .
Yeah there's so many ways to do the same things but only few are good :)
I can't claim to understand the indirection you discussed about running all of this in a subshell, so I don't know if
source
andalias
would work in bash with that strategy.
When you run direnv export $target_shell
(which is used by direnv hook $target_shell
), direnv runs bash in a sub-process that loads the .envrc and then runs direnv dump
. Direnv then reads and compares both environment to finally export the diff in the $target_shell format.
It's a bit complex but that way the .envrc is always executed in bash and you don't have to worry about shell compatibility. Any function or others defined in the sub-process don't leak into the current environment (which coincidentally also prevents this feature).
I'm going to close this one since there hasn't been much interest and I haven't missed it much myself. But if there is demand, please open up another issue. Even better if you can suggest ways to implement it.
I would use this feature. At the very least support for export -f
. I have projects, both personal and for work, that require particular startup scripts tailored to my environment, and it'd be really useful to just throw a function in .envrc.
+1! I could definitely use this. I've been using my own little cd
hook for a while to load some functions and aliases when I change into a directory; direnv
is a much more robust solution but not if I have to still keep my old hook around. As mentioned above, I use aliases like alias s="rails server"
, alias c="rails console"
, and at the moment alias ee="stty echo"
as a workaround for a bug in pry that routinely kills my terminal echo -- I don't really want any of those in my global namespace. Another use case: I have a local function called migrate
(for database migrations) in my Rails project that shadows a completely different (and rarely used) executable that has nothing to do with Rails. (As mentioned above, this is perhaps a less pressing use case than the aliases because I could just drop that in a ./bin directory and PATH_add ./bin
; still, it's handy to throw that function in my .envrc).
+1 for export -f
Unfortunately BASHFUNC* environment variables cause issues for other shell so we had to ignore them. See https://github.com/direnv/direnv/pull/157
If you want to maintain all your aliases in the .envrc you could add something like that in your main ~/.direnvrc file:
export_function() {
local name=$1
local alias_dir=$PWD/.direnv/aliases
mkdir -p "$alias_dir"
PATH_add "$alias_dir"
local target="$alias_dir/$name"
if declare -f "$name" >/dev/null; then
echo "#!/usr/bin/env bash" > "$target"
declare -f "$name" >> "$target" 2>/dev/null
echo "$name" >> "$target"
chmod +x "$target"
fi
}
then in your .envrc you can write something like:
woot() {
echo woot
}
export_function woot
An executable file is then automatically generate on each .envrc load inside of the project's .direnv/aliases folder and made available on the PATH.
I suppose a similar export_alias ee "stty echo"
could also be crafted
It seems that the complexity is mostly related to function exports.
Exporting aliases seems easier, at least on bash and ksh, where the alias
command prints out the currently defined aliases in such a way that they can be directly passed to the eval
built-in, i.e. all the quoting is correct (though slightly non-functionally different between bash and ksh). It looks to me that diffing the outputs of alias
before and after evaluating .envrc
should be relatively straightforward.
I suppose a similar export_alias ee "stty echo" could also be crafted
:+1: I could definitely use this feature.
Generally speaking, exporting aliases is a per project setting in the majority of cases; they would be rarely useful as global settings. This feature will allow me to declutter a lot of junk-aliases and hacks from my global namespace, hence greatly speeding up the bootstrap of all my virtual consoles (which could took quite a toll on terminal multiplexers with a very large number of connections).
@wbolster it seems easy on the surface but I don't know how if it will work for all shells. It also means to re-write the whole context serialisation to include the aliases.
@Dr-Terrible it's a bit of a hack but you can add this to your ~/.direnvrc:
# Example: export_alias zz "ls -la"
export_alias() {
local name=$1
shift
local alias_dir=$PWD/.direnv/aliases
local target="$alias_dir/$name"
mkdir -p "$alias_dir"
PATH_add "$alias_dir"
echo "#!/usr/bin/env bash -e" > "$target"
echo "$@" >> "$target"
chmod +x "$target"
}
I've prototyped another version of direnv hook
that runs a user-defined function direnv_postload
when the environment is just loaded. The function is executed in the context of current interactive shell, so it could easily define local functions and aliases associated with the current .envrc
.
In addition, a function direnv_preunload
could be defined to revert any environment-specific changes when the environment is unloaded (#129, #195), or when the shell exits.
Only zsh version is prototyped by now, I guess it could be ported to bash. https://gist.github.com/anpol/7a2706c2766eff454e047b7513fa511b
I imagine that $(direnv hook zsh)
would output a runtime code similar to mine; then the code returned by $(direnv export zsh)
would call all the necessary functions without any tricks, like so:
$ direnv export zsh
_direnv_notify_preunload '/path-to/prev-dir' '/path-to/next-dir'
_direnv_log_status 'loading /path-to/next-dir'
_direnv_log_diff '+VAR1' '~VAR2'
export VAR1=value; export VAR2=value;
_direnv_notify_postreload '/path-to/prev-dir' '/path-to/next-dir'
That would even allow for better customization of log messages (#68, #104, #219).
We should decide whether direnv_postload
/preunload
are worth implementing for all shells.
To move forward, some additional feedback is desired.
Hi, i did some modification with this.
export_alias() {
local name=$1
shift
local alias_dir=$PWD/.direnv/aliases/$(pwd)
local target="$alias_dir/$name"
mkdir -p "$alias_dir"
PATH_add "$alias_dir"
echo "#!/usr/bin/env bash" > "$target"
echo "$@ \"\$@\"" >> "$target"
chmod +x "$target"
}
How can I make direnv remove the pwd's aliase directory when eval so that it won't conflict with the new one?
You could rm
the folder on each load since it's going to be recreated.
alias_dir=$PWD/.direnv/aliases
rm -rf "$alias_dir"
export_alias() {
local name=$1
shift
local target="$alias_dir/$name"
mkdir -p "$alias_dir"
PATH_add "$alias_dir"
echo "#!/usr/bin/env bash" > "$target"
echo "$@ \"\$@\"" >> "$target"
chmod +x "$target"
}
@zimbatm thanks!
alias_dir=$PWD/.direnv/aliases
rm -rf "$alias_dir"
should be set in the .envrc
so that $PWD
resolves to the .envrc
folder
Hmm, isn't .direnvrc
evaled everytime when shell prompts? If that's not the case, it doesn't help much for simplicity since every .envrc
needs those lines added.
for those of you using zsh you might like to try this technique: https://coderwall.com/p/a3xreg/per-directory-zsh-config
I would like to suggest a minor enhancement for above export_function
by @zimbatm
export_function() {
local name=$1
local alias_dir=$PWD/.direnv/aliases
mkdir -p "$alias_dir"
PATH_add "$alias_dir"
local target="$alias_dir/$name"
if declare -f "$name" >/dev/null; then
echo "#!$SHELL" > "$target"
declare -f "$name" >> "$target" 2>/dev/null
# Notice that we add shell variables to the function trigger.
echo "$name \$*" >> "$target"
chmod +x "$target"
fi
}
So, the exported function could pickup shell variables, and use user default shell.
It should be noted that the export_function
script here is probably unsound, because it does not traverse the call graph to find other functions that are referenced. Splitting functions into separate scripts may not work properly. Consider:
#!/bin/sh
X=1
function a() {
echo "$X";
}
a
If we split a
into a separate file, and invoke it with ./a
instead, the variable X
is not available inside ./a
and so the output is empty.
It might be better to diff the output of declare -F
before and after loading the environment. Then, create a __functions__.sh
script which contains the definitions of all new functions (with declare -f
), with a final line, "$(basename -- "$0")" "$@"
, and then make a link to this file with the name ./<func_name>
for each new function.
You may also have to capture shell options (such as nullglob
) and reproduce them in __functions__.sh
. And additionally global variables...
There is a command, set
, which just dumps out the state of the shell, and you could use that, but it includes some read-only variables like SHELLOPTS
which you will have to handle separately. Not easy! Something like this.
That would be great to have direnv to manage alias map just like environment. Being able to compute diff before and after .envrc execution and adding/removing alias according.
The rc
in .envrc
suggest that more than env vars could be written in, unlike .env
.
Maybe something similar could be done for function using declare -f
.
My 2 cents.
The problem with using executables in the first place is that they do not behave like shell functions in a lot of ways. They're isolated from the environment and cannot see non-exported variables. In bash
at least, the only sound way would be to eval or source a script that defines the functions, then use unset -f
to remove it.
It's unlikely that this issue will ever be solved with the current design. Environment variables are unique in the sense that they are cross-shell compatible and inherited in sub-shells. Direnv is relying quite heavily on these two properties to work reliably and undo the changes when changing directory.
It's unfortunate because @jcpetruzza put quite a bit of effort in #443 but it relies on add/remove handlers to revert to the original config. It also would break down if a sub-shell is started as direnv doesn't know how to restore those aliases and functions in the sub-shell.
@zimbatm as a aside question are aliases defined in shell.nix (shellHook) supposed to work ? It does not seem to be the case here with bash.
It's the same issue. The aliases are defined in the .envrc
evaluation context but then cannot be exported to the host shell.
I got aliases working for fish shell using a lib function and a modified hook. It could easily be extended to support other shell.
Overview: 1) Export aliases in the environment variable DIRENV_ALIASES 2) The destroy/create the aliases anytime the DIRENV_ALIASES changes.
I added this to my .config/direnv/lib
export_alias() {
local name=$1
shift
if [ "$DIRENV_ALIASES" = "" ]
then
export DIRENV_ALIASES="${name} \"$@\""
else
DIRENV_ALIASES="$DIRENV_ALIASES:alias:${name} \"$@\""
fi
}
And I'm using this for my fish hook. This an update from the hook I submitted in PR #732 but the alias logic is in the __direnv_update function.
function direnv-hook
function __direnv_update
command direnv export fish | source
if test "$direnv_set_aliases" != "$DIRENV_ALIASES"
for name in $direnv_alias_names
functions --erase $name
end
set -e direnv_alias_names
for cmd in (echo $DIRENV_ALIASES | string split --no-empty ':alias:')
echo "direnv: alias $cmd"
eval alias $cmd
set parts (echo $cmd | string split --no-empty ' ')
set -g -a direnv_alias_names $parts[1]
end
set -g direnv_set_aliases $DIRENV_ALIASES
end
end
function __direnv_export_eval --on-event fish_prompt
# Run on each prompt to update the state
__direnv_update
if test "$direnv_fish_mode" != "disable_arrow"
# Handle cd history arrows between now and the next prompt
function __direnv_cd_hook --on-variable PWD
if test "$direnv_fish_mode" = "eval_after_arrow"
set -g __direnv_export_again 0
else
# default mode (eval_on_pwd)
__direnv_update
end
end
end
end
function __direnv_export_eval_2 --on-event fish_preexec
if set -q __direnv_export_again
set -e __direnv_export_again
__direnv_update
echo
end
# Once we're running commands, stop monitoring cd changes
# until we get to the prompt again
functions --erase __direnv_cd_hook
end
end
I like the postload and preunload hook idea. Does that exist already? If so, it can be used to cover many scenarios. For example, something like:
# .envrc
hook postload load-aliases.sh
hook postunload unload-aliases.sh
# load-aliases.sh
alias m=make
# unload-aliases.sh
unalias m
The above is for bash
but the contents of the load-aliases.sh
and unload-aliases.sh
scripts can be shell-specific.
FYI, I've come up with a solution that works with bash
(at least for the aliases and functions I'm working with -- it may break with more complicated quoting in definitions).
In .bashrc
:
function direnv-reset-def() {
eval "${DIRENV_RESET_DEF}"
}
function direnv-set-def() {
eval "${DIRENV_SET_DEF}"
}
eval "$(direnv hook bash)"
export PROMPT_COMMAND="direnv-reset-def;${PROMPT_COMMAND};direnv-set-def"
In ~/.bash/def.shlib
:
function is-def() {
flag=$1
name=$2
case "${flag}" in
-a)
[[ "$(type -t ${name} 2>/dev/null)" == 'alias' ]]
;;
-f)
[[ "$(type -t ${name} 2>/dev/null)" == 'function' ]]
;;
-v)
[[ -n "${!name+defined}" ]]
;;
esac
}
function get-set-def() {
flag=$1
name=$2
local def
case "${flag}" in
-a)
alias ${name}
;;
-f)
declare -f ${name}
;;
-v)
echo "${name}=${!name})"
;;
esac
}
function get-unset-def() {
flag=$1
name=$2
case "${flag}" in
-a)
echo "unalias ${name}"
;;
-f)
echo "unset -f ${name}"
;;
-v)
echo "unset ${name}"
;;
esac
}
function get-reset-def() {
flag=$1
varname=$2
if is-def ${flag} ${varname}
then
get-set-def ${flag} ${varname}
else
get-unset-def ${flag} ${varname}
fi
}
In .envrc
:
. ~/.bash/def.shlib
export DIRENV_RESET_DEF=$(cat <<EOF
$(get-reset-def -f some-func)
$(get-reset-def -a some-alias)
EOF
)
# typical direnv var settings go here
export DIRENV_SET_DEF=$(cat <<'EOF'
alias some-alias='true'
function some-function() {
cd "${GRAIL}/go/src/grail.com/${1:-${SERVICE}}"
}
EOF
)
eval "${DIRENV_SET_DEF}" # if DIRENV_SET_DEF has exported vars (eg whose values should automatically change when other var values change)
@wbolster it seems easy on the surface but I don't know how if it will work for all shells. It also means to re-write the whole context serialisation to include the aliases.
@Dr-Terrible it's a bit of a hack but you can add this to your ~/.direnvrc:
# Example: export_alias zz "ls -la" export_alias() { local name=$1 shift local alias_dir=$PWD/.direnv/aliases local target="$alias_dir/$name" mkdir -p "$alias_dir" PATH_add "$alias_dir" echo "#!/usr/bin/env bash -e" > "$target" echo "$@" >> "$target" chmod +x "$target" }
I noticed this adds multiple instances of the alias path to the PATH for every export_alias you have, so I tweaked it to check the PATH...
export_alias() {
local name=$1
shift
local alias_dir=$PWD/.direnv/aliases
local target="$alias_dir/$name"
mkdir -p "$alias_dir"
if ! [[ ":$PATH:" == *":$alias_dir:"* ]]; then
PATH_add "$alias_dir"
fi
echo "#!/usr/bin/env bash -e" > "$target"
echo "$@" >> "$target"
chmod +x "$target"
}
Is there any update on this? What is the current recommended solution?
After years of using direnv, this must be the feature I miss the most. Having simple aliases and functions to simplify tedious development tasks is so nice, for example:
function import-form() {
cat devdata/form-$1.json \
| docker-compose -f docker-compose.dev.yaml exec -T db mongoimport -d myproject -c forms
}
Are there any known plugins for direnv that allows it to export functions and aliases for ZSH? Is the plugin API powerful enough that I could implement this myself?
Hi, I'm using fish shell together with nix-direnv (nix flake). I wonder if the work around mentioned by @wderezin still work since there is no such command direnv export
now.
Hi, I'm using fish shell together with nix-direnv (nix flake). I wonder if the work around mentioned by @wderezin still work since there is no such command
direnv export
now.
@sandangel I just upgraded to v2.31.0 and my work around is still working for me. Can you provide a reference to where the export command was deprecated so I can research this?
I just saw there is no such direnv export
command in direnv --help
. So I wasn't sure if your workaround is still up-to-date. Let me try it. @wderezin . May I ask where do you define the direnv-hook
function? Is that in the .config/fish/config.fish
file?
@sandangel Yeah, it's not documented, but it definitely still exists.
@sandangel
I have a public repo you can look through. I use it so I can easily install my different shell environments and scripts on any system I use. https://github.com/wderezin/cli-tools
But in summary: I have a functions directory that the hook lives in. Then during fish init I add the functions directory to the path, and init the hook.
Links to the code parts:
Hope that helps.
@wderezin hmm, it doesn't work for me. I run the direnv-hook
function when I start fish interactive shell, also added the direnv.sh
file that has export_alias
function to ~/.config/direnv/lib/direnv.sh
. and finally used
export_alias tf "terraform"
in flake.nix
shellHook or .envrc
but neither of them work.
@wbolster it seems easy on the surface but I don't know how if it will work for all shells. It also means to re-write the whole context serialisation to include the aliases. @Dr-Terrible it's a bit of a hack but you can add this to your ~/.direnvrc:
# Example: export_alias zz "ls -la" export_alias() { local name=$1 shift local alias_dir=$PWD/.direnv/aliases local target="$alias_dir/$name" mkdir -p "$alias_dir" PATH_add "$alias_dir" echo "#!/usr/bin/env bash -e" > "$target" echo "$@" >> "$target" chmod +x "$target" }
I noticed this adds multiple instances of the alias path to the PATH for every export_alias you have, so I tweaked it to check the PATH...
export_alias() { local name=$1 shift local alias_dir=$PWD/.direnv/aliases local target="$alias_dir/$name" mkdir -p "$alias_dir" if ! [[ ":$PATH:" == *":$alias_dir:"* ]]; then PATH_add "$alias_dir" fi echo "#!/usr/bin/env bash -e" > "$target" echo "$@" >> "$target" chmod +x "$target" }
For those who rely on an alias to add arguments at the end
(example where greet world
outputs hello world
)
# without direnv
alias greet="echo hello"
# with direnv
export_alias greet 'echo hello $@'
PS: I tried adding it into the script first echo "$@" '$@' >> "$target"
, this worked but broke where I used alias as macros. eg. I used if statements etc inside an alias. But if you don't use such complex functionality, this might be sufficient and much cleaner.
Thanks everyone for their helpful notes on workarounds. Especially https://github.com/direnv/direnv/issues/73#issuecomment-979504909 (and all previous iterations) were extremely helpful.
In my case I need to alias an existing command to add some extra flags to it, which caused an infinite loop. However, explicitly setting PATH back to the old path in export_alias worked for me:
export_alias() {
local name=$1
shift
local alias_dir=$PWD/.direnv/aliases
local target="$alias_dir/$name"
local oldpath="$PATH"
mkdir -p "$alias_dir"
if ! [[ ":$PATH:" == *":$alias_dir:"* ]]; then
PATH_add "$alias_dir"
fi
echo "#!/usr/bin/env bash" > "$target"
echo "PATH=$oldpath" >> "$target"
echo "$@" >> "$target"
chmod +x "$target"
}
export_alias ansible-playbook 'ansible-playbook -e ara_playbook_labels=$(whoami) $@'
A native solution would still be great, but hopefully this little extra note helps others :)
export_alias() { local name=$1 shift local alias_dir=$PWD/.direnv/aliases local target="$alias_dir/$name" local oldpath="$PATH" mkdir -p "$alias_dir" if ! [[ ":$PATH:" == *":$alias_dir:"* ]]; then PATH_add "$alias_dir" fi echo "#!/usr/bin/env bash" > "$target" echo "PATH=$oldpath" >> "$target" echo "$@" >> "$target" chmod +x "$target" }
export_alias ansible-playbook 'ansible-playbook -e ara_playbook_labels=$(whoami) $@'
Hello,
In case you are using direnv
inside WSL and wish to use the previous trick, do not forget to quote your path:
export_alias() {
local name=$1
shift
local alias_dir=$PWD/.direnv/aliases
local target="$alias_dir/$name"
local oldpath="$PATH"
mkdir -p "$alias_dir"
if ! [[ ":$PATH:" == *":$alias_dir:"* ]]; then
PATH_add "$alias_dir"
fi
echo "#!/usr/bin/env bash" > "$target"
echo "PATH=\"$oldpath\"" >> "$target"
echo "$@" >> "$target"
chmod +x "$target"
}
Otherwise, you will have this kind of error when using Windows path:
<repo>/.direnv/aliases/tg: line 2: syntax error near unexpected token `('
<repo>/.direnv/aliases/tg: line 2: `PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files/dotnet/:/mnt/c/Program Files (x86)/NVIDIA Corporation/PhysX/Common'
Kind regards, JM
as this is still open after centuries....
devenv handles this quite simply https://devenv.sh/scripts/
easily integrates with direnv for per repo/folder dev shell setups
devenv handles this quite simply https://devenv.sh/scripts/
easily integrates with direnv for per repo/folder dev shell setups
AFAICT, devenv's scripts
defines new binaries it puts in PATH
, and its enterShell
hook gets run by direnv in the usual child process, so it doesn't have any more ability to set aliases than native direnv.
@lindhe unless you actually found a working alternative, posting messages like this is just noise for people watching this issue
It might be useful for the devs to know that this drives people away from their (otherwise excellent) software.
Just come across a case where someone was expecting functions and aliases to be picked up from the source script.
I realise what a pain this is. Any way we can reasonably make that work?