Skip to content

Shopify/shadowenv

Repository files navigation

Shadowenv

Shadowenv provides a way to perform a set of manipulations to the process environment upon entering a directory in a shell. These manipulations are reversed when leaving the directory, and there is some limited ability to make the manipulations dynamic.

shadowenv in action

In order to use shadowenv, add a line to your shell profile (.zshrc, .bash_profile, or config.fish) reading:

eval "$(shadowenv init bash)" # for bash
eval "$(shadowenv init zsh)"  # for zsh
shadowenv init fish | source  # for fish

With this code loaded, upon entering a directory containing a .shadowenv.d directory, any *.lisp files in that directory will be executed and you will see "activated shadowenv." in your shell.

The syntax for the .shadowenv.d/*.lisp files is Shadowlisp, a minimal Scheme-like language. Unlike other tools like direnv, this has the interesting property of allowing us to do things like simulate chruby reset upon entry into a directory without the user having chruby installed (and undo these changes to the environment when cd'ing back out):

(provide "ruby")

(when-let ((ruby-root (env/get "RUBY_ROOT")))
  (env/remove-from-pathlist "PATH" (path-concat ruby-root "bin"))
  (when-let ((gem-root (env/get "GEM_ROOT")))
    (env/remove-from-pathlist "PATH" (path-concat gem-root "bin"))
    (env/remove-from-pathlist "GEM_PATH" gem-root))
  (when-let ((gem-home (env/get "GEM_HOME")))
    (env/remove-from-pathlist "PATH" (path-concat gem-home "bin"))
    (env/remove-from-pathlist "GEM_PATH" gem-home)))

The intention isn't really for users to write these files directly, nor to commit them to repositories , but for other tool authors to generate configuration on the user's machine. Here's an example of a generated Shadowlisp file for activating ruby 2.7.1:

(provide "ruby" "2.7.1")

(when-let ((ruby-root (env/get "RUBY_ROOT")))
 (env/remove-from-pathlist "PATH" (path-concat ruby-root "bin"))
 (when-let ((gem-root (env/get "GEM_ROOT")))
   (env/remove-from-pathlist "PATH" (path-concat gem-root "bin")))
 (when-let ((gem-home (env/get "GEM_HOME")))
   (env/remove-from-pathlist "PATH" (path-concat gem-home "bin"))))

(env/set "GEM_PATH" ())
(env/set "GEM_HOME" ())
(env/set "RUBYOPT" ())

(env/set "RUBY_ROOT" "/opt/rubies/2.7.1")
(env/prepend-to-pathlist "PATH" "/opt/rubies/2.7.1/bin")
(env/set "RUBY_ENGINE" "ruby")
(env/set "RUBY_VERSION" "2.7.1")
(env/set "GEM_ROOT" "/opt/rubies/2.7.1/lib/ruby/gems/2.7.0")

(when-let ((gem-root (env/get "GEM_ROOT")))
  (env/prepend-to-pathlist "GEM_PATH" gem-root)
  (env/prepend-to-pathlist "PATH" (path-concat gem-root "bin")))

(let ((gem-home
      (path-concat (env/get "HOME") ".gem" (env/get "RUBY_ENGINE") (env/get "RUBY_VERSION"))))
  (do
    (env/set "GEM_HOME" gem-home)
    (env/prepend-to-pathlist "GEM_PATH" gem-home)
    (env/prepend-to-pathlist "PATH" (path-concat gem-home "bin"))))

.shadowenv.d

The .shadowenv.d directory will generally exist at the root of your repository (in the same directory as .git.

We strongly recommend creating gitignore'ing everything under .shadowenv.d (echo '*' > .shadowenv.d/.gitignore).

A .shadowenv.d will contain any number of *.lisp files. These are evaluated in the order in which the OS returns when reading the directory: generally alphabetically. We strongly recommend using a prefix like 090_something.lisp to make it easy to maintain ordering.

.shadowenv.d will also contain a .trust-<fingerprint> file if it has been marked as trusted. (see the trust section).

Language

See https://shopify.github.io/shadowenv/ for Shadowlisp documentation.

Integrations

Shadowenv has plugins for multiple editors and/or IDEs:

Trust

If you cd into a directory containing .shadowenv.d/*.lisp files, they will not be run and you will see a message indicating so. This is for security reasons: we don't want to enable random stuff downloaded from the internet to modify your PATH, for example.

You can run shadowenv trust to mark a directory as trusted.

Technically, running shadowenv trust will create a file at .shadowenv.d/.trust-<fingerprint>, indicating that it's okay for shadowenv to run this code. The .shadowenv.d/.trust-* file contains a cryptographic signature of the directory path. The key is generated the first time shadowenv is run, and the fingerprint is an identifier for the key.