10

I am trying to get shell-command and async-shell-command to integrate seamlessly with a couple of programs in my .bashrc file, specifically direnv in this example.

I found that if I customized shell-command-switch, I could get shell processes to load my profile as if it were a regular interactive login shell:

(setq shell-command-switch (purecopy "-ic"))
(setq explicit-bash-args '("-ic" "export EMACS=; stty echo; bash"))

I am also using exec-path-from-shell.

Say I have a ~/.bashrc file with:

...

eval "$(direnv hook $0)"
echo "foo"

Inside ~/code/foo I have a .envrc file with:

export PATH=$PWD/bin:$PATH
echo "bar"

If I run M-x shell with default-directory set to ~/code/foo, a bash shell will correctly load my profile and run the direnv hook to add that to my path:

direnv: loading .envrc
bar
direnv: export ~PATH
~/code/foo $ echo $PATH
/Users/username/code/foo/bin:/usr/local/bin:... # rest of $PATH

However if default-directory is still ~/code/foo and I run M-! echo $PATH, it correctly loads my .bashrc but doesn't execute the direnv hook of the current directory:

foo
/usr/local/bin:... # rest of $PATH without ./bin

I get the same result if I run M-! cd ~/code/foo && echo $PATH.

Is there a way I can advise or hook into shell-command or start-process to make it behave as if it were being sent from an interactive shell buffer?

waymondo
  • 1,384
  • 11
  • 16
  • What does your direnv hook look like? If it is defined in ~/.bashrc and you eval'd `(setq shell-command-switch "-ic")` then it should be evaluated along with every other command in ~/.bashrc. –  Oct 25 '14 at 07:08
  • The line in my ~/.bashrc is `eval "$(direnv hook $0)"`. This is getting executed, but the mechanism that should get fired when you are in a specific directory with an `.envrc` file isn't. – waymondo Oct 25 '14 at 22:51
  • Is nothing in the `.envrc` file evaluated? Or is it just environment variables that are not exported? Could you please provide a full example so that I can try to reproduce this? –  Oct 26 '14 at 11:29
  • @rekado - Thanks for taking a look at it. I updated my question to be more explicit for reproducing. – waymondo Oct 26 '14 at 19:09

4 Answers4

6

This doesn't seem to be a problem with Emacs but with bash. shell-command just executes call-process on the shell and passes arguments. I tried this on a regular shell:

bash -ic "cd ~/code/foo && echo $PATH"

~/.bashrc is sourced, but the direnv hook is not run. When direnv hook bash is executed, a function _direnv_hook is output and prepended to PROMPT_COMMAND.

_direnv_hook() {
  eval "$(direnv export bash)";
};
if ! [[ "$PROMPT_COMMAND" =~ _direnv_hook ]]; then
  PROMPT_COMMAND="_direnv_hook;$PROMPT_COMMAND";
fi

I suspect that PROMPT_COMMAND simply won't work. That's not a problem, though, because the _direnv_hook is really simple. We can simply prepend eval $(direnv export bash) to the shell command and it will work:

(let ((default-directory "~/code/foo/"))
  (shell-command "eval \"$(direnv export bash)\" && echo $PATH"))

This will print the augmented path to the messages buffer. Alternatively, execute with M-!:

cd ~/code/foo && eval "$(direnv export bash)" && echo $PATH

You don't need to (setq shell-command-switch "-ic") for this to work.

  • Thanks, this really helped put me on the right track. I was looking for a solution that doesn't involve adding the `eval "$(direnv export bash)"` in elisp, but this was extremely helpful and put me on the right path. – waymondo Nov 03 '14 at 20:02
6

Running eval "$(direnv hook $0)" defines a function that hooks into $PROMPT_COMMAND, which is never called when bash is run as bash -ic because there is no prompt. You can change the line:

eval "$(direnv hook $0)"

to:

eval "$(direnv hook $0)" && _direnv_hook

to explicitly call the hook function.

Edit: Just realized rekado gave a very similar answer.

Erik Hetzner
  • 765
  • 3
  • 6
  • This is the most concise answer pointing out my unfamiliarity with how direnv works with `$PROMPT_COMMAND`. – waymondo Nov 03 '14 at 20:21
5

Thanks to Rekado and Erik for pointing out how the direnv hook works by using $PROMPT_COMMAND. Since shell-command doesn't use a prompt, this wasn't getting executed.

While Erik's answer works in my example of calling a shell command with M-! with default-directory set, it wouldn't work in the following example:

(let ((default-directory "~/code/"))
  (shell-command "cd ~/code/foo && echo $PATH"))

After some googling, I found a way to create a preexec hook in bash to execute something before a command is executed. I took this idea and modified to fit my need for direnv. Here's what the relevant part of my ~/.bashrc looks like now:

eval "$(direnv hook $0)"
direnv_preexec () {
  [ -n "$COMP_LINE" ] && return # don't execute on completion
  [ "$BASH_COMMAND" \< "$PROMPT_COMMAND" ] && return # don't double execute on prompt hook
  eval "$(direnv export bash)"
}
trap "direnv_preexec && $BASH_COMMAND" DEBUG
waymondo
  • 1,384
  • 11
  • 16
3

nowadays you would likely want to use https://github.com/wbolster/emacs-direnv

it works similar to the hook that direnv installs in your shell. the emacs environment is updated on request (or automatically when switching buffers) to match what direnv states is the correct environment for the current directory.

by modifying exec-path and process-environment emacs will behave like your shell would do: execute programs from the right paths and with the right environment.