pax_global_header00006660000000000000000000000064150371416410014514gustar00rootroot0000000000000052 comment=7590ee2442104060bb11eedebd7bd6daf3d88fcd direnv-2.37.1/000077500000000000000000000000001503714164100130755ustar00rootroot00000000000000direnv-2.37.1/.github/000077500000000000000000000000001503714164100144355ustar00rootroot00000000000000direnv-2.37.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001503714164100166205ustar00rootroot00000000000000direnv-2.37.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000010031503714164100213040ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: 'Bug' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: **Expected behavior** A clear and concise description of what you expected to happen. **Environment** - OS: [e.g. macOS Mojave, Ubuntu 18.04, ...] - Shell: [e.g. bash, zsh, ...] - Direnv version [e.g. 2.20.0] **Additional context** Add any other context about the problem here. direnv-2.37.1/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011321503714164100223420ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: 'Feature' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. direnv-2.37.1/.github/dependabot.yml000066400000000000000000000003171503714164100172660ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" direnv-2.37.1/.github/workflows/000077500000000000000000000000001503714164100164725ustar00rootroot00000000000000direnv-2.37.1/.github/workflows/codeql.yml000066400000000000000000000045441503714164100204730ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL" on: push: branches: [ master ] pull_request: # The branches below must be a subset of the branches above branches: [ master ] merge_group: jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: [ 'go' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v3 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines # and modify them (or add more) to build your code if your project # uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 direnv-2.37.1/.github/workflows/go.yml000066400000000000000000000035211503714164100176230ustar00rootroot00000000000000name: Go on: push: branches: [ master ] pull_request: {} merge_group: permissions: contents: read jobs: build: name: Go Build runs-on: ${{ matrix.os }} strategy: matrix: os: - macos-latest - ubuntu-latest - windows-latest steps: - name: Check out code into the Go module directory uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5.5.0 with: go-version: '1.24' cache: true id: go - name: Set up Python if: runner.os != 'Windows' uses: actions/setup-python@v5.6.0 with: python-version: "3.9" id: python - name: Build env: GO111MODULE: on run: go build -v . - name: Test env: GO111MODULE: on run: go test -v ./... - name: Test Host bash # FIXME: make this work on Windows as well if: runner.os != 'Windows' env: GO111MODULE: on run: make test-stdlib test-bash - name: GitHub Actions Env Test Setup # FIXME: make this work on Windows as well if: runner.os != 'Windows' run: | cd test/scenarios/github-actions/ ../../../direnv allow ../../../direnv export gha >> "$GITHUB_ENV" - name: GitHub Actions Env Test Verification # FIXME: make this work on Windows as well if: runner.os != 'Windows' run: | [[ -z ${TEST_EXPORT_DIRENV_GITHUB_ACTIONS:-} ]] && echo "TEST_EXPORT_DIRENV_GITHUB_ACTIONS is unset or empty" >&2 && exit 1 tee TEST_EXPORT_DIRENV_GITHUB_ACTIONS.got <<<"$TEST_EXPORT_DIRENV_GITHUB_ACTIONS" echo "${GITHUB_SHA}"$'\n'"${GITHUB_RUN_ID}"$'\n'"${GITHUB_RUN_NUMBER}" | tee TEST_EXPORT_DIRENV_GITHUB_ACTIONS.want diff -u TEST_EXPORT_DIRENV_GITHUB_ACTIONS.want TEST_EXPORT_DIRENV_GITHUB_ACTIONS.got direnv-2.37.1/.github/workflows/nix.yml000066400000000000000000000035101503714164100200120ustar00rootroot00000000000000name: Nix on: push: branches: [ master ] pull_request: {} merge_group: permissions: contents: read jobs: build: name: Nix Build runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: cachix/install-nix-action@v31 - uses: DeterminateSystems/magic-nix-cache-action@main - name: Check Go version consistency run: | # Extract Go version from go.yml workflow GO_WORKFLOW_VERSION=$(grep -A 2 "uses: actions/setup-go" .github/workflows/go.yml | grep "go-version:" | sed "s/.*go-version: *'\([^']*\)'.*/\1/") # Get Go version used in Nix NIX_GO_VERSION=$(nix run --inputs-from . nixpkgs#go -- version | cut -d' ' -f3 | sed 's/go//') # Extract major.minor version for comparison (e.g., "1.24.3" -> "1.24") GO_WORKFLOW_MAJOR_MINOR=$(echo "$GO_WORKFLOW_VERSION" | cut -d. -f1,2) NIX_GO_MAJOR_MINOR=$(echo "$NIX_GO_VERSION" | cut -d. -f1,2) echo "go.yml uses Go version: $GO_WORKFLOW_VERSION" echo "Nix uses Go version: $NIX_GO_VERSION" # Check if major.minor versions match if [ "$GO_WORKFLOW_MAJOR_MINOR" != "$NIX_GO_MAJOR_MINOR" ]; then echo "WARNING: Go version mismatch between workflows!" echo "go.yml uses Go $GO_WORKFLOW_MAJOR_MINOR, but Nix uses Go $NIX_GO_MAJOR_MINOR" echo "Consider updating the go.yml workflow to use the same major.minor version." else echo "✓ Go versions are compatible: $GO_WORKFLOW_MAJOR_MINOR" fi - name: Run flake checks with nix-fast-build run: nix run --inputs-from . nixpkgs#nix-fast-build -- --no-nom --skip-cached --flake .#checks direnv-2.37.1/.github/workflows/release.yml000066400000000000000000000012161503714164100206350ustar00rootroot00000000000000on: push: # Sequence of patterns matched against refs/tags tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 name: Create Release permissions: contents: write jobs: build: name: Create Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Nix uses: cachix/install-nix-action@v30 - name: Setup Nix Cache uses: DeterminateSystems/magic-nix-cache-action@main - name: Build and Upload Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: nix develop --command make create-release direnv-2.37.1/.github/workflows/update-gomod2nix.yml000066400000000000000000000016501503714164100224050ustar00rootroot00000000000000name: Update gomod2nix on: pull_request permissions: contents: write jobs: dependabot: runs-on: ubuntu-latest if: ${{ github.actor == 'dependabot[bot]' }} steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 - name: Install Nix uses: cachix/install-nix-action@v31 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} nix_path: nixpkgs=channel:nixos-unstable - name: Update checksum run: | ./script/update-gomod2nix # git push if we have a diff if [[ -n $(git diff) ]]; then git config --global user.email "<49699333+dependabot[bot]@users.noreply.github.com>" git config --global user.name "dependabot[bot]" git commit -am "update gomod2nix" git push origin HEAD:${{ github.head_ref }} fi direnv-2.37.1/.gitignore000066400000000000000000000003121503714164100150610ustar00rootroot00000000000000*.sw? .direnv .gopath /.envrc /direnv /direnv.test /dist /test/config /test/data /test/scenarios/inherited/.envrc # for context: https://github.com/Homebrew/homebrew-bundle/pull/552 Brewfile.lock.json direnv-2.37.1/.golangci.yml000066400000000000000000000003761503714164100154670ustar00rootroot00000000000000version: "2" run: timeout: 5m linters: presets: - bugs - unused enable: - misspell - revive disable: # direnv is not a web server, context is not strictly necessary. - noctx formatters: gofumpt: extra-rules: true direnv-2.37.1/.mergify.yml000066400000000000000000000010301503714164100153320ustar00rootroot00000000000000queue_rules: - name: default queue_conditions: - base=master - label~=merge-queue|dependencies merge_conditions: - check-success=Analyze (go) - check-success=Go Build (macos-latest) - check-success=Go Build (ubuntu-latest) - check-success=Go Build (windows-latest) - check-success=Nix Build (macos-latest) - check-success=Nix Build (ubuntu-latest) merge_method: rebase pull_request_rules: - name: refactored queue action rule conditions: [] actions: queue: direnv-2.37.1/Brewfile000066400000000000000000000002551503714164100145610ustar00rootroot00000000000000# Run `brew bundle` to install these dev dependencies: brew "go" brew "shellcheck" brew "golangci-lint" brew "elvish" brew "fish" brew "tcsh" brew "powershell" brew "murex" direnv-2.37.1/CHANGELOG.md000066400000000000000000000747051503714164100147230ustar00rootroot00000000000000 2.37.1 / 2025-07-20 ================== * fix: regression in displaying export errors (#1469) 2.37.0 / 2025-07-02 ================== * docs: add github-actions page * docs: document sub-commands * docs: fix link to guix manual (#1421) * docs: re-generate manpages * feat(direnv export gha): strengthen export format * feat: add windows arm64 target (#1444) * fix(powershell): "export pwsh" to resolve PowerShell special character issues (#1448) * fix(python): do not include patch level in virtual environment names (#1423) * fix(use_nix): always restore special variables (#1424) * fix: accept true as valid DIRENV_DEBUG value (#1365) * fix: add trailing newline to error messages (#1426) * fix: delete duplicate ansi escape code v2.36.0 / 2025-04-11 ================== * direnv now requires go 1.24 (#1384) * doc: Correct duplicate usage of 'with' in the direnv(1) (#1394) * doc: note direnv version for log_{format,filter} (#1369) * feat: Add `use_flox` to stdlib.sh (#1372) * feat: logging filter (#1336) * fix use_nix: unset TMPDIR variables (#1409) * fix: A more universal fix for the python 3.14 `find_spec` deprecation warning (#1382) * fix: Don't give an error when the current directory doesn't exist (#1395) * fix: add support to fully reproducible guix shells (#1392) * fix: assert minimum powershell version (#1385) * fix: escape newlines in generated vimscript (#1347) * fix: fix empty array error in install.sh (#1406) * fix: optionally authenticate against github api during install (#1337) * fix: use_guix: Enable the watching of Guix related files. (#1353) 2.35.0 / 2024-10-07 ================== * doc: Add version requirement for load_dotenv option (#1326) * doc: change Guix link to its package search. (#1268) * doc: fix broken link (#1327) * doc: update elvish docs (#1305) * feat: add opam support (#1298) * fix: add NuShell into list of supported shells (#1260) * fix: close tmp file (#1272) * fix: direnv edit: use `editor` when EDITOR not found, closes #1246 (#1247) * fix: release script * fix: stdlib: enable flakes when use flake is used (#1299) * fix: stdlib: export GOBIN for layout_go (#1286) * fix: stdlib: update layout_python to resolve deprecation warning (#1176) * fix: using PWD in .env files (#1052) * test: Fix Murex python-layout test (#1293) 2.34.0 / 2024-03-01 ================== * doc: README.md, man pages: it's typos (#1230) * doc: add shell setup instructions for oh-my-zsh (#1070) * doc: added fetchurl manpage link to README.md * doc: document XDG_DATA_HOME (#1185) * doc: update installation.md for Gentoo (#1206) * feat: add Murex support (#1242) * feat: added systemd shell for export (#1126) * feat: allow to disable warn timeouts (#1209) * feat: hide env diff (#1223, #1234) * feat: made 'direnv export' non private (#1229) * fix: `use_julia` should not set LD_LIBRARY_PATH (#900) * fix: add missing deps for release in go.mod * fix: avoid use of regex in bash hook output (#1043) * fix: direnv.toml.1.md: add examples for $HOME expansion * fix: stdlib: use_flake: don't keep old generations around (#1089) * fix: stdlib: use_node: strip leading v from version (#1071) * fix: support Bash 5.1 array PROMPT_COMMAND (#1208) * fix: update stdlib.sh to avoid deprecation warning (#1221) * fix: update zsh syntax in internal/cmd/shell_zsh.go (#1075) 2.33.0 / 2023-11-29 ================== * doc: add a Nushell section to `hook.md` by @amtoine in https://github.com/direnv/direnv/pull/1175 * doc: fix broken links in installation.md by @just1602 in https://github.com/direnv/direnv/pull/1110 * doc: show how to run tests by @bukzor-sentryio in https://github.com/direnv/direnv/pull/1137 * doc: update NixOS installation instructions by @Gerg-L in https://github.com/direnv/direnv/pull/1172 * doc: update direnv.toml.1.md by @Ativerc in https://github.com/direnv/direnv/pull/1099 * feat: `direnv status --json` by @shivaraj-bh in https://github.com/direnv/direnv/pull/1142 * feat: add PowerShell Support by @bamsammich in https://github.com/direnv/direnv/pull/1171 * feat: add mergify configuration by @Mic92 in https://github.com/direnv/direnv/pull/1147 * feat: add support for armv7l platform in install.sh by @ardje in https://github.com/direnv/direnv/pull/1162 * feat: add watch print command by @Mic92 in https://github.com/direnv/direnv/pull/1198 * feat: alias `direnv disallow` to deny by @will in https://github.com/direnv/direnv/pull/1182 * feat: stdlib: create CACHEDIR.TAG inside .direnv by @Mic92 in https://github.com/direnv/direnv/pull/1148 * fix: `allowPath` for `LoadedRC` by @shivaraj-bh in https://github.com/direnv/direnv/pull/1157 * fix: don't prompt to allow if user explicitly denied by @Gabriella439 in https://github.com/direnv/direnv/pull/1158 * fix: man/direnv-stdlib: fix obsolete opam-env example by @mzacho in https://github.com/direnv/direnv/pull/1170 * fix: print correct path in source_env log message by @wentasah in https://github.com/direnv/direnv/pull/1144 * fix: quote tcsh $PATH, to avoid failure on whitespace by @bukzor-sentryio in https://github.com/direnv/direnv/pull/1139 * fix: remove redundant nil check in `CommandsDispatch` by @Juneezee in https://github.com/direnv/direnv/pull/1166 * fix: update nixpkgs and shellcheck by @Mic92 in https://github.com/direnv/direnv/pull/1146 2.32.3 / 2023-05-20 ================== * fix: incorrect escape sequences during Loads under git-bash (Windows) (#1085) * fix: skip some tests for IBM Z mainframe's z/OS operating system (#1094) * fix: stdlib: use_guix: Switch to guix shell. (#1045) * fix: stat the already open rc file rather than another path based one on it (#1044) * fix: remove deprecated io/ioutil uses (#1042) * fix: spelling fixes (#1041) * fix: appease Go 1.19 gofmt (#1040) * fix: pass BASH_PATH to make, matches the nixpkgs derivation (#1006) * fix: stdlib/layout_python: exclude patchlevel from $python_version (#1033) * doc: add Windows installation with winget (#1096) * doc: link 12factor webpage for more clarity (#1095) * website: add Plausible analytics 2.32.2 / 2022-11-24 ================== * doc: Add stdlib's layout_pyenv to docs (#969) * doc: Fix broken link (#991) * doc: Minor typo fix (#1013) * doc: `$XDG_CONFIG_HOME/direnv/direnv.toml` => add (typically ~/.config/direnv/direnv.toml) (#985) * doc: add quickenv to Related projects (#970) * feat: Update layout anaconda to accept a path to a yml file (#962) * feat: install.sh: can specify direnv version (#1012) * fix: elvish: replace deprecated `except` with `catch` (#987) * fix: installer.sh: make direnv executable for all * fix: path escaping (#975) * fix: stdlib: only use ANSI escape on TTY (#958) * fix: test: remove mentions of DIRENV_MTIME (#1009) * fix: test: use lowercase -d flag for base64 decoding of DIRENV_DIFF (#996) * update: build(deps): bump github.com/BurntSushi/toml from 1.1.0 to 1.2.0 (#974) 2.32.1 / 2022-06-21 ================== * feat: Support custom VIRTUAL_ENV for layout_python (#876) * fix: vendor go-dotenv (#955) 2.32.0 / 2022-06-13 ================== * feat: Add gha shell for GitHub Actions (#910) * feat: Enable ppc64le builds (#947) * feat: allow conda environment names to be detected from environment.yml (#909) * feat: source_up_if_exists: A strict_env compatible version of source_up (#921) * feat: Expand ~/ in whitelist paths (#931) * feat: Add "block" and "revoke" as aliases of the "deny" command (#935) * feat: Add "permit" and "grant" as aliases of the "allow" command (#935) * fix: update go-dotenv * fix: fetchurl: store files as hex (#930) * fix: fetchurl: only store 200 responses (#944) * fix: Ensure status log messages are printed with normal color (#884) * fix: Clarify handling of .env files (#941) * fix: Update shell_elvish.go (#896) * fix: stdlib.sh: remove dependency on tput (#932) * fix: Use setenv in vim to allow non alphanumeric vars (#901) * fix: install.sh: add information about bin_path (#920) * fix: Treat `mingw*` as windows (direnv/direnv#918) (#919) * fix: man: clarify paths (#929) * fix: installation.md: Fix Fedora package link (#915) * Merge pull request #874 from direnv/refactor * chore: rc: stop using --noprofile --norc * chore: rc: prepare stdin earlier * chore: rc: install interrupt handler earlier * chore: stdlib: factor out stdlib preparation * chore: fix CI * chore: source_env: show full path (#870) * chore: Sort shells in DetectShell * chore: Enable codeql action (#938) * chore: Set permissions for GitHub actions (#937) * go: bump golang.org/x/sys for linux/loong64 support (#946) * build(deps): bump actions/checkout from 2.4.0 to 3.0.0 (#922) * build(deps): bump actions/checkout from 3.0.0 to 3.0.1 (#933) * build(deps): bump actions/checkout from 3.0.1 to 3.0.2 (#936) * build(deps): bump actions/setup-go from 2.1.5 to 3.0.0 (#923) * build(deps): bump actions/setup-go from 3.0.0 to 3.1.0 (#943) * build(deps): bump actions/setup-go from 3.1.0 to 3.2.0 (#950) * build(deps): bump cachix/install-nix-action from 16 to 17 (#925) * build(deps): bump github.com/BurntSushi/toml from 0.4.1 to 1.1.0 (#924) 2.31.0 / 2022-03-26 ================== * Don't load .env files by default (#911) * doc: `~/.config/direnv/direnvrc` is the default * doc: fix the broken link to arch linux (#892) * Re-add accidentally deleted comment line (#881) * fix version test 2.30.3 / 2022-01-05 ================== * Allow skipping `.env` autoload (#878) * stdlib: add `env_vars_required` (#872) (#872) * Test whether version.txt contains semantic version (#871) 2.30.2 / 2021-12-28 ================== * FIX: version: trim surrounding spaces (#869) * build(deps): bump actions/setup-go from 2.1.4 to 2.1.5 (#866) * move most code under internal/cmd (#865) 2.30.1 / 2021-12-24 ================== * FIX: ignore .envrc and .env if they are not files (#864) 2.30.0 / 2021-12-23 ================== * Add automatic `.env` load (#845) * Resolve symlinks during `direnv deny` (#851) * update installer for Apple Silicon (#849) * stdlib: use_flake handle no layout dir (#861) * embed stdlib.sh (#782) * embed version.txt * go mod update * make dist: remove references to Go 2.29.0 / 2021-11-28 ================== * stdlib: add use_flake function (#847) * docs(direnv.toml) Add config.toml clarification (#831) * docs(install): fix macos links (#841) * Corrects stdlib link in Ruby docs (#837) * stdlib.sh: Fix removal of temp file (#830) * install.sh: add aarch64 support * Updated conditional for zsh hook to be more forgiving (#808) * Add -r flag for matching Git branches with a regexp (#800) * Add docs about pipenv (#797) * Enable syntax highlights to the quick demo code (#752) * Fixed extra quotes for lower alpha characters (#783) * Remove noisy warning about PS1 again (#781) 2.28.0 / 2021-03-12 ================== * Merge pull request #779 from wingrunr21/go_1_16 * Build for darwin/arm64. Resolves #738 * Update to go 1.16 * test: Fix errors for elvish test (#767) * tcsh: fix variable escaping (#778) * Change DESTDIR to PREFIX in development.md (#774) * go: use the /v2 prefix (#765) * Relax README's recommendation for nix-direnv (#763) * man/direnv.1.md: add FILES section (fix #758) (#759) * Add/update fish tests (#754) * build(deps): bump golang.org/x/mod from 0.4.0 to 0.4.1 (#749) * Fix typo "avaible" in install.sh (#750) * docs: improve the use_node documentation 2.27.0 / 2021-01-01 ================== * fixed fish shell hook to work with eval (#743) * dist: remove darwin/386 * nix: update to nixpkgs@nixos-20.09 * packaging: stop vendoring the Go code (#739) * packaging: change packaging. DESTDIR -> PREFIX, fish hook (#741) 2.26.0 / 2020-12-27 ================== * updated fish hook support issue (#732) * ci: add basic windows CI (#737) * test: fix shellcheck usage in ./test/stdlib.bash * test: fix use_julia test for NixOS * remove dead code: rootDir * fix: create temp dir in current working dir for one test (#735) * Add `dotenv_if_exists` (#734) * stdlib: add watch_dir command (#697) 2.25.2 / 2020-12-12 ================== There was a generation issue in 2.25.1. This release only bumps the version to do another release. 2.25.1 / 2020-12-11 ================== * stdlib.go: re-generate (fixes #707) * README: remove old Azure badge * build(deps): bump golang.org/x/mod from 0.3.0 to 0.4.0 (#730) 2.25.0 / 2020-12-03 ================== * dist: add linux/arm64 and linux/ppc64 * Added use_nodenv to stdlib (#727) * Fix proposal for #707, broken direnv compatibility under Windows (#723) * fix: layout anaconda (#717) * Add on_git_branch command to detect whether a specific git branch is checked out (#702) 2.24.0 / 2020-11-15 ================== * direnv_load: avoid leaking DIRENV_DUMP_FILE_PATH (#715) * Add strict_env and unstrict_env (#572) * stdlib: add `use_vim` to source local vimrc (#497) * stdlib: add source_env_if_exists (#714) * Wording (#713) * build(deps): bump actions/checkout from v2.3.3 to v2.3.4 (#709) * build(deps): bump cachix/install-nix-action from v11 to v12 (#710) * Fix XDG_CACHE_HOME path (#711) * rc: make file existence check more robust (#706) 2.23.1 / 2020-10-22 ================== * fix: handle links on Mac when using `allow` (#696) * fix: use restored env in exec (#695) * stdlib: add basename and dirname from realpath (#693) * stdlib.sh: remove tabs * dist: compile all the binaries statically 2.23.0 / 2020-10-10 ================== * stdlib: add source_url function (#562) * direnv: add fetchurl command (#686) * shell: Update Elvish hook to replace deprecated `explode` (#685) 2.22.1 / 2020-10-06 ================== * Look for stdlib in DIRENV_CONFIG (#679) * stdlib: use Bash 3.0-compatible array expansion (#676) * Clarify path to direnv.toml (#678) * stdlib/use_julia: fix a bug in parameter substitution for empty or (#667) * man: update the layout_go documentation * stdlib: adds GOPATH/bin to PATH (#670) 2.22.0 / 2020-09-01 ================== * stdlib: use_julia (#666) * stdlib: semver_search (#665) * direnv-stdlib.1: add layout julia (#661) * README: spelling correction (#660) * README.md: add shadowenv to similar projects (#659) * docs: remove Snap from the installations * OSX -> macOS (#655) * Update shell_fish.go to use \X for UTF encoding (#584) * Change XDG_CONFIG_DIR to XDG_CONFIG_HOME (#641) * Streamline core algorithm of export and exec (#636) * test: add failure test-case (#637) 2.21.3 / 2020-05-08 ================== * Replace `direnv expand_path` with pure bash (#631) * Fix #594 - write error to fd 3 on Windows (#634) * Make direnv hook output work on Windows (#632) * Update hook.md to remove ">" typo in Fish instructions (#624) * stdlib: `layout go` adds layout dir to GOPATH (#622) * direnv-stdlib.1: add layout php (#619) * stdlib: add PATH_rm [ ...] (#615) * Error handling tuples (#610) * Merge pull request #607 from punitagrawal/master * test: elvish: Fix evaluation function * stdlib.sh: Re-write grep pattern to avoid shell escape * man: Escape '.' at the beginning of line to remove manpage warning * stdlib: fix direnv_config_dir usage (#601) * direnv version: improve error message (#599) * README: fix NixOS link in installation.md (#589) * stdlib: add direnv_apply_dump (#587) * Simplify direnv_load and make it work even when the command crashes. (#568) * docs: fix fish installation instruction * test: test for utf-8 compatibility * config: add [global] section * config: add strict_env option * config: fix warn_timeout parsing (#582) * Github action for releases * config: fix the configuration file selection * stdlib: fix shellcheck warnings 2.21.2 / 2020-01-28 ================== Making things stable again. * stdlib: revert the `set -euo pipefail` change. It was causing too many issues for users. * direnv allow: fix the allow migration by also creating the parent target directory. 2.21.1 / 2020-01-26 ================== Fix release * stdlib: fix unused variable in `use node` * stdlib: fix unused variable in `source_up` * test: add stdlib test skeleton * add dist release utility 2.21.0 / 2020-01-25 ================== This is a massive release! ## Highlights You can now hit Ctrl-C during a long reload in bash and zsh and it will not loop anymore. Commands that use `direnv_load` won't fail when there is an output to stdout anymore (eg: `use_nix`). Direnv now also loads files from `.config/direnv/lib/*.sh`. This is intended to be used by third-party tools to augment direnv with their own stdlib functions. The `.envrc` is now loaded with `set -euo pipefail`. This will more likely expose issues with existing `.envrc` files. ## docs * Update README.md (#536) * Add link to asdf-direnv. (#535) * docs: fix invalid link (#533) * adds experimental curl based installer (#539) ## commands * change where the allow files are being stored * direnv status: also show the config * direnv exec: improve the error message * warn if PS1 is being exported * handle SIGINT during export in bash * export: display the full RC path instead of a relative one * direnv exec: the DIR argument is always required (#493) ## build * ci: use GitHub Actions instead of Azure Pipelines * staticcheck (#543) * use go modules * make: handle when /dev/stderr doesn't exist (#491) * site: use jekyll to render the website * Pin nixpkgs to current NixOS 19.09 channel (#526) ## shells * fix elvish hook * Use `fish_preexec` hook instead of `fish_prompt` (#512) * Use `fish_postexec` to make sure direnv hook executed 'after' the directory has changed when using `cd`. * improve zsh hook (#514) ## config.toml * rename the configuration from config.toml to direnv.toml (#498) * add warn_timeout option. DIRENV_WARN_TIMEOUT is now deprecated. ## stdlib * `direnv_load` can now handle stdout outputs * stdlib: add layout_julia * Handle failing pipenv on empty file and avoid an extra pipenv execution (#510) * fix `source_env` behaviour when the file doesn't exists (#487) * `watch_file` can now watch multiple files in a single invocation (#524) * `layout_python`: prefer venv over virtualenv. Do not export VIRTUAL_ENV if $python_version is unavailable or a virtual environment does not exist/can't be created * Adds layout_pyenv (#505) * Fix `source_up` docs to explain that search starts in parent directory (#518) * fix `path_add` to not leak local variables * `layout_pyenv`: support multiple python versions (#525) * Add a `direnv_version ` command to check the direnv version. * `dotenv`: handle undefined variables * source files from `.config/direnv/lib/*.sh` * stdlib: set `-euo pipefail` 2.20.1 / 2019-03-31 ================== * ci: try to fix releases 2.20.0 / 2019-03-31 ================== * CHANGE: Use source instead of eval on fish hook * DOC: Remove duplicate build badge (#465) * DOC: add note about auth (#463) * DOC: change nixos link (#460) * FIX: Corrects reverse patching when using exec cmd. (#466) * FIX: Perform stricter search for existing Anaconda environments (#462) * FIX: arity mismatch for elvish (#482) * FIX: avoid reloading on each prompt after error (#468) * FIX: improve bash hook handling of empty PROMPT_COMMAND (#473) * FIX: improved the tests for bash, zsh, fish and tcsh (#469) * MISC: migrated from Travis CI to Azure Pipelines (#484) 2.19.2 / 2019-02-09 ================== * FIX: file_times: check Stat and Lstat (#457) 2.19.1 / 2019-01-31 ================== * FIX: watched files now handle symlinks properly. Thanks @grahamc! #452 2.19.0 / 2019-01-11 ================== * NEW: add support for .env variable expansion. Thanks to @hakamadare! 2.18.2 / 2018-11-23 ================== * make: generate direnv.exe on windows (#417) 2.18.1 / 2018-11-22 ================== * travis: fix the release process 2.18.0 / 2018-11-22 ================== A lot of changes! * stdlib: add DIRENV_IN_ENVRC (#414) * Fix typo in readme. (#412) * Merge pull request #407 from zimbatm/direnv-dump-shell * direnv dump can now dump to arbitrary shells * add a new "gzenv" shell * move gzenv into new package * shell: introduce a dump capability * cleanup the shells * Add alias '--version' to version command. Closes #377. (#404) * Correctes spelling of openSUSE (#403) * testing: elvish 0.12 is released now (#402) * Merge pull request #397 from zimbatm/readme-packaging-status * README: add packaging status badge * README: remove equinox installation * direnv show_dump: new command to debug encoded env (#395) * Document possibility to unset vars (#392) * stdlib: fix typo * go dep: update Gopkg.lock * make: don't make shfmt a dependency * Avoid to add unnecessary trailing semicolon character (#384) * add asdf to the list of known projects * stdlib.go: re-generate * Add PHP layout to stdlib (#346) * make: fix formatting * README: add build status badge * Overhaul the build system (#375) * stdlib, layout_pipenv: handle `$PIPENV_PIPFILE` (#371) * README: improve the source build instructions 2.17.0 / 2018-06-17 ================== * CHANGE: hook expands the direnv path. Ensures that direnv can be executed even if the PATH is changed #369. * CHANGE: stdlib: direnv_load: disallow watching in child Allows the `use nix --pure` scenario in #368 * README: add OpenSuSE to the list of distros * Revert "use_nix: unset IN_NIX_SHELL" 2.16.0 / 2018-05-09 ================== * NEW: add support for elvish (#356) * NEW: config: allow to disable stdin on eval (#351) * DOC: Add the usage of source_up to the README (#347) * FIX: default.nix: fix compilation 2.15.2 / 2018-02-25 ================== * FIX: lintian warnings (#340) * FIX: release process (#342) 2.15.1 / 2018-02-24 ================== * FIX: support for go 1.10 (#339) 2.15.0 / 2018-02-23 ================== * NEW: TOML configuration file! (#332, #337) * NEW: support for allow folder whitelist (#332) * NEW: add anaconda support (#312) * CHANGE: use_nix: unset IN_NIX_SHELL 2.14.0 / 2017-12-13 ================== * NEW: Add support for Pipenv layout (#314) * CHANGE: direnv version: make public * FIX: direnv edit: run the command through bash * FIX: website: update ditto to v0.15 2.13.3 / 2017-11-30 ================== * FIX: fixes dotenv loading issue on macOS `''=''` 2.13.2 / 2017-11-28 ================== * FIX: direnv edit: fix path escaping * FIX: stdlib: fix find_up * FIX: stdlib: use absolute path in source_up * FIX: remove ruby as a build dependency * FIX: go-dotenv: update to latest master to fix a parsing error 2.13.1 / 2017-09-27 ================== * FIX: stdlib: make direnv_layout_dir lazy (#298) 2.13.0 / 2017-09-24 ================== * NEW: stdlib: configurable direnv_layout_dir * CHANGE: stdlib: source the direnvrc directly * FIX: permit empty NODE_VERSION_PREFIX variable * FIX: pwd: Don't use -P to remove symlinks (#295) * FIX: also reload when mtime goes back in time * FIX: Prevent `$HOME` path from being striked (#287) * BUILD: use the new `dep` tool to manage dependencies * BUILD: dotenv: move to vendor folder 2.12.2 / 2017-07-05 ================== * stdlib layout_python: fixes on no arg 2.12.1 / 2017-07-01 ================== * FIX: stdlib path_add(), see #278 * FIX: install from source instructions 2.12.0 / 2017-06-30 ================== * NEW: support multiple items in path_add and PATH_add (#276) * NEW: add a configurable DIRENV_WARN_TIMEOUT option (#273) * CHANGE: rewrite the dotenv parsing, now supports commented lines * CHANGE: pass additional args to virtualenv (#261) * FIX: stdlib watch_file(): escaping fix * FIX: only output color if $TERM is not dumb (#264) * FIX: the watch_file documentation 2.11.3 / 2017-03-02 ================== * FIX: node version sorting (#255) 2.11.2 / 2017-03-01 ================== * FIX: Typo in MANPATH_add always generates "PATH missing" error. (#256) 2.11.1 / 2017-02-20 ================== * FIX: only deploy the go 1.8 version 2.11.0 / 2017-02-20 ================== * NEW: stdlib.sh: introduce MANPATH_add (#248) * NEW: provide packages using the equinox service * CHANGE: test direnv with go 1.8 (#254) * FIX: Add warning about source_env/up * FIX: go-md2man install instruction 2.10.0 / 2016-12-10 ================== * NEW: `use guix` (#242) * CHANGE: use go-md2man to generate the man pages * FIX: tcsh escaping (#241) * FIX: doc typos and rewords (#226) 2.9.0 / 2016-07-03 ================== * NEW: use_nix() is now watching default.nix and shell.nix * NEW: Allow to fix the bash path at built time * FIX: Panic on `direnv current` with no argument * FIX: Permit empty NODE_VERSION_PREFIX variable * FIX: layout_python: fail properly when python is not found 2.8.1 / 2016-04-04 ================== * FIX: travis dist release 2.8.0 / 2016-03-27 ================== * NEW: `direnv export json` to facilitate IDE integration * NEW: watch functionality thanks to @avnik Now direnv also reload on associate .env and .envrc changes. * NEW: stdlib `watch_file` function thanks to @avnik Allows to monitor more files for change. * NEW: stdlib `use node` function thanks to @wilmoore * NEW: `direnv prune` to remove old allowed files thanks to @punitagrawal Only works with newly-generated files since we're not storing the path inside of them. 2.7.0 / 2015-08-08 ================== * NEW: use_nix() helper to stdlib. Thanks @gfxmonk * FIX: Added SHELLOPTS to ignored vars. Thanks @fernandomora * FIX: Removed shellcheck offenses in the stdlib, better escaping * FIX: typos. Thanks @camelpunch, @oppegard 2.6.1 / 2015-06-23 ================== * FIX: source_env handles missing .envrc gracefully. Thanks @gerhard * FIX: Empty variable as unloading in Vim. Thanks @p0deje * FIX: Corrected spelling mistake in deny command. Thanks @neanias 2.6.0 / 2015-02-15 ================== * NEW: tcsh is now supported ! Thanks @bbense * CHANGE: `direnv dump` now ignores `BASH_FUNC_` exports. Thanks @gfxmonk * CHANGE: Interactive input during load is now possible. Thanks @toao * FIX: allow workaround for tmux users: `alias tmux='direnv exec / tmux'` * FIX: hardened fish shell escaping thanks to @gfxmonk Thanks @bbense @vially and @dadooda for corrections in the docs 2.5.0 / 2014-11-04 ================== * NEW: Use a different virtualenv per python versions for easier version switching. Eg: ./.direnv/python-${python_version} * NEW: Makes `layout python3` a shortcut for `layout python python3`. Thanks @ghickman ! * NEW: Allows to specify which executable of python to use in `layout_python` * CHANGE: `layout python` now unsets $PYTHONHOME to better mimic virtualenv * CHANGE: Don't make virtualenvs relocatable. Fixes #137 * OTHER: Use Travis to push release builds to github 2.4.0 / 2014-06-15 ================== * NEW: Try to detect an editor in the PATH if EDITOR is not set. * NEW: Preliminary support for vim * NEW: New site: put the doc inside the project so it stays in sync * NEW: Support for Cygwin - Thanks @CMCDragonkai ! * NEW: Allow to disable logging by setting an empty `DIRENV_LOG_FORMAT` * NEW: stdlib `layout perl`. Thanks @halkeye ! * CHANGE: layout ruby: share the gem home starting from rubygems v2.2.0 * CHANGE: Allow arbitrary number of args in `log_status` * CHANGE: Bump command timeout to 5 seconds * FIX: Adds selected bash executable in `direnv status` * FIX: man changes, replaced abandoned ronn by md2man * FIX: `make install` was creating a ./bin directory * FIX: issue #114 - work for blank envs. Thanks @pwaller ! * FIX: man pages warning. Thanks @punitagrawal ! * FIX: Multi-arg EDITOR was broken #108 * FIX: typos in doc. Thanks @HeroicEric and @lmarlow ! * FIX: If two paths don't have a common ancestors, don't make them relative. * FIX: missing doc on layered .envrc. Thanks @take ! 2.3.0 / 2014-02-06 ================== * NEW: DIRENV_LOG_FORMAT environment variable can be used tocontrol log formatting * NEW: `direnv exec [DIR] ` to execute programs with an .envrc context * CHANGE: layout_python now tries to make your virtualenv relocatable * CHANGE: the export diff is not from the old env, not the current env * CHANGE: layout_go now also adds $PWD/bin in the PATH * FIX: Hides the DIRENV_ variables in the output diff. Fixes #94 * FIX: Makes sure the path used in the allow hash is absolute. See #95 * FIX: Set the executable bit on direnv on install * FIX: Some bash installs had a parse error in the hook. 2.2.1 / 2014-01-12 ================== The last release was heavily broken. Ooops ! * FIX: Refactored the whole export and diff mechanism. Fixes #92 regression. * CHANGE: DIRENV_BACKUP has been renamed to DIRENV_DIFF 2.2.0 / 2014-01-11 ================== Restart your shells on upgrade, the format of DIRENV_BACKUP has changed and is incompatible with previous versions. * NEW: `direnv_load ` stdlib function * CHANGE: Only backup the diff of environments. Fixes #82 * CHANGE: Renames `$DIRENV_PATH` to `$direnv` in the stdlib. * CHANGE: Allow/Deny mechanism now includes the path to make it more secure. * CHANGE: `direnv --help` is an alias to `direnv help` * CHANGE: more consistent log outputs and error messages * CHANGE: `direnv edit` only auto-allows the .envrc if it's mtime has changed. * CHANGE: Fixes old bash (OSX) segfault in some cases. See #81 * CHANGE: The stdlib `dotenv` now supports more .env syntax * FIX: Restore the environment properly after loading errors. 2.1.0 / 2013-11-10 ================== * Added support for the fish shell. See README.md for install instructions. * Stop recommending using $0 to detect the shell. Fixes #64. * Makes the zsh hook resistant to double-hooking. * Makes the bash hook resistant to double-hooking. * More precise direnv allow error message. Fixes #72 2.0.1 / 2013-07-27 ================== * Fixes shell detection corner case 2.0.0 / 2013-06-16 ================== When upgrading from direnv 1.x make sure to restart your shell. The rest is relatively backward-compatible. * changed the execution model. Everything is in a single static executable * most of the logic has been rewritten in Go * robust shell escaping (supports UTF-8 in env vars) * robust eval/export loop, avoids retrys on every prompt if there is an error * stdlib: added the `dotenv [PATH]` command to load .env files * command: added `direnv reload` to force-reload your environment direnv-2.37.1/CNAME000066400000000000000000000000131503714164100136350ustar00rootroot00000000000000direnv.net direnv-2.37.1/CONTRIBUTING.md000066400000000000000000000010651503714164100153300ustar00rootroot00000000000000# Making a new release The release process is now fully automated. Simply run: ```bash make prepare-release VERSION=v2.37.0 ``` This will: 1. Generate and open the changelog for editing 2. Prompt for confirmation to proceed 3. Update version.txt with the new version 4. Create a release branch and PR 5. Wait for the PR to be merged 6. Create and push the git tag 7. Trigger CI to build and publish the release automatically ## Testing releases To test the release process on your fork: ```bash make prepare-release VERSION=v2.37.0-test REPO=Mic92/direnv ``` direnv-2.37.1/GNUmakefile000066400000000000000000000133451503714164100151550ustar00rootroot00000000000000############################################################################ # Variables ############################################################################ # Set this to change the target installation path PREFIX = /usr/local BINDIR = ${PREFIX}/bin SHAREDIR = ${PREFIX}/share MANDIR = ${SHAREDIR}/man DISTDIR ?= dist # filename of the executable exe = direnv$(shell go env GOEXE) # Override the go executable GO = go # BASH_PATH can also be passed to hard-code the path to bash at build time SHELL = bash ############################################################################ # Common ############################################################################ .PHONY: all all: build man ## Build direnv and generate man pages (default) .PHONY: help help: ## Show this help message @echo "Available targets:" @awk 'BEGIN {FS = ":.*##"; printf "\n"} /^[a-zA-Z_-]+:.*##/ { printf " %-20s %s\n", $$1, $$2 }' $(MAKEFILE_LIST) export GO111MODULE=on ############################################################################ # Build ############################################################################ .PHONY: build build: direnv ## Build the direnv binary .PHONY: clean clean: ## Remove build artifacts rm -rf \ .gopath \ direnv GO_LDFLAGS = ifeq ($(shell uname), Darwin) # Fixes DYLD_INSERT_LIBRARIES issues # See https://github.com/direnv/direnv/issues/194 GO_LDFLAGS += -linkmode=external endif ifdef BASH_PATH GO_LDFLAGS += -X main.bashPath=$(BASH_PATH) endif ifneq ($(strip $(GO_LDFLAGS)),) GO_BUILD_FLAGS = -ldflags '$(GO_LDFLAGS)' endif SOURCES = $(wildcard *.go internal/*/*.go pkg/*/*.go) direnv: $(SOURCES) $(GO) build $(GO_BUILD_FLAGS) -o $(exe) ############################################################################ # Format all the things ############################################################################ .PHONY: fmt fmt-go fmt-sh fmt: fmt-go fmt-sh fmt-go: $(GO) fmt fmt-sh: @command -v shfmt >/dev/null || (echo "Could not format stdlib.sh because shfmt is missing. Run: go install mvdan.cc/sh/cmd/shfmt@latest"; false) shfmt -i 2 -w stdlib.sh ############################################################################ # Documentation ############################################################################ man_md = $(wildcard man/*.md) roffs = $(man_md:.md=) .PHONY: man man: $(roffs) ## Generate man pages %.1: %.1.md @command -v go-md2man >/dev/null || (echo "Could not generate man page because go-md2man is missing. Run: go install github.com/cpuguy83/go-md2man/v2@latest"; false) go-md2man -in $< -out $@ ############################################################################ # Testing ############################################################################ tests = \ test-shellcheck \ test-stdlib \ test-go \ test-go-lint \ test-go-fmt \ test-bash \ test-elvish \ test-fish \ test-tcsh \ test-zsh \ test-pwsh \ test-mx # Skip few checks for IBM Z mainframe's z/OS aka OS/390 ifeq ($(shell uname), OS/390) tests = \ test-stdlib \ test-go \ test-go-fmt \ test-bash endif .PHONY: $(tests) test: build $(tests) ## Run all tests @echo @echo SUCCESS! test-shellcheck: shellcheck stdlib.sh shellcheck ./test/stdlib.bash test-stdlib: build ./test/stdlib.bash test-go: $(GO) test -v ./... test-go-lint: golangci-lint run test-bash: bash ./test/direnv-test.bash # Needs elvish 0.12+ test-elvish: elvish ./test/direnv-test.elv test-fish: fish ./test/direnv-test.fish test-tcsh: tcsh -e ./test/direnv-test.tcsh test-zsh: zsh ./test/direnv-test.zsh test-pwsh: pwsh ./test/direnv-test.ps1 test-mx: murex -trypipe ./test/direnv-test.mx ############################################################################ # Installation ############################################################################ .PHONY: install install: all ## Install direnv to PREFIX (default: /usr/local) install -d $(DESTDIR)$(BINDIR) install $(exe) $(DESTDIR)$(BINDIR) install -d $(DESTDIR)$(MANDIR)/man1 cp -R man/*.1 $(DESTDIR)$(MANDIR)/man1 install -d $(DESTDIR)$(SHAREDIR)/fish/vendor_conf.d echo "$(BINDIR)/direnv hook fish | source" > $(DESTDIR)$(SHAREDIR)/fish/vendor_conf.d/direnv.fish .PHONY: dist dist: ## Build cross-platform binaries @mkdir -p $(DISTDIR) @echo "Building cross-platform binaries..." @platforms=" \ darwin/amd64 \ darwin/arm64 \ freebsd/386 \ freebsd/amd64 \ freebsd/arm \ linux/386 \ linux/amd64 \ linux/arm \ linux/arm64 \ linux/mips \ linux/mips64 \ linux/mips64le \ linux/mipsle \ linux/ppc64 \ linux/ppc64le \ linux/s390x \ netbsd/386 \ netbsd/amd64 \ netbsd/arm \ openbsd/386 \ openbsd/amd64 \ windows/386 \ windows/amd64 \ windows/arm64 \ "; \ for platform in $$platforms; do \ os=$${platform%/*}; \ arch=$${platform#*/}; \ echo "Building for $$os/$$arch..."; \ CGO_ENABLED=0 GOFLAGS="-trimpath" GOOS=$$os GOARCH=$$arch \ $(GO) build -ldflags="-s -w" -o "$(DISTDIR)/direnv.$$os-$$arch"; \ done .PHONY: prepare-release prepare-release: ## Interactive release preparation (changelog, PR, tag) ./script/prepare-release.sh $(VERSION) $(REPO) .PHONY: create-release create-release: dist ## Create GitHub release with binaries (CI only) @if [ -z "$$GITHUB_REF_NAME" ]; then \ echo "GITHUB_REF_NAME is not set. This target is meant to be run in GitHub Actions."; \ exit 1; \ fi @echo "Extracting release notes from CHANGELOG.md..." @release_notes=$$(awk '/^==================/{if(headers>0) exit} /^==================/{headers++; next} headers>0' CHANGELOG.md | sed '/^v[0-9]/d'); \ gh release create "$$GITHUB_REF_NAME" \ --title "Release $$GITHUB_REF_NAME" \ --notes "$$release_notes" \ --verify-tag gh release upload "$$GITHUB_REF_NAME" $(DISTDIR)/direnv.* direnv-2.37.1/LICENSE000066400000000000000000000020711503714164100141020ustar00rootroot00000000000000MIT License Copyright (c) 2019 zimbatm and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. direnv-2.37.1/README.md000066400000000000000000000171441503714164100143630ustar00rootroot00000000000000direnv -- unclutter your .profile ================================= [![Built with Nix](https://builtwithnix.org/badge.svg)](https://builtwithnix.org) [![Packaging status](https://repology.org/badge/tiny-repos/direnv.svg)](https://repology.org/project/direnv/versions) [![latest packaged version(s)](https://repology.org/badge/latest-versions/direnv.svg)](https://repology.org/project/direnv/versions) [![Support room on Matrix](https://img.shields.io/matrix/direnv:numtide.com.svg?label=%23direnv%3Anumtide.com&logo=matrix&server_fqdn=matrix.numtide.com)](https://matrix.to/#/#direnv:numtide.com) `direnv` is an extension for your shell. It augments existing shells with a new feature that can load and unload environment variables depending on the current directory. ## Use cases * Load [12factor apps](https://12factor.net/) environment variables * Create per-project isolated development environments * Load secrets for deployment ## How it works Before each prompt, direnv checks for the existence of a `.envrc` file (and [optionally](man/direnv.toml.1.md#codeloaddotenvcode) a `.env` file) in the current and parent directories. If the file exists (and is authorized), it is loaded into a **bash** sub-shell and all exported variables are then captured by direnv and then made available to the current shell. It supports hooks for all the common shells like bash, zsh, tcsh and fish. This allows project-specific environment variables without cluttering the `~/.profile` file. Because direnv is compiled into a single static executable, it is fast enough to be unnoticeable on each prompt. It is also language-agnostic and can be used to build solutions similar to rbenv, pyenv and phpenv. ## Getting Started ### Prerequisites * Unix-like operating system (macOS, Linux, ...) * A supported shell (bash, zsh, tcsh, fish, elvish, powershell, murex, nushell) ### Basic Installation 1. direnv is packaged in most distributions already. See [the installation documentation](docs/installation.md) for details. 2. [hook direnv into your shell](docs/hook.md). Now restart your shell. ### Quick demo To follow along in your shell once direnv is installed. ```shell # Create a new folder for demo purposes. $ mkdir ~/my-project $ cd ~/my-project # Show that the FOO environment variable is not loaded. $ echo ${FOO-nope} nope # Create a new .envrc. This file is bash code that is going to be loaded by # direnv. $ echo export FOO=foo > .envrc .envrc is not allowed # The security mechanism didn't allow to load the .envrc. Since we trust it, # let's allow its execution. $ direnv allow . direnv: reloading direnv: loading .envrc direnv export: +FOO # Show that the FOO environment variable is loaded. $ echo ${FOO-nope} foo # Exit the project $ cd .. direnv: unloading # And now FOO is unset again $ echo ${FOO-nope} nope ``` ### The stdlib Exporting variables by hand is a bit repetitive so direnv provides a set of utility functions that are made available in the context of the `.envrc` file. As an example, the `PATH_add` function is used to expand and prepend a path to the $PATH environment variable. Instead of `export PATH=$PWD/bin:$PATH` you can write `PATH_add bin`. It's shorter and avoids a common mistake where `$PATH=bin`. To find the documentation for all available functions check the [direnv-stdlib(1) man page](man/direnv-stdlib.1.md). It's also possible to create your own extensions by creating a bash file at `~/.config/direnv/direnvrc` or `~/.config/direnv/lib/*.sh`. This file is loaded before your `.envrc` and thus allows you to make your own extensions to direnv. Note that this functionality is not supported in `.env` files. If the coexistence of both is needed, one can use `.envrc` for leveraging stdlib and append `dotenv` at the end of it to instruct direnv to also read the `.env` file next. ## Docs * [Install direnv](docs/installation.md) * [Hook into your shell](docs/hook.md) * [Develop for direnv](docs/development.md) * [Manage your rubies with direnv and ruby-install](docs/ruby.md) * [Community Wiki](https://github.com/direnv/direnv/wiki) Make sure to take a look at the wiki! It contains all sorts of useful information such as common recipes, editor integration, tips-and-tricks. ### Man pages * [direnv(1) man page](man/direnv.1.md) * [direnv-fetchurl(1) man page](man/direnv-fetchurl.1.md) * [direnv-stdlib(1) man page](man/direnv-stdlib.1.md) * [direnv.toml(1) man page](man/direnv.toml.1.md) ### FAQ Based on GitHub issues interactions, here are the top things that have been confusing for users: 1. direnv has a standard library of functions, a collection of utilities that I found useful to have and accumulated over the years. You can find it here: https://github.com/direnv/direnv/blob/master/stdlib.sh 2. It's possible to override the stdlib with your own set of function by adding a bash file to `~/.config/direnv/direnvrc`. This file is loaded and its content made available to any `.envrc` file. 3. direnv is not loading the `.envrc` into the current shell. It's creating a new bash sub-process to load the stdlib, direnvrc and `.envrc`, and only exports the environment diff back to the original shell. This allows direnv to record the environment changes accurately and also work with all sorts of shells. It also means that aliases and functions are not exportable right now. ## Contributing Bug reports, contributions and forks are welcome. All bugs or other forms of discussion happen on http://github.com/direnv/direnv/issues . Or drop by on [Matrix](https://matrix.to/#/#direnv:numtide.com) to have a chat. If you ask a question make sure to stay around as not everyone is active all day. ### Testing To run our tests, use these commands: (you may need to install [homebrew](https://brew.sh/)) ``` brew bundle make test ``` ## Complementary projects Here is a list of projects you might want to look into if you are using direnv. * [starship](https://starship.rs/) - A cross-shell prompt. * [Projects for Nix integration](https://github.com/direnv/direnv/wiki/Nix) - choose from one of a variety of projects offering improvements over Direnv's built-in `use_nix` implementation. ## Related projects Here is a list of other projects found in the same design space. Feel free to submit new ones. * [Environment Modules](http://modules.sourceforge.net/) - one of the oldest (in a good way) environment-loading systems * [autoenv](https://github.com/hyperupcall/autoenv) - older, popular, and lightweight. * [zsh-autoenv](https://github.com/Tarrasch/zsh-autoenv) - a feature-rich mixture of autoenv and [smartcd](https://github.com/cxreg/smartcd): enter/leave events, nesting, stashing (Zsh-only). * [asdf](https://github.com/asdf-vm/asdf) - a pure bash solution that has a plugin system. The [asdf-direnv](https://github.com/asdf-community/asdf-direnv) plugin allows using asdf managed tools with direnv. * [ondir](https://github.com/alecthomas/ondir) - OnDir is a small program to automate tasks specific to certain directories * [shadowenv](https://shopify.github.io/shadowenv/) - uses an s-expression format to define environment changes that should be executed * [quickenv](https://github.com/untitaker/quickenv) - an alternative loader for `.envrc` files that does not hook into your shell and favors speed over convenience. * [mise](https://github.com/jdx/mise) - direnv, make and asdf all in one tool. ## Commercial support Looking for help or customization? Get in touch with Numtide to get a quote. We make it easy for companies to work with Open Source projects: ## COPYRIGHT [MIT licence](LICENSE) - Copyright (C) 2019 @zimbatm and [contributors](https://github.com/direnv/direnv/graphs/contributors) direnv-2.37.1/_config.yml000066400000000000000000000011241503714164100152220ustar00rootroot00000000000000theme: jekyll-theme-primer title: direnv description: "unclutter your .profile" url: "https://direnv.net" exclude: - test # see https://github.com/github/pages-gem/blob/754a725e4766d4329bb1dd0e07c638a045ad2c04/lib/github-pages/plugins.rb#L6-L42 plugins: - jemoji - jekyll-avatar - jekyll-default-layout - jekyll-feed - jekyll-mentions - jekyll-readme-index - jekyll-sitemap markdown: CommonMarkGhPages # see https://github.com/gjtorikian/commonmarker#parse-options commonmark: options: - FOOTNOTES - SMART extensions: - autolink - strikethrough - table direnv-2.37.1/_includes/000077500000000000000000000000001503714164100150425ustar00rootroot00000000000000direnv-2.37.1/_includes/head-custom.html000066400000000000000000000001771503714164100201460ustar00rootroot00000000000000 direnv-2.37.1/default.nix000066400000000000000000000014311503714164100152400ustar00rootroot00000000000000{ buildGoApplication, lib, stdenv, bash }: buildGoApplication { pname = "direnv"; version = lib.fileContents ./version.txt; subPackages = [ "." ]; src = ./.; pwd = ./.; modules = ./gomod2nix.toml; # we have no bash at the moment for windows BASH_PATH = lib.optionalString (!stdenv.hostPlatform.isWindows) "${bash}/bin/bash"; # replace the build phase to use the GNUMakefile instead buildPhase = '' ls -la ./vendor make BASH_PATH=$BASH_PATH ''; installPhase = '' echo $GOCACHE make install PREFIX=$out ''; meta = { description = "A shell extension that manages your environment"; homepage = "https://direnv.net"; license = lib.licenses.mit; maintainers = [ lib.maintainers.zimbatm ]; mainProgram = "direnv"; }; } direnv-2.37.1/docs/000077500000000000000000000000001503714164100140255ustar00rootroot00000000000000direnv-2.37.1/docs/development.md000066400000000000000000000012141503714164100166670ustar00rootroot00000000000000# Development Setup a go environment https://golang.org/doc/install > go >= 1.24 is required Clone the project: $ git clone git@github.com:direnv/direnv.git Build by just typing make: $ cd direnv $ make Test the projects: $ make test To install to /usr/local: $ make install Or to a different location like `~/.local`: $ make install PREFIX=~/.local ## Updating gomod2nix.toml Execute `./script/update-gomod2nix`; if you don't have nix locally, can do so via a docker container like so: $ docker run -it --platform linux/amd64 -v $(pwd):/workdir nixos/nix /bin/sh -c "cd workdir && ./script/update-gomod2nix" direnv-2.37.1/docs/github-actions.md000066400000000000000000000106151503714164100172720ustar00rootroot00000000000000# Using direnv with GitHub Actions direnv can be integrated into GitHub Actions workflows to manage environment variables from `.envrc` files. This is particularly useful for: - Maintaining consistent environment variables between local development and CI/CD - Managing secrets and configuration in a familiar way - Reusing existing `.envrc` files in your CI pipeline ## Basic Usage The `direnv export gha` command outputs environment variables in the format required by GitHub Actions. Here's a basic example: ```yaml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install direnv run: | curl -sfL https://direnv.net/install.sh | bash echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Load environment variables run: | direnv allow . direnv export gha > "$GITHUB_ENV" - name: Run tests run: | # Your environment variables from .envrc are now available echo "DATABASE_URL=$DATABASE_URL" make test ``` ## Installation in GitHub Actions There are several ways to install direnv in your GitHub Actions workflow: ### Using the install script (recommended) ```yaml - name: Install direnv run: | curl -sfL https://direnv.net/install.sh | bash echo "$HOME/.local/bin" >> $GITHUB_PATH ``` ### Using package managers On Ubuntu runners: ```yaml - name: Install direnv run: sudo apt-get update && sudo apt-get install -y direnv ``` On macOS runners: ```yaml - name: Install direnv run: brew install direnv ``` ### Using a specific version ```yaml - name: Install direnv run: | wget -O direnv https://github.com/direnv/direnv/releases/download/v2.34.0/direnv.linux-amd64 chmod +x direnv sudo mv direnv /usr/local/bin/ ``` ## Security Considerations 1. **Always use `direnv allow`**: Even in CI, direnv requires explicit permission to load `.envrc` files 2. **Be careful with secrets**: Don't echo or log sensitive environment variables 3. **Use GitHub Actions secrets**: Store sensitive values in GitHub Actions secrets rather than committing them to your repository ## Troubleshooting ### Environment variables not available Make sure you're using `> "$GITHUB_ENV"` to override to the GitHub environment file: ```yaml # Correct direnv export gha > "$GITHUB_ENV" # Incorrect - this just prints to stdout direnv export gha ``` ### Permission denied errors Ensure direnv is executable and in your PATH: ```yaml - name: Install and setup direnv run: | curl -sfL https://direnv.net/install.sh | bash echo "$HOME/.local/bin" >> $GITHUB_PATH # Force PATH update in current step export PATH="$HOME/.local/bin:$PATH" direnv allow . direnv export gha >> "$GITHUB_ENV" ``` ### Debugging To debug issues, you can examine what direnv is doing: ```yaml - name: Debug direnv run: | direnv version direnv status cat .envrc direnv allow . direnv export gha ``` ## Example: Complete Node.js Workflow Here's a complete example for a Node.js project: ```yaml name: Node.js CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x] steps: - uses: actions/checkout@v4 - name: Install direnv run: | curl -sfL https://direnv.net/install.sh | bash echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Load environment run: | # Create .envrc if it doesn't exist if [ ! -f .envrc ]; then cat > .envrc <<'EOF' export NODE_ENV=test export DATABASE_URL="postgresql://localhost/test_db" PATH_add node_modules/.bin EOF fi direnv allow . direnv export gha >> "$GITHUB_ENV" - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Run linter run: npm run lint ``` ## See Also - [GitHub Actions Environment Files](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files) - [direnv Documentation](https://direnv.net) - [direnv-stdlib(1)](../man/direnv-stdlib.1.md) direnv-2.37.1/docs/hook.md000066400000000000000000000044161503714164100153140ustar00rootroot00000000000000# Setup For direnv to work properly it needs to be hooked into the shell. Each shell has its own extension mechanism. Once the hook is configured, restart your shell for direnv to be activated. ## BASH Add the following line at the end of the `~/.bashrc` file: ```sh eval "$(direnv hook bash)" ``` Make sure it appears even after rvm, git-prompt and other shell extensions that manipulate the prompt. ## ZSH Add the following line at the end of the `~/.zshrc` file: ```sh eval "$(direnv hook zsh)" ``` ## Oh my zsh Oh my zsh has [a core plugin with direnv](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/direnv) support. Add direnv to the plugins array in your zshrc file: ```sh plugins=(... direnv) ``` ## FISH Add the following line at the end of the `~/.config/fish/config.fish` file: ```fish direnv hook fish | source ``` Fish supports 3 modes you can set with the global environment variable `direnv_fish_mode`: ```fish set -g direnv_fish_mode eval_on_arrow # trigger direnv at prompt, and on every arrow-based directory change (default) set -g direnv_fish_mode eval_after_arrow # trigger direnv at prompt, and only after arrow-based directory changes before executing command set -g direnv_fish_mode disable_arrow # trigger direnv at prompt only, this is similar functionality to the original behavior ``` ## TCSH Add the following line at the end of the `~/.cshrc` file: ```sh eval `direnv hook tcsh` ``` ## Elvish (0.12+) Run: ``` ~> mkdir -p ~/.config/elvish/lib ~> direnv hook elvish > ~/.config/elvish/lib/direnv.elv ``` and add the following line to your `~/.config/elvish/rc.elv` file: ``` use direnv ``` ## Nushell Add the following hook to your `$env.config.hooks.env_change.PWD` list in `config.nu`: ```nushell { || if (which direnv | is-empty) { return } direnv export json | from json | default {} | load-env } ``` > **Note** > you can follow the [`nu_scripts` of Nushell](https://github.com/nushell/nu_scripts/blob/main/nu-hooks/nu-hooks/direnv/config.nu) > for the always up-to-date version of the hook above ### PowerShell Add the following line to your `$PROFILE`: ```powershell Invoke-Expression "$(direnv hook pwsh)" ``` ## Murex Add the following line to your `~/.murex_profile` file: ```sh direnv hook murex -> source ``` direnv-2.37.1/docs/installation.md000066400000000000000000000032221503714164100170470ustar00rootroot00000000000000# Installation The installation has two parts. 1. Install the package or binary, which is presented in this document 2. [hook into your shell](hook.md). ## From system packages direnv is packaged for a variety of systems: * [Fedora](https://src.fedoraproject.org/rpms/direnv) * [Arch Linux](https://archlinux.org/packages/extra/x86_64/direnv/) * [Debian](https://packages.debian.org/search?keywords=direnv&searchon=names&suite=all§ion=all) * [Gentoo Guru](https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users) * [NetBSD pkgsrc-wip](http://www.pkgsrc.org/wip/) * [NixOS](https://search.nixos.org/options?query=programs.direnv) * [macOS Homebrew](https://formulae.brew.sh/formula/direnv#default) * [openSUSE](https://build.opensuse.org/package/show/openSUSE%3AFactory/direnv) * [MacPorts](https://ports.macports.org/port/direnv/) * [Ubuntu](https://packages.ubuntu.com/search?keywords=direnv&searchon=names&suite=all§ion=all) * [GNU Guix](https://packages.guix.gnu.org/search/?query=direnv) * [Windows](https://learn.microsoft.com/en-us/windows/package-manager/winget/) See also: [![Packaging status](https://repology.org/badge/vertical-allrepos/direnv.svg)](https://repology.org/metapackage/direnv) ## From binary builds To install binary builds you can run this bash installer: ```sh curl -sfL https://direnv.net/install.sh | bash ``` Binary builds for a variety of architectures are also available for [each release](https://github.com/direnv/direnv/releases). Fetch the binary, `chmod +x direnv` and put it somewhere in your `PATH`. ## Compile from source See the [Development](development.md) page. # Next step [hook installation](hook.md) direnv-2.37.1/docs/ruby.md000066400000000000000000000067641503714164100153450ustar00rootroot00000000000000# Manage your rubies with direnv and ruby-install direnv is just a shell extension that manages your environment variables depending on the folder you live in. In this article we will explore how it can be used in combination with [ruby-install](https://github.com/postmodern/ruby-install) to manage and select the version of ruby that you want to use in a project. ## The setup First install direnv. This is the quick version on OSX + Bash: ```bash brew install direnv echo 'eval $(direnv hook bash)' >> .bashrc exec $0 ``` Then use [ruby-install](https://github.com/postmodern/ruby-install) to install a couple of ruby versions. We're also creating a couple of aliases for convenience. ``` brew install ruby-install ruby-install ruby 1.9 ruby-install ruby 2.0 cd ~/.rubies ln -s 1.9.3-p448 1.9.3 ln -s 1.9.3-p448 1.9 ln -s 2.0.0-p247 2.0.0 ln -s 2.0.0-p247 2.0 ``` The end goal is that each project will have an `.envrc` file that contains a descriptive syntax like `use ruby 1.9.3` to selects the right ruby version for the project. For that regard we are going to use a couple of commands available in the [direnv stdlib](/man/direnv-stdlib.1.md) and expand it a bit in the `~/.config/direnv/direnvrc` file. Add this to the `~/.config/direnv/direnvrc` file (you have to create it if it doesn't exist): ```bash # Usage: use ruby # # Loads the specified ruby version into the environment # use_ruby() { local ruby_dir=$HOME/.rubies/$1 load_prefix $ruby_dir layout ruby } ``` That's it. Now in any project you can run `direnv edit .` and add `use ruby 1.9.3` or `use ruby 2.0` in the file like you want and direnv will select the right ruby version when you enter the project's folder. ## A bit of explanation The last part probably needs a bit more explanation. We make use of a couple of commands that are part of the [stdlib](/man/direnv-stdlib.1.md) which is available in the execution context of an envrc. `use` is a command dispatch that's just there to build the `use something something` dsl so that `use ruby ` will translate into `use_ruby `. `load_prefix` will add a couple of things into the environment, notably add `/bin` into the PATH. This is what makes the specified ruby available. And finally `layout ruby` who like `use` translates into the `layout_ruby` function call. It's used to describe common project layouts. In the stdlib, the ruby layout will configure rubygems (with the `GEM_HOME` environment variable) to install all the gems into the .direnv/ruby/RUBY_VERSION folder under the project root. This is a bit similar to rvm's gemsets except that they live inside your project's folder. It also configures bundler to install wrapper shims into the .direnv/bin folder which allows you to invoke the commands directly instead of prefixing your ruby programs with `bundle exec` all the time. ## Conclusion As you see this approach is not restricted to ruby. You could have various versions of python installed under ~/.pythons and a `use_python` defined in your ~/.direnvrc. Or perl, php, ... This is the good thing about direnv, it's not restricted to a single language. Actually, wouldn't it be great to have all your project's dependencies available when you enter the project folder ? Not only your ruby version but also the exact redis or mysql or ... version that you want to use, without having to start a VM. I think that's definitely possible using something like the [Nix package manager](http://nixos.org/nix/), something that still needs to be explored in a future post. direnv-2.37.1/flake.lock000066400000000000000000000046241503714164100150370ustar00rootroot00000000000000{ "nodes": { "flake-utils": { "inputs": { "systems": "systems" }, "locked": { "lastModified": 1694529238, "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", "owner": "numtide", "repo": "flake-utils", "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", "type": "github" }, "original": { "owner": "numtide", "repo": "flake-utils", "type": "github" } }, "gomod2nix": { "inputs": { "flake-utils": "flake-utils", "nixpkgs": [ "nixpkgs" ] }, "locked": { "lastModified": 1750314194, "narHash": "sha256-SjpXWEeB+UIMzuCAF94PuyAXpJdnBLF45JvI6o4wKIU=", "owner": "nix-community", "repo": "gomod2nix", "rev": "a5f75f563748599d448a4a076816041d7b0fc07e", "type": "github" }, "original": { "owner": "nix-community", "repo": "gomod2nix", "type": "github" } }, "nixpkgs": { "locked": { "lastModified": 1751443680, "narHash": "sha256-J9BZlSwB5WVu7YwiHMwIVdUYpdfRckANUweCgLY/Do4=", "owner": "NixOS", "repo": "nixpkgs", "rev": "aa8c0886d56fa6286f7758c6d6e816d8b0527433", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "gomod2nix": "gomod2nix", "nixpkgs": "nixpkgs", "systems": "systems_2" } }, "systems": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } }, "systems_2": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", "owner": "nix-systems", "repo": "default", "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", "type": "github" }, "original": { "owner": "nix-systems", "repo": "default", "type": "github" } } }, "root": "root", "version": 7 } direnv-2.37.1/flake.nix000066400000000000000000000055131503714164100147030ustar00rootroot00000000000000{ description = "A basic gomod2nix flake"; inputs.nixpkgs.url = "github:NixOS/nixpkgs"; inputs.gomod2nix.url = "github:nix-community/gomod2nix"; inputs.gomod2nix.inputs.nixpkgs.follows = "nixpkgs"; inputs.systems.url = "github:nix-systems/default"; outputs = { self, nixpkgs, gomod2nix, systems, }: let eachSystem = f: nixpkgs.lib.genAttrs (import systems) ( system: f rec { callPackage = pkgs.darwin.apple_sdk_11_0.callPackage or pkgs.callPackage; gomod2nixPkgs = gomod2nix.legacyPackages.${system}; inherit system; pkgs = nixpkgs.legacyPackages.${system}; } ); in { packages = eachSystem ({ callPackage, gomod2nixPkgs, ... }: { default = callPackage ./. { inherit (gomod2nixPkgs) buildGoApplication; }; } ); devShells = eachSystem ({ callPackage, gomod2nixPkgs, ... }: { default = callPackage ./shell.nix { inherit (gomod2nixPkgs) mkGoEnv gomod2nix; }; } ); checks = eachSystem ({ pkgs, system, ... }: let sourceFiles = pkgs.lib.fileset.toSource { root = ./.; fileset = pkgs.lib.fileset.unions [ ./go.mod ./go.sum ./GNUmakefile ./stdlib.sh ./version.txt ./README.md (pkgs.lib.fileset.fileFilter (file: file.hasExt "go") ./.) ./test ./internal ./pkg (pkgs.lib.fileset.fileFilter (file: file.name == ".envrc") ./.) ]; }; in { package = self.packages.${system}.default; tests = self.packages.${system}.default.overrideAttrs (old: { src = sourceFiles; nativeBuildInputs = (old.nativeBuildInputs or []) ++ self.devShells.${system}.default.nativeBuildInputs; buildPhase = '' export GOLANGCI_LINT_CACHE=$TMPDIR/golangci-cache export XDG_CACHE_HOME=$TMPDIR/cache export HOME=$TMPDIR/home mkdir -p $GOLANGCI_LINT_CACHE $XDG_CACHE_HOME $HOME # Patch shebangs in test files patchShebangs test/ make test ''; installPhase = '' mkdir -p $out touch $out/tests-passed ''; }); dist = self.packages.${system}.default.overrideAttrs (old: { src = sourceFiles; nativeBuildInputs = (old.nativeBuildInputs or []) ++ self.devShells.${system}.default.nativeBuildInputs; buildPhase = '' make dist ''; installPhase = '' mkdir -p $out cp -r dist/* $out/ ''; }); } ); }; } direnv-2.37.1/go.mod000066400000000000000000000003101503714164100141750ustar00rootroot00000000000000module github.com/direnv/direnv/v2 go 1.24 require ( github.com/BurntSushi/toml v1.5.0 github.com/mattn/go-isatty v0.0.20 golang.org/x/mod v0.25.0 ) require golang.org/x/sys v0.30.0 // indirect direnv-2.37.1/go.sum000066400000000000000000000013311503714164100142260ustar00rootroot00000000000000github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= direnv-2.37.1/gomod2nix.toml000066400000000000000000000007651503714164100157100ustar00rootroot00000000000000schema = 3 [mod] [mod."github.com/BurntSushi/toml"] version = "v1.5.0" hash = "sha256-wX8bEVo7swuuAlm0awTIiV1KNCAXnm7Epzwl+wzyqhw=" [mod."github.com/mattn/go-isatty"] version = "v0.0.20" hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ=" [mod."golang.org/x/mod"] version = "v0.25.0" hash = "sha256-nW75iR1HL00GTFqWGxnJM/bFSa2vg4nlb3I/pFV10UA=" [mod."golang.org/x/sys"] version = "v0.30.0" hash = "sha256-BuhWtwDkciVioc03rxty6G2vcZVnPX85lI7tgQOFVP8=" direnv-2.37.1/gzenv/000077500000000000000000000000001503714164100142265ustar00rootroot00000000000000direnv-2.37.1/gzenv/gzenv.go000066400000000000000000000032711503714164100157110ustar00rootroot00000000000000// Package gzenv implements a compressed environment format using json+gzip+base64. // It provides a quickly designed format to export the whole environment back into itself. package gzenv import ( "bytes" "compress/zlib" "encoding/base64" "encoding/json" "fmt" "log" "io" "strings" ) // Marshal encodes the object into the gzenv format func Marshal(obj interface{}) string { jsonData, err := json.Marshal(obj) if err != nil { panic(fmt.Errorf("marshal(): %w", err)) } zlibData := bytes.NewBuffer([]byte{}) w := zlib.NewWriter(zlibData) // we assume the zlib writer would never fail _, _ = w.Write(jsonData) if err := w.Close(); err != nil { log.Printf("Warning: failed to close zlib writer: %v", err) } base64Data := base64.URLEncoding.EncodeToString(zlibData.Bytes()) return base64Data } // Unmarshal restores the gzenv format back into a Go object func Unmarshal(gzenv string, obj interface{}) error { gzenv = strings.TrimSpace(gzenv) data, err := base64.URLEncoding.DecodeString(gzenv) if err != nil { return fmt.Errorf("unmarshal() base64 decoding: %w", err) } zlibReader := bytes.NewReader(data) w, err := zlib.NewReader(zlibReader) if err != nil { return fmt.Errorf("unmarshal() zlib opening: %w", err) } envData := bytes.NewBuffer([]byte{}) // G110: Potential DoS vulnerability via decompression bomb (gosec) // #nosec _, err = io.Copy(envData, w) if err != nil { return fmt.Errorf("unmarshal() zlib decoding: %w", err) } if err := w.Close(); err != nil { log.Printf("Warning: failed to close zlib reader: %v", err) } err = json.Unmarshal(envData.Bytes(), &obj) if err != nil { return fmt.Errorf("unmarshal() json parsing: %w", err) } return nil } direnv-2.37.1/install.sh000077500000000000000000000052641503714164100151110ustar00rootroot00000000000000#!/usr/bin/env bash # # A good old bash | curl script for direnv. # set -euo pipefail { # Prevent execution if this script was only partially downloaded log() { echo "[installer] $*" >&2 } die() { log "$@" exit 1 } at_exit() { ret=$? if [[ $ret -gt 0 ]]; then log "the script failed with error $ret.\n" \ "\n" \ "To report installation errors, submit an issue to\n" \ " https://github.com/direnv/direnv/issues/new/choose" fi exit "$ret" } trap at_exit EXIT kernel=$(uname -s | tr "[:upper:]" "[:lower:]") case "${kernel}" in mingw*) kernel=windows ;; esac case "$(uname -m)" in x86_64) machine=amd64 ;; i686 | i386) machine=386 ;; armv7l) machine=arm ;; aarch64 | arm64) machine=arm64 ;; *) die "Machine $(uname -m) not supported by the installer.\n" \ "Go to https://direnv for alternate installation methods." ;; esac log "kernel=$kernel machine=$machine" : "${use_sudo:=}" : "${bin_path:=}" if [[ -z "$bin_path" ]]; then log "bin_path is not set, you can set bin_path to specify the installation path" log "e.g. export bin_path=/path/to/installation before installing" log "looking for a writeable path from PATH environment variable" for path in $(echo "$PATH" | tr ':' '\n'); do if [[ -w $path ]]; then bin_path=$path break fi done fi if [[ -z "$bin_path" ]]; then die "did not find a writeable path in $PATH" fi echo "bin_path=$bin_path" if [[ -n "${version:-}" ]]; then release="tags/${version}" else release="latest" fi echo "release=$release" curl_args=( -fL "https://api.github.com/repos/direnv/direnv/releases/$release" ) if [[ -n "${DIRENV_GITHUB_API_TOKEN:-}" ]]; then # note: this doesn't actually print the token value. echo "using DIRENV_GITHUB_API_TOKEN for GitHub API authentication" curl_args+=(-H "Authorization: Bearer ${DIRENV_GITHUB_API_TOKEN}") fi log "looking for a download URL" download_url=$( curl "${curl_args[@]}" \ | grep browser_download_url \ | cut -d '"' -f 4 \ | grep "direnv.$kernel.$machine\$" ) echo "download_url=$download_url" log "downloading" curl -o "$bin_path/direnv" -fL "$download_url" chmod a+x "$bin_path/direnv" cat < 1 { if rcPath, err = filepath.Abs(args[1]); err != nil { return err } if rcPath, err = filepath.EvalSymlinks(rcPath); err != nil { return err } } else { if rcPath, err = os.Getwd(); err != nil { return err } } if _, err = os.Stat(config.AllowDir()); os.IsNotExist(err) { oldAllowDir := filepath.Join(config.ConfDir, "allow") if _, err = os.Stat(oldAllowDir); err == nil { fmt.Println(migrationMessage) fmt.Printf("moving %s to %s\n", oldAllowDir, config.AllowDir()) if err = os.MkdirAll(filepath.Dir(config.AllowDir()), 0755); err != nil { return err } if err = os.Rename(oldAllowDir, config.AllowDir()); err != nil { return err } fmt.Printf("creating a symlink back from %s to %s for back-compat.\n", config.AllowDir(), oldAllowDir) if err = os.Symlink(config.AllowDir(), oldAllowDir); err != nil { return err } fmt.Println("") fmt.Println("All done, have a nice day!") } } rc, err := FindRC(rcPath, config) if err != nil { return err } else if rc == nil { if config.LoadDotenv { return fmt.Errorf(".envrc or .env file not found") } return fmt.Errorf(".envrc file not found") } return rc.Allow() } direnv-2.37.1/internal/cmd/cmd_apply_dump.go000066400000000000000000000016031503714164100210000ustar00rootroot00000000000000package cmd import ( "fmt" "os" ) // CmdApplyDump is `direnv apply_dump FILE` var CmdApplyDump = &Cmd{ Name: "apply_dump", Desc: "Accepts a filename containing `direnv dump` output and generates a series of bash export statements to apply the given env", Args: []string{"FILE"}, Private: true, Action: actionSimple(cmdApplyDumpAction), } func cmdApplyDumpAction(env Env, args []string) (err error) { if len(args) < 2 { return fmt.Errorf("not enough arguments") } if len(args) > 2 { return fmt.Errorf("too many arguments") } filename := args[1] dumped, err := os.ReadFile(filename) if err != nil { return err } dumpedEnv, err := LoadEnv(string(dumped)) if err != nil { return err } diff := env.Diff(dumpedEnv) exports, err := diff.ToShell(Bash) if err != nil { return err } _, err = fmt.Println(exports) if err != nil { return err } return } direnv-2.37.1/internal/cmd/cmd_current.go000066400000000000000000000011571503714164100203140ustar00rootroot00000000000000package cmd import ( "errors" ) // CmdCurrent is `direnv current` var CmdCurrent = &Cmd{ Name: "current", Desc: "Reports whether direnv's view of a file is current (or stale)", Args: []string{"PATH"}, Private: true, Action: actionSimple(cmdCurrentAction), } func cmdCurrentAction(env Env, args []string) (err error) { if len(args) < 2 { err = errors.New("missing PATH argument") return } path := args[1] watches := NewFileTimes() watchString, ok := env[DIRENV_WATCHES] if ok { err = watches.Unmarshal(watchString) if err != nil { return } } err = watches.CheckOne(path) return } direnv-2.37.1/internal/cmd/cmd_deny.go000066400000000000000000000016221503714164100175660ustar00rootroot00000000000000package cmd import ( "fmt" "os" "path/filepath" ) // CmdDeny is `direnv deny [PATH_TO_RC]` var CmdDeny = &Cmd{ Name: "block", Desc: "Revokes the authorization of a given .envrc or .env file.", Args: []string{"[PATH_TO_RC]"}, Aliases: []string{"deny", "disallow", "revoke"}, Action: actionWithConfig(cmdDenyAction), } func cmdDenyAction(_ Env, args []string, config *Config) (err error) { var rcPath string if len(args) > 1 { if rcPath, err = filepath.Abs(args[1]); err != nil { return err } if rcPath, err = filepath.EvalSymlinks(rcPath); err != nil { return err } } else { if rcPath, err = os.Getwd(); err != nil { return } } rc, err := FindRC(rcPath, config) if err != nil { return err } else if rc == nil { if config.LoadDotenv { return fmt.Errorf(".envrc or .env file not found") } return fmt.Errorf(".envrc file not found") } return rc.Deny() } direnv-2.37.1/internal/cmd/cmd_dotenv.go000066400000000000000000000027121503714164100201270ustar00rootroot00000000000000package cmd import ( "fmt" "github.com/direnv/direnv/v2/pkg/dotenv" "os" "path/filepath" ) // CmdDotEnv is `direnv dotenv [SHELL [PATH_TO_DOTENV]]` // Transforms a .env file to evaluatable `export KEY=PAIR` statements. // // See: https://github.com/bkeepers/dotenv and https://github.com/ddollar/foreman var CmdDotEnv = &Cmd{ Name: "dotenv", Desc: "Transforms a .env file to evaluatable `export KEY=PAIR` statements", Args: []string{"[SHELL]", "[PATH_TO_DOTENV]"}, Private: true, Action: actionSimple(cmdDotEnvAction), } func cmdDotEnvAction(_ Env, args []string) (err error) { var shell Shell var newenv Env var target string if len(args) > 1 { shell = DetectShell(args[1]) } else { shell = Bash } if len(args) > 2 { target = args[2] } if target == "" { target = ".env" } var data []byte if data, err = os.ReadFile(target); err != nil { return } // Set PWD env var to the directory the .env file resides in. This results // in the least amount of surprise, as a dotenv file is most often defined // in the same directory it's loaded from, so referring to PWD should match // the directory of the .env file. path, err := filepath.Abs(target) if err != nil { return err } if err := os.Setenv("PWD", filepath.Dir(path)); err != nil { return err } newenv, err = dotenv.Parse(string(data)) if err != nil { return err } str, err := newenv.ToShell(shell) if err != nil { return err } fmt.Println(str) return } direnv-2.37.1/internal/cmd/cmd_dump.go000066400000000000000000000017431503714164100176000ustar00rootroot00000000000000package cmd import ( "fmt" "os" "strconv" ) // CmdDump is `direnv dump` var CmdDump = &Cmd{ Name: "dump", Desc: "Used to export the inner bash state at the end of execution", Args: []string{"[SHELL]", "[FILE]"}, Private: true, Action: actionSimple(cmdDumpAction), } func cmdDumpAction(env Env, args []string) (err error) { target := "gzenv" w := os.Stdout if len(args) > 1 { target = args[1] } var filePath string if len(args) > 2 { filePath = args[2] } else { filePath = os.Getenv(DIRENV_DUMP_FILE_PATH) } if filePath != "" { if num, err := strconv.Atoi(filePath); err == nil { w = os.NewFile(uintptr(num), filePath) } else { w, err = os.OpenFile(filePath, os.O_WRONLY, 0666) if err != nil { return err } } } shell := DetectShell(target) if shell == nil { return fmt.Errorf("unknown target shell '%s'", target) } dumpStr, err := shell.Dump(env) if err != nil { return err } _, err = fmt.Fprintln(w, dumpStr) return } direnv-2.37.1/internal/cmd/cmd_edit.go000066400000000000000000000043611503714164100175570ustar00rootroot00000000000000package cmd import ( "fmt" "log" "os" "os/exec" "path/filepath" "strings" ) // CmdEdit is `direnv edit [PATH_TO_RC]` var CmdEdit = &Cmd{ Name: "edit", Desc: `Opens PATH_TO_RC or the current .envrc or .env into an $EDITOR and allow the file to be loaded afterwards.`, Args: []string{"[PATH_TO_RC]"}, Action: actionWithConfig(cmdEditAction), } func cmdEditAction(env Env, args []string, config *Config) (err error) { var rcPath string var times *FileTimes var foundRC *RC defer log.SetPrefix(log.Prefix()) log.SetPrefix(log.Prefix() + "cmd_edit: ") foundRC, err = config.FindRC() if err != nil { return err } if foundRC != nil { times = &foundRC.times } if len(args) > 1 { rcPath = args[1] fi, _ := os.Stat(rcPath) if fi != nil && fi.IsDir() { rcPath = filepath.Join(rcPath, ".envrc") } } else { if foundRC == nil { return fmt.Errorf(".envrc or .env not found. Use `direnv edit .` to create a new .envrc in the current directory") } rcPath = foundRC.path } editor := env["EDITOR"] if editor == "" { logError(config, "$EDITOR not found.") editor = detectEditor(env["PATH"]) if editor == "" { err = fmt.Errorf("could not find a default editor in the PATH") return } } run := fmt.Sprintf("%s %s", editor, BashEscape(rcPath)) // G204: Subprocess launched with function call as argument or cmd arguments // #nosec cmd := exec.Command(config.BashPath, "-c", run) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err = cmd.Run(); err != nil { return } foundRC, err = FindRC(rcPath, config) logDebug("foundRC: %#v", foundRC) logDebug("times: %#v", times) if times != nil { logDebug("times.Check(): %#v", times.Check()) } if err == nil && foundRC != nil && (times == nil || times.Check() != nil) { err = foundRC.Allow() } return } // Utils // Editors contains a list of known editors and how to start them. var Editors = [][]string{ {"editor"}, {"subl", "-w"}, {"mate", "-w"}, {"open", "-t", "-W"}, // Opens with the default text editor on mac {"nano"}, {"vim"}, {"emacs"}, } func detectEditor(pathenv string) string { for _, editor := range Editors { if _, err := lookPath(editor[0], pathenv); err == nil { return strings.Join(editor, " ") } } return "" } direnv-2.37.1/internal/cmd/cmd_exec.go000066400000000000000000000027251503714164100175600ustar00rootroot00000000000000package cmd import ( "fmt" "os" "path/filepath" "syscall" ) // CmdExec is `direnv exec DIR ...` var CmdExec = &Cmd{ Name: "exec", Desc: "Executes a command after loading the first .envrc or .env found in DIR", Args: []string{"DIR", "COMMAND", "[...ARGS]"}, Action: actionWithConfig(cmdExecAction), } func cmdExecAction(env Env, args []string, config *Config) (err error) { var ( newEnv Env previousEnv Env rcPath string command string ) if len(args) < 2 { return fmt.Errorf("missing DIR and COMMAND arguments") } rcPath = filepath.Clean(args[1]) fi, err := os.Stat(rcPath) if err != nil { return } if fi.IsDir() { if len(args) < 3 { return fmt.Errorf("missing COMMAND argument") } command = args[2] args = args[2:] } else { command = rcPath rcPath = filepath.Dir(rcPath) args = args[1:] } // Restore pristine environment if needed if previousEnv, err = config.Revert(env); err != nil { return } previousEnv.CleanContext() // Load the rc if toLoad := findEnvUp(rcPath, config.LoadDotenv); toLoad != "" { if newEnv, err = config.EnvFromRC(toLoad, previousEnv); err != nil { return } } else { newEnv = previousEnv } var commandPath string commandPath, err = lookPath(command, newEnv["PATH"]) if err != nil { err = fmt.Errorf("command '%s' not found on PATH '%s'", command, newEnv["PATH"]) return } // #nosec G204 err = syscall.Exec(commandPath, args, newEnv.ToGoEnv()) return } direnv-2.37.1/internal/cmd/cmd_export.go000066400000000000000000000063501503714164100201530ustar00rootroot00000000000000package cmd import ( "fmt" "log" "sort" "strings" ) func supportedShellFormattedString() string { res := "[" for k := range supportedShellList { res += k + ", " } res = strings.TrimSuffix(res, ", ") res += "]" return res } // CmdExport is `direnv export $0` var CmdExport = &Cmd{ Name: "export", Desc: `Loads an .envrc or .env and prints the diff in terms of exports. Supported SHELL values are: ` + supportedShellFormattedString(), Args: []string{"SHELL"}, Private: false, Action: cmdWithWarnTimeout(actionWithConfig(exportCommand)), } func exportCommand(currentEnv Env, args []string, config *Config) (err error) { defer log.SetPrefix(log.Prefix()) log.SetPrefix(log.Prefix() + "export:") logDebug("start") var target string if len(args) > 1 { target = args[1] } shell := DetectShell(target) if shell == nil { return fmt.Errorf("unknown target shell '%s'", target) } logDebug("loading RCs") loadedRC := config.LoadedRC() toLoad := findEnvUp(config.WorkDir, config.LoadDotenv) if loadedRC == nil && toLoad == "" { return } logDebug("updating RC") log.SetPrefix(log.Prefix() + "update:") logDebug("Determining action:") logDebug("toLoad: %#v", toLoad) logDebug("loadedRC: %#v", loadedRC) switch { case toLoad == "": logDebug("no RC found, unloading") case loadedRC == nil: logDebug("no RC (implies no DIRENV_DIFF),loading") case loadedRC.path != toLoad: logDebug("new RC, loading") case loadedRC.times.Check() != nil: logDebug("file changed, reloading") default: logDebug("no update needed") return } var previousEnv, newEnv Env if previousEnv, err = config.Revert(currentEnv); err != nil { err = fmt.Errorf("Revert() failed: %w", err) logDebug("err: %v", err) return } if toLoad == "" { logStatus(config, "unloading") newEnv = previousEnv.Copy() newEnv.CleanContext() } else { newEnv, err = config.EnvFromRC(toLoad, previousEnv) if err != nil { logDebug("err: %v", err) // If loading fails, fall through and deliver a diff anyway, // but still exit with an error. This prevents retrying on // every prompt. } if newEnv == nil { // unless of course, the error was in hashing and timestamp loading, // in which case we have to abort because we don't know what timestamp // to put in the diff! return } } if out := diffStatus(previousEnv.Diff(newEnv)); out != "" && !config.HideEnvDiff { logStatus(config, "export %s", out) } diffString, diffErr := currentEnv.Diff(newEnv).ToShell(shell) if diffErr != nil { return fmt.Errorf("ToShell() failed: %w", diffErr) } logDebug("env diff %s", diffString) fmt.Print(diffString) return } // Return a string of +/-/~ indicators of an environment diff func diffStatus(oldDiff *EnvDiff) string { if oldDiff.Any() { var out []string for key := range oldDiff.Prev { _, ok := oldDiff.Next[key] if !ok && !direnvKey(key) { out = append(out, "-"+key) } } for key := range oldDiff.Next { _, ok := oldDiff.Prev[key] if direnvKey(key) { continue } if ok { out = append(out, "~"+key) } else { out = append(out, "+"+key) } } sort.Strings(out) return strings.Join(out, " ") } return "" } func direnvKey(key string) bool { return strings.HasPrefix(key, "DIRENV_") } direnv-2.37.1/internal/cmd/cmd_fetchurl.go000066400000000000000000000076541503714164100204560ustar00rootroot00000000000000package cmd import ( "fmt" "io" "log" "net/http" "os" "path/filepath" "strings" "github.com/direnv/direnv/v2/pkg/sri" "github.com/mattn/go-isatty" ) // CmdFetchURL is `direnv fetchurl []` var CmdFetchURL = &Cmd{ Name: "fetchurl", Desc: "Fetches a given URL into direnv's CAS", Args: []string{"", "[]"}, Action: actionWithConfig(cmdFetchURL), } func cmdFetchURL(_ Env, args []string, config *Config) (err error) { if len(args) < 2 { return fmt.Errorf("missing URL argument") } var ( algo = sri.SHA256 url string integrityHash string ) casDir := casDir(config) isTTY := isatty.IsTerminal(os.Stdout.Fd()) url = args[1] // Validate the SRI hash if it exists if len(args) > 2 { // Support Base64 where '/' have been replaced by '_' integrityHash = strings.ReplaceAll(args[2], "_", "/") hash, err := sri.Parse(integrityHash) if err != nil { return err } // Shortcut if the cache already has the file casFile := casPath(casDir, hash) if fileExists(casFile) { fmt.Println(casFile) return nil } } // Create the CAS directory if it doesn't exist if err = os.MkdirAll(casDir, os.FileMode(0755)); err != nil { return err } // Create a temporary file to copy the content into, before the CAS file // location can be calculated. tmpfile, err := os.CreateTemp(casDir, "tmp") if err != nil { return err } defer func() { if err := os.Remove(tmpfile.Name()); err != nil { log.Printf("Warning: failed to remove temp file %s: %v", tmpfile.Name(), err) } }() // clean up defer func() { if err := tmpfile.Close(); err != nil { log.Printf("Warning: failed to close temp file: %v", err) } }() // Get the URL // G107: Potential HTTP request made with variable url // #nosec resp, err := http.Get(url) if err != nil { return err } defer func() { if err := resp.Body.Close(); err != nil { log.Printf("Warning: failed to close response body: %v", err) } }() // Abort if we don't get a 200 back if resp.StatusCode != 200 { return fmt.Errorf("expected status code 200 but got %d", resp.StatusCode) } // While copying the content into the temporary location, also calculate the // SRI hash. w := sri.NewWriter(tmpfile, algo) if _, err = io.Copy(w, resp.Body); err != nil { return err } // Here is the new SRI hash calculatedHash := w.Sum() // Make the file read-only and executable for later if err = os.Chmod(tmpfile.Name(), os.FileMode(0500)); err != nil { return err } // Validate if a comparison hash was given if integrityHash != "" && calculatedHash.String() != integrityHash { return fmt.Errorf("hash mismatch. Expected '%s' but got '%s'", integrityHash, calculatedHash.String()) } // Derive the CAS file location from the SRI hash casFile := casPath(casDir, calculatedHash) // Put the file into the CAS store if it's not already there if !fileExists(casFile) { err = tmpfile.Close() if err != nil { return err } // Move the temporary file to the CAS location. if err = os.Rename(tmpfile.Name(), casFile); err != nil { return err } } if integrityHash == "" { if isTTY { // Print an example for terminal users fmt.Printf(`Found hash: %s Invoke fetchurl again with the hash as an argument to get the disk location: direnv fetchurl "%s" "%s" #=> %s `, calculatedHash, url, calculatedHash.String(), casFile) } else { // Only print the hash in scripting mode. Add one extra hurdle on // purpose to use fetchurl without the SRI hash. _, err = fmt.Println(calculatedHash) } } else { // Print the location to the CAS file _, err = fmt.Println(casFile) } return err } func casDir(c *Config) string { return filepath.Join(c.CacheDir, "cas") } // casPath returns filesystem path for SRI hashes func casPath(dir string, integrityHash *sri.Hash) string { // Use Hex encoding for the filesystem to avoid issues sriFile := integrityHash.Hex() return filepath.Join(dir, sriFile) } direnv-2.37.1/internal/cmd/cmd_help.go000066400000000000000000000016601503714164100175610ustar00rootroot00000000000000package cmd import ( "fmt" "strings" ) // CmdHelp is `direnv help` var CmdHelp = &Cmd{ Name: "help", Desc: "Shows this help", Args: []string{"[SHOW_PRIVATE]"}, Aliases: []string{"--help"}, Action: actionSimple(func(_ Env, args []string) (err error) { var showPrivate = len(args) > 1 fmt.Printf(`direnv v%s Usage: direnv COMMAND [...ARGS] Available commands ------------------ `, version) for _, cmd := range CmdList { var opts string if len(cmd.Args) > 0 { opts = " " + strings.Join(cmd.Args, " ") } if cmd.Private { if showPrivate { fmt.Printf("*%s%s:\n %s\n", cmd.Name, opts, cmd.Desc) } } else { fmt.Printf("%s%s:\n", cmd.Name, opts) for _, alias := range cmd.Aliases { if alias[0:1] != "-" { fmt.Printf("%s%s:\n", alias, opts) } } fmt.Printf(" %s\n", cmd.Desc) } } if showPrivate { fmt.Println("* = private commands") } return }), } direnv-2.37.1/internal/cmd/cmd_hook.go000066400000000000000000000020651503714164100175710ustar00rootroot00000000000000package cmd import ( "fmt" "os" "strings" "text/template" ) // HookContext are the variables available during hook template evaluation type HookContext struct { // SelfPath is the unescaped absolute path to direnv SelfPath string } // CmdHook is `direnv hook $0` var CmdHook = &Cmd{ Name: "hook", Desc: "Used to setup the shell hook", Args: []string{"SHELL"}, Action: actionSimple(cmdHookAction), } func cmdHookAction(_ Env, args []string) (err error) { var target string if len(args) > 1 { target = args[1] } selfPath, err := os.Executable() if err != nil { return err } // Convert Windows path if needed selfPath = strings.ReplaceAll(selfPath, "\\", "/") ctx := HookContext{selfPath} shell := DetectShell(target) if shell == nil { return fmt.Errorf("unknown target shell '%s'", target) } hookStr, err := shell.Hook() if err != nil { return err } hookTemplate, err := template.New("hook").Parse(hookStr) if err != nil { return err } err = hookTemplate.Execute(os.Stdout, ctx) if err != nil { return err } return } direnv-2.37.1/internal/cmd/cmd_log.go000066400000000000000000000011631503714164100174100ustar00rootroot00000000000000package cmd import ( "errors" "fmt" ) // CmdLog is `direnv log [--status | --error] ` var CmdLog = &Cmd{ Name: "log", Desc: "Logs a given message", Args: []string{"[--status | --error]", ""}, Action: actionWithConfig(cmdLog), } func cmdLog(_ Env, args []string, c *Config) (err error) { if len(args) != 3 { return errors.New("invalid arguments") } logType := args[1] message := args[2] switch logType { case "--status", "-status": logStatus(c, message) case "--error", "-error": logError(c, message) default: return fmt.Errorf("invalid log-type '%s'", logType) } return nil } direnv-2.37.1/internal/cmd/cmd_prune.go000066400000000000000000000021021503714164100177520ustar00rootroot00000000000000package cmd import ( "log" "os" "path" "strings" ) // CmdPrune is `direnv prune` var CmdPrune = &Cmd{ Name: "prune", Desc: "Removes old allowed files", Action: actionWithConfig(cmdPruneAction), } func cmdPruneAction(_ Env, _ []string, config *Config) (err error) { var dir *os.File var fi os.FileInfo var dirList []string var envrc []byte allowed := config.AllowDir() if dir, err = os.Open(allowed); err != nil { return err } defer func() { if err := dir.Close(); err != nil { log.Printf("Warning: failed to close directory: %v", err) } }() if dirList, err = dir.Readdirnames(0); err != nil { return err } for _, hash := range dirList { filename := path.Join(allowed, hash) if fi, err = os.Stat(filename); err != nil { return err } if !fi.IsDir() { if envrc, err = os.ReadFile(filename); err != nil { return err } envrcStr := strings.TrimSpace(string(envrc)) // skip old files, w/o path inside if envrcStr == "" { continue } if !fileExists(envrcStr) { _ = os.Remove(filename) } } } return nil } direnv-2.37.1/internal/cmd/cmd_reload.go000066400000000000000000000005741503714164100201020ustar00rootroot00000000000000package cmd import ( "fmt" ) // CmdReload is `direnv reload` var CmdReload = &Cmd{ Name: "reload", Desc: "Triggers an env reload", Action: actionWithConfig(func(_ Env, _ []string, config *Config) error { foundRC, err := config.FindRC() if err != nil { return err } if foundRC == nil { return fmt.Errorf(".envrc not found") } return foundRC.Touch() }), } direnv-2.37.1/internal/cmd/cmd_show_dump.go000066400000000000000000000011651503714164100206360ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "os" "github.com/direnv/direnv/v2/gzenv" ) // CmdShowDump is `direnv show_dump` var CmdShowDump = &Cmd{ Name: "show_dump", Desc: "Show the data inside of a dump for debugging purposes", Args: []string{"DUMP"}, Private: true, Action: actionSimple(cmdShowDumpAction), } func cmdShowDumpAction(_ Env, args []string) (err error) { if len(args) < 2 { return fmt.Errorf("missing DUMP argument") } var f interface{} err = gzenv.Unmarshal(args[1], &f) if err != nil { return err } e := json.NewEncoder(os.Stdout) e.SetIndent("", " ") return e.Encode(f) } direnv-2.37.1/internal/cmd/cmd_status.go000066400000000000000000000045531503714164100201600ustar00rootroot00000000000000package cmd import ( "encoding/json" "fmt" "path/filepath" ) // CmdStatus is `direnv status` var CmdStatus = &Cmd{ Name: "status", Desc: "Prints some debug status information", Args: []string{"[--json]"}, Action: actionWithConfig(func(_ Env, args []string, config *Config) error { if len(args) > 1 && (args[1] == "-json" || args[1] == "--json") { loadedRC := config.LoadedRC() foundRC, err := config.FindRC() if err != nil { return err } jsonOutput := map[string]interface{}{ "config": map[string]string{ "SelfPath": config.SelfPath, "ConfigDir": config.ConfDir, }, "state": map[string]interface{}{}, } if loadedRC != nil { jsonOutput["state"].(map[string]interface{})["loadedRC"] = map[string]interface{}{ "path": loadedRC.path, "allowed": loadedRC.Allowed(), } } else { jsonOutput["state"].(map[string]interface{})["loadedRC"] = nil } if foundRC != nil { jsonOutput["state"].(map[string]interface{})["foundRC"] = map[string]interface{}{ "path": foundRC.path, "allowed": foundRC.Allowed(), } } else { jsonOutput["state"].(map[string]interface{})["foundRC"] = nil } jsonBytes, err := json.MarshalIndent(jsonOutput, "", " ") if err != nil { fmt.Println(err) return nil } fmt.Println(string(jsonBytes)) } else { fmt.Println("direnv exec path", config.SelfPath) fmt.Println("DIRENV_CONFIG", config.ConfDir) fmt.Println("bash_path", config.BashPath) fmt.Println("disable_stdin", config.DisableStdin) fmt.Println("warn_timeout", config.WarnTimeout) fmt.Println("whitelist.prefix", config.WhitelistPrefix) fmt.Println("whitelist.exact", config.WhitelistExact) loadedRC := config.LoadedRC() foundRC, err := config.FindRC() if err != nil { return err } if loadedRC != nil { formatRC("Loaded", loadedRC) } else { fmt.Println("No .envrc or .env loaded") } if foundRC != nil { formatRC("Found", foundRC) } else { fmt.Println("No .envrc or .env found") } } return nil }), } func formatRC(desc string, rc *RC) { workDir := filepath.Dir(rc.path) fmt.Println(desc, "RC path", rc.path) for idx := range *(rc.times.list) { fmt.Println(desc, "watch:", (*rc.times.list)[idx].Formatted(workDir)) } fmt.Println(desc, "RC allowed", rc.Allowed()) fmt.Println(desc, "RC allowPath", rc.allowPath) } direnv-2.37.1/internal/cmd/cmd_stdlib.go000066400000000000000000000004561503714164100201140ustar00rootroot00000000000000package cmd import ( "fmt" ) // CmdStdlib is `direnv stdlib` var CmdStdlib = &Cmd{ Name: "stdlib", Desc: "Displays the stdlib available in the .envrc execution context", Action: actionWithConfig(func(_ Env, _ []string, config *Config) error { fmt.Println(getStdlib(config)) return nil }), } direnv-2.37.1/internal/cmd/cmd_version.go000066400000000000000000000016431503714164100203170ustar00rootroot00000000000000package cmd import ( "fmt" "strings" "golang.org/x/mod/semver" ) // CmdVersion is `direnv version` var CmdVersion = &Cmd{ Name: "version", Desc: "prints the version or checks that direnv is older than VERSION_AT_LEAST.", Args: []string{"[VERSION_AT_LEAST]"}, Aliases: []string{"--version"}, Action: actionSimple(func(_ Env, args []string) error { semVersion := ensureVPrefixed(version) if len(args) > 1 { atLeast := ensureVPrefixed(args[1]) if !semver.IsValid(atLeast) { return fmt.Errorf("%s is not a valid semver version", atLeast) } cmp := semver.Compare(semVersion, atLeast) if cmp < 0 { return fmt.Errorf("current version %s is older than the desired version %s", semVersion, atLeast) } } else { fmt.Println(version) } return nil }), } func ensureVPrefixed(version string) string { if !strings.HasPrefix(version, "v") { return "v" + version } return version } direnv-2.37.1/internal/cmd/cmd_version_test.go000066400000000000000000000006501503714164100213530ustar00rootroot00000000000000package cmd import ( "golang.org/x/mod/semver" "os" "strings" "testing" ) func TestVersionDotTxt(t *testing.T) { bs, err := os.ReadFile("../../version.txt") if err != nil { t.Fatalf("failed to read ../../version.txt: %v", err) } version = strings.TrimSpace(string(bs)) if !semver.IsValid(ensureVPrefixed(string(version))) { t.Fatalf(`version.txt does not contain a valid semantic version: %q`, version) } } direnv-2.37.1/internal/cmd/cmd_watch.go000066400000000000000000000021111503714164100177270ustar00rootroot00000000000000package cmd import ( "fmt" "os" ) // CmdWatch is `direnv watch SHELL [PATH...]` var CmdWatch = &Cmd{ Name: "watch", Desc: "Adds a path to the list that direnv watches for changes", Args: []string{"SHELL", "PATH..."}, Private: true, Action: actionSimple(cmdWatchAction), } func cmdWatchAction(env Env, args []string) (err error) { var shellName string if len(args) < 2 { return fmt.Errorf("a path is required to add to the list of watches") } if len(args) >= 2 { shellName = args[1] } else { shellName = "bash" } shell := DetectShell(shellName) if shell == nil { return fmt.Errorf("unknown target shell '%s'", shellName) } watches := NewFileTimes() watchString, ok := env[DIRENV_WATCHES] if ok { err = watches.Unmarshal(watchString) if err != nil { return } } for _, arg := range args[2:] { err = watches.Update(arg) if err != nil { return } } e := make(ShellExport) e.Add(DIRENV_WATCHES, watches.Marshal()) exportStr, err := shell.Export(e) if err != nil { return err } _, _ = os.Stdout.WriteString(exportStr) return } direnv-2.37.1/internal/cmd/cmd_watch_dir.go000066400000000000000000000025331503714164100205750ustar00rootroot00000000000000package cmd import ( "fmt" "os" "path/filepath" ) // CmdWatchDir is `direnv watch-dir SHELL PATH` var CmdWatchDir = &Cmd{ Name: "watch-dir", Desc: "Recursively adds a directory to the list that direnv watches for changes", Args: []string{"SHELL", "DIR"}, Private: true, Action: actionSimple(watchDirCommand), } func watchDirCommand(env Env, args []string) (err error) { if len(args) < 3 { return fmt.Errorf("a directory is required to add to the list of watches") } shellName := args[1] dir := args[2] shell := DetectShell(shellName) if shell == nil { return fmt.Errorf("unknown target shell '%s'", shellName) } if _, err := os.Stat(dir); os.IsNotExist(err) { return fmt.Errorf("dir '%s' does not exist", dir) } watches := NewFileTimes() watchString, ok := env[DIRENV_WATCHES] if ok { err = watches.Unmarshal(watchString) if err != nil { return err } } err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } return watches.NewTime(path, info.ModTime().Unix(), true) }) if err != nil { return fmt.Errorf("failed to recursively watch dir '%s': %w", dir, err) } e := make(ShellExport) e.Add(DIRENV_WATCHES, watches.Marshal()) exportStr, err := shell.Export(e) if err != nil { return err } _, _ = os.Stdout.WriteString(exportStr) return } direnv-2.37.1/internal/cmd/cmd_watch_list.go000066400000000000000000000031401503714164100207650ustar00rootroot00000000000000package cmd import ( "bufio" "errors" "fmt" "io" "os" "strconv" "strings" ) // CmdWatchList is `direnv watch-list` var CmdWatchList = &Cmd{ Name: "watch-list", Desc: "Pipe pairs of `mtime path` to stdin to build a list of files to watch.", Args: []string{"[SHELL]"}, Private: true, Action: actionSimple(watchListCommand), } func watchListCommand(env Env, args []string) (err error) { var shellName string if len(args) >= 2 { shellName = args[1] } else { shellName = "bash" } shell := DetectShell(shellName) if shell == nil { return fmt.Errorf("unknown target shell '%s'", shellName) } watches := NewFileTimes() watchString, ok := env[DIRENV_WATCHES] if ok { err = watches.Unmarshal(watchString) if err != nil { return err } } // Read `mtime path` lines from stdin reader := bufio.NewReader(os.Stdin) i := 1 for { line, err := reader.ReadString('\n') if err == nil { elems := strings.SplitN(line, " ", 2) if len(elems) != 2 { return fmt.Errorf("line %d: expected to contain two elements", i) } mtime, err := strconv.Atoi(elems[0]) if err != nil { return fmt.Errorf("line %d: %w", i, err) } path := elems[1][:len(elems[1])-1] // add to watches err = watches.NewTime(path, int64(mtime), true) if err != nil { return err } } else if errors.Is(err, io.EOF) { break } else { return fmt.Errorf("line %d: %w", i, err) } i++ } e := make(ShellExport) e.Add(DIRENV_WATCHES, watches.Marshal()) exportStr, err := shell.Export(e) if err != nil { return err } _, _ = os.Stdout.WriteString(exportStr) return } direnv-2.37.1/internal/cmd/cmd_watch_print.go000066400000000000000000000012131503714164100211450ustar00rootroot00000000000000package cmd import ( "fmt" ) // CmdWatchPrint is `direnv watch-print` var CmdWatchPrint = &Cmd{ Name: "watch-print", Desc: "prints the watched paths", Args: []string{"[--null]"}, Private: true, Action: actionSimple(cmdWatchPrintAction), } func cmdWatchPrintAction(env Env, args []string) (err error) { watches := NewFileTimes() watchString, ok := env[DIRENV_WATCHES] separator := '\n' if len(args) > 1 && args[1] == "--null" { separator = 0 } if ok { err = watches.Unmarshal(watchString) if err != nil { return } } for _, watch := range *watches.list { fmt.Printf("%s%c", watch.Path, separator) } return } direnv-2.37.1/internal/cmd/commands.go000066400000000000000000000046711503714164100176140ustar00rootroot00000000000000package cmd import ( "fmt" "strings" "time" ) type actionSimple func(env Env, args []string) error func (fn actionSimple) Call(env Env, args []string, _ *Config) error { return fn(env, args) } type actionWithConfig func(env Env, args []string, config *Config) error func (fn actionWithConfig) Call(env Env, args []string, config *Config) error { var err error if config == nil { config, err = LoadConfig(env) if err != nil { return err } } return fn(env, args, config) } type action interface { Call(env Env, args []string, config *Config) error } // Cmd represents a direnv sub-command type Cmd struct { Name string Desc string Args []string Aliases []string Private bool Action action } // CmdList contains the list of all direnv sub-commands var CmdList []*Cmd func init() { CmdList = []*Cmd{ CmdAllow, CmdApplyDump, CmdShowDump, CmdDeny, CmdDotEnv, CmdDump, CmdEdit, CmdExec, CmdExport, CmdFetchURL, CmdHelp, CmdHook, CmdPrune, CmdReload, CmdStatus, CmdStdlib, CmdVersion, CmdWatch, CmdWatchDir, CmdWatchList, CmdWatchPrint, CmdCurrent, CmdLog, } } func cmdWithWarnTimeout(fn action) action { return actionWithConfig(func(env Env, args []string, config *Config) (err error) { // Disable warning if WarnTimeout is <= 0 if config.WarnTimeout <= 0 { return fn.Call(env, args, config) } done := make(chan bool, 1) go func() { select { case <-done: return case <-time.After(config.WarnTimeout): logError(config, "(%v) is taking a while to execute. Use CTRL-C to give up.", args) } }() err = fn.Call(env, args, config) done <- true return err }) } // CommandsDispatch is called by the main() function to dispatch to a sub-command func CommandsDispatch(env Env, args []string) error { var command *Cmd var commandName string var commandPrefix string var commandArgs []string if len(args) < 2 { commandName = "help" commandPrefix = args[0] commandArgs = []string{} } else { commandName = args[1] commandPrefix = strings.Join(args[0:2], " ") commandArgs = append([]string{commandPrefix}, args[2:]...) } for _, cmd := range CmdList { if cmd.Name == commandName { command = cmd break } for _, alias := range cmd.Aliases { if alias == commandName { command = cmd } } } if command == nil { return fmt.Errorf("command \"%s\" not found", commandPrefix) } return command.Action.Call(env, commandArgs, nil) } direnv-2.37.1/internal/cmd/config.go000066400000000000000000000171301503714164100172520ustar00rootroot00000000000000package cmd import ( "fmt" "os" "os/exec" "path/filepath" "regexp" "strings" "time" toml "github.com/BurntSushi/toml" "github.com/direnv/direnv/v2/xdg" ) // Config represents the direnv configuration and state. type Config struct { Env Env WorkDir string // Current directory ConfDir string CacheDir string DataDir string SelfPath string BashPath string RCFile string TomlPath string HideEnvDiff bool DisableStdin bool StrictEnv bool LoadDotenv bool LogFormat string LogFilter *regexp.Regexp LogColor bool WarnTimeout time.Duration WhitelistPrefix []string WhitelistExact map[string]bool } type tomlDuration struct { time.Duration } func (d *tomlDuration) UnmarshalText(text []byte) error { var err error d.Duration, err = time.ParseDuration(string(text)) return err } type tomlConfig struct { *tomlGlobal // For backward-compatibility Global *tomlGlobal `toml:"global"` Whitelist tomlWhitelist `toml:"whitelist"` } type tomlGlobal struct { BashPath string `toml:"bash_path"` DisableStdin bool `toml:"disable_stdin"` StrictEnv bool `toml:"strict_env"` SkipDotenv bool `toml:"skip_dotenv"` // deprecated, use load_dotenv LoadDotenv bool `toml:"load_dotenv"` WarnTimeout *tomlDuration `toml:"warn_timeout"` HideEnvDiff bool `toml:"hide_env_diff"` LogFormat string `toml:"log_format"` LogFilter string `toml:"log_filter"` } type tomlWhitelist struct { Prefix []string `toml:"prefix"` Exact []string `toml:"exact"` } // Expand a path string prefixed with ~/ to the current user's home directory. // Example: if current user is user1 with home directory in /home/user1, then // ~/project -> /home/user1/project // It's useful to allow paths with ~/, so that direnv.toml can be reused via // dotfiles repos across systems with different standard home paths // (compare Linux /home and macOS /Users). func expandTildePath(path string) (pathExpanded string) { pathExpanded = path if strings.HasPrefix(path, "~/") { if homedir, homedirErr := os.UserHomeDir(); homedirErr == nil { pathExpanded = filepath.Join(homedir, path[2:]) } } return pathExpanded } // LoadConfig opens up the direnv configuration from the Env. func LoadConfig(env Env) (config *Config, err error) { config = &Config{ Env: env, } config.ConfDir = env[DIRENV_CONFIG] if config.ConfDir == "" { config.ConfDir = xdg.ConfigDir(env, "direnv") } if config.ConfDir == "" { err = fmt.Errorf("couldn't find a configuration directory for direnv") return } var exePath string if exePath, err = os.Executable(); err != nil { err = fmt.Errorf("LoadConfig() os.Executable() failed: %w", err) return } // Fix for mingsys exePath = strings.ReplaceAll(exePath, "\\", "/") config.SelfPath = exePath var wdErr error if config.WorkDir, wdErr = os.Getwd(); wdErr != nil { // handled by `findEnvUp` return //nolint:nilnesserr } // Default Warn Timeout config.WarnTimeout = 5 * time.Second // Default log format config.LogFormat = defaultLogFormat config.RCFile = env[DIRENV_FILE] config.WhitelistPrefix = make([]string, 0) config.WhitelistExact = make(map[string]bool) // Load the TOML config config.TomlPath = filepath.Join(config.ConfDir, "direnv.toml") if _, statErr := os.Stat(config.TomlPath); statErr != nil { config.TomlPath = filepath.Join(config.ConfDir, "config.toml") if _, statErr := os.Stat(config.TomlPath); statErr != nil { config.TomlPath = "" } } if config.TomlPath != "" { // Declare global once and then share it between the top-level and Global // keys. The goal here is to let the decoder fill global regardless of if // the values are in the [global] section or not. The reason we do that is // to keep backward-compatibility with the old top-level notation. var global tomlGlobal tomlConf := tomlConfig{ tomlGlobal: &global, Global: &global, } if _, err = toml.DecodeFile(config.TomlPath, &tomlConf); err != nil { err = fmt.Errorf("LoadConfig() failed to parse %s: %w", config.TomlPath, err) return } config.LogColor = os.Getenv("TERM") != "dumb" format, ok := env["DIRENV_LOG_FORMAT"] if ok { config.LogFormat = format } else if global.LogFormat != "" { config.LogFormat = global.LogFormat } if global.LogFilter != "" { filterRegexp, err := regexp.Compile(global.LogFilter) if err != nil { err = fmt.Errorf("error in log filter: %w", err) return nil, err } config.LogFilter = filterRegexp } config.HideEnvDiff = tomlConf.HideEnvDiff for _, path := range tomlConf.Whitelist.Prefix { config.WhitelistPrefix = append(config.WhitelistPrefix, expandTildePath(path)) } for _, path := range tomlConf.Whitelist.Exact { if !strings.HasSuffix(path, "/.envrc") && !strings.HasSuffix(path, "/.env") { path = filepath.Join(path, ".envrc") } config.WhitelistExact[expandTildePath(path)] = true } if tomlConf.SkipDotenv { logError(config, "skip_dotenv has been inverted to load_dotenv.") } config.BashPath = tomlConf.BashPath config.DisableStdin = tomlConf.DisableStdin config.LoadDotenv = tomlConf.LoadDotenv config.StrictEnv = tomlConf.StrictEnv if tomlConf.WarnTimeout != nil { config.WarnTimeout = tomlConf.WarnTimeout.Duration } } if ts := env.Fetch("DIRENV_WARN_TIMEOUT", ""); ts != "" { timeout, err := time.ParseDuration(ts) if err == nil { config.WarnTimeout = timeout } else { logError(config, "invalid DIRENV_WARN_TIMEOUT: "+err.Error()) } } if config.BashPath == "" { if env[DIRENV_BASH] != "" { config.BashPath = env[DIRENV_BASH] } else if bashPath != "" { config.BashPath = bashPath } else if config.BashPath, err = exec.LookPath("bash"); err != nil { err = fmt.Errorf("can't find bash: %w", err) return } } if config.CacheDir == "" { config.CacheDir = xdg.CacheDir(env, "direnv") } if config.CacheDir == "" { err = fmt.Errorf("couldn't find a cache directory for direnv") return } if config.DataDir == "" { config.DataDir = xdg.DataDir(env, "direnv") } if config.DataDir == "" { err = fmt.Errorf("couldn't find a data directory for direnv") return } return } // AllowDir is the folder where all the "allow" files are stored. func (config *Config) AllowDir() string { return filepath.Join(config.DataDir, "allow") } // DenyDir is the folder where all the "deny" files are stored. func (config *Config) DenyDir() string { return filepath.Join(config.DataDir, "deny") } // LoadedRC returns a RC file if any has been loaded func (config *Config) LoadedRC() *RC { if config.Env[DIRENV_FILE] == "" { logDebug("RCFile is blank - loadedRC is nil") return nil } rcPath := config.Env[DIRENV_FILE] timesString := config.Env[DIRENV_WATCHES] return RCFromEnv(rcPath, timesString, config) } // EnvFromRC loads an RC from a specified path and returns the new environment func (config *Config) EnvFromRC(path string, previousEnv Env) (Env, error) { rc, err := RCFromPath(path, config) if err != nil { return nil, err } return rc.Load(previousEnv) } // FindRC looks for a RC file in the config environment func (config *Config) FindRC() (*RC, error) { return FindRC(config.WorkDir, config) } // Revert undoes the recorded changes (if any) to the supplied environment, // returning a new environment func (config *Config) Revert(env Env) (Env, error) { if config.Env[DIRENV_DIFF] == "" { return env.Copy(), nil } diff, err := LoadEnvDiff(config.Env[DIRENV_DIFF]) if err == nil { return diff.Reverse().Patch(env), nil } return nil, err } direnv-2.37.1/internal/cmd/const.go000066400000000000000000000004661503714164100171370ustar00rootroot00000000000000package cmd // nolint const ( DIRENV_CONFIG = "DIRENV_CONFIG" DIRENV_BASH = "DIRENV_BASH" DIRENV_DEBUG = "DIRENV_DEBUG" DIRENV_DIR = "DIRENV_DIR" DIRENV_FILE = "DIRENV_FILE" DIRENV_WATCHES = "DIRENV_WATCHES" DIRENV_DIFF = "DIRENV_DIFF" DIRENV_DUMP_FILE_PATH = "DIRENV_DUMP_FILE_PATH" ) direnv-2.37.1/internal/cmd/env.go000066400000000000000000000054041503714164100165760ustar00rootroot00000000000000package cmd import ( "encoding/json" "os" "strings" "github.com/direnv/direnv/v2/gzenv" ) // Env is a map representation of environment variables. type Env map[string]string // GetEnv turns the classic unix environment variables into a map of // key->values which is more handy to work with. // // NOTE: We don't support having two variables with the same name. // I've never seen it used in the wild but according to POSIX it's allowed. func GetEnv() Env { env := make(Env) for _, kv := range os.Environ() { kv2 := strings.SplitN(kv, "=", 2) key := kv2[0] value := kv2[1] env[key] = value } return env } // CleanContext removes all the direnv-related environment variables. Call // this after reverting the environment, otherwise direnv will just be amnesic // about the previously-loaded environment. func (env Env) CleanContext() { delete(env, DIRENV_DIFF) delete(env, DIRENV_DIR) delete(env, DIRENV_FILE) delete(env, DIRENV_DUMP_FILE_PATH) delete(env, DIRENV_WATCHES) } // LoadEnv unmarshals the env back from a gzenv string func LoadEnv(gzenvStr string) (env Env, err error) { env = make(Env) err = gzenv.Unmarshal(gzenvStr, &env) return } // LoadEnvJSON unmarshals the env back from a JSON string func LoadEnvJSON(jsonBytes []byte) (env Env, err error) { env = make(Env) err = json.Unmarshal(jsonBytes, &env) return env, err } // Copy returns a fresh copy of the env. Because the env is a map under the // hood, we want to get a copy whenever we mutate it and want to keep the // original around. func (env Env) Copy() Env { newEnv := make(Env) for key, value := range env { newEnv[key] = value } return newEnv } // ToGoEnv should really be named ToUnixEnv. It turns the env back into a list // of "key=value" strings like returns by os.Environ(). func (env Env) ToGoEnv() []string { goEnv := make([]string, len(env)) index := 0 for key, value := range env { goEnv[index] = strings.Join([]string{key, value}, "=") index++ } return goEnv } // ToShell outputs the environment into an evaluatable string that is // understood by the target shell func (env Env) ToShell(shell Shell) (string, error) { e := make(ShellExport) for key, value := range env { e.Add(key, value) } return shell.Export(e) } // Serialize marshals the env into the gzenv format func (env Env) Serialize() string { return gzenv.Marshal(env) } // Diff returns the diff between the current env and the passed env func (env Env) Diff(other Env) *EnvDiff { return BuildEnvDiff(env, other) } // Fetch tries to get the value associated with the given 'key', or returns // the provided default if none is set. // // Note that empty environment variables are considered to be set. func (env Env) Fetch(key, def string) string { v, ok := env[key] if !ok { v = def } return v } direnv-2.37.1/internal/cmd/env_diff.go000066400000000000000000000061071503714164100175670ustar00rootroot00000000000000package cmd import ( "strings" "github.com/direnv/direnv/v2/gzenv" ) // IgnoredKeys is list of keys we don't want to deal with var IgnoredKeys = map[string]bool{ // direnv env config "DIRENV_CONFIG": true, "DIRENV_BASH": true, // should only be available inside of the .envrc or .env "DIRENV_IN_ENVRC": true, "COMP_WORDBREAKS": true, // Avoids segfaults in bash "PS1": true, // PS1 should not be exported, fixes problem in bash // variables that should change freely "OLDPWD": true, "PWD": true, "SHELL": true, "SHELLOPTS": true, "SHLVL": true, "_": true, } // EnvDiff represents the diff between two environments type EnvDiff struct { Prev map[string]string `json:"p"` Next map[string]string `json:"n"` } // NewEnvDiff is an empty constructor for EnvDiff func NewEnvDiff() *EnvDiff { return &EnvDiff{make(map[string]string), make(map[string]string)} } // BuildEnvDiff analyses the changes between 'e1' and 'e2' and builds an // EnvDiff out of it. func BuildEnvDiff(e1, e2 Env) *EnvDiff { diff := NewEnvDiff() in := func(key string, e Env) bool { _, ok := e[key] return ok } for key := range e1 { if IgnoredEnv(key) { continue } if e2[key] != e1[key] || !in(key, e2) { diff.Prev[key] = e1[key] } } for key := range e2 { if IgnoredEnv(key) { continue } if e2[key] != e1[key] || !in(key, e1) { diff.Next[key] = e2[key] } } return diff } // LoadEnvDiff unmarshalls a gzenv string back into an EnvDiff. func LoadEnvDiff(gzenvStr string) (diff *EnvDiff, err error) { diff = new(EnvDiff) err = gzenv.Unmarshal(gzenvStr, diff) return } // Any returns if the diff contains any changes. func (diff *EnvDiff) Any() bool { return len(diff.Prev) > 0 || len(diff.Next) > 0 } // ToShell applies the env diff as a set of commands that are understood by // the target `shell`. The outputted string is then meant to be evaluated in // the target shell. func (diff *EnvDiff) ToShell(shell Shell) (string, error) { e := make(ShellExport) for key := range diff.Prev { _, ok := diff.Next[key] if !ok { e.Remove(key) } } for key, value := range diff.Next { e.Add(key, value) } return shell.Export(e) } // Patch applies the diff to the given env and returns a new env with the // changes applied. func (diff *EnvDiff) Patch(env Env) (newEnv Env) { newEnv = make(Env) for k, v := range env { newEnv[k] = v } for key := range diff.Prev { delete(newEnv, key) } for key, value := range diff.Next { newEnv[key] = value } return newEnv } // Reverse flips the diff so that it applies the other way around. func (diff *EnvDiff) Reverse() *EnvDiff { return &EnvDiff{diff.Next, diff.Prev} } // Serialize marshalls the environment diff to the gzenv format. func (diff *EnvDiff) Serialize() string { return gzenv.Marshal(diff) } //// Utils // IgnoredEnv returns true if the key should be ignored in environment diffs. func IgnoredEnv(key string) bool { if strings.HasPrefix(key, "__fish") { return true } if strings.HasPrefix(key, "BASH_FUNC_") { return true } _, found := IgnoredKeys[key] return found } direnv-2.37.1/internal/cmd/env_diff_test.go000066400000000000000000000020141503714164100206170ustar00rootroot00000000000000package cmd import ( "reflect" "testing" ) func TestEnvDiff(t *testing.T) { diff := &EnvDiff{map[string]string{"FOO": "bar"}, map[string]string{"BAR": "baz"}} out := diff.Serialize() diff2, err := LoadEnvDiff(out) if err != nil { t.Error("parse error", err) } if len(diff2.Prev) != 1 { t.Error("len(diff2.prev) != 1", len(diff2.Prev)) } if len(diff2.Next) != 1 { t.Error("len(diff2.next) != 0", len(diff2.Next)) } } // Issue #114 // Check that empty environment variables correctly appear in the diff func TestEnvDiffEmptyValue(t *testing.T) { before := Env{} after := Env{"FOO": ""} diff := BuildEnvDiff(before, after) if !reflect.DeepEqual(diff.Next, map[string]string(after)) { t.Errorf("diff.Next != after (%#+v != %#+v)", diff.Next, after) } } func TestIgnoredEnv(t *testing.T) { if !IgnoredEnv(DIRENV_BASH) { t.Fail() } if IgnoredEnv(DIRENV_DIFF) { t.Fail() } if !IgnoredEnv("_") { t.Fail() } if !IgnoredEnv("__fish_foo") { t.Fail() } if !IgnoredEnv("__fishx") { t.Fail() } } direnv-2.37.1/internal/cmd/env_test.go000066400000000000000000000005021503714164100176270ustar00rootroot00000000000000package cmd import ( "testing" ) func TestEnv(t *testing.T) { env := Env{"FOO": "bar"} out := env.Serialize() env2, err := LoadEnv(out) if err != nil { t.Error("parse error", err) } if env2["FOO"] != "bar" { t.Error("FOO != bar", env2["FOO"]) } if len(env2) != 1 { t.Error("len != 1", len(env2)) } } direnv-2.37.1/internal/cmd/file_times.go000066400000000000000000000107631503714164100201320ustar00rootroot00000000000000package cmd import ( "fmt" "os" "path/filepath" "time" "github.com/direnv/direnv/v2/gzenv" ) // FileTime represents a single recorded file status type FileTime struct { Path string `json:"path"` Modtime int64 `json:"modtime"` Exists bool `json:"exists"` } // FileTimes represent a record of all the known files and times type FileTimes struct { list *[]FileTime } // NewFileTimes creates a new empty FileTimes func NewFileTimes() (times FileTimes) { list := make([]FileTime, 0) times.list = &list return } // Update gets the latest stats on the path and updates the record. func (times *FileTimes) Update(path string) (err error) { var modtime int64 var exists bool stat, err := getLatestStat(path) if os.IsNotExist(err) { exists = false } else { exists = true if err != nil { return } modtime = stat.ModTime().Unix() } err = times.NewTime(path, modtime, exists) return } // NewTime add the file on path, with modtime and exists flag to the list of known // files. func (times *FileTimes) NewTime(path string, modtime int64, exists bool) (err error) { var time *FileTime path, err = filepath.Abs(path) if err != nil { return } path = filepath.Clean(path) for idx := range *(times.list) { if (*times.list)[idx].Path == path { time = &(*times.list)[idx] break } } if time == nil { newTimes := append(*times.list, FileTime{Path: path}) times.list = &newTimes time = &((*times.list)[len(*times.list)-1]) } time.Modtime = modtime time.Exists = exists return } type checkFailed struct { message string } func (err checkFailed) Error() string { return err.message } // Check validates all the recorded file times func (times *FileTimes) Check() (err error) { if len(*times.list) == 0 { return checkFailed{"Times list is empty"} } for idx := range *times.list { err = (*times.list)[idx].Check() if err != nil { return } } return } // CheckOne compares notes between the given path and the recorded times func (times *FileTimes) CheckOne(path string) (err error) { path, err = filepath.Abs(path) if err != nil { return } for idx := range *times.list { if time := (*times.list)[idx]; time.Path == path { err = time.Check() return } } return checkFailed{fmt.Sprintf("File %q is unknown", path)} } // Check verifies that the file is good and hasn't changed func (times *FileTime) Check() (err error) { stat, err := getLatestStat(times.Path) switch { case os.IsNotExist(err): if times.Exists { logDebug("Stat Check: %s: gone", times.Path) return checkFailed{fmt.Sprintf("File %q is missing (Stat)", times.Path)} } case err != nil: logDebug("Stat Check: %s: ERR: %v", times.Path, err) return err case !times.Exists: logDebug("Check: %s: appeared", times.Path) return checkFailed{fmt.Sprintf("File %q newly created", times.Path)} case stat.ModTime().Unix() != times.Modtime: logDebug("Check: %s: stale (stat: %v, lastcheck: %v)", times.Path, stat.ModTime().Unix(), times.Modtime) return checkFailed{fmt.Sprintf("File %q has changed", times.Path)} } logDebug("Check: %s: up to date", times.Path) return nil } // Formatted shows the times in a user-friendly format. func (times *FileTime) Formatted(relDir string) string { timeBytes, err := time.Unix(times.Modtime, 0).MarshalText() if err != nil { timeBytes = []byte("<>") } path, err := filepath.Rel(relDir, times.Path) if err != nil { path = times.Path } return fmt.Sprintf("%q - %s", path, timeBytes) } // Marshal dumps the times into gzenv format func (times *FileTimes) Marshal() string { return gzenv.Marshal(*times.list) } // Unmarshal loads the watches back from gzenv func (times *FileTimes) Unmarshal(from string) error { return gzenv.Unmarshal(from, times.list) } func getLatestStat(path string) (os.FileInfo, error) { var lstatModTime int64 var statModTime int64 // Check the examine-a-symlink case first: lstat, err := os.Lstat(path) if err != nil { logDebug("getLatestStat,Lstat: %s: error: %v", path, err) return nil, err } lstatModTime = lstat.ModTime().Unix() stat, err := os.Stat(path) if err != nil { logDebug("getLatestStat,Stat: %s: error: %v (Lstat time: %v)", path, err, lstatModTime) return nil, err } statModTime = stat.ModTime().Unix() if lstatModTime > statModTime { logDebug("getLatestStat: %s: Lstat: %v, Stat: %v -> preferring Lstat", path, lstatModTime, statModTime) return lstat, nil } logDebug("getLatestStat: %s: Lstat: %v, Stat: %v -> preferring Stat", path, lstatModTime, statModTime) return stat, nil } direnv-2.37.1/internal/cmd/file_times_test.go000066400000000000000000000044231503714164100211650ustar00rootroot00000000000000package cmd import ( "bytes" "encoding/json" "testing" "time" ) func TestUpdate(t *testing.T) { times := NewFileTimes() _ = times.Update("file_times.go") if len(*times.list) != 1 { t.Error("Length of updated list not 1") } if !(*times.list)[0].Exists { t.Error("Existing file marked not existing") } } func TestFTJsons(t *testing.T) { ft := FileTime{"something.txt", time.Now().Unix(), true} marshalled, err := json.Marshal(ft) if err != nil { t.Error("FileTime failed to marshal:", err) } if bytes.NewBuffer(marshalled).String() == "{}" { t.Error(ft, "marshals as empty object") } } func TestRoundTrip(t *testing.T) { watches := NewFileTimes() _ = watches.Update("file_times.go") rtChk := NewFileTimes() _ = rtChk.Unmarshal(watches.Marshal()) compareFTs(t, watches, rtChk, "length", func(ft FileTimes) interface{} { return len(*ft.list) }) compareFTs(t, watches, rtChk, "first path", func(ft FileTimes) interface{} { return (*ft.list)[0].Path }) } func compareFTs(t *testing.T, left, right FileTimes, desc string, compare func(ft FileTimes) (res interface{})) { lc, rc := compare(left), compare(right) if lc != rc { t.Error("FileTimes didn't round trip.", "Original", desc, "was:", lc, "RT", desc, "was:", rc) } } func TestCanonicalAdds(t *testing.T) { fts := NewFileTimes() _ = fts.NewTime("docs/../file_times.go", 0, true) _ = fts.NewTime("file_times.go", 0, true) if len(*fts.list) > 1 { t.Error("Double add of the same file") } } func TestCheckPasses(t *testing.T) { fts := NewFileTimes() _ = fts.Update("file_times.go") err := fts.Check() if err != nil { t.Error("Check that should pass fails with:", err) } } func TestCheckStale(t *testing.T) { fts := NewFileTimes() _ = fts.NewTime("file_times.go", 0, true) err := fts.Check() if err == nil { t.Error("Check that should fail because stale passes") } } func TestCheckAppeared(t *testing.T) { fts := NewFileTimes() _ = fts.NewTime("file_times.go", 0, false) err := fts.Check() if err == nil { t.Error("Check that should fail because appeared passes") } } func TestCheckGone(t *testing.T) { fts := NewFileTimes() _ = fts.NewTime("nosuchfileevarright.go", time.Now().Unix()+1000, true) err := fts.Check() if err == nil { t.Error("Check that should fail because gone passes") } } direnv-2.37.1/internal/cmd/log.go000066400000000000000000000025151503714164100165670ustar00rootroot00000000000000package cmd import ( "fmt" "log" "strings" ) const ( defaultLogFormat = "direnv: %s" errorColor = "\033[31m" clearColor = "\033[0m" ) var debugging bool func setupLogging(env Env) { log.SetFlags(0) log.SetPrefix("") if val, ok := env[DIRENV_DEBUG]; ok && (val == "1" || strings.EqualFold(val, "true")) { debugging = true log.SetFlags(log.Ltime) log.SetPrefix("direnv: ") } } func logError(c *Config, msg string, a ...interface{}) { if c.LogColor { logMsg(defaultLogFormat, msg, a...) } else { logMsg(errorColor+defaultLogFormat+clearColor, msg, a...) } } func logStatus(c *Config, msg string, a ...interface{}) { format := c.LogFormat shouldLog := true if c.LogFilter != nil { shouldLog = c.LogFilter.MatchString(msg) } if shouldLog && format != "" { if c.LogColor { logMsg(format, msg, a...) } else { logMsg(fmt.Sprintf("%s%s", clearColor, format), msg, a...) } } } func logDebug(msg string, a ...interface{}) { if !debugging { return } defer log.SetFlags(log.Flags()) log.SetFlags(log.Flags() | log.Lshortfile) msg = fmt.Sprintf(msg, a...) _ = log.Output(2, msg) } func logMsg(format, msg string, a ...interface{}) { defer log.SetFlags(log.Flags()) defer log.SetPrefix(log.Prefix()) log.SetFlags(0) log.SetPrefix("") msg = fmt.Sprintf(format+"\n", msg) log.Printf(msg, a...) } direnv-2.37.1/internal/cmd/look_path.go000066400000000000000000000016771503714164100177760ustar00rootroot00000000000000package cmd import ( "errors" "os" "strings" ) // Similar to os/exec.LookPath except we pass in the PATH func lookPath(file string, pathenv string) (string, error) { if strings.Contains(file, "/") { err := findExecutable(file) if err == nil { return file, nil } return "", err } if pathenv == "" { return "", errNotFound } for _, dir := range strings.Split(pathenv, ":") { if dir == "" { // Unix shell semantics: path element "" means "." dir = "." } path := dir + "/" + file if err := findExecutable(path); err == nil { return path, nil } } return "", errNotFound } // ErrNotFound is the error resulting if a path search failed to find an executable file. var errNotFound = errors.New("executable file not found in $PATH") func findExecutable(file string) error { d, err := os.Stat(file) if err != nil { return err } if m := d.Mode(); !m.IsDir() && m&0111 != 0 { return nil } return os.ErrPermission } direnv-2.37.1/internal/cmd/mod.go000066400000000000000000000012021503714164100165550ustar00rootroot00000000000000package cmd import ( "fmt" "os" ) var ( bashPath string stdlib string version string ) // Main is the main entrypoint to direnv func Main(env Env, args []string, modBashPath string, modStdlib string, modVersion string) error { // We drop $PWD from caller since it can include symlinks, which will // break relative path access when finding .envrc or .env in a parent. _ = os.Unsetenv("PWD") setupLogging(env) bashPath = modBashPath stdlib = modStdlib version = modVersion err := CommandsDispatch(env, args) if err != nil { fmt.Fprintf(os.Stderr, "%sdirenv: error %v%s\n", errorColor, err, clearColor) } return err } direnv-2.37.1/internal/cmd/rc.go000066400000000000000000000207311503714164100164120ustar00rootroot00000000000000package cmd import ( "context" "crypto/sha256" "errors" "fmt" "io" "io/fs" "log" "os" "os/exec" "os/signal" "path/filepath" "strings" "time" ) // RC represents the .envrc or .env file type RC struct { path string allowPath string denyPath string times FileTimes config *Config } // FindRC looks for ".envrc" and ".env" files up in the file hierarchy. func FindRC(wd string, config *Config) (*RC, error) { rcPath := findEnvUp(wd, config.LoadDotenv) if rcPath == "" { return nil, nil } return RCFromPath(rcPath, config) } // RCFromPath inits the RC from a given path func RCFromPath(path string, config *Config) (*RC, error) { fileHash, err := fileHash(path) if err != nil { return nil, err } allowPath := filepath.Join(config.AllowDir(), fileHash) pathHash, err := pathHash(path) if err != nil { return nil, err } denyPath := filepath.Join(config.DenyDir(), pathHash) times := NewFileTimes() err = times.Update(path) if err != nil { return nil, err } err = times.Update(allowPath) if err != nil { return nil, err } err = times.Update(denyPath) if err != nil { return nil, err } return &RC{path, allowPath, denyPath, times, config}, nil } // RCFromEnv inits the RC from the environment func RCFromEnv(path, marshalledTimes string, config *Config) *RC { fileHash, err := fileHash(path) if err != nil { return nil } allowPath := filepath.Join(config.AllowDir(), fileHash) times := NewFileTimes() err = times.Unmarshal(marshalledTimes) if err != nil { return nil } pathHash, err := pathHash(path) if err != nil { return nil } denyPath := filepath.Join(config.DenyDir(), pathHash) return &RC{path, allowPath, denyPath, times, config} } // Allow grants the RC as allowed to load func (rc *RC) Allow() (err error) { if rc.allowPath == "" { return fmt.Errorf("cannot allow empty path") } if err = os.MkdirAll(filepath.Dir(rc.allowPath), 0755); err != nil { return } if err = allow(rc.path, rc.allowPath); err != nil { return } if err = rc.times.Update(rc.allowPath); err != nil { return } if _, err = os.Stat(rc.denyPath); err != nil { if errors.Is(err, fs.ErrNotExist) { return nil } return err } return os.Remove(rc.denyPath) } // Deny revokes the permission of the RC file to load func (rc *RC) Deny() (err error) { if err = os.MkdirAll(filepath.Dir(rc.denyPath), 0755); err != nil { return } if err = os.WriteFile(rc.denyPath, []byte(rc.path+"\n"), 0644); /* #nosec G306 -- these deny files are not private */ err != nil { return } if _, err = os.Stat(rc.allowPath); err != nil { if errors.Is(err, fs.ErrNotExist) { return nil } return err } return os.Remove(rc.allowPath) } // AllowStatus represents the permission status of an RC file. type AllowStatus int const ( // Allowed indicates the RC file is permitted to load. Allowed AllowStatus = iota // NotAllowed indicates the RC file has not been granted permission. NotAllowed // Denied indicates the RC file has been explicitly denied. Denied ) // Allowed checks if the RC file has been granted loading func (rc *RC) Allowed() AllowStatus { _, err := os.Stat(rc.denyPath) if err == nil { return Denied } // happy path is if this envrc has been explicitly allowed, O(1)ish common case _, err = os.Stat(rc.allowPath) if err == nil { return Allowed } // when whitelisting we want to be (path) absolutely sure we've not been duped with a symlink path, err := filepath.Abs(rc.path) // seems unlikely that we'd hit this, but have to handle it if err != nil { return NotAllowed } // exact whitelists are O(1)ish to check, so look there first if rc.config.WhitelistExact[path] { return Allowed } // finally we check if any of our whitelist prefixes match for _, prefix := range rc.config.WhitelistPrefix { if strings.HasPrefix(path, prefix) { return Allowed } } return NotAllowed } // Path returns the path to the RC file func (rc *RC) Path() string { return rc.path } // Touch updates the mtime of the RC file. This is mainly used to trigger a // reload in direnv. func (rc *RC) Touch() error { return touch(rc.path) } const notAllowed = "%s is blocked. Run `direnv allow` to approve its content" // Load evaluates the RC file and returns the new Env or error. // // This functions is key to the implementation of direnv. func (rc *RC) Load(previousEnv Env) (newEnv Env, err error) { config := rc.config wd := config.WorkDir direnv := config.SelfPath newEnv = previousEnv.Copy() newEnv[DIRENV_WATCHES] = rc.times.Marshal() defer func() { // Record directory changes even if load is disallowed or fails newEnv[DIRENV_DIR] = "-" + filepath.Dir(rc.path) newEnv[DIRENV_FILE] = rc.path newEnv[DIRENV_DIFF] = previousEnv.Diff(newEnv).Serialize() }() // Abort if the file is not allowed switch rc.Allowed() { case NotAllowed: err = fmt.Errorf(notAllowed, rc.Path()) return case Allowed: case Denied: return } // Allow RC loads to be canceled with SIGINT ctx, cancel := context.WithCancel(context.Background()) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) go func() { <-c cancel() }() // check what type of RC we're processing // use different exec method for each fn := "source_env" if filepath.Base(rc.path) == ".env" { fn = "dotenv" } // Set stdin based on the config var stdin *os.File if config.DisableStdin { stdin, err = os.Open(os.DevNull) if err != nil { return } } else { stdin = os.Stdin } prelude := "" if config.StrictEnv { prelude = "set -euo pipefail && " } // Non-Windows platforms will already use slashes. However, on Windows // backslashes are used by default which can result in unexpected escapes // like \b or \r in paths. Force slash usage to avoid issues on Windows. slashSeparatedPath := filepath.ToSlash(rc.Path()) arg := fmt.Sprintf( `%seval "$("%s" stdlib)" && __main__ %s %s`, prelude, direnv, fn, BashEscape(slashSeparatedPath), ) // G204: Subprocess launched with function call as argument or cmd arguments // #nosec cmd := exec.CommandContext(ctx, config.BashPath, "-c", arg) cmd.Dir = wd cmd.Env = newEnv.ToGoEnv() cmd.Stdin = stdin cmd.Stderr = os.Stderr var out []byte if out, err = cmd.Output(); err == nil && len(out) > 0 { var newEnv2 Env newEnv2, err = LoadEnvJSON(out) if err == nil { newEnv = newEnv2 } } return } /// Utils func eachDir(path string) (paths []string) { path, err := filepath.Abs(path) if err != nil { return } paths = []string{path} if path == "/" { return } for i := len(path) - 1; i >= 0; i-- { if path[i] == os.PathSeparator { path = path[:i] if path == "" { path = "/" } paths = append(paths, path) } } return } func fileExists(path string) bool { // Some broken filesystems like SSHFS return file information on stat() but // then cannot open the file. So we use os.Open. f, err := os.Open(path) if err != nil { return false } defer func() { if err := f.Close(); err != nil { log.Printf("Warning: failed to close file: %v", err) } }() // Next, check that the file is a regular file. fi, err := f.Stat() if err != nil { return false } return fi.Mode().IsRegular() } func fileHash(path string) (hash string, err error) { if path, err = filepath.Abs(path); err != nil { return } fd, err := os.Open(path) if err != nil { return } hasher := sha256.New() _, err = hasher.Write([]byte(path + "\n")) if err != nil { return } if _, err = io.Copy(hasher, fd); err != nil { return } return fmt.Sprintf("%x", hasher.Sum(nil)), nil } func pathHash(path string) (hash string, err error) { if path, err = filepath.Abs(path); err != nil { return } hasher := sha256.New() _, err = hasher.Write([]byte(path + "\n")) if err != nil { return } return fmt.Sprintf("%x", hasher.Sum(nil)), nil } // Creates a file func touch(path string) (err error) { t := time.Now() return os.Chtimes(path, t, t) } func allow(path string, allowPath string) (err error) { // G306: Expect WriteFile permissions to be 0600 or less // #nosec return os.WriteFile(allowPath, []byte(path+"\n"), 0644) } func findEnvUp(searchDir string, loadDotenv bool) (path string) { if loadDotenv { return findUp(searchDir, ".envrc", ".env") } return findUp(searchDir, ".envrc") } func findUp(searchDir string, fileNames ...string) (path string) { if searchDir == "" { return "" } for _, dir := range eachDir(searchDir) { for _, fileName := range fileNames { path := filepath.Join(dir, fileName) if fileExists(path) { return path } } } return "" } direnv-2.37.1/internal/cmd/rc_test.go000066400000000000000000000004531503714164100174500ustar00rootroot00000000000000package cmd import ( "runtime" "testing" ) func TestSomething(t *testing.T) { paths := eachDir("/foo/b//bar/") if len(paths) != 4 { t.Fail() } // TODO: fix me for windows if runtime.GOOS != "windows" { paths = eachDir("/") if len(paths) != 1 && paths[0] != "/" { t.Fail() } } } direnv-2.37.1/internal/cmd/shell.go000066400000000000000000000030061503714164100171110ustar00rootroot00000000000000package cmd import ( "path/filepath" ) // Shell is the interface that represents the interaction with the host shell. type Shell interface { // Hook is the string that gets evaluated into the host shell config and // setups direnv as a prompt hook. Hook() (string, error) // Export outputs the ShellExport as an evaluatable string on the host shell Export(e ShellExport) (string, error) // Dump outputs and evaluatable string that sets the env in the host shell Dump(env Env) (string, error) } // ShellExport represents environment variables to add and remove on the host // shell. type ShellExport map[string]*string // Add represents the addition of a new environment variable func (e ShellExport) Add(key, value string) { e[key] = &value } // Remove represents the removal of a given `key` environment variable. func (e ShellExport) Remove(key string) { e[key] = nil } var supportedShellList = map[string]Shell{ "bash": Bash, "elvish": Elvish, "fish": Fish, "gha": GitHubActions, "gzenv": GzEnv, "json": JSON, "murex": Murex, "tcsh": Tcsh, "vim": Vim, "zsh": Zsh, "pwsh": Pwsh, "systemd": Systemd, } // DetectShell returns a Shell instance from the given target. // // target is usually $0 and can also be prefixed by `-` func DetectShell(target string) Shell { target = filepath.Base(target) // $0 starts with "-" if target[0:1] == "-" { target = target[1:] } detectedShell, isValid := supportedShellList[target] if isValid { return detectedShell } return nil } direnv-2.37.1/internal/cmd/shell_bash.go000066400000000000000000000072751503714164100201220ustar00rootroot00000000000000package cmd import "fmt" type bash struct{} // Bash shell instance var Bash Shell = bash{} const bashHook = ` _direnv_hook() { local previous_exit_status=$?; trap -- '' SIGINT; eval "$("{{.SelfPath}}" export bash)"; trap - SIGINT; return $previous_exit_status; }; if [[ ";${PROMPT_COMMAND[*]:-};" != *";_direnv_hook;"* ]]; then if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then PROMPT_COMMAND=(_direnv_hook "${PROMPT_COMMAND[@]}") else PROMPT_COMMAND="_direnv_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}" fi fi ` func (sh bash) Hook() (string, error) { return bashHook, nil } func (sh bash) Export(e ShellExport) (string, error) { var out string for key, value := range e { if value == nil { out += sh.unset(key) } else { out += sh.export(key, *value) } } return out, nil } func (sh bash) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func (sh bash) export(key, value string) string { return "export " + sh.escape(key) + "=" + sh.escape(value) + ";" } func (sh bash) unset(key string) string { return "unset " + sh.escape(key) + ";" } func (sh bash) escape(str string) string { return BashEscape(str) } /* * Escaping */ // nolint const ( ACK = 6 TAB = 9 LF = 10 CR = 13 US = 31 SPACE = 32 AMPERSTAND = 38 SINGLE_QUOTE = 39 STAR = 42 PLUS = 43 NINE = 57 COLON = 58 EQUALS = 61 QUESTION = 63 UPPERCASE_Z = 90 OPEN_BRACKET = 91 BACKSLASH = 92 UNDERSCORE = 95 CLOSE_BRACKET = 93 BACKTICK = 96 LOWERCASE_Z = 122 OPEN_CURLY_BRACE = 123 CLOSE_CURLY_BRACE = 125 TILDE = 126 DEL = 127 ) // BashEscape escapes strings for safe use in Bash. // Based on https://github.com/solidsnack/shell-escape/blob/master/Text/ShellEscape/Bash.hs /* A Bash escaped string. The strings are wrapped in @$\'...\'@ if any bytes within them must be escaped; otherwise, they are left as is. Newlines and other control characters are represented as ANSI escape sequences. High bytes are represented as hex codes. Thus Bash escaped strings will always fit on one line and never contain non-ASCII bytes. */ func BashEscape(str string) string { if str == "" { return "''" } in := []byte(str) out := "" i := 0 l := len(in) escape := false hex := func(char byte) { escape = true out += fmt.Sprintf("\\x%02x", char) } backslash := func(char byte) { escape = true out += string([]byte{BACKSLASH, char}) } escaped := func(str string) { escape = true out += str } quoted := func(char byte) { escape = true out += string([]byte{char}) } literal := func(char byte) { out += string([]byte{char}) } for i < l { char := in[i] switch { case char == ACK: hex(char) case char == TAB: escaped(`\t`) case char == LF: escaped(`\n`) case char == CR: escaped(`\r`) case char <= US: hex(char) case char <= AMPERSTAND: quoted(char) case char == SINGLE_QUOTE: backslash(char) case char <= PLUS: quoted(char) case char <= NINE: literal(char) case char <= QUESTION: quoted(char) case char <= UPPERCASE_Z: literal(char) case char == OPEN_BRACKET: quoted(char) case char == BACKSLASH: backslash(char) case char == UNDERSCORE: literal(char) case char <= CLOSE_BRACKET: quoted(char) case char <= BACKTICK: quoted(char) case char <= TILDE: quoted(char) case char == DEL: hex(char) default: hex(char) } i++ } if escape { out = "$'" + out + "'" } return out } direnv-2.37.1/internal/cmd/shell_elvish.go000066400000000000000000000016331503714164100204670ustar00rootroot00000000000000package cmd import ( "bytes" "encoding/json" ) type elvish struct{} // Elvish add support for the elvish shell var Elvish Shell = elvish{} func (elvish) Hook() (string, error) { return `## hook for direnv set @edit:before-readline = $@edit:before-readline { try { var m = [("{{.SelfPath}}" export elvish | from-json)] if (> (count $m) 0) { set m = (all $m) keys $m | each { |k| if $m[$k] { set-env $k $m[$k] } else { unset-env $k } } } } catch e { echo $e } } `, nil } func (sh elvish) Export(e ShellExport) (string, error) { buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(e) if err != nil { return "", err } return buf.String(), nil } func (sh elvish) Dump(env Env) (string, error) { buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(env) if err != nil { return "", err } return buf.String(), nil } var ( _ Shell = (*elvish)(nil) ) direnv-2.37.1/internal/cmd/shell_fish.go000066400000000000000000000047051503714164100201310ustar00rootroot00000000000000package cmd import ( "fmt" "strings" ) type fish struct{} // Fish adds support for the fish shell as a host var Fish Shell = fish{} const fishHook = ` function __direnv_export_eval --on-event fish_prompt; "{{.SelfPath}}" export fish | source; if test "$direnv_fish_mode" != "disable_arrow"; function __direnv_cd_hook --on-variable PWD; if test "$direnv_fish_mode" = "eval_after_arrow"; set -g __direnv_export_again 0; else; "{{.SelfPath}}" export fish | source; end; end; end; end; function __direnv_export_eval_2 --on-event fish_preexec; if set -q __direnv_export_again; set -e __direnv_export_again; "{{.SelfPath}}" export fish | source; echo; end; functions --erase __direnv_cd_hook; end; ` func (sh fish) Hook() (string, error) { return fishHook, nil } func (sh fish) Export(e ShellExport) (string, error) { var out string for key, value := range e { if value == nil { out += sh.unset(key) } else { out += sh.export(key, *value) } } return out, nil } func (sh fish) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func (sh fish) export(key, value string) string { if key == "PATH" { command := "set -x -g PATH" for _, path := range strings.Split(value, ":") { command += " " + sh.escape(path) } return command + ";" } return "set -x -g " + sh.escape(key) + " " + sh.escape(value) + ";" } func (sh fish) unset(key string) string { return "set -e -g " + sh.escape(key) + ";" } func (sh fish) escape(str string) string { in := []byte(str) out := "'" i := 0 l := len(in) hex := func(char byte) { out += fmt.Sprintf("'\\X%02x'", char) } backslash := func(char byte) { out += string([]byte{BACKSLASH, char}) } escaped := func(str string) { out += "'" + str + "'" } literal := func(char byte) { out += string([]byte{char}) } for i < l { char := in[i] switch { case char == TAB: escaped(`\t`) case char == LF: escaped(`\n`) case char == CR: escaped(`\r`) case char <= US: hex(char) case char == SINGLE_QUOTE: backslash(char) case char == BACKSLASH: backslash(char) case char <= TILDE: literal(char) case char == DEL: hex(char) default: hex(char) } i++ } out += "'" return out } direnv-2.37.1/internal/cmd/shell_gha.go000066400000000000000000000050301503714164100177270ustar00rootroot00000000000000package cmd import ( "crypto/rand" "fmt" "os" "regexp" "strings" "time" ) type gha struct{} // GitHubActions shell instance var GitHubActions Shell = gha{} var validKeyPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) func (sh gha) Hook() (string, error) { return "", fmt.Errorf("Hook not implemented for GitHub Actions shell") } func (sh gha) Export(e ShellExport) (string, error) { var b strings.Builder for key, value := range e { if !validKeyPattern.MatchString(key) { // Skip invalid environment variable keys fmt.Fprintf(os.Stderr, "direnv: Skipping invalid environment variable key: %s\n", key) continue } if value == nil { sh.unset(&b, key) } else { if err := sh.export(&b, key, *value); err != nil { return "", err } } } return b.String(), nil } func (sh gha) Dump(env Env) (string, error) { var b strings.Builder for key, value := range env { if !validKeyPattern.MatchString(key) { // Skip invalid environment variable keys fmt.Fprintf(os.Stderr, "direnv: Skipping invalid environment variable key: %s\n", key) continue } if err := sh.export(&b, key, value); err != nil { return "", err } } return b.String(), nil } func (sh gha) export(b *strings.Builder, key, value string) error { // Generate a random delimiter delimiter := sh.generateDelimiter() // Check if key or value contains delimiter (should be extremely rare) if strings.Contains(key, delimiter) || strings.Contains(value, delimiter) { // Log the collision and regenerate delimiter fmt.Fprintf(os.Stderr, "direnv: Delimiter collision detected for key %s, regenerating delimiter\n", key) delimiter = sh.generateDelimiter() // If still colliding (astronomically unlikely), error out if strings.Contains(key, delimiter) || strings.Contains(value, delimiter) { return fmt.Errorf("delimiter collision after regeneration for key %s", key) } } b.WriteString(key) b.WriteString("<<") b.WriteString(delimiter) b.WriteByte('\n') b.WriteString(value) b.WriteByte('\n') b.WriteString(delimiter) b.WriteByte('\n') return nil } func (sh gha) unset(_ *strings.Builder, _ string) { // Don't do anything. > $GITHUB_ENV will overwrite the existing env. } func (sh gha) generateDelimiter() string { // Generate random bytes for delimiter randomBytes := make([]byte, 16) _, err := rand.Read(randomBytes) if err != nil { // Fallback to timestamp-based delimiter return fmt.Sprintf("ghadelimiter_%d", time.Now().UnixNano()) } // Convert to hex string return fmt.Sprintf("ghadelimiter_%x", randomBytes) } direnv-2.37.1/internal/cmd/shell_gzenv.go000066400000000000000000000007361503714164100203310ustar00rootroot00000000000000package cmd import ( "errors" "github.com/direnv/direnv/v2/gzenv" ) type gzenvShell int // GzEnv is not a real shell. used for internal purposes. var GzEnv Shell = gzenvShell(0) func (s gzenvShell) Hook() (string, error) { return "", errors.New("the gzenv shell doesn't support hooking") } func (s gzenvShell) Export(e ShellExport) (string, error) { return gzenv.Marshal(e), nil } func (s gzenvShell) Dump(env Env) (string, error) { return gzenv.Marshal(env), nil } direnv-2.37.1/internal/cmd/shell_json.go000066400000000000000000000013121503714164100201400ustar00rootroot00000000000000package cmd import ( "encoding/json" "errors" ) // jsonShell is not a real shell type jsonShell struct{} // JSON is not really a shell but it fits. Useful to add support to editor and // other external tools that understand JSON as a format. var JSON Shell = jsonShell{} func (sh jsonShell) Hook() (string, error) { return "", errors.New("this feature is not supported") } func (sh jsonShell) Export(e ShellExport) (string, error) { out, err := json.MarshalIndent(e, "", " ") if err != nil { return "", err } return string(out), nil } func (sh jsonShell) Dump(env Env) (string, error) { out, err := json.MarshalIndent(env, "", " ") if err != nil { return "", err } return string(out), nil } direnv-2.37.1/internal/cmd/shell_murex.go000066400000000000000000000015751503714164100203420ustar00rootroot00000000000000package cmd import ( "bytes" "encoding/json" ) type murex struct{} // Murex is the shell implementation for Murex shell. var Murex Shell = murex{} const murexHook = `event: onPrompt direnv_hook=before { "{{.SelfPath}}" export murex -> set exports if { $exports != "" } { $exports -> :json: formap key value { if { is-null value } then { !export "$key" } else { $value -> export "$key" } } } }` func (sh murex) Hook() (string, error) { return murexHook, nil } func (sh murex) Dump(env Env) (string, error) { buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(env) if err != nil { return "", err } return buf.String(), nil } func (sh murex) Export(e ShellExport) (string, error) { buf := new(bytes.Buffer) err := json.NewEncoder(buf).Encode(e) if err != nil { return "", err } return buf.String(), nil } var ( _ Shell = (*murex)(nil) ) direnv-2.37.1/internal/cmd/shell_pwsh.go000066400000000000000000000131461503714164100201600ustar00rootroot00000000000000package cmd import ( "fmt" ) type pwsh struct{} // Pwsh shell instance var Pwsh Shell = pwsh{} func (sh pwsh) Hook() (string, error) { const hook = `using namespace System; using namespace System.Management.Automation; if ($PSVersionTable.PSVersion.Major -lt 7 -or ($PSVersionTable.PSVersion.Major -eq 7 -and $PSVersionTable.PSVersion.Minor -lt 2)) { throw "direnv: PowerShell version $($PSVersionTable.PSVersion) does not meet the minimum required version 7.2!" } $hook = [EventHandler[LocationChangedEventArgs]] { param([object] $source, [LocationChangedEventArgs] $eventArgs) end { $export = ({{.SelfPath}} export pwsh) -join [Environment]::NewLine; if ($export) { Invoke-Expression -Command $export; } } }; $currentAction = $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction; if ($currentAction) { $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($currentAction, $hook); } else { $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $hook; }; ` return hook, nil } func (sh pwsh) Export(e ShellExport) (string, error) { var out string for key, value := range e { if key != "" { if value == nil { out += sh.unset(key) } else { out += sh.export(key, *value) } } } return out, nil } func (sh pwsh) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func (sh pwsh) export(key, value string) string { return fmt.Sprintf("${env:%s}='%s';", sh.escapeEnvKey(key), sh.escapeVerbatimString(value)) } func (sh pwsh) unset(key string) string { return fmt.Sprintf("Remove-Item -LiteralPath 'env:/%s';", sh.escapeVerbatimEnvKey(key)) } func (pwsh) escapeEnvKey(str string) string { return PowerShellEscapeEnvKey(str) } // PowerShellEscapeEnvKey escapes environment variable keys for PowerShell. func PowerShellEscapeEnvKey(str string) string { if str == "" { return "__DiReNv_UnReAcHaBlE__" } in := []byte(str) out := "" i := 0 l := len(in) escaped := func(str string) { out += str } hex := func(char byte) { out += fmt.Sprintf("\\x%02x", char) } literal := func(char byte) { out += string([]byte{char}) } for i < l { char := in[i] switch char { case STAR: hex(char) case COLON: hex(char) case EQUALS: hex(char) case QUESTION: hex(char) case OPEN_BRACKET: hex(char) case CLOSE_BRACKET: hex(char) case OPEN_CURLY_BRACE: escaped("`{") case CLOSE_CURLY_BRACE: escaped("`}") default: literal(char) } i++ } return out } func (pwsh) escapeVerbatimEnvKey(str string) string { return PowerShellEscapeVerbatimEnvKey(str) } // PowerShellEscapeVerbatimEnvKey escapes environment variable keys using verbatim strings for PowerShell. func PowerShellEscapeVerbatimEnvKey(str string) string { if str == "" { return "__DiReNv_UnReAcHaBlE__" } in := []byte(str) out := "" i := 0 l := len(in) escaped := func(str string) { out += str } literal := func(char byte) { out += string([]byte{char}) } for i < l { char := in[i] switch char { case SINGLE_QUOTE: escaped("''") default: literal(char) } i++ } return out } func (pwsh) escapeVerbatimString(str string) string { return PowerShellEscapeVerbatimString(str) } // PowerShellEscapeVerbatimString escapes strings using verbatim string literals for PowerShell. func PowerShellEscapeVerbatimString(str string) string { if str == "" { return "" } in := []byte(str) out := "" i := 0 l := len(in) escaped := func(str string) { out += str } literal := func(char byte) { out += string([]byte{char}) } for i < l { char := in[i] switch char { case SINGLE_QUOTE: escaped("''") default: literal(char) } i++ } return out } /* 1. Minimal handling required for verbatim strings: Characters in a verbatim string (e.g.: 'a single quoted string') don't require escaping except for the single quote character itself which is escaped by doubling it (i.e.: '''' -eq "'" -and ''''.Length -eq 1). 2. Handling any exported newline or carriage return characters from the PowerShell hook: Newline or carriage return characters in any part of the output of `direnv export pwsh` will produce an array of strings when imported into PowerShell. To join all parts of the array into a single string, the following is done in the PowerShell hook: `(direnv export pwsh) -join [Environment]::NewLine` 3. Allowing PowerShell variable names with special characters: PowerShell environment variable names may contain "special characters" when enclosed in curly braces like this: ${env:name-with-special-chars-like-dashes} = 'value' The following special characters may NOT be used in such names: *, ?, :, =, [, ] These invalid special characters are mapped to hex codes (e.g.: "*" -> "\x2A"). Curly braces may be used, if escaped with a backtick: `{, `} For more info on Pwsh variable names that include special characters see: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_variables#variable-names-that-include-special-characters 4. Paranoid handling of paths when removing environment variables: Use `Remove-Item -LiteralPath ` instead of `Remove-Item -Path ` to avoid any potential wildcard interpretations. 5. Paranoid handling of potentially empty key names: I'm not sure if `Remove-Item -LiteralPath 'env:'` or `${env:} = 'value'` could be abused so two overlapping steps are taken to avoid this issue: a. Empty key names are skipped. b. Empty key names are replaced with "__DiReNv_UnReAcHaBlE__". */ direnv-2.37.1/internal/cmd/shell_systemd.go000066400000000000000000000043741503714164100206720ustar00rootroot00000000000000package cmd import ( "errors" "strings" ) // systemdShell is not a real shell type systemdShell struct{} // Systemd is not really a shell but is useful to add support // to systemd EnvironmentFile(https://0pointer.de/public/systemd-man/systemd.exec.html#EnvironmentFile=) var Systemd Shell = systemdShell{} func (sh systemdShell) Hook() (string, error) { return "", errors.New("this feature is not supported") } func (sh systemdShell) Export(e ShellExport) (string, error) { var out string for key, value := range e { if value != nil { out += sh.export(key, *value) } } return out, nil } func (sh systemdShell) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func cutEncapsulated(valueToTest, encapsulatingValue string) (cutValue string, wasEncapsulated bool) { withoutPrefix, startsWithEncapsulatingValue := strings.CutPrefix(valueToTest, encapsulatingValue) if startsWithEncapsulatingValue { withoutPrefixAndSuffix, endsWithEncapsulatingValue := strings.CutSuffix(withoutPrefix, encapsulatingValue) if endsWithEncapsulatingValue { return withoutPrefixAndSuffix, true } } return valueToTest, false } func sanitizeValue(value string) string { containSpecialChar := false specialCharacterList := []string{"\n", "\\", `"`, `'`} for _, specialChar := range specialCharacterList { if strings.ContainsAny(value, specialChar) { containSpecialChar = true } } sanitizedValue := value if containSpecialChar { // Since the value contains special characters it needs to be quoted valueWithoutEncapsulation, encapsulatedBySingleQuotes := cutEncapsulated(value, `'`) if encapsulatedBySingleQuotes { sanitizedValue = `'` + strings.ReplaceAll(valueWithoutEncapsulation, `'`, `\'`) + `'` } else { valueWithoutEncapsulation, encapsulatedByDoubleQuotes := cutEncapsulated(value, `"`) logDebug("encapsulated by double quotes : %v", encapsulatedByDoubleQuotes) sanitizedValue = `"` + strings.ReplaceAll(valueWithoutEncapsulation, `"`, `\"`) + `"` } } // if the value doesn't contains special characters then we don't touch it return sanitizedValue } func (sh systemdShell) export(key, value string) string { return key + "=" + sanitizeValue(value) + "\n" } direnv-2.37.1/internal/cmd/shell_systemd_test.go000066400000000000000000000045411503714164100217250ustar00rootroot00000000000000package cmd import ( "fmt" "strings" "testing" ) func StringPtr(value string) *string { tmp := value return &tmp } func TestCutEncapsulated_ok(t *testing.T) { input := "\"encapsulated string with \n line return\"" cutInput, isEncapsulated := cutEncapsulated(input, "\"") if !isEncapsulated { t.Error("Test TestCutEncapsulated_ok failing.") } assertEqual(t, "encapsulated string with \n line return", cutInput) } func TestExport_ok(t *testing.T) { env := Env{ "Key": " just a Value", "Ex1": `'single quotes ' works like that'`, "Ex2": `however, you can't use quotes inline`, "Ex3": `double quotes " are doing the "same" way`, "Ex4": `quotes allows escapes: \n \x`, "Ex5": `quotes are doing trick for chars: ", \, $`, "Ex6": `naked values allows escapes: \a, \b, \c`, "Ex7": `and even \$, and even at the beginning`, "Ex8": `\x all the rest`, "Ex9": `in naked values backslash \allows splitting values`, "Ex10": `quotes\nallow multi lines`, "Ex11": `'with single quotes around it and '' single quotes in it '''`, "Ex12": `"with quotes around it and quotes "" in "" it"`, } systemdExporter := Systemd actualOutput, err := env.ToShell(systemdExporter) if err != nil { t.Fatalf("ToShell() failed: %v", err) } expectedOutputMap := map[string]string{ "Key": " just a Value", "Ex1": "'single quotes \\' works like that'", "Ex2": "\"however, you can't use quotes inline\"", "Ex3": "\"double quotes \\\" are doing the \\\"same\\\" way\"", "Ex4": `"quotes allows escapes: \n \x"`, "Ex5": "\"quotes are doing trick for chars: \\\", \\, $\"", "Ex6": "\"naked values allows escapes: \\a, \\b, \\c\"", "Ex7": "\"and even \\$, and even at the beginning\"", "Ex8": "\"\\x all the rest\"", "Ex9": "\"in naked values backslash \\allows splitting values\"", "Ex10": `"quotes\nallow multi lines"`, "Ex11": `'with single quotes around it and \'\' single quotes in it \'\''`, "Ex12": `"with quotes around it and quotes \"\" in \"\" it"`, } for key, expectedValue := range expectedOutputMap { _, afterPrefix, PrefixFound := strings.Cut(actualOutput, key+"=") actualValue, _, SuffixFound := strings.Cut(afterPrefix, "\n") if !PrefixFound || !SuffixFound { t.Error("Test for systemd shell failed, couldn't not found expected key=value") } assertEqual(t, fmt.Sprint(expectedValue), fmt.Sprint(actualValue)) } } direnv-2.37.1/internal/cmd/shell_tcsh.go000066400000000000000000000044341503714164100201400ustar00rootroot00000000000000package cmd import ( "fmt" "strings" ) type tcsh struct{} // Tcsh adds support for the tickle shell var Tcsh Shell = tcsh{} func (sh tcsh) Hook() (string, error) { return "alias precmd 'eval `{{.SelfPath}} export tcsh`'", nil } func (sh tcsh) Export(e ShellExport) (string, error) { var out string for key, value := range e { if value == nil { out += sh.unset(key) } else { out += sh.export(key, *value) } } return out, nil } func (sh tcsh) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func (sh tcsh) export(key, value string) string { if key == "PATH" { command := "set path = (" for _, path := range strings.Split(value, ":") { command += " " + sh.escape(path) } return command + " );" } return "setenv " + sh.escape(key) + " " + sh.escape(value) + " ;" } func (sh tcsh) unset(key string) string { return "unsetenv " + sh.escape(key) + " ;" } func (sh tcsh) escape(str string) string { if str == "" { return "''" } in := []byte(str) out := "" i := 0 l := len(in) hex := func(char byte) { out += fmt.Sprintf("\\x%02x", char) } backslash := func(char byte) { out += string([]byte{BACKSLASH, char}) } escaped := func(str string) { out += str } quoted := func(char byte) { out += `"` + string([]byte{char}) + `"` } literal := func(char byte) { out += string([]byte{char}) } for i < l { char := in[i] switch { case char == ACK: hex(char) case char == TAB: escaped(`\t`) case char == LF: escaped(`\n`) case char == CR: escaped(`\r`) case char == SPACE: backslash(char) case char <= US: hex(char) case char <= AMPERSTAND: quoted(char) case char == SINGLE_QUOTE: backslash(char) case char <= PLUS: quoted(char) case char <= NINE: literal(char) case char <= QUESTION: quoted(char) case char <= UPPERCASE_Z: literal(char) case char == OPEN_BRACKET: quoted(char) case char == BACKSLASH: backslash(char) case char == UNDERSCORE: literal(char) case char <= LOWERCASE_Z: literal(char) case char <= CLOSE_BRACKET: quoted(char) case char <= BACKTICK: quoted(char) case char <= TILDE: quoted(char) case char == DEL: hex(char) default: hex(char) } i++ } return out } direnv-2.37.1/internal/cmd/shell_test.go000066400000000000000000000016141503714164100201530ustar00rootroot00000000000000package cmd import ( "testing" ) func TestBashEscape(t *testing.T) { assertEqual(t, `''`, BashEscape("")) assertEqual(t, `$'escape\'quote'`, BashEscape("escape'quote")) assertEqual(t, `$'foo\r\n\tbar'`, BashEscape("foo\r\n\tbar")) assertEqual(t, `$'foo bar'`, BashEscape("foo bar")) assertEqual(t, `$'\xc3\xa9'`, BashEscape("Ê")) } func TestShellDetection(t *testing.T) { assertNotNil(t, DetectShell("-bash")) assertNotNil(t, DetectShell("-/bin/bash")) assertNotNil(t, DetectShell("-/usr/local/bin/bash")) assertNotNil(t, DetectShell("-zsh")) assertNotNil(t, DetectShell("-/bin/zsh")) assertNotNil(t, DetectShell("-/usr/local/bin/zsh")) } func assertNotNil(t *testing.T, a Shell) { if a == nil { t.Error("Expected not to be nil") } } func assertEqual(t *testing.T, expected, actual string) { if expected != actual { t.Errorf("Expected \"%v\" to equal \"%v\"", expected, actual) } } direnv-2.37.1/internal/cmd/shell_vim.go000066400000000000000000000022701503714164100177660ustar00rootroot00000000000000package cmd import ( "errors" "strings" ) type vim struct{} // Vim adds support for vim. Not really a shell but it's handy. var Vim Shell = vim{} func (sh vim) Hook() (string, error) { return "", errors.New("this feature is not supported. Install the direnv.vim plugin instead") } func (sh vim) Export(e ShellExport) (string, error) { var out string for key, value := range e { if value == nil { out += sh.unset(key) } else { out += sh.export(key, *value) } } return out, nil } func (sh vim) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func (sh vim) export(key, value string) string { return "call setenv(" + sh.escapeKey(key) + "," + sh.escapeValue(value) + ")\n" } func (sh vim) unset(key string) string { return "call setenv(" + sh.escapeKey(key) + ",v:null)\n" } // TODO: support keys with special chars or fail func (sh vim) escapeKey(str string) string { return sh.escapeValue(str) } // TODO: Make sure this escaping is valid func (sh vim) escapeValue(str string) string { replacer := strings.NewReplacer( "\n", "\\n", "'", "''", ) return "'" + replacer.Replace(str) + "'" } direnv-2.37.1/internal/cmd/shell_zsh.go000066400000000000000000000023071503714164100200000ustar00rootroot00000000000000package cmd // ZSH is a singleton instance of ZSH_T type zsh struct{} // Zsh adds support for the venerable Z shell. var Zsh Shell = zsh{} const zshHook = ` _direnv_hook() { trap -- '' SIGINT eval "$("{{.SelfPath}}" export zsh)" trap - SIGINT } typeset -ag precmd_functions if (( ! ${precmd_functions[(I)_direnv_hook]} )); then precmd_functions=(_direnv_hook $precmd_functions) fi typeset -ag chpwd_functions if (( ! ${chpwd_functions[(I)_direnv_hook]} )); then chpwd_functions=(_direnv_hook $chpwd_functions) fi ` func (sh zsh) Hook() (string, error) { return zshHook, nil } func (sh zsh) Export(e ShellExport) (string, error) { var out string for key, value := range e { if value == nil { out += sh.unset(key) } else { out += sh.export(key, *value) } } return out, nil } func (sh zsh) Dump(env Env) (string, error) { var out string for key, value := range env { out += sh.export(key, value) } return out, nil } func (sh zsh) export(key, value string) string { return "export " + sh.escape(key) + "=" + sh.escape(value) + ";" } func (sh zsh) unset(key string) string { return "unset " + sh.escape(key) + ";" } func (sh zsh) escape(str string) string { return BashEscape(str) } direnv-2.37.1/internal/cmd/stdlib.go000066400000000000000000000003351503714164100172650ustar00rootroot00000000000000package cmd import "strings" // getStdlib returns the stdlib.sh, with references to direnv replaced. func getStdlib(config *Config) string { return strings.Replace(stdlib, "$(command -v direnv)", config.SelfPath, 1) } direnv-2.37.1/main.go000066400000000000000000000007151503714164100143530ustar00rootroot00000000000000// Package main implements the direnv command-line tool. package main import ( _ "embed" "github.com/direnv/direnv/v2/internal/cmd" "os" "strings" ) var ( // Configured at compile time bashPath string //go:embed stdlib.sh stdlib string //go:embed version.txt version string ) func main() { var ( env = cmd.GetEnv() args = os.Args ) err := cmd.Main(env, args, bashPath, stdlib, strings.TrimSpace(version)) if err != nil { os.Exit(1) } } direnv-2.37.1/man/000077500000000000000000000000001503714164100136505ustar00rootroot00000000000000direnv-2.37.1/man/direnv-fetchurl.1000066400000000000000000000046361503714164100170440ustar00rootroot00000000000000.nh .TH DIRENV-FETCHURL 1 "2019" direnv "User Manuals" .SH NAME direnv fetchurl \- Fetch a URL to disk .SH SYNOPSIS \fIdirenv fetchurl\fP [] .SH DESCRIPTION This command downloads the given URL into a fixed disk location, based on the content of the retrieved file. .PP This has been introduced to avoid a dependency on \fBcurl\fR or \fBwget\fR, while also promoting a more secure way to fetch data from the Internet. Use this instead of \fBcurl | sh\fR\&. .PP This command has two modes of operation: .IP " 1." 5 Just pass the URL to discover the integrity hash. .IP " 2." 5 Pass the URL \fIand\fP the integrity hash to get back the on-disk location. .PP Since the on-disk location is based on the hash, it also acts as a cache. One implication of this design is that URLs must be stable and always return the same content. .PP Downloaded files are marked as read-only and executable so it can also be used to fetch and execute static binaries. .SH OPTIONS A HTTP URL that returns content on HTTP GET requests. 301 and other redirects are followed. .PP When passed, the integrity of the retrieved content will be validated against the given hash. The hash encoding is based on the SRI W3C specification (see https://www.w3.org/TR/SRI/ ). .SH OUTPUT \fIdirenv fetchurl\fP outputs different content based on the argument. .PP If the \fBintegrity-hash\fR is being passed, it will output the path to the on-disk location, if the retrieval was successful. .PP If only the \fBurl\fR is being passed, it will output the hash as well as some human-readable instruction. If stdout is not a tty, only the hash will be displayed. .SH EXAMPLE .EX $ ./direnv fetchurl https://releases.nixos.org/nix/nix-2.3.7/install Found hash: sha256-7Gxl5GzI9juc/U30Igh/pY+p6+gj5Waohfwql3jHIds= Invoke fetchurl again with the hash as an argument to get the disk location: direnv fetchurl "https://releases.nixos.org/nix/nix-2.3.7/install" "sha256-7Gxl5GzI9juc/U30Igh/pY+p6+gj5Waohfwql3jHIds=" #=> /home/zimbatm/.cache/direnv/cas/sha256-7Gxl5GzI9juc_U30Igh_pY+p6+gj5Waohfwql3jHIds= .EE .SH ENVIRONMENT VARIABLES \fBXDG_CACHE_HOME\fP This variable is used to select the on-disk location of the fetched URLs as \fB$XDG_CACHE_HOME/direnv/cas\fR\&. If \fBXDG_CACHE_HOME\fP is unset or empty, defaults to \fB$HOME/.cache\fR\&. .SH COPYRIGHT MIT licence - Copyright (C) 2019 @zimbatm and contributors .SH SEE ALSO direnv-stdlib(1) direnv-2.37.1/man/direnv-fetchurl.1.md000066400000000000000000000047311503714164100174370ustar00rootroot00000000000000DIRENV-FETCHURL 1 "2019" direnv "User Manuals" ============================================== NAME ---- direnv fetchurl - Fetch a URL to disk SYNOPSIS -------- *direnv fetchurl* [] DESCRIPTION ----------- This command downloads the given URL into a fixed disk location, based on the content of the retrieved file. This has been introduced to avoid a dependency on `curl` or `wget`, while also promoting a more secure way to fetch data from the Internet. Use this instead of `curl | sh`. This command has two modes of operation: 1. Just pass the URL to discover the integrity hash. 2. Pass the URL *and* the integrity hash to get back the on-disk location. Since the on-disk location is based on the hash, it also acts as a cache. One implication of this design is that URLs must be stable and always return the same content. Downloaded files are marked as read-only and executable so it can also be used to fetch and execute static binaries. OPTIONS ------- A HTTP URL that returns content on HTTP GET requests. 301 and other redirects are followed. When passed, the integrity of the retrieved content will be validated against the given hash. The hash encoding is based on the SRI W3C specification (see https://www.w3.org/TR/SRI/ ). OUTPUT ------ *direnv fetchurl* outputs different content based on the argument. If the `integrity-hash` is being passed, it will output the path to the on-disk location, if the retrieval was successful. If only the `url` is being passed, it will output the hash as well as some human-readable instruction. If stdout is not a tty, only the hash will be displayed. EXAMPLE ------- $ ./direnv fetchurl https://releases.nixos.org/nix/nix-2.3.7/install Found hash: sha256-7Gxl5GzI9juc/U30Igh/pY+p6+gj5Waohfwql3jHIds= Invoke fetchurl again with the hash as an argument to get the disk location: direnv fetchurl "https://releases.nixos.org/nix/nix-2.3.7/install" "sha256-7Gxl5GzI9juc/U30Igh/pY+p6+gj5Waohfwql3jHIds=" #=> /home/zimbatm/.cache/direnv/cas/sha256-7Gxl5GzI9juc_U30Igh_pY+p6+gj5Waohfwql3jHIds= ENVIRONMENT VARIABLES --------------------- **XDG_CACHE_HOME** This variable is used to select the on-disk location of the fetched URLs as `$XDG_CACHE_HOME/direnv/cas`. If **XDG_CACHE_HOME** is unset or empty, defaults to `$HOME/.cache`. COPYRIGHT --------- MIT licence - Copyright (C) 2019 @zimbatm and contributors SEE ALSO -------- direnv-stdlib(1) direnv-2.37.1/man/direnv-stdlib.1000066400000000000000000000322201503714164100164770ustar00rootroot00000000000000.nh .TH DIRENV-STDLIB 1 "2019" direnv "User Manuals" .SH NAME direnv-stdlib \- functions for the \fB\&.envrc\fR .SH SYNOPSIS \fBdirenv stdlib\fR .SH DESCRIPTION Outputs a bash script called the \fIstdlib\fP\&. The following commands are included in that script and loaded in the context of an \fB\&.envrc\fR\&. In addition, it also loads the file in \fB~/.config/direnv/direnvrc\fR if it exists. .SH STDLIB .SS \fBhas \fR Returns 0 if the \fIcommand\fP is available. Returns 1 otherwise. It can be a binary in the PATH or a shell function. .PP Example: .EX if has curl; then echo "Yes we do" fi .EE .SS \fBexpand_path []\fR Outputs the absolute path of \fIrel_path\fP relative to \fIrelative_to\fP or the current directory. .PP Example: .EX cd /usr/local/games expand_path ../foo # output: /usr/local/foo .EE .SS \fBdotenv []\fR Loads a ".env" file into the current environment. .SS \fBdotenv_if_exists []\fR Loads a ".env" file into the current environment, but only if it exists. .SS \fBuser_rel_path \fR Transforms an absolute path \fIabs_path\fP into a user-relative path if possible. .PP Example: .EX echo $HOME # output: /home/user user_rel_path /home/user/my/project # output: ~/my/project user_rel_path /usr/local/lib # output: /usr/local/lib .EE .SS \fBfind_up \fR Outputs the path of \fIfilename\fP when searched from the current directory up to /. Returns 1 if the file has not been found. .PP Example: .EX cd /usr/local/my mkdir -p project/foo touch bar cd project/foo find_up bar # output: /usr/local/my/bar .EE .SS \fBsource_env \fR Loads another \fB\&.envrc\fR either by specifying its path or filename. .PP NOTE: the other \fB\&.envrc\fR is not checked by the security framework. .SS \fBsource_env_if_exists \fR Loads another ".envrc", but only if it exists. .PP NOTE: contrary to \fBsource_env\fR, this only works when passing a path to a file, not a directory. .PP Example: .EX source_env_if_exists .envrc.private .EE .SS \fBenv_vars_required [ ...]\fR Logs error for every variable not present in the environment or having an empty value. .br Typically this is used in combination with source_env and source_env_if_exists. .PP Example: .EX # expect .envrc.private to provide tokens source_env .envrc.private # check presence of tokens env_vars_required GITHUB_TOKEN OTHER_TOKEN .EE .SS \fBsource_up []\fR Loads another \fB\&.envrc\fR if found with the find_up command. Returns 1 if no file is found. .PP NOTE: the other \fB\&.envrc\fR is not checked by the security framework. .SS \fBsource_up_if_exists []\fR Loads another \fB\&.envrc\fR if found with the find_up command. If one is not found, nothing happens. .PP NOTE: the other \fB\&.envrc\fR is not checked by the security framework. .SS \fBsource_url \fR Loads another script from the given \fBurl\fR\&. Before loading it will check the integrity using the provided \fBintegrity-hash\fR\&. .PP To find the value of the \fBintegrity-hash\fR, call \fBdirenv fetchurl \fR and extract the hash from the outputted message. .PP See also \fBdirenv-fetchurl(1)\fR for more details. .SS \fBfetchurl []\fR Fetches the given \fBurl\fR onto disk and outputs its path location on stdout. .PP If the \fBintegrity-hash\fR argument is provided, it will also check the integrity of the script. .PP See also \fBdirenv-fetchurl(1)\fR for more details. .SS \fBdirenv_apply_dump \fR Loads the output of \fBdirenv dump\fR that was stored in a file. .SS \fBdirenv_load []\fR Applies the environment generated by running \fIargv\fP as a command. This is useful for adopting the environment of a child process - cause that process to run "direnv dump" and then wrap the results with direnv_load. .PP Example: .EX direnv_load opam exec direnv dump .EE .SS \fBPATH_add \fR Prepends the expanded \fIpath\fP to the PATH environment variable. It prevents a common mistake where PATH is replaced by only the new \fIpath\fP\&. .PP Example: .EX pwd # output: /home/user/my/project PATH_add bin echo $PATH # output: /home/user/my/project/bin:/usr/bin:/bin .EE .SS \fBMANPATH_add \fR Prepends the expanded \fIpath\fP to the MANPATH environment variable. It takes care of man-specific heuritic. .SS \fBpath_add \fR Works like \fBPATH_add\fR except that it's for an arbitrary \fIvarname\fP\&. .SS \fBPATH_rm [ ...]\fR Removes directories that match any of the given shell patterns from the PATH environment variable. Order of the remaining directories is preserved in the resulting PATH. .PP Bash pattern syntax: https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html .PP Example: .EX echo $PATH # output: /dontremove/me:/remove/me:/usr/local/bin/:... PATH_rm '/remove/*' echo $PATH # output: /dontremove/me:/usr/local/bin/:... .EE .SS \fBload_prefix \fR Expands some common path variables for the given \fIprefix_path\fP prefix. This is useful if you installed something in the \fIprefix_path\fP using \fB\&./configure --prefix=$prefix_path && make install\fR and want to use it in the project. .PP Variables set: .EX CPATH LD_LIBRARY_PATH LIBRARY_PATH MANPATH PATH PKG_CONFIG_PATH .EE .PP Example: .EX \&./configure --prefix=$HOME/rubies/ruby-1.9.3 make && make install # Then in the .envrc load_prefix ~/rubies/ruby-1.9.3 .EE .SS \fBsemver_search \fR Search a directory for the highest version number in SemVer format (X.Y.Z). .PP Examples: .EX $ tree . \&. |-- dir |-- program-1.4.0 |-- program-1.4.1 |-- program-1.5.0 $ semver_search "dir" "program-" "1.4.0" 1.4.0 $ semver_search "dir" "program-" "1.4" 1.4.1 $ semver_search "dir" "program-" "1" 1.5.0 .EE .SS \fBlayout \fR A semantic dispatch used to describe common project layouts. .SS \fBlayout go\fR Adds "$(direnv_layout_dir)/go" to the GOPATH environment variable. And also adds "$PWD/bin" to the PATH environment variable. .SS \fBlayout julia\fR Sets the \fBJULIA_PROJECT\fR environment variable to the current directory. .SS \fBlayout node\fR Adds "$PWD/node_modules/.bin" to the PATH environment variable. .SS \fBlayout opam\fR Sets environment variables from \fBopam env\fR\&. .SS \fBlayout php\fR Adds "$PWD/vendor/bin" to the PATH environment variable. .SS \fBlayout perl\fR Setup environment variables required by perl's local::lib See http://search.cpan.org/dist/local-lib/lib/local/lib.pm for more details. .SS \fBlayout pipenv\fR Similar to \fBlayout python\fR, but uses Pipenv to build a virtualenv from the \fBPipfile\fR located in the same directory. The path can be overridden by the \fBPIPENV_PIPFILE\fR environment variable. .PP Note that unlike invoking Pipenv manually, this does not load environment variables from a \fB\&.env\fR file automatically. You may want to add \fBdotenv .env\fR to copy that behavior. .SS \fBlayout pyenv [ ...]\fR Similar to \fBlayout python\fR, but uses pyenv to build a virtualenv with the specified Python interpreter version. .PP Multiple versions may be specified separated by spaces; please refer to the pyenv documentation for more information. .SS \fBlayout python []\fR Creates and loads a virtualenv environment under \fB$PWD/.direnv/python-$python_version\fR\&. This forces the installation of any egg into the project's sub-folder. .PP It's possible to specify the python executable if you want to use different versions of python (eg: \fBlayout python python3\fR). .PP Note that previously virtualenv was located under \fB$PWD/.direnv/virtualenv\fR and will be re-used by direnv if it exists. .SS \fBlayout python3\fR A shortcut for \fBlayout python python3\fR .SS \fBlayout ruby\fR Sets the GEM_HOME environment variable to \fB$PWD/.direnv/ruby/RUBY_VERSION\fR\&. This forces the installation of any gems into the project's sub-folder. If you're using bundler it will create wrapper programs that can be invoked directly instead of using the \fBbundle exec\fR prefix. .SS \fBuse []\fR A semantic command dispatch intended for loading external dependencies into the environment. .PP Example: .EX use_ruby() { echo "Ruby $1" } use ruby 1.9.3 # output: Ruby 1.9.3 .EE .SS \fBuse julia \fR Loads the specified Julia version. You must specify a path to the directory with installed Julia versions using $JULIA_VERSIONS. You can optionally override the prefix for folders inside $JULIA_VERSIONS (default \fBjulia-\fR) using $JULIA_VERSION_PREFIX. If no exact match for \fB\fR is found a search will be performed and the latest version will be loaded. .PP Examples (.envrc): .EX use julia 1.5.1 # loads $JULIA_VERSIONS/julia-1.5.1 use julia 1.5 # loads $JULIA_VERSIONS/julia-1.5.1 use julia master # loads $JULIA_VERSIONS/julia-master .EE .SS \fBuse rbenv\fR Loads rbenv which add the ruby wrappers available on the PATH. .SS \fBuse nix [...]\fR Load environment variables from \fBnix-shell\fR\&. .PP If you have a \fBdefault.nix\fR or \fBshell.nix\fR these will be used by default, but you can also specify packages directly (e.g \fBuse nix -p ocaml\fR). .PP See http://nixos.org/nix/manual/#sec-nix-shell .SS \fBuse flake []\fR Load the build environment of a derivation similar to \fBnix develop\fR\&. .PP By default it will load the current folder flake.nix devShell attribute. Or pass an "installable" like "nixpkgs#hello" to load all the build dependencies of the hello package from the latest nixpkgs. .PP Note that the flakes feature is hidden behind an experimental flag, which you will have to enable on your own. Flakes is not considered stable yet. .SS \fBuse guix [...]\fR Load environment variables from \fBguix shell\fR\&. .PP Any arguments given will be passed to guix shell. For example, \fBuse guix hello\fR would setup an environment including the hello package. To create an environment with the hello dependencies, the \fB--development\fR flag is used \fBuse guix --development hello\fR\&. Other options include \fB--file\fR which allows loading an environment from a file. .PP See https://guix.gnu.org/manual/en/guix.html#Invoking-guix-shell .SS \fBrvm [...]\fR Should work just like in the shell if you have rvm installed. .SS \fBuse node []\fR: Loads the specified NodeJS version into the environment. .PP If a partial NodeJS version is passed (i.e. \fB4.2\fR), a fuzzy match is performed and the highest matching version installed is selected. .PP If no version is passed, it will look at the '.nvmrc' or '.node-version' files in the current directory if they exist. .PP Environment Variables: .IP \(bu 2 $NODE_VERSIONS (required) Points to a folder that contains all the installed Node versions. That folder must exist. .IP \(bu 2 $NODE_VERSION_PREFIX (optional) [default="node-v"] Overrides the default version prefix. .SS \fBuse vim []\fR Prepends the specified vim script (or .vimrc.local by default) to the \fBDIRENV_EXTRA_VIMRC\fR environment variable. .PP This variable is understood by the direnv/direnv.vim extension. When found, it will source it after opening files in the directory. .SS \fBwatch_file [ ...]\fR Adds each file to direnv's watch-list. If the file changes direnv will reload the environment on the next prompt. .PP Example (.envrc): .EX watch_file Gemfile .EE .SS \fBdirenv_version \fR Checks that the direnv version is at least old as \fBversion_at_least\fR\&. This can be useful when sharing an \fB\&.envrc\fR and to make sure that the users are up to date. .SS \fBstrict_env [ ...]\fR Turns on shell execution strictness. This will force the .envrc evaluation context to exit immediately if: .IP \(bu 2 any command in a pipeline returns a non-zero exit status that is not otherwise handled as part of \fBif\fR, \fBwhile\fR, or \fBuntil\fR tests, return value negation (\fB!\fR), or part of a boolean (\fB&&\fR or \fB||\fR) chain. .IP \(bu 2 any variable that has not explicitly been set or declared (with either \fBdeclare\fR or \fBlocal\fR) is referenced. .PP If followed by a command-line, the strictness applies for the duration of the command. .PP Example (Whole Script): .EX strict_env has curl .EE .PP Example (Command): .EX strict_env has curl .EE .SS \fBunstrict_env [ ...]\fR Turns off shell execution strictness. If followed by a command-line, the strictness applies for the duration of the command. .PP Example (Whole Script): .EX unstrict_env has curl .EE .PP Example (Command): .EX unstrict_env has curl .EE .SS \fBon_git_branch []\fR Returns 0 if within a git repository with given \fBbranch_name\fR\&. If no branch name is provided, then returns 0 when within \fIany\fP branch. Requires the git command to be installed. Returns 1 otherwise. .PP When a branch is specified, then \fB\&.git/HEAD\fR is watched so that entering/exiting a branch triggers a reload. .PP Example (.envrc): .EX if on_git_branch child_changes; then export MERGE_BASE_BRANCH=parent_changes fi if on_git_branch; then echo "Thanks for contributing to a GitHub project!" fi .EE .SH COPYRIGHT MIT licence - Copyright (C) 2019 @zimbatm and contributors .SH SEE ALSO direnv(1), direnv.toml(1) direnv-2.37.1/man/direnv-stdlib.1.md000066400000000000000000000312601503714164100171010ustar00rootroot00000000000000DIRENV-STDLIB 1 "2019" direnv "User Manuals" ============================================ NAME ---- direnv-stdlib - functions for the `.envrc` SYNOPSIS -------- `direnv stdlib` DESCRIPTION ----------- Outputs a bash script called the *stdlib*. The following commands are included in that script and loaded in the context of an `.envrc`. In addition, it also loads the file in `~/.config/direnv/direnvrc` if it exists. STDLIB ------ ### `has ` Returns 0 if the *command* is available. Returns 1 otherwise. It can be a binary in the PATH or a shell function. Example: if has curl; then echo "Yes we do" fi ### `expand_path []` Outputs the absolute path of *rel_path* relative to *relative_to* or the current directory. Example: cd /usr/local/games expand_path ../foo # output: /usr/local/foo ### `dotenv []` Loads a ".env" file into the current environment. ### `dotenv_if_exists []` Loads a ".env" file into the current environment, but only if it exists. ### `user_rel_path ` Transforms an absolute path *abs_path* into a user-relative path if possible. Example: echo $HOME # output: /home/user user_rel_path /home/user/my/project # output: ~/my/project user_rel_path /usr/local/lib # output: /usr/local/lib ### `find_up ` Outputs the path of *filename* when searched from the current directory up to /. Returns 1 if the file has not been found. Example: cd /usr/local/my mkdir -p project/foo touch bar cd project/foo find_up bar # output: /usr/local/my/bar ### `source_env ` Loads another `.envrc` either by specifying its path or filename. NOTE: the other `.envrc` is not checked by the security framework. ### `source_env_if_exists ` Loads another ".envrc", but only if it exists. NOTE: contrary to `source_env`, this only works when passing a path to a file, not a directory. Example: source_env_if_exists .envrc.private ### `env_vars_required [ ...]` Logs error for every variable not present in the environment or having an empty value. Typically this is used in combination with source_env and source_env_if_exists. Example: # expect .envrc.private to provide tokens source_env .envrc.private # check presence of tokens env_vars_required GITHUB_TOKEN OTHER_TOKEN ### `source_up []` Loads another `.envrc` if found with the find_up command. Returns 1 if no file is found. NOTE: the other `.envrc` is not checked by the security framework. ### `source_up_if_exists []` Loads another `.envrc` if found with the find_up command. If one is not found, nothing happens. NOTE: the other `.envrc` is not checked by the security framework. ### `source_url ` Loads another script from the given `url`. Before loading it will check the integrity using the provided `integrity-hash`. To find the value of the `integrity-hash`, call `direnv fetchurl ` and extract the hash from the outputted message. See also `direnv-fetchurl(1)` for more details. ### `fetchurl []` Fetches the given `url` onto disk and outputs its path location on stdout. If the `integrity-hash` argument is provided, it will also check the integrity of the script. See also `direnv-fetchurl(1)` for more details. ### `direnv_apply_dump ` Loads the output of `direnv dump` that was stored in a file. ### `direnv_load []` Applies the environment generated by running *argv* as a command. This is useful for adopting the environment of a child process - cause that process to run "direnv dump" and then wrap the results with direnv_load. Example: direnv_load opam exec direnv dump ### `PATH_add ` Prepends the expanded *path* to the PATH environment variable. It prevents a common mistake where PATH is replaced by only the new *path*. Example: pwd # output: /home/user/my/project PATH_add bin echo $PATH # output: /home/user/my/project/bin:/usr/bin:/bin ### `MANPATH_add ` Prepends the expanded *path* to the MANPATH environment variable. It takes care of man-specific heuritic. ### `path_add ` Works like `PATH_add` except that it's for an arbitrary *varname*. ### `PATH_rm [ ...]` Removes directories that match any of the given shell patterns from the PATH environment variable. Order of the remaining directories is preserved in the resulting PATH. Bash pattern syntax: https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html Example: echo $PATH # output: /dontremove/me:/remove/me:/usr/local/bin/:... PATH_rm '/remove/*' echo $PATH # output: /dontremove/me:/usr/local/bin/:... ### `load_prefix ` Expands some common path variables for the given *prefix_path* prefix. This is useful if you installed something in the *prefix_path* using `./configure --prefix=$prefix_path && make install` and want to use it in the project. Variables set: CPATH LD_LIBRARY_PATH LIBRARY_PATH MANPATH PATH PKG_CONFIG_PATH Example: ./configure --prefix=$HOME/rubies/ruby-1.9.3 make && make install # Then in the .envrc load_prefix ~/rubies/ruby-1.9.3 ### `semver_search ` Search a directory for the highest version number in SemVer format (X.Y.Z). Examples: $ tree . . |-- dir |-- program-1.4.0 |-- program-1.4.1 |-- program-1.5.0 $ semver_search "dir" "program-" "1.4.0" 1.4.0 $ semver_search "dir" "program-" "1.4" 1.4.1 $ semver_search "dir" "program-" "1" 1.5.0 ### `layout ` A semantic dispatch used to describe common project layouts. ### `layout go` Adds "$(direnv_layout_dir)/go" to the GOPATH environment variable. And also adds "$PWD/bin" to the PATH environment variable. ### `layout julia` Sets the `JULIA_PROJECT` environment variable to the current directory. ### `layout node` Adds "$PWD/node_modules/.bin" to the PATH environment variable. ### `layout opam` Sets environment variables from `opam env`. ### `layout php` Adds "$PWD/vendor/bin" to the PATH environment variable. ### `layout perl` Setup environment variables required by perl's local::lib See http://search.cpan.org/dist/local-lib/lib/local/lib.pm for more details. ### `layout pipenv` Similar to `layout python`, but uses Pipenv to build a virtualenv from the `Pipfile` located in the same directory. The path can be overridden by the `PIPENV_PIPFILE` environment variable. Note that unlike invoking Pipenv manually, this does not load environment variables from a `.env` file automatically. You may want to add `dotenv .env` to copy that behavior. ### `layout pyenv [ ...]` Similar to `layout python`, but uses pyenv to build a virtualenv with the specified Python interpreter version. Multiple versions may be specified separated by spaces; please refer to the pyenv documentation for more information. ### `layout python []` Creates and loads a virtualenv environment under `$PWD/.direnv/python-$python_version`. This forces the installation of any egg into the project's sub-folder. It's possible to specify the python executable if you want to use different versions of python (eg: `layout python python3`). Note that previously virtualenv was located under `$PWD/.direnv/virtualenv` and will be re-used by direnv if it exists. ### `layout python3` A shortcut for `layout python python3` ### `layout ruby` Sets the GEM_HOME environment variable to `$PWD/.direnv/ruby/RUBY_VERSION`. This forces the installation of any gems into the project's sub-folder. If you're using bundler it will create wrapper programs that can be invoked directly instead of using the `bundle exec` prefix. ### `use []` A semantic command dispatch intended for loading external dependencies into the environment. Example: use_ruby() { echo "Ruby $1" } use ruby 1.9.3 # output: Ruby 1.9.3 ### `use julia ` Loads the specified Julia version. You must specify a path to the directory with installed Julia versions using $JULIA_VERSIONS. You can optionally override the prefix for folders inside $JULIA_VERSIONS (default `julia-`) using $JULIA_VERSION_PREFIX. If no exact match for `` is found a search will be performed and the latest version will be loaded. Examples (.envrc): use julia 1.5.1 # loads $JULIA_VERSIONS/julia-1.5.1 use julia 1.5 # loads $JULIA_VERSIONS/julia-1.5.1 use julia master # loads $JULIA_VERSIONS/julia-master ### `use rbenv` Loads rbenv which add the ruby wrappers available on the PATH. ### `use nix [...]` Load environment variables from `nix-shell`. If you have a `default.nix` or `shell.nix` these will be used by default, but you can also specify packages directly (e.g `use nix -p ocaml`). See http://nixos.org/nix/manual/#sec-nix-shell ### `use flake []` Load the build environment of a derivation similar to `nix develop`. By default it will load the current folder flake.nix devShell attribute. Or pass an "installable" like "nixpkgs#hello" to load all the build dependencies of the hello package from the latest nixpkgs. Note that the flakes feature is hidden behind an experimental flag, which you will have to enable on your own. Flakes is not considered stable yet. ### `use guix [...]` Load environment variables from `guix shell`. Any arguments given will be passed to guix shell. For example, `use guix hello` would setup an environment including the hello package. To create an environment with the hello dependencies, the `--development` flag is used `use guix --development hello`. Other options include `--file` which allows loading an environment from a file. See https://guix.gnu.org/manual/en/guix.html#Invoking-guix-shell ### `rvm [...]` Should work just like in the shell if you have rvm installed. ### `use node []`: Loads the specified NodeJS version into the environment. If a partial NodeJS version is passed (i.e. `4.2`), a fuzzy match is performed and the highest matching version installed is selected. If no version is passed, it will look at the '.nvmrc' or '.node-version' files in the current directory if they exist. Environment Variables: - $NODE_VERSIONS (required) Points to a folder that contains all the installed Node versions. That folder must exist. - $NODE_VERSION_PREFIX (optional) [default="node-v"] Overrides the default version prefix. ### `use vim []` Prepends the specified vim script (or .vimrc.local by default) to the `DIRENV_EXTRA_VIMRC` environment variable. This variable is understood by the direnv/direnv.vim extension. When found, it will source it after opening files in the directory. ### `watch_file [ ...]` Adds each file to direnv's watch-list. If the file changes direnv will reload the environment on the next prompt. Example (.envrc): watch_file Gemfile ### `direnv_version ` Checks that the direnv version is at least old as `version_at_least`. This can be useful when sharing an `.envrc` and to make sure that the users are up to date. ### `strict_env [ ...]` Turns on shell execution strictness. This will force the .envrc evaluation context to exit immediately if: - any command in a pipeline returns a non-zero exit status that is not otherwise handled as part of `if`, `while`, or `until` tests, return value negation (`!`), or part of a boolean (`&&` or `||`) chain. - any variable that has not explicitly been set or declared (with either `declare` or `local`) is referenced. If followed by a command-line, the strictness applies for the duration of the command. Example (Whole Script): strict_env has curl Example (Command): strict_env has curl #### `unstrict_env [ ...]` Turns off shell execution strictness. If followed by a command-line, the strictness applies for the duration of the command. Example (Whole Script): unstrict_env has curl Example (Command): unstrict_env has curl ### `on_git_branch []` Returns 0 if within a git repository with given `branch_name`. If no branch name is provided, then returns 0 when within _any_ branch. Requires the git command to be installed. Returns 1 otherwise. When a branch is specified, then `.git/HEAD` is watched so that entering/exiting a branch triggers a reload. Example (.envrc): if on_git_branch child_changes; then export MERGE_BASE_BRANCH=parent_changes fi if on_git_branch; then echo "Thanks for contributing to a GitHub project!" fi COPYRIGHT --------- MIT licence - Copyright (C) 2019 @zimbatm and contributors SEE ALSO -------- direnv(1), direnv.toml(1) direnv-2.37.1/man/direnv.1000066400000000000000000000147301503714164100152260ustar00rootroot00000000000000.nh .TH DIRENV 1 "2019" direnv "User Manuals" .SH NAME direnv \- unclutter your .profile .SH SYNOPSIS \fBdirenv\fR \fIcommand\fP ... .SH DESCRIPTION \fBdirenv\fR is an environment variable manager for your shell. It knows how to hook into bash, zsh and fish shell to load or unload environment variables depending on your current directory. This allows you to have project-specific environment variables and not clutter the "~/.profile" file. .PP Before each prompt it checks for the existence of an \fB\&.envrc\fR file in the current and parent directories. If the file exists, it is loaded into a bash sub-shell and all exported variables are then captured by direnv and then made available to your current shell, while unset variables are removed. .PP Because direnv is compiled into a single static executable it is fast enough to be unnoticeable on each prompt. It is also language agnostic and can be used to build solutions similar to rbenv, pyenv, phpenv, ... .SH EXAMPLE .EX $ cd ~/my_project $ echo ${FOO-nope} nope $ echo export FOO=foo > .envrc \\.envrc is not allowed $ direnv allow . direnv: reloading direnv: loading .envrc direnv export: +FOO $ echo ${FOO-nope} foo $ cd .. direnv: unloading direnv export: ~PATH $ echo ${FOO-nope} nope .EE .SH SETUP For direnv to work properly it needs to be hooked into the shell. Each shell has its own extension mechanism: .SS BASH Add the following line at the end of the \fB~/.bashrc\fR file: .EX eval "$(direnv hook bash)" .EE .PP Make sure it appears even after rvm, git-prompt and other shell extensions that manipulate the prompt. .SS ZSH Add the following line at the end of the \fB~/.zshrc\fR file: .EX eval "$(direnv hook zsh)" .EE .SS FISH Add the following line at the end of the \fB$XDG_CONFIG_HOME/fish/config.fish\fR file: .EX direnv hook fish | source .EE .PP Fish supports 3 modes you can set with the global environment variable \fBdirenv_fish_mode\fR: .EX set -g direnv_fish_mode eval_on_arrow # trigger direnv at prompt, and on every arrow-based directory change (default) set -g direnv_fish_mode eval_after_arrow # trigger direnv at prompt, and only after arrow-based directory changes before executing command set -g direnv_fish_mode disable_arrow # trigger direnv at prompt only, this is similar functionality to the original behavior .EE .SS TCSH Add the following line at the end of the \fB~/.cshrc\fR file: .EX eval `direnv hook tcsh` .EE .SS Elvish Run: .EX ~> mkdir -p ~/.config/elvish/lib ~> direnv hook elvish > ~/.config/elvish/lib/direnv.elv .EE .PP and add the following line to your \fB~/.config/elvish/rc.elv\fR file: .EX use direnv .EE .SS PowerShell Add the following line to your \fB$PROFILE\fR: .EX Invoke-Expression "$(direnv hook pwsh)" .EE .SS Murex Add the following line to your \fB~/.murex_profile\fR: .EX direnv hook murex -> source .EE .SH COMMANDS .TP \fBdirenv allow [PATH_TO_RC]\fR Grants direnv permission to load the given .envrc or .env file. .TP \fBdirenv deny [PATH_TO_RC]\fR Revokes the authorization of a given .envrc or .env file. .TP \fBdirenv edit [PATH_TO_RC]\fR Opens PATH_TO_RC or the current .envrc or .env into an $EDITOR and allow the file to be loaded afterwards. .TP \fBdirenv exec DIR COMMAND [...ARGS]\fR Executes a command after loading the first .envrc or .env found in DIR. .TP \fBdirenv export SHELL\fR Loads an .envrc or .env and prints the diff in terms of exports. Supported shells: bash, zsh, fish, tcsh, elvish, pwsh, murex, json, vim, gha (GitHub Actions), gzenv, systemd. .TP \fBdirenv fetchurl []\fR Fetches a given URL into direnv's CAS. .TP \fBdirenv help\fR Shows this help. .TP \fBdirenv hook SHELL\fR Used to setup the shell hook. .TP \fBdirenv prune\fR Removes old allowed files. .TP \fBdirenv reload\fR Triggers an env reload. .TP \fBdirenv status\fR Prints some debug status information. .TP \fBdirenv stdlib\fR Displays the stdlib available in the .envrc execution context. .TP \fBdirenv version\fR Prints the version or checks that direnv is older than VERSION_AT_LEAST. .SH USAGE In some target folder, create an \fB\&.envrc\fR file and add some export(1) and unset(1) directives in it. .PP On the next prompt you will notice that direnv complains about the \fB\&.envrc\fR being blocked. This is the security mechanism to avoid loading new files automatically. Otherwise any git repo that you pull, or tar archive that you unpack, would be able to wipe your hard drive once you \fBcd\fR into it. .PP So here we are pretty sure that it won't do anything bad. Type \fBdirenv allow .\fR and watch direnv loading your new environment. Note that \fBdirenv edit .\fR is a handy shortcut that opens the file in your $EDITOR and automatically reloads it if the file's modification time has changed. .PP Now that the environment is loaded you can notice that once you \fBcd\fR out of the directory it automatically gets unloaded. If you \fBcd\fR back into it it's loaded again. That's the base of the mechanism that allows you to build cool things. .PP Exporting variables by hand is a bit repetitive so direnv provides a set of utility functions that are made available in the context of the \fB\&.envrc\fR file. Check the direnv-stdlib(1) man page for more details. You can also define your own extensions inside \fB$XDG_CONFIG_HOME/direnv/direnvrc\fR or \fB$XDG_CONFIG_HOME/direnv/lib/*.sh\fR files. .PP Hopefully this is enough to get you started. .SH ENVIRONMENT .TP \fBXDG_CONFIG_HOME\fR Defaults to \fB$HOME/.config\fR\&. .TP \fBXDG_DATA_HOME\fR Defaults to \fB$HOME/.local/share\fR\&. .SH FILES .TP \fB$XDG_CONFIG_HOME/direnv/direnv.toml\fR Direnv configuration. See direnv.toml(1). .TP \fB$XDG_CONFIG_HOME/direnv/direnvrc\fR Bash code loaded before every \fB\&.envrc\fR\&. Good for personal extensions. .TP \fB$XDG_CONFIG_HOME/direnv/lib/*.sh\fR Bash code loaded before every \fB\&.envrc\fR\&. Good for third-party extensions. .TP \fB$XDG_DATA_HOME/direnv/allow\fR Records which \fB\&.envrc\fR files have been \fBdirenv allow\fRed. .SH CONTRIBUTE Bug reports, contributions and forks are welcome. .PP All bugs or other forms of discussion happen on \[la]http://github.com/direnv/direnv/issues\[ra] .PP There is also a wiki available where you can share your usage patterns or other tips and tricks \[la]https://github.com/direnv/direnv/wiki\[ra] .PP Or drop by on the #direnv channel on FreeNode \[la]irc://#direnv@FreeNode\[ra] to have a chat. .SH COPYRIGHT MIT licence - Copyright (C) 2019 @zimbatm and contributors .SH SEE ALSO direnv-stdlib(1), direnv.toml(1), direnv-fetchurl(1) direnv-2.37.1/man/direnv.1.md000066400000000000000000000144321503714164100156240ustar00rootroot00000000000000DIRENV 1 "2019" direnv "User Manuals" =========================================== NAME ---- direnv - unclutter your .profile SYNOPSIS -------- `direnv` *command* ... DESCRIPTION ----------- `direnv` is an environment variable manager for your shell. It knows how to hook into bash, zsh and fish shell to load or unload environment variables depending on your current directory. This allows you to have project-specific environment variables and not clutter the "~/.profile" file. Before each prompt it checks for the existence of an `.envrc` file in the current and parent directories. If the file exists, it is loaded into a bash sub-shell and all exported variables are then captured by direnv and then made available to your current shell, while unset variables are removed. Because direnv is compiled into a single static executable it is fast enough to be unnoticeable on each prompt. It is also language agnostic and can be used to build solutions similar to rbenv, pyenv, phpenv, ... EXAMPLE ------- ``` $ cd ~/my_project $ echo ${FOO-nope} nope $ echo export FOO=foo > .envrc \.envrc is not allowed $ direnv allow . direnv: reloading direnv: loading .envrc direnv export: +FOO $ echo ${FOO-nope} foo $ cd .. direnv: unloading direnv export: ~PATH $ echo ${FOO-nope} nope ``` SETUP ----- For direnv to work properly it needs to be hooked into the shell. Each shell has its own extension mechanism: ### BASH Add the following line at the end of the `~/.bashrc` file: ```sh eval "$(direnv hook bash)" ``` Make sure it appears even after rvm, git-prompt and other shell extensions that manipulate the prompt. ### ZSH Add the following line at the end of the `~/.zshrc` file: ```sh eval "$(direnv hook zsh)" ``` ### FISH Add the following line at the end of the `$XDG_CONFIG_HOME/fish/config.fish` file: ```fish direnv hook fish | source ``` Fish supports 3 modes you can set with the global environment variable `direnv_fish_mode`: ```fish set -g direnv_fish_mode eval_on_arrow # trigger direnv at prompt, and on every arrow-based directory change (default) set -g direnv_fish_mode eval_after_arrow # trigger direnv at prompt, and only after arrow-based directory changes before executing command set -g direnv_fish_mode disable_arrow # trigger direnv at prompt only, this is similar functionality to the original behavior ``` ### TCSH Add the following line at the end of the `~/.cshrc` file: ```sh eval `direnv hook tcsh` ``` ### Elvish Run: ``` ~> mkdir -p ~/.config/elvish/lib ~> direnv hook elvish > ~/.config/elvish/lib/direnv.elv ``` and add the following line to your `~/.config/elvish/rc.elv` file: ``` use direnv ``` ### PowerShell Add the following line to your `$PROFILE`: ```powershell Invoke-Expression "$(direnv hook pwsh)" ``` ### Murex Add the following line to your `~/.murex_profile`: ``` direnv hook murex -> source ``` COMMANDS -------- `direnv allow [PATH_TO_RC]` : Grants direnv permission to load the given .envrc or .env file. `direnv deny [PATH_TO_RC]` : Revokes the authorization of a given .envrc or .env file. `direnv edit [PATH_TO_RC]` : Opens PATH_TO_RC or the current .envrc or .env into an $EDITOR and allow the file to be loaded afterwards. `direnv exec DIR COMMAND [...ARGS]` : Executes a command after loading the first .envrc or .env found in DIR. `direnv export SHELL` : Loads an .envrc or .env and prints the diff in terms of exports. Supported shells: bash, zsh, fish, tcsh, elvish, pwsh, murex, json, vim, gha (GitHub Actions), gzenv, systemd. `direnv fetchurl []` : Fetches a given URL into direnv's CAS. `direnv help` : Shows this help. `direnv hook SHELL` : Used to setup the shell hook. `direnv prune` : Removes old allowed files. `direnv reload` : Triggers an env reload. `direnv status` : Prints some debug status information. `direnv stdlib` : Displays the stdlib available in the .envrc execution context. `direnv version` : Prints the version or checks that direnv is older than VERSION_AT_LEAST. USAGE ----- In some target folder, create an `.envrc` file and add some export(1) and unset(1) directives in it. On the next prompt you will notice that direnv complains about the `.envrc` being blocked. This is the security mechanism to avoid loading new files automatically. Otherwise any git repo that you pull, or tar archive that you unpack, would be able to wipe your hard drive once you `cd` into it. So here we are pretty sure that it won't do anything bad. Type `direnv allow .` and watch direnv loading your new environment. Note that `direnv edit .` is a handy shortcut that opens the file in your $EDITOR and automatically reloads it if the file's modification time has changed. Now that the environment is loaded you can notice that once you `cd` out of the directory it automatically gets unloaded. If you `cd` back into it it's loaded again. That's the base of the mechanism that allows you to build cool things. Exporting variables by hand is a bit repetitive so direnv provides a set of utility functions that are made available in the context of the `.envrc` file. Check the direnv-stdlib(1) man page for more details. You can also define your own extensions inside `$XDG_CONFIG_HOME/direnv/direnvrc` or `$XDG_CONFIG_HOME/direnv/lib/*.sh` files. Hopefully this is enough to get you started. ENVIRONMENT ----------- `XDG_CONFIG_HOME` : Defaults to `$HOME/.config`. `XDG_DATA_HOME` : Defaults to `$HOME/.local/share`. FILES ----- `$XDG_CONFIG_HOME/direnv/direnv.toml` : Direnv configuration. See direnv.toml(1). `$XDG_CONFIG_HOME/direnv/direnvrc` : Bash code loaded before every `.envrc`. Good for personal extensions. `$XDG_CONFIG_HOME/direnv/lib/*.sh` : Bash code loaded before every `.envrc`. Good for third-party extensions. `$XDG_DATA_HOME/direnv/allow` : Records which `.envrc` files have been `direnv allow`ed. CONTRIBUTE ---------- Bug reports, contributions and forks are welcome. All bugs or other forms of discussion happen on There is also a wiki available where you can share your usage patterns or other tips and tricks Or drop by on the [#direnv channel on FreeNode](irc://#direnv@FreeNode) to have a chat. COPYRIGHT --------- MIT licence - Copyright (C) 2019 @zimbatm and contributors SEE ALSO -------- direnv-stdlib(1), direnv.toml(1), direnv-fetchurl(1) direnv-2.37.1/man/direnv.toml.1000066400000000000000000000120031503714164100161670ustar00rootroot00000000000000.nh .TH DIRENV.TOML 1 "2019" direnv "User Manuals" .SH NAME direnv.toml \- the direnv configuration file .SH DESCRIPTION A configuration file in TOML \[la]https://github.com/toml\-lang/toml\[ra] format to specify a variety of configuration options for direnv. The file is read from \fB$XDG_CONFIG_HOME/direnv/direnv.toml\fR (typically ~/.config/direnv/direnv.toml). .PP .RS .PP For versions v2.21.0 and below use config.toml instead of direnv.toml .RE .SH FORMAT See the TOML GitHub Repository \[la]https://github.com/toml\-lang/toml\[ra] for details about the syntax of the configuration file. .SH CONFIG The configuration is specified in sections which each have their own top-level tables \[la]https://github.com/toml\-lang/toml#table\[ra], with key/value pairs specified in each section. .PP Example: .EX [section] key = "value" .EE .PP The following sections are supported: .SH [global] .SS \fBbash_path\fR This allows one to hard-code the position of bash. It maybe be useful to set this to avoid having direnv to fail when PATH is being mutated. .SS \fBdisable_stdin\fR If set to \fBtrue\fR, stdin is disabled (redirected to /dev/null) during the \fB\&.envrc\fR evaluation. .SS \fBload_dotenv\fR .PP .RS .PP direnv >= 2.31.0 is required .RE .PP If set to \fBtrue\fR, also look for and load \fB\&.env\fR files on top of the \fB\&.envrc\fR files. If both \fB\&.envrc\fR and \fB\&.env\fR files exist, the \fB\&.envrc\fR will always be chosen first. .SS \fBstrict_env\fR If set to \fBtrue\fR, the \fB\&.envrc\fR will be loaded with \fBset -euo pipefail\fR\&. This option will be the default in the future. .SS \fBwarn_timeout\fR Specify how long to wait before warning the user that the command is taking too long to execute. Defaults to "5s". .PP A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "Âĩs"), "ms", "s", "m", "h". .PP This feature is disabled if the duration is lower or equal to zero. Will be overwritten if the environment variable \fBDIRENV_WARN_TIMEOUT\fR is set to any of the above values. .SS \fBhide_env_diff\fR Set to \fBtrue\fR to hide the diff of the environment variables when loading the \fB\&.envrc\fR\&. Defaults to \fBfalse\fR\&. .SS \fBlog_format\fR Sets the log format for direnv outputs. Set to "-" to disable normal logging. .PP .RS .PP direnv >= 2.36.0 is required .RE .SS \fBlog_filter\fR A Regexp that can be used to filter out some of the logs. .PP .RS .PP direnv >= 2.36.0 is required .RE .SH [whitelist] Specifying whitelist directives marks specific directory hierarchies or specific directories as "trusted" -- direnv will evaluate any matching .envrc files regardless of whether they have been specifically allowed. \fBThis feature should be used with great care\fP, as anyone with the ability to write files to that directory (including collaborators on VCS repositories) will be able to execute arbitrary code on your computer. .PP There are two types of whitelist directives supported: .SS \fBprefix\fR Accepts an array of strings. If any of the strings in this list are a prefix of an .envrc file's absolute path, that file will be implicitly allowed, regardless of contents or past usage of \fBdirenv allow\fR or \fBdirenv deny\fR\&. .PP Example: .EX [whitelist] prefix = [ "/home/user/code/project-a", "~/code/project-b" ] .EE .PP In this example, the following .envrc files will be implicitly allowed: .IP \(bu 2 \fB/home/user/code/project-a/.envrc\fR .IP \(bu 2 \fB/home/user/code/project-a/subdir/.envrc\fR .IP \(bu 2 \fB~/code/project-b/.envrc\fR .IP \(bu 2 \fB~/code/project-b/subdir/.envrc\fR .IP \(bu 2 and so on .PP In this example, the following .envrc files will not be implicitly allowed (although they can be explicitly allowed by running \fBdirenv allow\fR): .IP \(bu 2 \fB/home/user/project-c/.envrc\fR .IP \(bu 2 \fB/opt/random/.envrc\fR .SS \fBexact\fR Accepts an array of strings. Each string can be a directory name or the full path to an .envrc file. If a directory name is passed, it will be treated as if it had been passed as itself with \fB/.envrc\fR appended. After resolving the filename, each string will be checked for being an exact match with an .envrc file's absolute path. If they match exactly, that .envrc file will be implicitly allowed, regardless of contents or past usage of \fBdirenv allow\fR or \fBdirenv deny\fR\&. .PP Example: .EX [whitelist] exact = [ "/home/user/project-a/.envrc", "~/project-b/subdir-a" ] .EE .PP In this example, the following .envrc files will be implicitly allowed, and no others: .IP \(bu 2 \fB/home/user/code/project-a/.envrc\fR .IP \(bu 2 \fB~/project-b/subdir-a\fR .PP In this example, the following .envrc files will not be implicitly allowed (although they can be explicitly allowed by running \fBdirenv allow\fR): .IP \(bu 2 \fB/home/user/code/project-b/subproject-c/.envrc\fR .IP \(bu 2 \fB~/code/.envrc\fR .SH COPYRIGHT MIT licence - Copyright (C) 2019 @zimbatm and contributors .SH SEE ALSO direnv(1), direnv-stdlib(1) direnv-2.37.1/man/direnv.toml.1.md000066400000000000000000000112551503714164100165760ustar00rootroot00000000000000DIRENV.TOML 1 "2019" direnv "User Manuals" ========================================== NAME ---- direnv.toml - the direnv configuration file DESCRIPTION ----------- A configuration file in [TOML](https://github.com/toml-lang/toml) format to specify a variety of configuration options for direnv. The file is read from `$XDG_CONFIG_HOME/direnv/direnv.toml` (typically ~/.config/direnv/direnv.toml). > For versions v2.21.0 and below use config.toml instead of direnv.toml FORMAT ------ See the [TOML GitHub Repository](https://github.com/toml-lang/toml) for details about the syntax of the configuration file. CONFIG ------ The configuration is specified in sections which each have their own top-level [tables](https://github.com/toml-lang/toml#table), with key/value pairs specified in each section. Example: ```toml [section] key = "value" ``` The following sections are supported: ## [global] ### `bash_path` This allows one to hard-code the position of bash. It maybe be useful to set this to avoid having direnv to fail when PATH is being mutated. ### `disable_stdin` If set to `true`, stdin is disabled (redirected to /dev/null) during the `.envrc` evaluation. ### `load_dotenv` > direnv >= 2.31.0 is required If set to `true`, also look for and load `.env` files on top of the `.envrc` files. If both `.envrc` and `.env` files exist, the `.envrc` will always be chosen first. ### `strict_env` If set to `true`, the `.envrc` will be loaded with `set -euo pipefail`. This option will be the default in the future. ### `warn_timeout` Specify how long to wait before warning the user that the command is taking too long to execute. Defaults to "5s". A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "Âĩs"), "ms", "s", "m", "h". This feature is disabled if the duration is lower or equal to zero. Will be overwritten if the environment variable `DIRENV_WARN_TIMEOUT` is set to any of the above values. ### `hide_env_diff` Set to `true` to hide the diff of the environment variables when loading the `.envrc`. Defaults to `false`. ### `log_format` Sets the log format for direnv outputs. Set to "-" to disable normal logging. > direnv >= 2.36.0 is required ### `log_filter` A Regexp that can be used to filter out some of the logs. > direnv >= 2.36.0 is required ## [whitelist] Specifying whitelist directives marks specific directory hierarchies or specific directories as "trusted" -- direnv will evaluate any matching .envrc files regardless of whether they have been specifically allowed. **This feature should be used with great care**, as anyone with the ability to write files to that directory (including collaborators on VCS repositories) will be able to execute arbitrary code on your computer. There are two types of whitelist directives supported: ### `prefix` Accepts an array of strings. If any of the strings in this list are a prefix of an .envrc file's absolute path, that file will be implicitly allowed, regardless of contents or past usage of `direnv allow` or `direnv deny`. Example: ```toml [whitelist] prefix = [ "/home/user/code/project-a", "~/code/project-b" ] ``` In this example, the following .envrc files will be implicitly allowed: * `/home/user/code/project-a/.envrc` * `/home/user/code/project-a/subdir/.envrc` * `~/code/project-b/.envrc` * `~/code/project-b/subdir/.envrc` * and so on In this example, the following .envrc files will not be implicitly allowed (although they can be explicitly allowed by running `direnv allow`): * `/home/user/project-c/.envrc` * `/opt/random/.envrc` ### `exact` Accepts an array of strings. Each string can be a directory name or the full path to an .envrc file. If a directory name is passed, it will be treated as if it had been passed as itself with `/.envrc` appended. After resolving the filename, each string will be checked for being an exact match with an .envrc file's absolute path. If they match exactly, that .envrc file will be implicitly allowed, regardless of contents or past usage of `direnv allow` or `direnv deny`. Example: ```toml [whitelist] exact = [ "/home/user/project-a/.envrc", "~/project-b/subdir-a" ] ``` In this example, the following .envrc files will be implicitly allowed, and no others: * `/home/user/code/project-a/.envrc` * `~/project-b/subdir-a` In this example, the following .envrc files will not be implicitly allowed (although they can be explicitly allowed by running `direnv allow`): * `/home/user/code/project-b/subproject-c/.envrc` * `~/code/.envrc` COPYRIGHT --------- MIT licence - Copyright (C) 2019 @zimbatm and contributors SEE ALSO -------- direnv(1), direnv-stdlib(1) direnv-2.37.1/pkg/000077500000000000000000000000001503714164100136565ustar00rootroot00000000000000direnv-2.37.1/pkg/dotenv/000077500000000000000000000000001503714164100151555ustar00rootroot00000000000000direnv-2.37.1/pkg/dotenv/README.md000066400000000000000000000012631503714164100164360ustar00rootroot00000000000000# go-dotenv Go parsing library for the dotenv format. There is no formal definition of the dotenv format but it has been introduced by https://github.com/bkeepers/dotenv which is thus canonical. This library is a port of that. This library was developed specifically for [direnv](https://direnv.net). ## Features * `k=v` format * bash `export k=v` format * yaml `k: v` format * variable expansion, including default values as in `${FOO:-default}` * comments ## Missing * probably needs API breakage ## Alternatives Some other good alternatives with various variations. * https://github.com/joho/godotenv * https://github.com/lazureykis/dotenv * https://github.com/subosito/gotenv direnv-2.37.1/pkg/dotenv/parse.go000066400000000000000000000067601503714164100166270ustar00rootroot00000000000000// Package dotenv implements the parsing of the .env format. // // There is no formal definition of the format but it has been introduced by // https://github.com/bkeepers/dotenv which is thus canonical. package dotenv import ( "fmt" "os" "regexp" "strings" ) // LINE is the regexp matching a single line const LINE = ` \A \s* (?:|#.*| # comment line (?:export\s+)? # optional export ([\w\.]+) # key (?:\s*=\s*|:\s+?) # separator ( # optional value begin '(?:\'|[^'])*' # single quoted value | # or "(?:\"|[^"])*" # double quoted value | # or [^\s#\n]+ # unquoted value )? # value end \s* (?:\#.*)? # optional comment ) \z ` var linesRe = regexp.MustCompile(`[\r\n]+`) var lineRe = regexp.MustCompile( regexp.MustCompile(`\s+`).ReplaceAllLiteralString( regexp.MustCompile(`\s+# .*`).ReplaceAllLiteralString(LINE, ""), "")) // Parse reads a string in the .env format and returns a map of the extracted key=values. // // Ported from https://github.com/bkeepers/dotenv/blob/84f33f48107c492c3a99bd41c1059e7b4c1bb67a/lib/dotenv/parser.rb func Parse(data string) (map[string]string, error) { var dotenv = make(map[string]string) for _, line := range linesRe.Split(data, -1) { if !lineRe.MatchString(line) { return nil, fmt.Errorf("invalid line: %s", line) } match := lineRe.FindStringSubmatch(line) // commented or empty line if len(match) == 0 { continue } if len(match[1]) == 0 { continue } key := match[1] value := match[2] parseValue(key, value, dotenv) } return dotenv, nil } // MustParse works the same as Parse but panics on error func MustParse(data string) map[string]string { env, err := Parse(data) if err != nil { panic(err) } return env } func parseValue(key string, value string, dotenv map[string]string) { if len(value) <= 1 { dotenv[key] = value return } singleQuoted := false if value[0:1] == "'" && value[len(value)-1:] == "'" { // single-quoted string, do not expand singleQuoted = true value = value[1 : len(value)-1] } else if value[0:1] == `"` && value[len(value)-1:] == `"` { value = value[1 : len(value)-1] value = expandNewLines(value) value = unescapeCharacters(value) } if !singleQuoted { value = expandEnv(value, dotenv) } dotenv[key] = value } var escRe = regexp.MustCompile(`\\([^$])`) func unescapeCharacters(value string) string { return escRe.ReplaceAllString(value, "$1") } func expandNewLines(value string) string { value = strings.ReplaceAll(value, "\\n", "\n") value = strings.ReplaceAll(value, "\\r", "\r") return value } func expandEnv(value string, dotenv map[string]string) string { expander := func(value string) string { envKey, defaultValue, hasDefault := splitKeyAndDefault(value, ":-") expanded, found := lookupDotenv(envKey, dotenv) if found { return expanded } return getFromEnvOrDefault(envKey, defaultValue, hasDefault) } return os.Expand(value, expander) } func splitKeyAndDefault(value string, sep string) (string, string, bool) { var i = strings.Index(value, sep) if i == -1 { return value, "", false } return value[0:i], value[i+len(sep):], true } func lookupDotenv(value string, dotenv map[string]string) (string, bool) { retval, ok := dotenv[value] return retval, ok } func getFromEnvOrDefault(envKey string, defaultValue string, hasDefault bool) string { var envValue = os.Getenv(envKey) if len(envValue) == 0 && hasDefault { return defaultValue } return envValue } direnv-2.37.1/pkg/dotenv/parse_test.go000066400000000000000000000173301503714164100176610ustar00rootroot00000000000000package dotenv_test import ( "os" "testing" dotenv "github.com/direnv/direnv/v2/pkg/dotenv" ) func shouldNotHaveEmptyKey(t *testing.T, env map[string]string) { if _, ok := env[""]; ok { t.Error("should not have empty key") } } func envShouldContain(t *testing.T, env map[string]string, key string, value string) { if env[key] != value { t.Errorf("%s: %s, expected %s", key, env[key], value) } } // See the reference implementation: // https://github.com/bkeepers/dotenv/blob/master/lib/dotenv/environment.rb // TODO: support shell variable expansions const TestExportedEnv = `export OPTION_A=2 export OPTION_B='\n' # foo #export OPTION_C=3 export OPTION_D= export OPTION_E="foo" ` func TestDotEnvExported(t *testing.T) { env := dotenv.MustParse(TestExportedEnv) shouldNotHaveEmptyKey(t, env) if env["OPTION_A"] != "2" { t.Error("OPTION_A") } if env["OPTION_B"] != "\\n" { t.Error("OPTION_B") } if env["OPTION_C"] != "" { t.Error("OPTION_C", env["OPTION_C"]) } if v, ok := env["OPTION_D"]; v != "" || !ok { t.Error("OPTION_D") } if env["OPTION_E"] != "foo" { t.Error("OPTION_E") } } const TestPlainEnv = `OPTION_A=1 OPTION_B=2 OPTION_C= 3 OPTION_D =4 OPTION_E = 5 OPTION_F= OPTION_G = SMTP_ADDRESS=smtp # This is a comment ` func TestDotEnvPlain(t *testing.T) { env := dotenv.MustParse(TestPlainEnv) shouldNotHaveEmptyKey(t, env) if env["OPTION_A"] != "1" { t.Error("OPTION_A") } if env["OPTION_B"] != "2" { t.Error("OPTION_B") } if env["OPTION_C"] != "3" { t.Error("OPTION_C") } if env["OPTION_D"] != "4" { t.Error("OPTION_D") } if env["OPTION_E"] != "5" { t.Error("OPTION_E") } if v, ok := env["OPTION_F"]; v != "" || !ok { t.Error("OPTION_F") } if v, ok := env["OPTION_G"]; v != "" || !ok { t.Error("OPTION_G") } if env["SMTP_ADDRESS"] != "smtp" { t.Error("SMTP_ADDRESS") } } const TestSoloEmptyEnv = "SOME_VAR=" func TestSoloEmpty(t *testing.T) { env := dotenv.MustParse(TestSoloEmptyEnv) shouldNotHaveEmptyKey(t, env) v, ok := env["SOME_VAR"] if !ok { t.Error("SOME_VAR missing") } if v != "" { t.Error("SOME_VAR should be empty") } } const TestQuotedEnv = `OPTION_A='1' OPTION_B='2' OPTION_C='' OPTION_D='\n' OPTION_E="1" OPTION_F="2" OPTION_G="" OPTION_H="\n" #OPTION_I="3" ` func TestDotEnvQuoted(t *testing.T) { env := dotenv.MustParse(TestQuotedEnv) shouldNotHaveEmptyKey(t, env) if env["OPTION_A"] != "1" { t.Error("OPTION_A") } if env["OPTION_B"] != "2" { t.Error("OPTION_B") } if env["OPTION_C"] != "" { t.Error("OPTION_C") } if env["OPTION_D"] != "\\n" { t.Error("OPTION_D") } if env["OPTION_E"] != "1" { t.Error("OPTION_E") } if env["OPTION_F"] != "2" { t.Error("OPTION_F") } if env["OPTION_G"] != "" { t.Error("OPTION_G") } if env["OPTION_H"] != "\n" { t.Error("OPTION_H") } if env["OPTION_I"] != "" { t.Error("OPTION_I") } } const TestYAMLEnv = `OPTION_A: 1 OPTION_B: '2' OPTION_C: '' OPTION_D: '\n' #OPTION_E: '333' OPTION_F: ` func TestDotEnvYAML(t *testing.T) { env := dotenv.MustParse(TestYAMLEnv) shouldNotHaveEmptyKey(t, env) if env["OPTION_A"] != "1" { t.Error("OPTION_A") } if env["OPTION_B"] != "2" { t.Error("OPTION_B") } if env["OPTION_C"] != "" { t.Error("OPTION_C") } if env["OPTION_D"] != "\\n" { t.Error("OPTION_D") } if env["OPTION_E"] != "" { t.Error("OPTION_E") } if v, ok := env["OPTION_F"]; v != "" || !ok { t.Error("OPTION_F") } } func TestFailingMustParse(t *testing.T) { defer func() { r := recover() if r == nil { t.Error("should panic") } }() dotenv.MustParse("...") } const TestCommentOverrideEnv = ` VARIABLE=value #VARIABLE=disabled_value ` func TestCommentOverride(t *testing.T) { env := dotenv.MustParse(TestCommentOverrideEnv) shouldNotHaveEmptyKey(t, env) if env["VARIABLE"] != "value" { t.Error("VARIABLE should == value, not", env["VARIABLE"]) } } const TestVariableExpansionEnv = ` OPTION_A=$FOO OPTION_B="$FOO" OPTION_C=${FOO} OPTION_D="${FOO}" OPTION_E='$FOO' OPTION_F=$FOO/bar OPTION_G="$FOO/bar" OPTION_H=${FOO}/bar OPTION_I="${FOO}/bar" OPTION_J='$FOO/bar' OPTION_K=$BAR OPTION_L="$BAR" OPTION_M=${BAR} OPTION_N="${BAR}" OPTION_O='$BAR' OPTION_P=$BAR/baz OPTION_Q="$BAR/baz" OPTION_R=${BAR}/baz OPTION_S="${BAR}/baz" OPTION_T='$BAR/baz' OPTION_U="$OPTION_A/bar" OPTION_V=$OPTION_A/bar OPTION_W="$OPTION_A/bar" OPTION_X=${OPTION_A}/bar OPTION_Y="${OPTION_A}/bar" OPTION_Z='$OPTION_A/bar' OPTION_A1="$OPTION_A/bar/${OPTION_H}/$FOO" ` func TestVariableExpansion(t *testing.T) { err := os.Setenv("FOO", "foo") if err != nil { t.Fatalf("unable to set environment variable for testing: %s", err) } env := dotenv.MustParse(TestVariableExpansionEnv) shouldNotHaveEmptyKey(t, env) envShouldContain(t, env, "OPTION_A", "foo") envShouldContain(t, env, "OPTION_B", "foo") envShouldContain(t, env, "OPTION_C", "foo") envShouldContain(t, env, "OPTION_D", "foo") envShouldContain(t, env, "OPTION_E", "$FOO") envShouldContain(t, env, "OPTION_F", "foo/bar") envShouldContain(t, env, "OPTION_G", "foo/bar") envShouldContain(t, env, "OPTION_H", "foo/bar") envShouldContain(t, env, "OPTION_I", "foo/bar") envShouldContain(t, env, "OPTION_J", "$FOO/bar") envShouldContain(t, env, "OPTION_K", "") envShouldContain(t, env, "OPTION_L", "") envShouldContain(t, env, "OPTION_M", "") envShouldContain(t, env, "OPTION_N", "") envShouldContain(t, env, "OPTION_O", "$BAR") envShouldContain(t, env, "OPTION_P", "/baz") envShouldContain(t, env, "OPTION_Q", "/baz") envShouldContain(t, env, "OPTION_R", "/baz") envShouldContain(t, env, "OPTION_S", "/baz") envShouldContain(t, env, "OPTION_T", "$BAR/baz") envShouldContain(t, env, "OPTION_U", "foo/bar") envShouldContain(t, env, "OPTION_V", "foo/bar") envShouldContain(t, env, "OPTION_W", "foo/bar") envShouldContain(t, env, "OPTION_X", "foo/bar") envShouldContain(t, env, "OPTION_Y", "foo/bar") envShouldContain(t, env, "OPTION_Z", "$OPTION_A/bar") envShouldContain(t, env, "OPTION_A1", "foo/bar/foo/bar/foo") } const TestVariableExpansionWithDefaultsEnv = ` OPTION_A="${FOO:-}" OPTION_B="${FOO:-default}" OPTION_C='${FOO:-default}' OPTION_D="${FOO:-default}/bar" OPTION_E='${FOO:-default}/bar' OPTION_F="$FOO:-default" OPTION_G="$BAR:-default" OPTION_H="${BAR:-}" OPTION_I="${BAR:-default}" OPTION_J='${BAR:-default}' OPTION_K="${BAR:-default}/bar" OPTION_L='${BAR:-default}/bar' OPTION_M="${OPTION_A:-}" OPTION_N="${OPTION_A:-default}" OPTION_O='${OPTION_A:-default}' OPTION_P="${OPTION_A:-default}/bar" OPTION_Q='${OPTION_A:-default}/bar' OPTION_R="${:-}" OPTION_S="${BAR:-:-}" ` func TestVariableExpansionWithDefaults(t *testing.T) { err := os.Setenv("FOO", "foo") if err != nil { t.Fatalf("unable to set environment variable for testing: %s", err) } env := dotenv.MustParse(TestVariableExpansionWithDefaultsEnv) shouldNotHaveEmptyKey(t, env) envShouldContain(t, env, "OPTION_A", "foo") envShouldContain(t, env, "OPTION_B", "foo") envShouldContain(t, env, "OPTION_C", "${FOO:-default}") envShouldContain(t, env, "OPTION_D", "foo/bar") envShouldContain(t, env, "OPTION_E", "${FOO:-default}/bar") envShouldContain(t, env, "OPTION_F", "foo:-default") envShouldContain(t, env, "OPTION_G", ":-default") envShouldContain(t, env, "OPTION_H", "") envShouldContain(t, env, "OPTION_I", "default") envShouldContain(t, env, "OPTION_J", "${BAR:-default}") envShouldContain(t, env, "OPTION_K", "default/bar") envShouldContain(t, env, "OPTION_L", "${BAR:-default}/bar") envShouldContain(t, env, "OPTION_M", "foo") envShouldContain(t, env, "OPTION_N", "foo") envShouldContain(t, env, "OPTION_O", "${OPTION_A:-default}") envShouldContain(t, env, "OPTION_P", "foo/bar") envShouldContain(t, env, "OPTION_Q", "${OPTION_A:-default}/bar") envShouldContain(t, env, "OPTION_R", "") // this is actually invalid in bash, but what to do here? envShouldContain(t, env, "OPTION_S", ":-") } direnv-2.37.1/pkg/sri/000077500000000000000000000000001503714164100144535ustar00rootroot00000000000000direnv-2.37.1/pkg/sri/parse.go000066400000000000000000000012631503714164100161160ustar00rootroot00000000000000package sri import ( "fmt" "strings" ) // Parse a SRI hash func Parse(sriHash string) (*Hash, error) { elems := strings.SplitN(sriHash, "-", 2) if len(elems) != 2 { return nil, fmt.Errorf("sri: not a hash %v", sriHash) } // Get the algo var algo Algo switch elems[0] { case string(SHA256): algo = SHA256 case string(SHA384): algo = SHA384 case string(SHA512): algo = SHA512 default: return nil, fmt.Errorf("sri: unsupported algo %s", elems[0]) } // Get the hash dbuf := make([]byte, b64Enc.DecodedLen(len(elems[1]))) n, err := b64Enc.Decode(dbuf, []byte(elems[1])) if err != nil { return nil, err } sum := dbuf[:n] return &Hash{string(algo), sum}, nil } direnv-2.37.1/pkg/sri/sri.go000066400000000000000000000014461503714164100156040ustar00rootroot00000000000000// Package sri implements helper functions to calculate SubResource Integrity // hashes. // https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity package sri import ( b64 "encoding/base64" "encoding/hex" ) // Algo is a supported hashing algorithm type Algo string const ( // SHA256 algo SHA256 = Algo("sha256") // SHA384 algo SHA384 = Algo("sha384") // SHA512 algo SHA512 = Algo("sha512") ) // Base64 encoding to use var b64Enc = b64.StdEncoding // Hash represents a SRI-hash type Hash struct { algo string sum []byte } // String returns a SRI-encoded string func (h *Hash) String() string { return h.algo + "-" + b64Enc.EncodeToString(h.sum) } // Hex return a hex-encoded representation of the sum func (h *Hash) Hex() string { return hex.EncodeToString(h.sum) } direnv-2.37.1/pkg/sri/sri_test.go000066400000000000000000000017041503714164100166400ustar00rootroot00000000000000package sri import ( "strings" "testing" ) func TestWriter(t *testing.T) { var b strings.Builder s := "testdata" // Generated with: // `echo -n "testdata" | openssl dgst -sha256 -binary - | // openssl base64 -A` expectedHash := "sha256-gQ/y+yQqXe5CIPLLDmpRmJH7Z/L4KKbKtO+IlGM7H1A=" w := NewWriter(&b, SHA256) // Check the writer n, err := w.Write([]byte(s)) if err != nil { t.Fatalf("write error: %s", err) } if n != len(s) { t.Fatalf("expected len %d but got %d", len(s), n) } if b.String() != s { t.Fatal("data has not been forwarded") } // Check that the hash has been calculated properly x := w.Sum().String() if x != expectedHash { t.Fatal("hash mismatch") } } func TestParser(t *testing.T) { expectedHash := "sha256-gQ/y+yQqXe5CIPLLDmpRmJH7Z/L4KKbKtO+IlGM7H1A=" hash, err := Parse(expectedHash) if err != nil { t.Fatalf("parse error: %v", err) } if hash.String() != expectedHash { t.Fatal("hash mismatch") } } direnv-2.37.1/pkg/sri/writer.go000066400000000000000000000015511503714164100163200ustar00rootroot00000000000000package sri import ( "crypto/sha256" "crypto/sha512" "hash" "io" ) // Writer is like a hash.Hash with a Sum function type Writer struct { w io.Writer algo Algo h hash.Hash } // NewWriter returns a SRI writer that forwards the write while calculating // the SRI hash. func NewWriter(w io.Writer, algo Algo) Writer { var h hash.Hash switch algo { case SHA256: h = sha256.New() case SHA384: h = sha512.New384() case SHA512: h = sha512.New() default: panic("unsupported SRI algo") } return Writer{w, algo, h} } func (w Writer) Write(b []byte) (int, error) { // First write to the underlying storage n, err := w.w.Write(b) if err == nil { // This should always succeed _, _ = w.h.Write(b) } return n, err } // Sum returns the calculated SRI hash func (w Writer) Sum() *Hash { sum := w.h.Sum(nil) return &Hash{string(w.algo), sum} } direnv-2.37.1/script/000077500000000000000000000000001503714164100144015ustar00rootroot00000000000000direnv-2.37.1/script/dist-push000077500000000000000000000041741503714164100162550ustar00rootroot00000000000000#!/usr/bin/env bash # # Upload binary artifacts when a new release is made. # # Usage: ./script/dist-push [] # # Depends on: bash, coreutils, jq, curl set -euo pipefail # Ensure that the GITHUB_TOKEN secret is included if [[ -z "${GITHUB_TOKEN:-}" ]]; then echo "Set the GITHUB_TOKEN env variable." exit 1 fi # Prepare the headers for our curl-command. AUTH_HEADER="Authorization: token ${GITHUB_TOKEN}" # Set the github repository in CI : "${GITHUB_REPOSITORY:=direnv/direnv}" # Create the correct Upload URL. if [[ -n "${1:-}" ]]; then RELEASE_ID=$(curl -sfL \ -H "${AUTH_HEADER}" \ "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/tags/$1" \ | jq .id) else # if not tag is given, assume we are in CI RELEASE_ID=$(jq --raw-output '.release.id' "$GITHUB_EVENT_PATH") fi # start from the project root cd "$(dirname "$0")/.." # make sure we have all the dist files make dist # For each matching file.. for file in dist/*; do echo "Processing '${file}'" filename=$(basename "${file}") upload_url="https://uploads.github.com/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${filename}" echo "Upload URL is '${upload_url}'" # Generate a temporary file. tmp=$(mktemp) # Upload the artifact - capturing HTTP response-code in our output file. if ! response=$( curl \ -sSL \ -XPOST \ -H "${AUTH_HEADER}" \ --upload-file "${file}" \ --header "Content-Type:application/octet-stream" \ --write-out "%{http_code}" \ --output "$tmp" \ "${upload_url}" ); then echo "**********************************" echo " curl command did not return zero." echo " Aborting" echo "**********************************" cat "$tmp" rm "$tmp" exit 1 fi # If upload is not successful, we must abort if [[ $response -ge 400 ]]; then echo "***************************" echo " upload was not successful." echo " Aborting" echo " HTTP status is $response" echo "**********************************" cat "$tmp" rm "$tmp" exit 1 fi # Show pretty output, since we already have jq jq . <"$tmp" rm "$tmp" done direnv-2.37.1/script/prepare-release.sh000077500000000000000000000066331503714164100200240ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" cd "$SCRIPT_DIR/.." # Parse arguments version="${1:-}" repo="${2:-direnv/direnv}" if [[ -z $version ]]; then echo "USAGE: $0 version [owner/repo]" >&2 echo "Example: $0 v2.37.0" >&2 echo "Example: $0 v2.37.0-test Mic92/direnv" >&2 exit 1 fi # Remove 'v' prefix if present for consistency version_number="${version#v}" version_tag="v${version_number}" # Determine remote URL if [[ "$repo" == "direnv/direnv" ]]; then remote_url="origin" else remote_url="git@github.com:${repo}.git" fi if [[ "$(git symbolic-ref --short HEAD)" != "master" ]]; then echo "must be on master branch" >&2 exit 1 fi waitForPr() { local pr_branch=$1 while true; do if gh pr view "$pr_branch" --repo "$repo" --json state --jq '.state' | grep -q 'MERGED'; then break fi echo "Waiting for PR to be merged..." sleep 5 done } # Ensure we are up-to-date uncommitted_changes=$(git diff --compact-summary) if [[ -n $uncommitted_changes ]]; then echo -e "There are uncommitted changes, exiting:\n${uncommitted_changes}" >&2 exit 1 fi if [[ "$remote_url" == "origin" ]]; then git fetch origin unpushed_commits=$(git log --format=oneline origin/master..master) if [[ $unpushed_commits != "" ]]; then echo -e "\nThere are unpushed changes, exiting:\n$unpushed_commits" >&2 exit 1 fi git pull origin master else # For forks, we need to fetch from the fork repo git fetch "$remote_url" master git pull "$remote_url" master fi # Make sure tag does not exist if git tag -l | grep -q "^${version_tag}\$"; then echo "Tag ${version_tag} already exists, exiting" >&2 exit 1 fi echo "Generating changelog..." git changelog echo "" read -p "Continue with release ${version_tag}? (y/N): " -r if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Release aborted. Resetting changes..." exit 0 fi echo "Updating version.txt to ${version_number}..." echo "${version_number}" > version.txt echo "Creating release branch..." git branch -D "release-${version_tag}" 2>/dev/null || true git checkout -b "release-${version_tag}" echo "Committing changes..." git add version.txt CHANGELOG.md git commit -m "Release ${version_tag}" echo "Pushing release branch..." if [[ "$remote_url" == "origin" ]]; then git push origin "release-${version_tag}" else git push "$remote_url" "release-${version_tag}" fi echo "Creating pull request..." pr_url=$(gh pr create \ --repo "$repo" \ --base master \ --head "release-${version_tag}" \ --title "Release ${version_tag}" \ --body "Release ${version_tag}") # Extract PR number from URL pr_number=$(echo "$pr_url" | grep -oE '[0-9]+$') echo "Enabling auto-merge..." gh pr merge "$pr_number" --repo "$repo" --auto --merge echo "Switching back to master..." git checkout master echo "Waiting for PR to be merged..." waitForPr "release-${version_tag}" echo "Fetching latest master..." if [[ "$remote_url" == "origin" ]]; then git pull origin master else git pull "$remote_url" master fi echo "Creating and pushing tag..." git tag "${version_tag}" if [[ "$remote_url" == "origin" ]]; then git push origin "${version_tag}" else git push "$remote_url" "${version_tag}" fi echo "" echo "✅ Release ${version_tag} has been created!" echo "🚀 CI will now build and publish the binaries automatically." echo "đŸ“Ļ Check the release at: https://github.com/${repo}/releases/tag/${version_tag}" direnv-2.37.1/script/update-gomod2nix000077500000000000000000000001501503714164100175110ustar00rootroot00000000000000#!/usr/bin/env bash nix develop --extra-experimental-features "nix-command flakes" '.#' -c "gomod2nix" direnv-2.37.1/shell.nix000066400000000000000000000014521503714164100147260ustar00rootroot00000000000000{ stdenv , pkgs , mkGoEnv , gomod2nix , git , git-extras , gnumake , go , go-md2man , gox , bashInteractive , elvish , fish , tcsh , zsh , powershell , murex , golangci-lint , python3 , ruby , shellcheck , shfmt , cacert }: stdenv.mkDerivation { name = "shell"; nativeBuildInputs = with pkgs; [ (mkGoEnv { pwd = ./.; go = go; }) go # Build git git-extras # for git-changelog gnumake go-md2man gomod2nix # Shells bashInteractive elvish fish tcsh zsh powershell murex golangci-lint python3 ruby shellcheck shfmt ]; shellHook = '' unset GOPATH GOROOT # needed in pure shell export HOME=''${HOME:-$TMPDIR} export GO111MODULE=on export SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt ''; } direnv-2.37.1/stdlib.sh000077500000000000000000001156731503714164100147320ustar00rootroot00000000000000#!/usr/bin/env bash # # These are the commands available in an .envrc context # # ShellCheck exceptions: # # SC1090: Can't follow non-constant source. Use a directive to specify location. # SC1091: Not following: (file missing) # SC1117: Backslash is literal in "\n". Prefer explicit escaping: "\\n". # SC2059: Don't use variables in the printf format string. Use printf "..%s.." "$foo". shopt -s gnu_errfmt shopt -s nullglob shopt -s extglob # NOTE: don't touch the RHS, it gets replaced at runtime direnv="$(command -v direnv)" # Where direnv configuration should be stored direnv_config_dir="${DIRENV_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/direnv}" # This variable can be used by programs to detect when they are running inside # of a .envrc evaluation context. It is ignored by the direnv diffing # algorithm and so it won't be re-exported. export DIRENV_IN_ENVRC=1 __env_strictness() { local mode tmpfile old_shell_options local -i res tmpfile="$(mktemp)" res=0 mode="$1" shift set +o | grep 'pipefail\|nounset\|errexit' >"$tmpfile" old_shell_options=$(<"$tmpfile") rm -f "$tmpfile" case "$mode" in strict) set -o errexit -o nounset -o pipefail ;; unstrict) set +o errexit +o nounset +o pipefail ;; *) log_error "Unknown strictness mode '${mode}'." exit 1 ;; esac if (($#)); then "${@}" res=$? eval "$old_shell_options" fi # Force failure if the inner script has failed and the mode is strict if [[ $mode = strict && $res -gt 0 ]]; then exit 1 fi return $res } # Usage: strict_env [ ...] # # Turns on shell execution strictness. This will force the .envrc # evaluation context to exit immediately if: # # - any command in a pipeline returns a non-zero exit status that is # not otherwise handled as part of `if`, `while`, or `until` tests, # return value negation (`!`), or part of a boolean (`&&` or `||`) # chain. # - any variable that has not explicitly been set or declared (with # either `declare` or `local`) is referenced. # # If followed by a command-line, the strictness applies for the duration # of the command. # # Example: # # strict_env # has curl # # strict_env has curl strict_env() { __env_strictness strict "$@" } # Usage: unstrict_env [ ...] # # Turns off shell execution strictness. If followed by a command-line, the # strictness applies for the duration of the command. # # Example: # # unstrict_env # has curl # # unstrict_env has curl unstrict_env() { if (($#)); then __env_strictness unstrict "$@" else set +o errexit +o nounset +o pipefail fi } # Usage: direnv_layout_dir # # Prints the folder path that direnv should use to store layout content. # This needs to be a function as $PWD might change during source_env/up. # # The output defaults to $PWD/.direnv. direnv_layout_dir() { echo "${direnv_layout_dir:-$PWD/.direnv}" } # Usage: log_status [ ...] # # Logs a status message. Acts like echo, # but wraps output in the standard direnv log format # and directs it to stderr rather than stdout. # # Example: # # log_status "Loading ..." # log_status() { "$direnv" log -status "$*" } # Usage: log_error [ ...] # # Logs an error message. Acts like echo, # but wraps output in the standard direnv log format # and directs it to stderr rather than stdout. # # Example: # # log_error "Unable to find specified directory!" log_error() { "$direnv" log -error "$*" } # Usage: has # # Returns 0 if the is available. Returns 1 otherwise. It can be a # binary in the PATH or a shell function. # # Example: # # if has curl; then # echo "Yes we do" # fi # has() { type "$1" &>/dev/null } # Usage: join_args [args...] # # Joins all the passed arguments into a single string that can be evaluated by bash # # This is useful when one has to serialize an array of arguments back into a string join_args() { printf '%q ' "$@" } # Usage: expand_path [] # # Outputs the absolute path of relative to or the # current directory. # # Example: # # cd /usr/local/games # expand_path ../foo # # output: /usr/local/foo # expand_path() { local REPLY realpath.absolute "${2+"$2"}" "${1+"$1"}" echo "$REPLY" } # --- vendored from https://github.com/bashup/realpaths realpath.dirname() { REPLY=. ! [[ $1 =~ /+[^/]+/*$|^//$ ]] || REPLY="${1%"${BASH_REMATCH[0]}"}" REPLY=${REPLY:-/} } realpath.basename() { REPLY=/ ! [[ $1 =~ /*([^/]+)/*$ ]] || REPLY="${BASH_REMATCH[1]}" } realpath.absolute() { REPLY=$PWD local eg=extglob ! shopt -q $eg || eg= ${eg:+shopt -s $eg} while (($#)); do case $1 in // | //[^/]*) REPLY=// set -- "${1:2}" "${@:2}" ;; /*) REPLY=/ set -- "${1##+(/)}" "${@:2}" ;; */*) set -- "${1%%/*}" "${1##"${1%%/*}"+(/)}" "${@:2}" ;; '' | .) shift ;; ..) realpath.dirname "$REPLY" shift ;; *) REPLY="${REPLY%/}/$1" shift ;; esac done ${eg:+shopt -u $eg} } # --- # Usage: dotenv [] # # Loads a ".env" file into the current environment # dotenv() { local path=${1:-} if [[ -z $path ]]; then path=$PWD/.env elif [[ -d $path ]]; then path=$path/.env fi watch_file "$path" if ! [[ -f $path ]]; then log_error ".env at $path not found" return 1 fi eval "$("$direnv" dotenv bash "$@")" } # Usage: dotenv_if_exists [] # # Loads a ".env" file into the current environment, but only if it exists. # dotenv_if_exists() { local path=${1:-} if [[ -z $path ]]; then path=$PWD/.env elif [[ -d $path ]]; then path=$path/.env fi watch_file "$path" if ! [[ -f $path ]]; then return fi eval "$("$direnv" dotenv bash "$@")" } # Usage: user_rel_path # # Transforms an absolute path into a user-relative path if # possible. # # Example: # # echo $HOME # # output: /home/user # user_rel_path /home/user/my/project # # output: ~/my/project # user_rel_path /usr/local/lib # # output: /usr/local/lib # user_rel_path() { local abs_path=${1#-} if [[ -z $abs_path ]]; then return; fi if [[ -n $HOME ]]; then local rel_path=${abs_path#"$HOME"} if [[ $rel_path != "$abs_path" ]]; then abs_path=~$rel_path fi fi echo "$abs_path" } # Usage: find_up # # Outputs the path of when searched from the current directory up to # /. Returns 1 if the file has not been found. # # Example: # # cd /usr/local/my # mkdir -p project/foo # touch bar # cd project/foo # find_up bar # # output: /usr/local/my/bar # find_up() { ( while true; do if [[ -f $1 ]]; then echo "$PWD/$1" return 0 fi if [[ $PWD == / ]] || [[ $PWD == // ]]; then return 1 fi cd .. done ) } # Usage: source_env # # Loads another ".envrc" either by specifying its path or filename. # # NOTE: the other ".envrc" is not checked by the security framework. source_env() { local rcpath=${1/#\~/$HOME} if has cygpath; then rcpath=$(cygpath -u "$rcpath") fi local REPLY if [[ -d $rcpath ]]; then rcpath=$rcpath/.envrc fi if [[ ! -e $rcpath ]]; then log_status "referenced $rcpath does not exist" return 1 fi realpath.dirname "$rcpath" local rcpath_dir=$REPLY realpath.basename "$rcpath" local rcpath_base=$REPLY local rcfile rcfile=$(user_rel_path "$rcpath") watch_file "$rcpath" pushd "$(pwd 2>/dev/null)" >/dev/null || return 1 pushd "$rcpath_dir" >/dev/null || return 1 if [[ -f ./$rcpath_base ]]; then log_status "loading $(user_rel_path "$(expand_path "$rcpath_base")")" # shellcheck disable=SC1090 . "./$rcpath_base" else log_status "referenced $rcfile does not exist" fi popd >/dev/null || return 1 popd >/dev/null || return 1 } # Usage: source_env_if_exists # # Loads another ".envrc", but only if it exists. # # NOTE: contrary to source_env, this only works when passing a path to a file, # not a directory. # # Example: # # source_env_if_exists .envrc.private # source_env_if_exists() { watch_file "$1" if [[ -f "$1" ]]; then source_env "$1"; fi } # Usage: env_vars_required [ ...] # # Logs error for every variable not present in the environment or having an empty value. # Typically this is used in combination with source_env and source_env_if_exists. # # Example: # # # expect .envrc.private to provide tokens # source_env .envrc.private # # check presence of tokens # env_vars_required GITHUB_TOKEN OTHER_TOKEN # env_vars_required() { local environment local -i ret environment=$(env) ret=0 for var in "$@"; do if [[ "$environment" != *"$var="* || -z ${!var:-} ]]; then log_error "env var $var is required but missing/empty" ret=1 fi done return "$ret" } # Usage: watch_file [ ...] # # Adds each to the list of files that direnv will watch for changes - # useful when the contents of a file influence how variables are set - # especially in direnvrc # watch_file() { eval "$("$direnv" watch bash "$@")" } # Usage: watch_dir # # Adds to the list of dirs that direnv will recursively watch for changes watch_dir() { eval "$("$direnv" watch-dir bash "$1")" } # Usage: _source_up [] [true|false] # # Private helper function for source_up and source_up_if_exists. The second # parameter determines if it's an error for the file we're searching for to # not exist. _source_up() { local envrc file=${1:-.envrc} local ok_if_not_exist=${2} envrc=$(cd .. && (find_up "$file" || true)) if [[ -n $envrc ]]; then source_env "$envrc" elif $ok_if_not_exist; then return 0 else log_error "No ancestor $file found" return 1 fi } # Usage: source_up [] # # Loads another ".envrc" if found with the find_up command. Returns 1 if no # file is found. # # NOTE: the other ".envrc" is not checked by the security framework. source_up() { _source_up "${1:-}" false } # Usage: source_up_if_exists [] # # Loads another ".envrc" if found with the find_up command. If one is not # found, nothing happens. # # NOTE: the other ".envrc" is not checked by the security framework. source_up_if_exists() { _source_up "${1:-}" true } # Usage: fetchurl [] # # Fetches a URL and outputs a file with its content. If the # is given it will also validate the content of the file before returning it. fetchurl() { "$direnv" fetchurl "$@" } # Usage: source_url # # Fetches a URL and evaluates its content. source_url() { local url=$1 integrity_hash=${2:-} path if [[ -z $url ]]; then log_error "source_url: argument missing" return 1 fi if [[ -z $integrity_hash ]]; then log_error "source_url: argument missing. Use \`direnv fetchurl $url\` to find out the hash." return 1 fi log_status "loading $url ($integrity_hash)" path=$(fetchurl "$url" "$integrity_hash") # shellcheck disable=SC1090 source "$path" } # Usage: direnv_load # e.g: direnv_load opam-env exec -- "$direnv" dump # # Applies the environment generated by running as a # command. This is useful for adopting the environment of a child # process - cause that process to run "direnv dump" and then wrap # the results with direnv_load. # # shellcheck disable=SC1090 direnv_load() { # Backup watches in case of `nix-shell --pure` local prev_watches=$DIRENV_WATCHES local temp_dir output_file script_file exit_code old_direnv_dump_file_path # Prepare a temporary place for dumps and such. temp_dir=$(mktemp -dt direnv.XXXXXX) || { log_error "Could not create temporary directory." return 1 } output_file="$temp_dir/output" script_file="$temp_dir/script" old_direnv_dump_file_path=${DIRENV_DUMP_FILE_PATH:-} # Chain the following commands explicitly so that we can capture the exit code # of the whole chain. Crucially this ensures that we don't return early (via # `set -e`, for example) and hence always remove the temporary directory. touch "$output_file" && DIRENV_DUMP_FILE_PATH="$output_file" "$@" && { test -s "$output_file" || { log_error "Environment not dumped; did you invoke 'direnv dump'?" false } } && "$direnv" apply_dump "$output_file" >"$script_file" && source "$script_file" || exit_code=$? # Scrub temporary directory rm -rf "$temp_dir" # Restore watches if the dump wiped them if [[ -z "${DIRENV_WATCHES:-}" ]]; then export DIRENV_WATCHES=$prev_watches fi # Restore DIRENV_DUMP_FILE_PATH if needed if [[ -n "$old_direnv_dump_file_path" ]]; then export DIRENV_DUMP_FILE_PATH=$old_direnv_dump_file_path else unset DIRENV_DUMP_FILE_PATH fi # Exit accordingly return ${exit_code:-0} } # Usage: direnv_apply_dump # # Loads the output of `direnv dump` that was stored in a file. direnv_apply_dump() { local path=$1 eval "$("$direnv" apply_dump "$path")" } # Usage: PATH_add [ ...] # # Prepends the expanded to the PATH environment variable, in order. # It prevents a common mistake where PATH is replaced by only the new , # or where a trailing colon is left in PATH, resulting in the current directory # being considered in the PATH. Supports adding multiple directories at once. # # Example: # # pwd # # output: /my/project # PATH_add bin # echo $PATH # # output: /my/project/bin:/usr/bin:/bin # PATH_add bam boum # echo $PATH # # output: /my/project/bam:/my/project/boum:/my/project/bin:/usr/bin:/bin # PATH_add() { path_add PATH "$@" } # Usage: path_add [ ...] # # Works like PATH_add except that it's for an arbitrary . path_add() { local path i var_name="$1" # split existing paths into an array declare -a path_array IFS=: read -ra path_array <<<"${!1-}" shift # prepend the passed paths in the right order for ((i = $#; i > 0; i--)); do path_array=("$(expand_path "${!i}")" ${path_array[@]+"${path_array[@]}"}) done # join back all the paths path=$( IFS=: echo "${path_array[*]}" ) # and finally export back the result to the original variable export "$var_name=$path" } # Usage: MANPATH_add # # Prepends a path to the MANPATH environment variable while making sure that # `man` can still lookup the system manual pages. # # If MANPATH is not empty, man will only look in MANPATH. # So if we set MANPATH=$path, man will only look in $path. # Instead, prepend to `man -w` (which outputs man's default paths). # MANPATH_add() { local old_paths="${MANPATH:-$(man -w)}" local dir dir=$(expand_path "$1") export "MANPATH=$dir:$old_paths" } # Usage: PATH_rm [ ...] # Removes directories that match any of the given shell patterns from # the PATH environment variable. Order of the remaining directories is # preserved in the resulting PATH. # # Bash pattern syntax: # https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html # # Example: # # echo $PATH # # output: /dontremove/me:/remove/me:/usr/local/bin/:... # PATH_rm '/remove/*' # echo $PATH # # output: /dontremove/me:/usr/local/bin/:... # PATH_rm() { path_rm PATH "$@" } # Usage: path_rm [ ...] # # Works like PATH_rm except that it's for an arbitrary . path_rm() { local path i discard var_name="$1" # split existing paths into an array declare -a path_array IFS=: read -ra path_array <<<"${!1}" shift patterns=("$@") results=() # iterate over path entries, discard entries that match any of the patterns # shellcheck disable=SC2068 for path in ${path_array[@]+"${path_array[@]}"}; do discard=false # shellcheck disable=SC2068 for pattern in ${patterns[@]+"${patterns[@]}"}; do if [[ "$path" == +($pattern) ]]; then discard=true break fi done if ! $discard; then results+=("$path") fi done # join the result paths result=$( IFS=: echo "${results[*]}" ) # and finally export back the result to the original variable export "$var_name=$result" } # Usage: load_prefix # # Expands some common path variables for the given prefix. This is # useful if you installed something in the using # $(./configure --prefix= && make install) and want to use it in # the project. # # Variables set: # # CPATH # LD_LIBRARY_PATH # LIBRARY_PATH # MANPATH # PATH # PKG_CONFIG_PATH # # Example: # # ./configure --prefix=$HOME/rubies/ruby-1.9.3 # make && make install # # Then in the .envrc # load_prefix ~/rubies/ruby-1.9.3 # load_prefix() { local REPLY realpath.absolute "$1" MANPATH_add "$REPLY/man" MANPATH_add "$REPLY/share/man" path_add CPATH "$REPLY/include" path_add LD_LIBRARY_PATH "$REPLY/lib" path_add LIBRARY_PATH "$REPLY/lib" path_add PATH "$REPLY/bin" path_add PKG_CONFIG_PATH "$REPLY/lib/pkgconfig" } # Usage: semver_search # # Search a directory for the highest version number in SemVer format (X.Y.Z). # # Examples: # # $ tree . # . # |-- dir # |-- program-1.4.0 # |-- program-1.4.1 # |-- program-1.5.0 # $ semver_search "dir" "program-" "1.4.0" # 1.4.0 # $ semver_search "dir" "program-" "1.4" # 1.4.1 # $ semver_search "dir" "program-" "1" # 1.5.0 # semver_search() { local version_dir=${1:-} local prefix=${2:-} local partial_version=${3:-} # Look for matching versions in $version_dir path # Strip possible "/" suffix from $version_dir, then use that to # strip $version_dir/$prefix prefix from line. # Sort by version: split by "." then reverse numeric sort for each piece of the version string # The first one is the highest find "$version_dir" -maxdepth 1 -mindepth 1 -type d -name "${prefix}${partial_version}*" | while IFS= read -r line; do echo "${line#"${version_dir%/}"/"${prefix}"}"; done | sort -t . -k 1,1rn -k 2,2rn -k 3,3rn | head -1 } # Usage: layout # # A semantic dispatch used to describe common project layouts. # layout() { local funcname="layout_$1" shift "$funcname" "$@" local layout_dir layout_dir=$(direnv_layout_dir) if [[ -d "$layout_dir" && ! -f "$layout_dir/CACHEDIR.TAG" ]]; then echo 'Signature: 8a477f597d28d172789f06886806bc55 # This file is a cache directory tag created by direnv. # For information about cache directory tags, see: # http://www.brynosaurus.com/cachedir/' >"$layout_dir/CACHEDIR.TAG" fi } # Usage: layout go # # Adds "$(direnv_layout_dir)/go" to the GOPATH environment variable. # Furthermore "$(direnv_layout_dir)/go/bin" is set as the value for the GOBIN environment variable and added to the PATH environment variable. layout_go() { path_add GOPATH "$(direnv_layout_dir)/go" bindir="$(direnv_layout_dir)/go/bin" PATH_add "$bindir" export GOBIN="$bindir" } # Usage: layout node # # Adds "$PWD/node_modules/.bin" to the PATH environment variable. layout_node() { PATH_add node_modules/.bin } # Usage: layout opam # # Sets environment variables from `opam env`. layout_opam() { export OPAMSWITCH=$PWD eval "$(opam env "$@")" } # Usage: layout perl # # Setup environment variables required by perl's local::lib # See http://search.cpan.org/dist/local-lib/lib/local/lib.pm for more details # layout_perl() { local libdir libdir=$(direnv_layout_dir)/perl5 export LOCAL_LIB_DIR=$libdir export PERL_MB_OPT="--install_base '$libdir'" export PERL_MM_OPT="INSTALL_BASE=$libdir" path_add PERL5LIB "$libdir/lib/perl5" path_add PERL_LOCAL_LIB_ROOT "$libdir" PATH_add "$libdir/bin" } # Usage: layout php # # Adds "$PWD/vendor/bin" to the PATH environment variable layout_php() { PATH_add vendor/bin } # Usage: layout python # # Creates and loads a virtual environment. # You can specify the path of the virtual environment through VIRTUAL_ENV # environment variable, otherwise it will be set to # "$direnv_layout_dir/python-$python_version". # For python older then 3.3 this requires virtualenv to be installed. # # It's possible to specify the python executable if you want to use different # versions of python. # layout_python() { local old_env local python=${1:-python} [[ $# -gt 0 ]] && shift old_env=$(direnv_layout_dir)/virtualenv unset PYTHONHOME if [[ -d $old_env && $python == python ]]; then VIRTUAL_ENV=$old_env else local python_version ve # shellcheck disable=SC2046 read -r python_version ve <<<$($python < [] # # Activates anaconda for the provided environment. # The can be one of the following: # 1. Name of an environment # 2. Prefix path to an environment # 3. Path to a yml-formatted file specifying the environment # # Environment creation will use environment.yml, if # available, when a name or prefix is provided. Otherwise, # an empty environment will be created. # # is optional and will default to the one # found in the system environment. # layout_anaconda() { local env_spec=$1 local env_name local env_loc local env_config local conda local REPLY if [[ $# -gt 1 ]]; then conda=${2} else conda=$(command -v conda) fi realpath.dirname "$conda" PATH_add "$REPLY" if [[ "${env_spec##*.}" == "yml" ]]; then env_config=$env_spec elif [[ "${env_spec%%/*}" == "." ]]; then # "./foo" relative prefix realpath.absolute "$env_spec" env_loc="$REPLY" elif [[ ! "$env_spec" == "${env_spec#/}" ]]; then # "/foo" absolute prefix env_loc="$env_spec" elif [[ -n "$env_spec" ]]; then # "name" specified env_name="$env_spec" else # Need at least one env_config=environment.yml fi # If only config, it needs a name field if [[ -n "$env_config" ]]; then if [[ -e "$env_config" ]]; then env_name="$(grep -- '^name:' "$env_config")" env_name="${env_name/#name:*([[:space:]])/}" if [[ -z "$env_name" ]]; then log_error "Unable to find 'name' in '$env_config'" return 1 fi else log_error "Unable to find config '$env_config'" return 1 fi fi # Try to find location based on name if [[ -z "$env_loc" ]]; then # Update location if already created env_loc=$("$conda" env list | grep -- '^'"$env_name"'\s') env_loc="${env_loc##* }" fi # Check for environment existence if [[ ! -d "$env_loc" ]]; then # Create if necessary if [[ -z "$env_config" ]] && [[ -n "$env_name" ]]; then if [[ -e environment.yml ]]; then "$conda" env create --file environment.yml --name "$env_name" else "$conda" create -y --name "$env_name" fi elif [[ -n "$env_config" ]]; then "$conda" env create --file "$env_config" elif [[ -n "$env_loc" ]]; then if [[ -e environment.yml ]]; then "$conda" env create --file environment.yml --prefix "$env_loc" else "$conda" create -y --prefix "$env_loc" fi fi if [[ -z "$env_loc" ]]; then # Update location if already created env_loc=$("$conda" env list | grep -- '^'"$env_name"'\s') env_loc="${env_loc##* }" fi fi eval "$("$conda" shell.bash activate "$env_loc")" } # Usage: layout pipenv # # Similar to layout_python, but uses Pipenv to build a # virtualenv from the Pipfile located in the same directory. # layout_pipenv() { PIPENV_PIPFILE="${PIPENV_PIPFILE:-Pipfile}" if [[ ! -f "$PIPENV_PIPFILE" ]]; then log_error "No Pipfile found. Use \`pipenv\` to create a \`$PIPENV_PIPFILE\` first." exit 2 fi VIRTUAL_ENV=$( pipenv --venv 2>/dev/null true ) if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then pipenv install --dev VIRTUAL_ENV=$(pipenv --venv) fi PATH_add "$VIRTUAL_ENV/bin" export PIPENV_ACTIVE=1 export VIRTUAL_ENV } # Usage: layout pyenv [ ...] # # Example: # # layout pyenv 3.6.7 # # Uses pyenv and layout_python to create and load a virtual environment. # You can specify the path of the virtual environment through VIRTUAL_ENV # environment variable, otherwise it will be set to # "$direnv_layout_dir/python-$python_version". # layout_pyenv() { unset PYENV_VERSION # layout_python prepends each python version to the PATH, so we add each # version in reverse order so that the first listed version ends up # first in the path local i for ((i = $#; i > 0; i--)); do local python_version=${!i} local pyenv_python pyenv_python=$(pyenv root)/versions/${python_version}/bin/python if [[ -x "$pyenv_python" ]]; then if layout_python "$pyenv_python"; then # e.g. Given "use pyenv 3.6.9 2.7.16", PYENV_VERSION becomes "3.6.9:2.7.16" PYENV_VERSION=${python_version}${PYENV_VERSION:+:$PYENV_VERSION} fi else log_error "pyenv: version '$python_version' not installed" return 1 fi done [[ -n "$PYENV_VERSION" ]] && export PYENV_VERSION } # Usage: layout ruby # # Sets the GEM_HOME environment variable to "$(direnv_layout_dir)/ruby/RUBY_VERSION". # This forces the installation of any gems into the project's sub-folder. # If you're using bundler it will create wrapper programs that can be invoked # directly instead of using the $(bundle exec) prefix. # layout_ruby() { BUNDLE_BIN=$(direnv_layout_dir)/bin if ruby -e "exit Gem::VERSION > '2.2.0'" 2>/dev/null; then GEM_HOME=$(direnv_layout_dir)/ruby else local ruby_version ruby_version=$(ruby -e"puts (defined?(RUBY_ENGINE) ? RUBY_ENGINE : 'ruby') + '-' + RUBY_VERSION") GEM_HOME=$(direnv_layout_dir)/ruby-${ruby_version} fi export BUNDLE_BIN export GEM_HOME PATH_add "$GEM_HOME/bin" PATH_add "$BUNDLE_BIN" } # Usage: layout julia # # Sets the JULIA_PROJECT environment variable to the current directory. layout_julia() { export JULIA_PROJECT=$PWD } # Usage: use [] # # A semantic command dispatch intended for loading external dependencies into # the environment. # # Example: # # use_ruby() { # echo "Ruby $1" # } # use ruby 1.9.3 # # output: Ruby 1.9.3 # use() { local cmd=$1 log_status "using $*" shift "use_$cmd" "$@" } # Usage: use julia [] # Loads specified Julia version. # # Environment Variables: # # - $JULIA_VERSIONS (required) # You must specify a path to your installed Julia versions with the `$JULIA_VERSIONS` variable. # # - $JULIA_VERSION_PREFIX (optional) [default="julia-"] # Overrides the default version prefix. # use_julia() { local version=${1:-} local julia_version_prefix=${JULIA_VERSION_PREFIX-julia-} local search_version local julia_prefix if [[ -z ${JULIA_VERSIONS:-} || -z $version ]]; then log_error "Must specify the \$JULIA_VERSIONS environment variable and a Julia version!" return 1 fi julia_prefix="${JULIA_VERSIONS}/${julia_version_prefix}${version}" if [[ ! -d ${julia_prefix} ]]; then search_version=$(semver_search "${JULIA_VERSIONS}" "${julia_version_prefix}" "${version}") julia_prefix="${JULIA_VERSIONS}/${julia_version_prefix}${search_version}" fi if [[ ! -d $julia_prefix ]]; then log_error "Unable to find Julia version ($version) in ($JULIA_VERSIONS)!" return 1 fi if [[ ! -x $julia_prefix/bin/julia ]]; then log_error "Unable to load Julia binary (julia) for version ($version) in ($JULIA_VERSIONS)!" return 1 fi PATH_add "$julia_prefix/bin" MANPATH_add "$julia_prefix/share/man" log_status "Successfully loaded $(julia --version), from prefix ($julia_prefix)" } # Usage: use rbenv # # Loads rbenv which add the ruby wrappers available on the PATH. # use_rbenv() { eval "$(rbenv init -)" } # Usage: rvm [...] # # Should work just like in the shell if you have rvm installed.# # rvm() { unset rvm if [[ -n ${rvm_scripts_path:-} ]]; then # shellcheck disable=SC1090,SC1091 source "${rvm_scripts_path}/rvm" elif [[ -n ${rvm_path:-} ]]; then # shellcheck disable=SC1090,SC1091 source "${rvm_path}/scripts/rvm" else # shellcheck disable=SC1090,SC1091 source "$HOME/.rvm/scripts/rvm" fi rvm "$@" } # Usage: use node [] # # Loads the specified NodeJS version into the environment. # # If a partial NodeJS version is passed (i.e. `4.2`), a fuzzy match # is performed and the highest matching version installed is selected. # # If no version is passed, it will look at the '.nvmrc' or '.node-version' # files in the current directory if they exist. # # Environment Variables: # # - $NODE_VERSIONS (required) # Points to a folder that contains all the installed Node versions. That # folder must exist. # # - $NODE_VERSION_PREFIX (optional) [default="node-v"] # Overrides the default version prefix. # use_node() { local version=${1:-} local via="" local node_version_prefix=${NODE_VERSION_PREFIX-node-v} local search_version local node_prefix if [[ -z ${NODE_VERSIONS:-} || ! -d $NODE_VERSIONS ]]; then log_error "You must specify a \$NODE_VERSIONS environment variable and the directory specified must exist!" return 1 fi if [[ -z $version && -f .nvmrc ]]; then version=$(<.nvmrc) via=".nvmrc" fi if [[ -z $version && -f .node-version ]]; then version=$(<.node-version) via=".node-version" fi version=${version#v} if [[ -z $version ]]; then log_error "I do not know which NodeJS version to load because one has not been specified!" return 1 fi # Search for the highest version matching $version in the folder search_version=$(semver_search "$NODE_VERSIONS" "${node_version_prefix}" "${version}") node_prefix="${NODE_VERSIONS}/${node_version_prefix}${search_version}" if [[ ! -d $node_prefix ]]; then log_error "Unable to find NodeJS version ($version) in ($NODE_VERSIONS)!" return 1 fi if [[ ! -x $node_prefix/bin/node ]]; then log_error "Unable to load NodeJS binary (node) for version ($version) in ($NODE_VERSIONS)!" return 1 fi load_prefix "$node_prefix" if [[ -z $via ]]; then log_status "Successfully loaded NodeJS $(node --version), from prefix ($node_prefix)" else log_status "Successfully loaded NodeJS $(node --version) (via $via), from prefix ($node_prefix)" fi } # Usage: use nodenv # # Example: # # use nodenv 15.2.1 # # Uses nodenv, use_node and layout_node to add the chosen node version and # "$PWD/node_modules/.bin" to the PATH # use_nodenv() { local node_version="${1}" local node_versions_dir local nodenv_version node_versions_dir="$(nodenv root)/versions" nodenv_version="${node_versions_dir}/${node_version}" if [[ -e "$nodenv_version" ]]; then # Put the selected node version in the PATH NODE_VERSIONS="${node_versions_dir}" NODE_VERSION_PREFIX="" use_node "${node_version}" # Add $PWD/node_modules/.bin to the PATH layout_node else log_error "nodenv: version '$node_version' not installed. Use \`nodenv install ${node_version}\` to install it first." return 1 fi } # Usage: use_nix [...] # # Load environment variables from `nix-shell`. # If you have a `default.nix` or `shell.nix` these will be # used by default, but you can also specify packages directly # (e.g `use nix -p ocaml`). # use_nix() { local -A values_to_restore=( ["NIX_BUILD_TOP"]=${NIX_BUILD_TOP:-__UNSET__} ["TMP"]=${TMP:-__UNSET__} ["TMPDIR"]=${TMPDIR:-__UNSET__} ["TEMP"]=${TEMP:-__UNSET__} ["TEMPDIR"]=${TEMPDIR:-__UNSET__} ["terminfo"]=${terminfo:-__UNSET__} ) direnv_load nix-shell --show-trace "$@" --run "$(join_args "$direnv" dump)" for key in "${!values_to_restore[@]}"; do local value=${values_to_restore[$key]} if [[ $value == __UNSET__ ]]; then unset "$key" else export "$key=$value" fi done if [[ $# == 0 ]]; then watch_file default.nix shell.nix fi } # Usage: use_flake [] # # Load the build environment of a derivation similar to `nix develop`. # # By default it will load the current folder flake.nix devShell attribute. Or # pass an "installable" like "nixpkgs#hello" to load all the build # dependencies of the hello package from the latest nixpkgs. # # Note that the flakes feature is hidden behind an experimental flag, which # you will have to enable on your own. Flakes is not considered stable yet. use_flake() { watch_file flake.nix watch_file flake.lock mkdir -p "$(direnv_layout_dir)" eval "$(nix --extra-experimental-features "nix-command flakes" print-dev-env --profile "$(direnv_layout_dir)/flake-profile" "$@")" nix --extra-experimental-features "nix-command flakes" profile wipe-history --profile "$(direnv_layout_dir)/flake-profile" } # Usage: use_flox [...] # # Load environment variables from `flox activate`. By default uses the .flox # directory in the current directory. # # You can specify a remote environment with '--remote=/' where # / is the FloxHub environment name (e.g. `use_flox --remote=myorg/env`). # # The '--trust' flag can be added to automatically trust remote environments: # use_flox --trust --remote=myorg/env # # An alternate local environment directory can be specified with '--dir=', # where contains a .flox directory. # # Example: # # use_flox --remote=acme/production # use_flox --dir=/path/to/env # # Note: Custom commands are not supported since flox activate is used for loading. function use_flox() { local flox_dir=".flox" local args=() while [[ $# -gt 0 ]]; do case "$1" in --dir=*) flox_dir="${1#*=}"/.flox args+=("$1") shift ;; --dir) if [[ $# -lt 2 ]]; then printf "direnv(use_flox): --dir flag requires a path argument\n" >&2 return 1 fi flox_dir="$2"/.flox args+=("$1" "$2") shift 2 ;; *) args+=("$1") shift ;; esac done if [[ ! -d "$flox_dir" ]]; then printf "direnv(use_flox): \`.flox\` directory not found at %s\n" "$flox_dir" >&2 printf "direnv(use_flox): Did you run \`flox init\` in this directory?\n" >&2 return 1 fi direnv_load flox activate "${args[@]}" -- "$direnv" dump if [[ ${#args[@]} -eq 0 ]]; then watch_dir "$flox_dir/env/" watch_file "$flox_dir/env.json" watch_file "$flox_dir/env.lock" fi } # Usage: use_guix [...] # # Load environment variables from `guix shell`. # Any arguments given will be passed to guix shell. For example, # `use guix hello` would setup an environment including the hello # package. To create an environment with the hello dependencies, the # `--development` flag is used `use guix --development hello`. Other # options include `--file` which allows loading an environment from a # file. For a full list of options, consult the documentation for the # `guix shell` command. # If a channels.scm is available, `guix time-machine -C channels.scm` # is automatically invoked before creating the shell. use_guix() { watch_file guix.scm watch_file manifest.scm watch_file channels.scm if [ -f channels.scm ] then log_status "Using Guix version from channels.scm" eval "$(guix time-machine -C channels.scm -- shell "$@" --search-paths)" else eval "$(guix shell "$@" --search-paths)" fi } # Usage: use_vim [] # # Prepends the specified vim script (or .vimrc.local by default) to the # `DIRENV_EXTRA_VIMRC` environment variable. # # This variable is understood by the direnv/direnv.vim extension. When found, # it will source it after opening files in the directory. use_vim() { local extra_vimrc=${1:-.vimrc.local} path_add DIRENV_EXTRA_VIMRC "$extra_vimrc" } # Usage: direnv_version # # Checks that the direnv version is at least old as . direnv_version() { "$direnv" version "$@" } # Usage: on_git_branch [] OR on_git_branch -r [] # # Returns 0 if within a git repository with given `branch_name`. If no branch # name is provided, then returns 0 when within _any_ branch. Requires the git # command to be installed. Returns 1 otherwise. # # When the `-r` flag is specified, then the second argument is interpreted as a # regexp pattern for matching a branch name. # # Regardless, when a branch is specified, then `.git/HEAD` is watched so that # entering/exiting a branch triggers a reload. # # Example (.envrc): # # if on_git_branch; then # echo "Thanks for contributing to a GitHub project!" # fi # # if on_git_branch child_changes; then # export MERGE_BASE_BRANCH=parent_changes # fi # # if on_git_branch -r '.*py2'; then # layout python2 # else # layout python # fi on_git_branch() { local git_dir if ! has git; then log_error "on_git_branch needs git, which could not be found on your system" return 1 elif ! git_dir=$(git rev-parse --absolute-git-dir 2>/dev/null); then log_error "on_git_branch could not locate the .git directory corresponding to the current working directory" return 1 elif [ -z "$1" ]; then return 0 elif [[ "$1" = "-r" && -z "$2" ]]; then log_error "missing regexp pattern after \`-r\` flag" return 1 fi watch_file "$git_dir/HEAD" local git_branch git_branch=$(git branch --show-current) if [ "$1" = '-r' ]; then [[ "$git_branch" =~ $2 ]] else [ "$1" = "$git_branch" ] fi } # Usage: __main__ [...] # # Used by rc.go __main__() { # reserve stdout for dumping exec 3>&1 exec 1>&2 # shellcheck disable=SC2317 __dump_at_exit() { local ret=$? "$direnv" dump json "" >&3 trap - EXIT exit "$ret" } trap __dump_at_exit EXIT # load direnv libraries for lib in "$direnv_config_dir/lib/"*.sh; do # shellcheck disable=SC1090 source "$lib" done # load the global ~/.direnvrc if present if [[ -f $direnv_config_dir/direnvrc ]]; then # shellcheck disable=SC1090,SC1091 source "$direnv_config_dir/direnvrc" >&2 elif [[ -f $HOME/.direnvrc ]]; then # shellcheck disable=SC1090,SC1091 source "$HOME/.direnvrc" >&2 fi # and finally load the .envrc "$@" } direnv-2.37.1/test/000077500000000000000000000000001503714164100140545ustar00rootroot00000000000000direnv-2.37.1/test/.envrc000066400000000000000000000000001503714164100151600ustar00rootroot00000000000000direnv-2.37.1/test/config/000077500000000000000000000000001503714164100153215ustar00rootroot00000000000000direnv-2.37.1/test/config/direnv/000077500000000000000000000000001503714164100166105ustar00rootroot00000000000000direnv-2.37.1/test/config/direnv/direnvrc000066400000000000000000000000001503714164100203350ustar00rootroot00000000000000direnv-2.37.1/test/direnv-test-common.sh000077500000000000000000000152641503714164100201550ustar00rootroot00000000000000# Test script for Bourne-shell extensions. Set TARGET_SHELL # to the shell to be tested (bash, zsh, etc) before sourcing it. if [[ -z "$TARGET_SHELL" ]]; then echo "TARGET_SHELL variable not set" exit 1 fi set -e cd "$(dirname "$0")" TEST_DIR=$PWD export XDG_CONFIG_HOME=${TEST_DIR}/config export XDG_DATA_HOME=${TEST_DIR}/data PATH=$(dirname "$TEST_DIR"):$PATH export PATH # Reset the direnv loading if any export DIRENV_CONFIG=$PWD unset DIRENV_BASH unset DIRENV_DIR unset DIRENV_FILE unset DIRENV_WATCHES unset DIRENV_DIFF mkdir -p "${XDG_CONFIG_HOME}/direnv" touch "${XDG_CONFIG_HOME}/direnv/direnvrc" has() { type -P "$1" &>/dev/null } direnv_eval() { eval "$(direnv export "$TARGET_SHELL")" } test_start() { cd "$TEST_DIR/scenarios/$1" direnv allow if [[ "$DIRENV_DEBUG" == "1" ]]; then echo fi echo "## Testing $1 ##" if [[ "$DIRENV_DEBUG" == "1" ]]; then echo fi } test_stop() { rm -f "${XDG_CONFIG_HOME}/direnv/direnv.toml" cd / direnv_eval } test_eq() { if [[ "$1" != "$2" ]]; then echo "FAILED: '$1' == '$2'" exit 1 fi } test_neq() { if [[ "$1" == "$2" ]]; then echo "FAILED: '$1' != '$2'" exit 1 fi } ### RUN ### direnv allow || true direnv_eval test_start base echo "Setting up" direnv_eval test_eq "$HELLO" "world" WATCHES=$DIRENV_WATCHES echo "Reloading (should be no-op)" direnv_eval test_eq "$WATCHES" "$DIRENV_WATCHES" sleep 1 echo "Updating envrc and reloading (should reload)" touch .envrc direnv_eval test_neq "$WATCHES" "$DIRENV_WATCHES" echo "Leaving dir (should clear env set by dir's envrc)" cd .. direnv_eval echo "${HELLO}" test -z "${HELLO}" unset WATCHES test_stop test_start inherit cp ../base/.envrc ../inherited/.envrc direnv_eval echo "HELLO should be world:" "$HELLO" sleep 1 echo "export HELLO=goodbye" > ../inherited/.envrc direnv_eval test_eq "$HELLO" "goodbye" test_stop if has ruby; then test_start "ruby-layout" direnv_eval test_neq "$GEM_HOME" "" test_stop fi # Make sure directories with spaces are fine test_start "space dir" direnv_eval test_eq "$SPACE_DIR" "true" test_stop test_start "child-env" direnv_eval test_eq "$PARENT_PRE" "1" test_eq "$CHILD" "1" test_eq "$PARENT_POST" "1" test -z "$REMOVE_ME" test_stop test_start "special-vars" export DIRENV_BASH=$(command -v bash) export DIRENV_CONFIG=foobar direnv_eval || true test -n "$DIRENV_BASH" test_eq "$DIRENV_CONFIG" "foobar" unset DIRENV_BASH unset DIRENV_CONFIG test_stop test_start "dump" direnv_eval test_eq "$LS_COLORS" "*.ogg=38;5;45:*.wav=38;5;45" test_eq "$THREE_BACKSLASHES" '\\\' test_eq "$LESSOPEN" "||/usr/bin/lesspipe.sh %s" test_stop test_start "empty-var" direnv_eval test_neq "${FOO-unset}" "unset" test_eq "${FOO}" "" test_stop test_start "empty-var-unset" export FOO="" direnv_eval test_eq "${FOO-unset}" "unset" unset FOO test_stop test_start "in-envrc" direnv_eval set +e ./test-in-envrc es=$? set -e test_eq "$es" "1" test_stop test_start "missing-file-source-env" direnv_eval test_stop test_start "symlink-changed" # when using a symlink, reload if the symlink changes, or if the # target file changes. ln -fs ./state-A ./symlink direnv_eval test_eq "${STATE}" "A" sleep 1 ln -fs ./state-B ./symlink direnv_eval test_eq "${STATE}" "B" test_stop test_start "symlink-dir" # we can allow and deny the target direnv allow foo direnv deny foo # we can allow and deny the symlink direnv allow bar direnv deny bar test_stop test_start "utf-8" direnv_eval test_eq "${UTFSTUFF}" "♀♂" test_stop test_start "failure" # Test that DIRENV_DIFF and DIRENV_WATCHES are set even after a failure. # # This is needed so that direnv doesn't go into a loop when the loading # fails. test_eq "${DIRENV_DIFF:-}" "" test_eq "${DIRENV_WATCHES:-}" "" direnv_eval test_neq "${DIRENV_DIFF:-}" "" test_neq "${DIRENV_WATCHES:-}" "" test_stop test_start "watch-dir" echo "No watches by default" test_eq "${DIRENV_WATCHES}" "${WATCHES}" direnv_eval if ! direnv watch-print | grep -q "testdir"; then echo "FAILED: testdir added to watches" exit 1 fi if ! direnv show_dump "${DIRENV_WATCHES}" | grep -q "testfile"; then echo "FAILED: testfile not added to DIRENV_WATCHES" exit 1 fi echo "After eval, watches have changed" test_neq "${DIRENV_WATCHES}" "${WATCHES}" test_stop test_start "load-envrc-before-env" direnv_eval test_eq "${HELLO}" "bar" test_stop test_start "load-env" echo "[global] load_dotenv = true" > "${XDG_CONFIG_HOME}/direnv/direnv.toml" direnv allow direnv_eval test_eq "${HELLO}" "world" test_stop test_start "skip-env" direnv_eval test -z "${SKIPPED}" test_stop if has python; then test_start "python-layout" rm -rf .direnv direnv_eval test -n "${VIRTUAL_ENV:-}" if [[ ":$PATH:" != *":${VIRTUAL_ENV}/bin:"* ]]; then echo "FAILED: VIRTUAL_ENV/bin not added to PATH" exit 1 fi if [[ ! -f .direnv/CACHEDIR.TAG ]]; then echo "the layout dir should contain that file to filter that folder out of backups" exit 1 fi test_stop test_start "python-custom-virtual-env" direnv_eval test "${VIRTUAL_ENV:-}" -ef ./foo if [[ ":$PATH:" != *":${PWD}/foo/bin:"* ]]; then echo "FAILED: VIRTUAL_ENV/bin not added to PATH" exit 1 fi test_stop fi test_start "aliases" direnv deny # check that allow/deny aliases work direnv permit && direnv_eval && test -n "${HELLO}" direnv block && direnv_eval && test -z "${HELLO}" direnv grant && direnv_eval && test -n "${HELLO}" direnv revoke && direnv_eval && test -z "${HELLO}" direnv grant && direnv_eval && test -n "${HELLO}" direnv disallow && direnv_eval && test -z "${HELLO}" test_stop # shellcheck disable=SC2016 test_start '$test' direnv_eval [[ $FOO = bar ]] test_stop # Make sure that directories with names that can end up creating paths like # \b or \r are not broken (Windows specific issue). test_start 'special-characters/backspace/return' direnv_eval test_eq "${HI}" "there" test_stop # Context: foo/bar is a symlink to ../baz. foo/ contains and .envrc file # BUG: foo/bar is resolved in the .envrc execution context and so can't find # the .envrc file. # # Apparently, the CHDIR syscall does that so I don't know how to work around # the issue. # # test_start "symlink-bug" # cd foo/bar # direnv_eval # test_stop # Pending: test that the mtime is looked on the original file # test_start "utils" # LINK_TIME=`direnv file-mtime link-to-somefile` # touch somefile # NEW_LINK_TIME=`direnv file-mtime link-to-somefile` # test "$LINK_TIME" = "$NEW_LINK_TIME" # test_stop direnv-2.37.1/test/direnv-test.bash000066400000000000000000000001701503714164100171550ustar00rootroot00000000000000#!/usr/bin/env bash export TARGET_SHELL=bash # shellcheck disable=SC1091 source "$(dirname "$0")/direnv-test-common.sh" direnv-2.37.1/test/direnv-test.elv000077500000000000000000000043731503714164100170420ustar00rootroot00000000000000#!/usr/bin/env elvish use path set E:TEST_DIR = (path:dir (src)[name]) set-env XDG_CONFIG_HOME $E:TEST_DIR/config set-env XDG_DATA_HOME $E:TEST_DIR/data set E:PATH = (path:dir $E:TEST_DIR):$E:PATH cd $E:TEST_DIR ## reset the direnv loading if any set-env DIRENV_CONFIG $pwd unset-env DIRENV_BASH unset-env DIRENV_DIR unset-env DIRENV_FILE unset-env DIRENV_WATCHES unset-env DIRENV_DIFF mkdir -p $E:XDG_CONFIG_HOME/direnv touch $E:XDG_CONFIG_HOME/direnv/direnvrc fn direnv-eval { try { var m = (direnv export elvish | from-json) var k keys $m | each {|k| if $m[$k] { set-env $k $m[$k] } else { unset-env $k } } } catch e { nop } } fn test-debug { if (==s $E:DIRENV_DEBUG "1") { echo } } fn test-eq {|a b| if (!=s $a $b) { fail "FAILED: '"$a"' == '"$b"'" } } fn test-neq {|a b| if (==s $a $b) { fail "FAILED: '"$a"' != '"$b"'" } } fn test-scenario {|name fct| cd $E:TEST_DIR/scenarios/$name direnv allow test-debug echo "\n## Testing "$name" ##" test-debug $fct cd $E:TEST_DIR direnv-eval } ### RUN ### try { direnv allow } catch e { nop } direnv-eval test-scenario base { echo "Setting up" direnv-eval test-eq $E:HELLO "world" set E:WATCHES = $E:DIRENV_WATCHES echo "Reloading (should be no-op)" direnv-eval test-eq $E:WATCHES $E:DIRENV_WATCHES sleep 1 echo "Updating envrc and reloading (should reload)" touch .envrc direnv-eval test-neq $E:WATCHES $E:DIRENV_WATCHES echo "Leaving dir (should clear env set by dir's envrc)" cd .. direnv-eval test-eq $E:HELLO "" } test-scenario inherit { cp ../base/.envrc ../inherited/.envrc direnv-eval echo "HELLO should be world:"$E:HELLO test-eq $E:HELLO "world" sleep 1 echo "export HELLO=goodbye" > ../inherited/.envrc direnv-eval test-eq $E:HELLO "goodbye" } test-scenario "ruby-layout" { direnv-eval test-neq $E:GEM_HOME "" } test-scenario "space dir" { direnv-eval test-eq $E:SPACE_DIR "true" } test-scenario "child-env" { direnv-eval test-eq $E:PARENT_PRE "1" test-eq $E:CHILD "1" test-eq $E:PARENT_POST "1" test-eq $E:REMOVE_ME "" } test-scenario "utf-8" { direnv-eval test-eq $E:UTFSTUFF "♀♂" } ## TODO: special-vars ## TODO: dump ## TODO: empty-var ## TODO: empty-var-unset test-scenario "missing-file-source-env" { direnv-eval } direnv-2.37.1/test/direnv-test.fish000077500000000000000000000106151503714164100172010ustar00rootroot00000000000000#!/usr/bin/env fish function test_eq --argument-names a b if not test (count $argv) = 2 echo "Error: " (count $argv) " arguments passed to `eq`: $argv" exit 1 end if not test $a = $b printf "Error:\n - expected: %s\n - got: %s\n" "$a" "$b" exit 1 end end function test_neq --argument-names a b if not test (count $argv) = 2 echo "Error: " (count $argv) " arguments passed to `neq`: $argv" exit 1 end if test $a = $b printf "Error:\n - expected: %s\n - got: %s\n" "$a" "$b" exit 1 end end function has type -q $argv[1] end cd (dirname (status -f)) set TEST_DIR $PWD set XDG_CONFIG_HOME $TEST_DIR/config set XDG_DATA_HOME $TEST_DIR/data set -gx PATH (dirname $TEST_DIR) $PATH # Reset the direnv loading if any set -x DIRENV_CONFIG $PWD set -e DIRENV_BASH set -e DIRENV_DIR set -e DIRENV_FILE set -e DIRENV_WATCHES set -e DIRENV_DIFF function direnv_eval #direnv export fish # for debugging direnv export fish | source end function test_start -a name cd "$TEST_DIR/scenarios/$name" direnv allow echo "## Testing $name ##" end function test_stop cd / direnv_eval end ### RUN ### direnv allow direnv_eval test_start base begin echo "Setting up" direnv_eval test_eq "$HELLO" world set WATCHES $DIRENV_WATCHES echo "Reloading (should be no-op)" direnv_eval test_eq "$WATCHES" "$DIRENV_WATCHES" sleep 1 echo "Updating envrc and reloading (should reload)" touch .envrc direnv_eval test_neq "$WATCHES" "$DIRENV_WATCHES" echo "Leaving dir (should clear env set by dir's envrc)" cd .. direnv_eval echo $HELLO test -z "$HELLO" || exit 1 set -e WATCHES end test_stop test_start inherit begin cp ../base/.envrc ../inherited/.envrc direnv_eval echo "HELLO should be world:" "$HELLO" sleep 1 echo "export HELLO=goodbye" >../inherited/.envrc direnv_eval test_eq "$HELLO" goodbye end test_stop if has ruby test_start ruby-layout begin direnv_eval test_neq "$GEM_HOME" "" end test_stop end # Make sure directories with spaces are fine test_start "space dir" begin direnv_eval test_eq "$SPACE_DIR" true end test_stop test_start child-env begin direnv_eval test_eq "$PARENT_PRE" 1 test_eq "$CHILD" 1 test_eq "$PARENT_POST" 1 test -z "$REMOVE_ME" || exit 1 end test_stop test_start special-vars begin set -x DIRENV_BASH (command -s bash) set -x DIRENV_CONFIG foobar direnv_eval || true test -n "$DIRENV_BASH" || exit 1 test_eq "$DIRENV_CONFIG" foobar set -e DIRENV_BASH set -e DIRENV_CONFIG end test_stop test_start dump begin set -e LS_COLORS direnv_eval test_eq "$LS_COLORS" "*.ogg=38;5;45:*.wav=38;5;45" test_eq "$LESSOPEN" "||/usr/bin/lesspipe.sh %s" test_eq "$THREE_BACKSLASHES" "\\\\\\" end test_stop test_start empty-var begin direnv_eval set -q FOO || exit 1 test_eq "$FOO" "" end test_stop test_start empty-var-unset begin set -x FOO "" direnv_eval set -q FOO && exit 1 set -e FOO end test_stop test_start in-envrc begin direnv_eval ./test-in-envrc test_eq $status 1 end test_stop test_start missing-file-source-env begin direnv_eval end test_stop test_start symlink-changed begin # when using a symlink, reload if the symlink changes, or if the # target file changes. ln -fs ./state-A ./symlink direnv_eval test_eq "$STATE" A sleep 1 ln -fs ./state-B ./symlink direnv_eval test_eq "$STATE" B end test_stop # Currently broken # test_start utf-8 # begin # direnv_eval # test_eq "$UTFSTUFF" "♀♂" # end # test_stop test_start failure begin # Test that DIRENV_DIFF and DIRENV_WATCHES are set even after a failure. # # This is needed so that direnv doesn't go into a loop when the loading # fails. test_eq "$DIRENV_DIFF" "" test_eq "$DIRENV_WATCHES" "" direnv_eval test_neq "$DIRENV_DIFF" "" test_neq "$DIRENV_WATCHES" "" end test_stop test_start watch-dir begin echo "No watches by default" test_eq "$DIRENV_WATCHES" "$WATCHES" direnv_eval if ! direnv show_dump $DIRENV_WATCHES | grep -q testfile echo "FAILED: testfile not added to DIRENV_WATCHES" exit 1 end echo "After eval, watches have changed" test_neq "$DIRENV_WATCHES" "$WATCHES" end test_stop direnv-2.37.1/test/direnv-test.mx000077500000000000000000000142211503714164100166710ustar00rootroot00000000000000#!/usr/bin/env -S murex -trypipe runmode trypipe module cd ${ dirname $ARGV[2] } global TEST_DIR=$PWD export XDG_CONFIG_HOME=$TEST_DIR/config export XDG_DATA_HOME=$TEST_DIR/data $PATH -> :paths: append ${ dirname $TEST_DIR } -> export PATH # Reset the direnv loading if any export DIRENV_CONFIG=$PWD unsafe { !set DIRENV_BASH !set DIRENV_DIR !set DIRENV_FILE !set DIRENV_WATCHES !set DIRENV_DIFF } mkdir -p $XDG_CONFIG_HOME/direnv touch $XDG_CONFIG_HOME/direnv/direnvrc function direnv_eval { direnv export murex -> set exports if { $exports != "" } then { $exports -> :json: formap key value { if { is-null value } { !export "$key" } else { $value -> export "$key" } } } } function test_start { cd $TEST_DIR/scenarios/$1 direnv allow if { $DIRENV_DEBUG == "1" } then { out } out "## Testing $1 ##" if { $DIRENV_DEBUG == "1" } then { out } } function test_stop { rm -f "$XDG_CONFIG_HOME/direnv/direnv.toml" cd / direnv_eval } function test_eq { if { $1 != $2 } then { out "FAILED: '$1' == '$2'" exit 1 } } function test_neq { if { $1 == $2 } then { out "FAILED: '$1' != '$2'" exit 1 } } function test_empty { if { $1 != "" } then { out "FAILED: '$1' != ''" exit 1 } } function test_nonempty { if { $1 == "" } then { out "FAILED: '$1' == ''" exit 1 } } function which_bash { $PATH -> :paths: foreach p { if { g $p/bash -> f +fx } then { g $p/bash -> f +fx -> foreach found_bash { if { $found_bash != 'null' } then { out $found_bash return } } } } } unsafe { direnv allow } direnv_eval test_start base out "Setting up" direnv_eval test_eq "$HELLO" "world" set WATCHES=$DIRENV_WATCHES out "Reloading (should be no-op)" direnv_eval test_eq $WATCHES $DIRENV_WATCHES sleep 1 out "Updating envrc and reloading (should reload)" touch .envrc direnv_eval test_neq $WATCHES $DIRENV_WATCHES out "Leaving dir (should clear env set by dir's envrc)" cd .. direnv_eval !if { is-null HELLO } then { test_empty $HELLO } !set WATCHES test_stop test_start inherit cp ../base/.envrc ../inherited/.envrc direnv_eval out "HELLO should be world:" "$HELLO" sleep 1 out "export HELLO=goodbye" |> ../inherited/.envrc direnv_eval test_eq "$HELLO" "goodbye" test_stop if { which ruby } then { test_start "ruby-layout" direnv_eval test_neq "$GEM_HOME" "" test_stop } test_start "space dir" direnv_eval test_eq "$SPACE_DIR" "true" test_stop test_start "child-env" direnv_eval test_eq "$PARENT_PRE" "1" test_eq "$CHILD" "1" test_eq "$PARENT_POST" "1" !if { is-null REMOVE_ME } then { test_empty "$REMOVE_ME" } test_stop test_start "special-vars" export DIRENV_BASH=${ which_bash } export DIRENV_CONFIG=foobar unsafe { direnv_eval } test_nonempty "$DIRENV_BASH" test_eq "$DIRENV_CONFIG" "foobar" !export DIRENV_BASH !export DIRENV_CONFIG test_stop test_start "dump" direnv_eval test_eq "$LS_COLORS" "*.ogg=38;5;45:*.wav=38;5;45" test_eq "$THREE_BACKSLASHES" '\\\' test_eq "$LESSOPEN" "||/usr/bin/lesspipe.sh %s" test_stop test_start "empty-var" direnv_eval test_neq ${ $FOO ?? "unset" } "unset" test_eq $FOO "" test_stop test_start "empty-var-unset" export FOO="" direnv_eval test_eq ${ $FOO ?? "unset" } "unset" !export FOO test_stop test_start "in-envrc" direnv_eval unsafe { ./test-in-envrc exitnum -> set es } test_eq $es "1" test_stop test_start "missing-file-source-env" direnv_eval test_stop test_start "symlink-changed" ln -fs ./state-A ./symlink direnv_eval test_eq "$STATE" "A" sleep 1 ln -fs ./state-B ./symlink direnv_eval test_eq "$STATE" "B" test_stop test_start "symlink-dir" direnv allow foo direnv deny foo direnv allow bar direnv deny bar test_stop test_start "utf-8" direnv_eval test_eq "$UTFSTUFF" '♀♂' test_stop test_start "failure" test_eq ${ $DIRENV_DIFF ?? "" } "" test_eq ${ $DIRENV_WATCHES ?? "" } "" unsafe { direnv export murex -> set exports } if { $exports != "" } then { $exports -> :json: formap key value { if { is-null value } { !export "$key" } else { $value -> export "$key" } } } test_neq ${ $DIRENV_DIFF ?? "" } "" test_neq ${ $DIRENV_WATCHES ?? "" } "" test_stop test_start "watch-dir" out "No watches by default" !if { is-null DIRENV_WATCHES WATCHES } then { test_eq "$DIRENV_WATCHES" "$WATCHES" } direnv_eval !if { direnv watch-print -> grep "testdir" } then { out "FAILED: testdir added to watches" exit 1 } !if { direnv show_dump "$DIRENV_WATCHES" -> grep "testfile" } then { out "FAILED: testfile not added to DIRENV_WATCHES" exit 1 } out "After eval, watches have changed" test_neq { DIRENV_WATCHES ?? '' } { WATCHES ?? '' } test_stop test_start "load-envrc-before-env" direnv_eval test_eq "$HELLO" "bar" test_stop test_start "load-env" out "[global] load_dotenv = true" |> "$XDG_CONFIG_HOME/direnv/direnv.toml" direnv allow direnv_eval test_eq "$HELLO" "world" test_stop test_start "skip-env" direnv_eval !if { is-null SKIPPED } then { test_empty "$SKIPPED" } test_stop if { ${ which python } != 'unknown' } then { test_start "python-layout" rm -rf .direnv direnv_eval test_nonempty "$VIRTUAL_ENV" set bool python_in_path = false $PATH -> :paths: foreach paths_entry { if { "$paths_entry" == "$VIRTUAL_ENV/bin" } then { set python_in_path = true } } !if { $python_in_path } then { out "FAILED: VIRTUAL_ENV/bin not added to PATH" exit 1 } !if { g .direnv/CACHEDIR.TAG -> f +f } then { out "the layout dir should contain that file to filter that folder out of backups" exit 1 } test_stop } test_start "aliases" direnv deny direnv permit direnv_eval test_nonempty "$HELLO" direnv block direnv_eval !if { is-null HELLO } then { test_empty "$HELLO" } direnv grant direnv_eval test_nonempty "$HELLO" direnv revoke direnv_eval !if { is-null HELLO } then { test_empty "$HELLO" } direnv grant direnv_eval test_nonempty "$HELLO" direnv disallow direnv_eval !if { is-null HELLO } then { test_empty "$HELLO" } test_stop test_start '$test' direnv_eval test_eq "$FOO" "bar" test_stop test_start 'special-characters/backspace/return' direnv_eval test_eq "$HI" "there" test_stop direnv-2.37.1/test/direnv-test.ps1000066400000000000000000000176611503714164100167600ustar00rootroot00000000000000#!/usr/bin/env pwsh $OriginalLocation = $PWD $TestDir = Split-Path $MyInvocation.MyCommand.Path -Parent Set-Location $TestDir $env:XDG_CONFIG_HOME = "$TestDir/config" $env:XDG_DATA_HOME = "$TestDir/data" # Save original PATH value $OriginalPATH = $env:PATH $env:PATH = "$(Split-Path $TestDir -Parent):$($env:PATH)" # Reset the direnv loading if any $env:DIRENV_CONFIG = $TestDir $envVarsToReset = @( "DIRENV_BASH", "DIRENV_DIR", "DIRENV_FILE", "DIRENV_WATCHES", "DIRENV_DIFF" ) foreach ($var in $envVarsToReset) { if (Get-Item "env:/$var" -ErrorAction SilentlyContinue) { Remove-Item "env:/$var" } } if (!(Get-Item "${Env:XDG_CONFIG_HOME}/direnv/direnvrc" -ErrorVariable SilentlyContinue)) { New-Item -Path "${Env:XDG_CONFIG_HOME}/direnv/direnvrc" -ItemType File } function Stop-TestSuite { [CmdletBinding()] param ([int]$ExitCode = 0) Stop-DirenvTest $env:PATH = $OriginalPATH Set-Location $OriginalLocation exit $ExitCode } function Invoke-Test { param ( [Parameter()] [ValidateNotNullOrEmpty()] [string]$Name, [Parameter()] [ValidateNotNullOrEmpty()] [scriptblock]$Test ) try { Start-DirenvTest $Name Invoke-Command -Command $Test Stop-DirenvTest } catch { Write-Error "${Name}: $_" Stop-TestSuite 1 } } function Invoke-DirenvEval { $export = direnv export pwsh if ($export) { Invoke-Expression $export } } function Assert-Equal { [CmdletBinding()] param ( [string]$Expect, [string]$Actual ) if ($Expect -ne $Actual) { throw "FAILED: '$Expected' == '$Actual'" } } function Assert-NotEqual { [CmdletBinding()] param ( [string]$Expect, [string]$Actual ) if ($Expect -eq $Actual) { throw "FAILED: '$Expected' != '$Actual'" } } function Assert-Empty { [CmdletBinding()] param ([string]$Actual) if (-not [string]::IsNullOrEmpty($Actual)) { throw "FAILED: '$Actual' not empty" } } function Assert-NotEmpty { [CmdletBinding()] param ([string]$Actual) if ([string]::IsNullOrEmpty($Actual)) { throw "FAILED: '$Actual' empty" } } function Start-DirenvTest { [CmdletBinding()] param ([string]$Scenario) Set-Location "$TestDir/scenarios/$Scenario" direnv allow if ($env:DIRENV_DEBUG -eq "1") { Write-Host } Write-Host "## Testing $Scenario ##" -ForegroundColor Green if ($env:DIRENV_DEBUG -eq "1") { Write-Host } } function Stop-DirenvTest { Remove-Item "${Env:XDG_CONFIG_HOME}/direnv/direnv.toml" -ErrorAction SilentlyContinue cd\ # Built-in function to move to root. Invoke-DirenvEval } #region Test direnv allow Invoke-DirenvEval Invoke-Test "base" -Test { Write-Host "Setting up" Invoke-DirenvEval Assert-Equal $env:HELLO "World" $env:WATCHES = $env:DIRENV_WATCHES Write-Host "Reloading (should be no-op)" Invoke-DirenvEval Assert-Equal $env:WATCHES $env:DIRENV_WATCHES Write-Host "Updating envrc and reloading (should reload)" touch .envrc Invoke-DirenvEval Assert-NotEqual $env:WATCHES $env:DIRENV_WATCHES Write-Host "Leaving dir (should clear env set by dir's envrc)" Set-Location .. Invoke-DirenvEval Write-Host $env:HELLO Assert-Empty $env:HELLO Remove-Item "env:/WATCHES" } Invoke-Test "inherit" -Test { Copy-Item "../base/.envrc" "../inherited/.envrc" Invoke-DirenvEval Assert-Equal $env:HELLO "World" Start-Sleep 1 Write-Output "export HELLO=goodbye" | Out-File -FilePath "../inherited/.envrc" Invoke-DirenvEval Assert-Equal $env:HELLO "goodbye" } if (Get-Command ruby -ErrorAction SilentlyContinue) { Invoke-Test "ruby-layout" -Test { Invoke-DirenvEval Assert-NotEmpty $env:GEM_HOME } } Invoke-Test "space dir" -Test { Invoke-DirenvEval Assert-Equal $env:SPACE_DIR "true" } Invoke-Test "child-env" -Test { Invoke-DirenvEval Assert-Equal $env:PARENT_PRE "1" Assert-Equal $env:CHILD "1" Assert-Equal $env:PARENT_POST "1" Assert-Empty $env:REMOVE_ME } Invoke-Test "special-vars" -Test { $env:DIRENV_BASH = (Get-Command bash).Source $env:DIRENV_CONFIG = "foobar" Invoke-DirenvEval Assert-NotEmpty $env:DIRENV_BASH Assert-Equal $env:DIRENV_CONFIG "foobar" Remove-Item env:/DIRENV_BASH Remove-Item env:/DIRENV_CONFIG } Invoke-Test "dump" -Test { Invoke-DirenvEval Assert-Equal $env:LS_COLORS "*.ogg=38;5;45:*.wav=38;5;45" Assert-Equal $env:THREE_BACKSLASHES '\\\' Assert-Equal $env:LESSOPEN "||/usr/bin/lesspipe.sh %s" } #region empty-var scenario # PowerShell assumes an empty environment variable is unset. #endregion Invoke-Test "empty-var-unset" -Test { $env:FOO = "" Invoke-DirenvEval if (-not $env:FOO) { $env:FOO = "unset" } Assert-Equal $env:FOO "unset" Remove-Item env:/FOO } Invoke-Test "in-envrc" -Test { Invoke-DirenvEval ./test-in-envrc Assert-Equal $LASTEXITCODE "1" } Invoke-Test "missing-file-source-env" -Test { Invoke-DirenvEval } Invoke-Test "symlink-changed" -Test { ln -fs ./state-A ./symlink Invoke-DirenvEval Assert-Equal $env:STATE "A" Start-Sleep 1 ln -fs ./state-B ./symlink Invoke-DirenvEval Assert-Equal $env:STATE "B" } Invoke-Test "symlink-dir" -Test { # we can allow and deny the target direnv allow foo direnv deny foo # we can allow and deny the symlink direnv allow bar direnv deny bar } Invoke-Test "utf-8" -Test { Invoke-DirenvEval Assert-Equal $env:UTFSTUFF "♀♂" } Invoke-Test "failure" -Test { # Test that DIRENV_DIFF and DIRENV_WATCHES are set even after a failure. # # This is needed so that direnv doesn't go into a loop when the loading # fails. Assert-Empty $env:DIRENV_DIFF Assert-Empty $env:DIRENV_WATCHES Invoke-DirenvEval Assert-NotEmpty $env:DIRENV_DIFF Assert-NotEmpty $env:DIRENV_WATCHES } Invoke-Test "watch-dir" -Test { Write-Host "no watches by default" Assert-Equal $env:DIRENV_WATCHES $env:WATCHES Invoke-DirenvEval if (-not (direnv show_dump $env:DIRENV_WATCHES | Select-String "testfile")) { throw "FAILED: testfile not added to DIRENV_WATCHES" } Write-Host "After eval, watches have changed" Assert-NotEqual $env:DIRENV_WATCHES $env:WATCHES } Invoke-Test "load-envrc-before-env" -Test { Invoke-DirenvEval Assert-Equal $env:HELLO "bar" } Invoke-Test "load-env" -Test { Write-Output @" [global] load_dotenv = true "@ | Out-File "${env:XDG_CONFIG_HOME}/direnv/direnv.toml" direnv allow Invoke-DirenvEval Assert-Equal $env:HELLO "world" } Invoke-Test "skip-env" -Test { Invoke-DirenvEval Assert-Empty $env:SKIPPED } if (Get-Command python -ErrorAction SilentlyContinue) { Invoke-Test "python-layout" -Test { if (Get-Item .direnv -ErrorAction SilentlyContinue) { Remove-Item .direnv -Force -Recurse -ErrorAction SilentlyContinue } Invoke-DirenvEval Assert-NotEmpty $env:VIRTUAL_ENV if (($env:PATH -split ":") -notcontains "${env:VIRTUAL_ENV}/bin") { throw "FAILED: VIRTUAL_ENV/bin not added to PATH" } if (-not (Get-Item ./.direnv/CACHEDIR.TAG -ErrorAction SilentlyContinue)) { throw "the layout dir should contain that file to filter that folder out of backups" } } Invoke-Test "python-custom-virtual-env" -Test { Invoke-DirenvEval if (-not (Get-Item $env:VIRTUAL_ENV)) { throw "${env:VIRTUAL_ENV} does not exist" } if (($env:PATH -split ":") -notcontains "$PWD/foo/bin") { throw "FAILED: VIRTUAL_ENV/bin not added to PATH" } } } Invoke-Test "aliases" -Test { direnv deny # check that allow/deny aliases work Write-Host "direnv permit" direnv permit Invoke-DirenvEval Assert-NotEmpty $env:HELLO Write-Host "direnv block" direnv block Invoke-DirenvEval Assert-Empty $env:HELLO Write-Host "direnv grant" direnv grant Invoke-DirenvEval Assert-NotEmpty $env:HELLO Write-Host "direnv revoke" direnv revoke Invoke-DirenvEval Assert-Empty $env:HELLO } Invoke-Test '$test' -Test { Invoke-DirenvEval Assert-Equal $env:FOO "bar" } Invoke-Test "special-characters/backspace/return" -Test { Invoke-DirenvEval Assert-Equal $env:HI "there" } #endregion Stop-TestSuite direnv-2.37.1/test/direnv-test.tcsh000077500000000000000000000065531503714164100172170ustar00rootroot00000000000000#!/usr/bin/env tcsh -e cd `dirname $0` setenv TEST_DIR $PWD setenv PATH `dirname $TEST_DIR`:"$PATH" setenv XDG_CONFIG_HOME $TEST_DIR/config setenv XDG_DATA_HOME $TEST_DIR/data # Reset the direnv loading if any setenv DIRENV_CONFIG $PWD unsetenv DIRENV_BASH unsetenv DIRENV_DIR unsetenv DIRENV_FILE unsetenv DIRENV_WATCHES unsetenv DIRENV_DIFF alias direnv_eval 'eval `direnv export tcsh`' # test_start() { # cd "$TEST_DIR/scenarios/$1" # direnv allow # echo "## Testing $1 ##" # } # test_stop { # cd $TEST_DIR # direnv_eval # } ### RUN ### direnv allow || true direnv_eval cd $TEST_DIR/scenarios/base echo "Testing base" direnv_eval test "$HELLO" = "world" setenv WATCHES $DIRENV_WATCHES direnv_eval test "$WATCHES" = "$DIRENV_WATCHES" sleep 1 touch .envrc direnv_eval test "$WATCHES" != "$DIRENV_WATCHES" cd .. direnv_eval test 0 -eq "$?HELLO" cd $TEST_DIR ; direnv_eval cd $TEST_DIR/scenarios/inherit cp ../base/.envrc ../inherited/.envrc direnv allow echo "Testing inherit" direnv_eval test "$HELLO" = "world" sleep 1 echo "export HELLO=goodbye" > ../inherited/.envrc direnv_eval test "$HELLO" = "goodbye" cd $TEST_DIR ; direnv_eval cd $TEST_DIR/scenarios/ruby-layout direnv allow echo "Testing ruby-layout" direnv_eval test "$GEM_HOME" != "" cd $TEST_DIR ; direnv_eval # Make sure directories with spaces are fine cd $TEST_DIR/scenarios/"space dir" direnv allow echo "Testing space dir" direnv_eval test "$SPACE_DIR" = "true" cd $TEST_DIR ; direnv_eval cd $TEST_DIR/scenarios/child-env direnv allow echo "Testing child-env" direnv_eval test "$PARENT_PRE" = "1" test "$CHILD" = "1" test "$PARENT_POST" = "1" test 0 -eq "$?REMOVE_ME" cd $TEST_DIR ; direnv_eval # cd $TEST_DIR/scenarios/special-vars # direnv allow # echo "Testing special-vars" # setenv DIRENV_BASH `which bash` # setenv DIRENV_CONFIG foobar # direnv_eval || true # test -n "$DIRENV_BASH" # test "$DIRENV_CONFIG" = "foobar" # unsetenv DIRENV_BASH # unsetenv DIRENV_CONFIG # cd $TEST_DIR ; direnv_eval cd $TEST_DIR/scenarios/"empty-var" direnv allow echo "Testing empty-var" direnv_eval test "$?FOO" -eq 1 test "$FOO" = "" cd $TEST_DIR ; direnv_eval cd $TEST_DIR/scenarios/"empty-var-unset" direnv allow echo "Testing empty-var-unset" setenv FOO "" direnv_eval test "$?FOO" -eq '0' unsetenv FOO cd $TEST_DIR ; direnv_eval cd $TEST_DIR/scenarios/"parenthesis" direnv allow echo "Testing parenthesis" direnv_eval test "$FOO" = "aaa(bbb)ccc" unsetenv FOO cd $TEST_DIR ; direnv_eval # Currently broken # cd $TEST_DIR/scenarios/"utf-8" # direnv allow # echo "Testing utf-8" # direnv_eval # test "$UTFSTUFF" -eq '♀♂' # cd $TEST_DIR ; direnv_eval # Context: foo/bar is a symlink to ../baz. foo/ contains and .envrc file # BUG: foo/bar is resolved in the .envrc execution context and so can't find # the .envrc file. # # Apparently, the CHDIR syscall does that so I don't know how to work around # the issue. # # cd $TEST_DIR/scenarios/"symlink-bug" # cd foo/bar # direnv_eval # cd $TEST_DIR ; direnv_eval # Pending: test that the mtime is looked on the original file # cd $TEST_DIR/scenarios/"utils" # LINK_TIME=`direnv file-mtime link-to-somefile` # touch somefile # NEW_LINK_TIME=`direnv file-mtime link-to-somefile` # test "$LINK_TIME" = "$NEW_LINK_TIME" # cd $TEST_DIR ; direnv_eval direnv-2.37.1/test/direnv-test.zsh000077500000000000000000000001301503714164100170430ustar00rootroot00000000000000#!/usr/bin/env zsh export TARGET_SHELL=zsh source "$(dirname $0)/direnv-test-common.sh" direnv-2.37.1/test/scenarios/000077500000000000000000000000001503714164100160425ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/$test/000077500000000000000000000000001503714164100170655ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/$test/.envrc000066400000000000000000000000171503714164100202010ustar00rootroot00000000000000export FOO=bar direnv-2.37.1/test/scenarios/aliases/000077500000000000000000000000001503714164100174635ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/aliases/.envrc000066400000000000000000000000231503714164100205740ustar00rootroot00000000000000export HELLO=world direnv-2.37.1/test/scenarios/base/000077500000000000000000000000001503714164100167545ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/base/.envrc000066400000000000000000000000231503714164100200650ustar00rootroot00000000000000export HELLO=world direnv-2.37.1/test/scenarios/child-env/000077500000000000000000000000001503714164100177135ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/child-env/.envrc000066400000000000000000000002061503714164100210270ustar00rootroot00000000000000set -e export PARENT_PRE=1 export REMOVE_ME=1 direnv_load bash -c 'export CHILD=1; unset REMOVE_ME; direnv dump' export PARENT_POST=1 direnv-2.37.1/test/scenarios/dump/000077500000000000000000000000001503714164100170075ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/dump/.envrc000066400000000000000000000006561503714164100201340ustar00rootroot00000000000000direnv_load env \ LS_COLORS='*.ogg=38;5;45:*.wav=38;5;45' \ LESSOPEN='||/usr/bin/lesspipe.sh %s' \ THREE_BACKSLASHES='\\\' \ bash -c "echo to stdout && echo to stderr >&2 && direnv dump" log_status "direnv_load would previously hang if DIRENV_DUMP_FILE_PATH was not opened." log_status "Expect the following to emit an error (which we suppress)." log_status "As long as it doesn't hang, we're okay." direnv_load true || true direnv-2.37.1/test/scenarios/empty-var-unset/000077500000000000000000000000001503714164100211225ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/empty-var-unset/.envrc000066400000000000000000000000111503714164100222300ustar00rootroot00000000000000unset FOOdirenv-2.37.1/test/scenarios/empty-var/000077500000000000000000000000001503714164100177665ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/empty-var/.envrc000066400000000000000000000000151503714164100211000ustar00rootroot00000000000000export FOO=""direnv-2.37.1/test/scenarios/failure/000077500000000000000000000000001503714164100174715ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/failure/.envrc000066400000000000000000000000071503714164100206040ustar00rootroot00000000000000exit 5 direnv-2.37.1/test/scenarios/github-actions/000077500000000000000000000000001503714164100207625ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/github-actions/.envrc000066400000000000000000000002601503714164100220760ustar00rootroot00000000000000export TEST_EXPORT_DIRENV_GITHUB_ACTIONS="${GITHUB_SHA:-MISSING_GITHUB_SHA}"$'\n'"${GITHUB_RUN_ID:MISSING_GITHUB_RUN_ID}"$'\n'"${GITHUB_RUN_NUMBER:-MISSING_GITHUB_RUN_NUMBER}" direnv-2.37.1/test/scenarios/in-envrc/000077500000000000000000000000001503714164100175635ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/in-envrc/test-in-envrc000077500000000000000000000000701503714164100222040ustar00rootroot00000000000000#!/bin/sh test -n "$DIRENV_IN_ENVRC" && echo "in envrc" direnv-2.37.1/test/scenarios/inherit/000077500000000000000000000000001503714164100175045ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/inherit/.envrc000066400000000000000000000000531503714164100206200ustar00rootroot00000000000000source_env ../inherited/.envrc echo $HELLO direnv-2.37.1/test/scenarios/inherited/000077500000000000000000000000001503714164100200155ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/inherited/.gitkeep000066400000000000000000000000001503714164100214340ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/load-env/000077500000000000000000000000001503714164100175475ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/load-env/.env000066400000000000000000000000141503714164100203330ustar00rootroot00000000000000HELLO=world direnv-2.37.1/test/scenarios/load-envrc-before-env/000077500000000000000000000000001503714164100221225ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/load-envrc-before-env/.env000066400000000000000000000000121503714164100227040ustar00rootroot00000000000000HELLO=foo direnv-2.37.1/test/scenarios/load-envrc-before-env/.envrc000066400000000000000000000000211503714164100232310ustar00rootroot00000000000000export HELLO=bar direnv-2.37.1/test/scenarios/missing-file-source-env/000077500000000000000000000000001503714164100225145ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/missing-file-source-env/.envrc000066400000000000000000000000561503714164100236330ustar00rootroot00000000000000source_env "$(mktemp -d -t $RANDOM.XXXXXXXX)" direnv-2.37.1/test/scenarios/parenthesis/000077500000000000000000000000001503714164100203675ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/parenthesis/.envrc000066400000000000000000000000311503714164100214770ustar00rootroot00000000000000export FOO="aaa(bbb)ccc" direnv-2.37.1/test/scenarios/python-custom-virtual-env/000077500000000000000000000000001503714164100231455ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/python-custom-virtual-env/.envrc000066400000000000000000000000451503714164100242620ustar00rootroot00000000000000export VIRTUAL_ENV=foo layout python direnv-2.37.1/test/scenarios/python-custom-virtual-env/foo/000077500000000000000000000000001503714164100237305ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/python-custom-virtual-env/foo/bin/000077500000000000000000000000001503714164100245005ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/python-custom-virtual-env/foo/bin/.keep000066400000000000000000000000001503714164100254130ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/python-layout/000077500000000000000000000000001503714164100206765ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/python-layout/.envrc000066400000000000000000000000161503714164100220110ustar00rootroot00000000000000layout python direnv-2.37.1/test/scenarios/ruby-layout.env000066400000000000000000000001731503714164100210510ustar00rootroot00000000000000export PATH=`pwd`/ruby-layout/bin:$PATH export RUBYLIB=`pwd`/ruby-layout/lib export GEM_HOME=`pwd`/ruby-layout/vendor/gems direnv-2.37.1/test/scenarios/ruby-layout/000077500000000000000000000000001503714164100203365ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/ruby-layout/.envrc000066400000000000000000000000161503714164100214510ustar00rootroot00000000000000layout "ruby" direnv-2.37.1/test/scenarios/skip-env/000077500000000000000000000000001503714164100175765ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/skip-env/.env000066400000000000000000000000171503714164100203650ustar00rootroot00000000000000SKIPPED=dotenv direnv-2.37.1/test/scenarios/space dir/000077500000000000000000000000001503714164100176745ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/space dir/.envrc000066400000000000000000000000451503714164100210110ustar00rootroot00000000000000PATH_add "bin" export SPACE_DIR=true direnv-2.37.1/test/scenarios/special-characters/000077500000000000000000000000001503714164100215775ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/special-characters/backspace/000077500000000000000000000000001503714164100235135ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/special-characters/backspace/return/000077500000000000000000000000001503714164100250325ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/special-characters/backspace/return/.envrc000066400000000000000000000000201503714164100261400ustar00rootroot00000000000000export HI=there direnv-2.37.1/test/scenarios/special-vars/000077500000000000000000000000001503714164100204335ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/special-vars/.envrc000066400000000000000000000000001503714164100215370ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-bug/000077500000000000000000000000001503714164100203035ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-bug/baz/000077500000000000000000000000001503714164100210575ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-bug/baz/.keep000066400000000000000000000000001503714164100217720ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-bug/foo/000077500000000000000000000000001503714164100210665ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-bug/foo/.envrc000066400000000000000000000000001503714164100221720ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-bug/foo/bar000077700000000000000000000000001503714164100217062..ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-changed/000077500000000000000000000000001503714164100211175ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-changed/.envrc000066400000000000000000000000411503714164100222300ustar00rootroot00000000000000watch_file ./symlink . ./symlink direnv-2.37.1/test/scenarios/symlink-changed/.gitignore000066400000000000000000000000101503714164100230760ustar00rootroot00000000000000symlink direnv-2.37.1/test/scenarios/symlink-changed/state-A000066400000000000000000000000171503714164100223360ustar00rootroot00000000000000export STATE=A direnv-2.37.1/test/scenarios/symlink-changed/state-B000066400000000000000000000000171503714164100223370ustar00rootroot00000000000000export STATE=B direnv-2.37.1/test/scenarios/symlink-dir/000077500000000000000000000000001503714164100203045ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-dir/bar000077700000000000000000000000001503714164100214742fooustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-dir/foo/000077500000000000000000000000001503714164100210675ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/symlink-dir/foo/.envrc000066400000000000000000000000001503714164100221730ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/use-rvm/000077500000000000000000000000001503714164100174405ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/use-rvm/.envrc000066400000000000000000000000161503714164100205530ustar00rootroot00000000000000rvm use 1.8.7 direnv-2.37.1/test/scenarios/utf-8/000077500000000000000000000000001503714164100170055ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/utf-8/.envrc000066400000000000000000000000271503714164100201220ustar00rootroot00000000000000export UTFSTUFF=♀♂ direnv-2.37.1/test/scenarios/utils/000077500000000000000000000000001503714164100172025ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/utils/link-to-file000077700000000000000000000000001503714164100231402somefileustar00rootroot00000000000000direnv-2.37.1/test/scenarios/utils/somefile000066400000000000000000000000001503714164100207160ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/watch-dir/000077500000000000000000000000001503714164100177245ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/watch-dir/.envrc000066400000000000000000000000221503714164100210340ustar00rootroot00000000000000watch_dir testdir direnv-2.37.1/test/scenarios/watch-dir/testdir/000077500000000000000000000000001503714164100214025ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/watch-dir/testdir/subdir/000077500000000000000000000000001503714164100226725ustar00rootroot00000000000000direnv-2.37.1/test/scenarios/watch-dir/testdir/subdir/testfile000066400000000000000000000000001503714164100244220ustar00rootroot00000000000000direnv-2.37.1/test/show-direnv-diff.sh000077500000000000000000000003341503714164100175660ustar00rootroot00000000000000#!/bin/sh # # Small util to display the content of the current DIRENV_DIFF env var. # GZIP_HEADER="\x1f\x8b\x08\x00\x00\x00\x00\x00" (printf $GZIP_HEADER; echo $DIRENV_DIFF | base64 -d) | gzip -dc | python -mjson.tool direnv-2.37.1/test/stdlib.bash000077500000000000000000000112771503714164100162070ustar00rootroot00000000000000#!/usr/bin/env bash set -euo pipefail # always execute relative to here cd "$(dirname "$0")" # add the built direnv to the path root=$(cd .. && pwd -P) export PATH=$root:$PATH load_stdlib() { # shellcheck disable=SC1090,SC1091 source "$root/stdlib.sh" } assert_eq() { if [[ $1 != "$2" ]]; then echo "expected '$1' to equal '$2'" return 1 fi } test_name() { echo "--- $*" } test_name dotenv ( load_stdlib workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT cd "$workdir" # Try to source a file that doesn't exist - should not succeed dotenv .env.non_existing_file && return 1 # Try to source a file that exists echo "export FOO=bar" > .env dotenv .env [[ $FOO = bar ]] ) test_name dotenv_if_exists ( load_stdlib workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT cd "$workdir" # Try to source a file that doesn't exist - should succeed dotenv_if_exists .env.non_existing_file || return 1 # Try to source a file that exists echo "export FOO=bar" > .env dotenv_if_exists .env [[ $FOO = bar ]] ) test_name find_up ( load_stdlib path=$(find_up "README.md") assert_eq "$path" "$root/README.md" ) test_name source_up ( load_stdlib cd scenarios/inherited source_up ) test_name direnv_apply_dump ( tmpfile=$(mktemp) # shellcheck disable=SC2317 cleanup() { rm "$tmpfile"; } trap cleanup EXIT load_stdlib FOO=bar direnv dump > "$tmpfile" direnv_apply_dump "$tmpfile" assert_eq "$FOO" bar ) test_name PATH_rm ( load_stdlib export PATH=/usr/local/bin:/home/foo/bin:/usr/bin:/home/foo/.local/bin PATH_rm '/home/foo/*' assert_eq "$PATH" /usr/local/bin:/usr/bin ) test_name path_rm ( load_stdlib somevar=/usr/local/bin:/usr/bin:/home/foo/.local/bin path_rm somevar '/home/foo/*' assert_eq "$somevar" /usr/local/bin:/usr/bin ) test_name expand_path ( load_stdlib tmpdir=$(mktemp -d) trap 'rm -rf $tmpdir' EXIT cd "$tmpdir" ret=$(expand_path ./bar) assert_eq "$ret" "$tmpdir/bar" ) test_name semver_search ( load_stdlib versions=$(mktemp -d) trap 'rm -rf $versions' EXIT mkdir "$versions/program-1.4.0" mkdir "$versions/program-1.4.1" mkdir "$versions/program-1.5.0" mkdir "$versions/1.6.0" assert_eq "$(semver_search "$versions" "program-" "1.4.0")" "1.4.0" assert_eq "$(semver_search "$versions" "program-" "1.4")" "1.4.1" assert_eq "$(semver_search "$versions" "program-" "1")" "1.5.0" assert_eq "$(semver_search "$versions" "program-" "1.8")" "" assert_eq "$(semver_search "$versions" "" "1.6")" "1.6.0" assert_eq "$(semver_search "$versions" "program-" "")" "1.5.0" assert_eq "$(semver_search "$versions" "" "")" "1.6.0" ) test_name use_julia ( load_stdlib JULIA_VERSIONS=$(TMPDIR=. mktemp -d -t tmp.XXXXXXXXXX) trap 'rm -rf $JULIA_VERSIONS' EXIT test_julia() { version_prefix="$1" version="$2" # Fake the existence of a julia binary julia=$JULIA_VERSIONS/$version_prefix$version/bin/julia mkdir -p "$(dirname "$julia")" echo "#!$(command -v bash) echo \"test-julia $version\"" > "$julia" chmod +x "$julia" # Locally disable set -u (see https://github.com/direnv/direnv/pull/667) if ! [[ "$(set +u; use julia "$version" 2>&1)" =~ Successfully\ loaded\ test-julia\ $version ]]; then return 1 fi } # Default JULIA_VERSION_PREFIX unset JULIA_VERSION_PREFIX test_julia "julia-" "1.0.0" test_julia "julia-" "1.1" # Custom JULIA_VERSION_PREFIX JULIA_VERSION_PREFIX="jl-" test_julia "jl-" "1.2.0" test_julia "jl-" "1.3" # Empty JULIA_VERSION_PREFIX # shellcheck disable=SC2034 JULIA_VERSION_PREFIX= test_julia "" "1.4.0" test_julia "" "1.5" ) test_name source_env_if_exists ( load_stdlib workdir=$(mktemp -d) trap 'rm -rf "$workdir"' EXIT cd "$workdir" # Try to source a file that doesn't exist source_env_if_exists non_existing_file # Try to source a file that exists echo "export FOO=bar" > existing_file source_env_if_exists existing_file [[ $FOO = bar ]] # Expect correct path being logged export HOME=$workdir output="$(source_env_if_exists existing_file 2>&1 > /dev/null)" [[ "${output#*'loading ~/existing_file'}" != "$output" ]] ) test_name env_vars_required ( load_stdlib export FOO=1 env_vars_required FOO # these should all fail # shellcheck disable=SC2034 BAR=1 export BAZ= output="$(env_vars_required BAR BAZ MISSING 2>&1 > /dev/null || echo "--- result: $?")" [[ "${output#*'--- result: 1'}" != "$output" ]] [[ "${output#*'BAR is required'}" != "$output" ]] [[ "${output#*'BAZ is required'}" != "$output" ]] [[ "${output#*'MISSING is required'}" != "$output" ]] ) # test strict_env and unstrict_env ./strict_env_test.bash echo OK direnv-2.37.1/test/strict_env_test.bash000077500000000000000000000026361503714164100201440ustar00rootroot00000000000000#! /usr/bin/env bash # Note: this is _explicitly_ not setting `set -euo pipefail` # because we are testing functions that configure that. declare base expected actual base="${TMPDIR:-/tmp}/$(basename "$0").$$" expected="$base".expected actual="$base".actual declare -i success success=0 # always execute relative to here cd "$(dirname "$0")" || exit 1 # add the built direnv to the path root=$(cd .. && pwd -P) export PATH=$root:$PATH load_stdlib() { # shellcheck disable=SC1090 source "$root/stdlib.sh" } test_fail() { echo "FAILED $*: expected is not actual" exit 1 } test_strictness() { local step step="$1" echo "$2" > "$expected" set -o | grep 'errexit\|nounset\|pipefail' > "$actual" diff -u "$expected" "$actual" || test_fail "$step" (( success += 1 )) } load_stdlib test_strictness 'after source' $'errexit off nounset off pipefail off' strict_env test_strictness 'after strict_env' $'errexit on nounset on pipefail on' unstrict_env echo HELLO > /dev/null test_strictness "after unstrict_env with command" $'errexit on nounset on pipefail on' strict_env echo HELLO > /dev/null test_strictness "after strict_env with command" $'errexit on nounset on pipefail on' unstrict_env test_strictness 'after unstrict_env' $'errexit off nounset off pipefail off' echo "OK ($success tests)" direnv-2.37.1/version.txt000066400000000000000000000000071503714164100153200ustar00rootroot000000000000002.37.1 direnv-2.37.1/xdg/000077500000000000000000000000001503714164100136575ustar00rootroot00000000000000direnv-2.37.1/xdg/xdg.go000066400000000000000000000027511503714164100147750ustar00rootroot00000000000000// Package xdg is a minimal implementation of the XDG specification. // // https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html package xdg import ( "path/filepath" ) // DataDir returns the data folder for the application func DataDir(env map[string]string, programName string) string { if env["XDG_DATA_HOME"] != "" { return filepath.Join(env["XDG_DATA_HOME"], programName) } else if env["HOME"] != "" { return filepath.Join(env["HOME"], ".local", "share", programName) } // In theory we could also read /etc/passwd and look for the home based on // the process' UID return "" } // ConfigDir returns the config folder for the application // // The XDG_CONFIG_DIRS case is not being handled func ConfigDir(env map[string]string, programName string) string { if env["XDG_CONFIG_HOME"] != "" { return filepath.Join(env["XDG_CONFIG_HOME"], programName) } else if env["HOME"] != "" { return filepath.Join(env["HOME"], ".config", programName) } // In theory we could also read /etc/passwd and look for the home based on // the process' UID return "" } // CacheDir returns the cache directory for the application func CacheDir(env map[string]string, programName string) string { if env["XDG_CACHE_HOME"] != "" { return filepath.Join(env["XDG_CACHE_HOME"], programName) } else if env["HOME"] != "" { return filepath.Join(env["HOME"], ".cache", programName) } // In theory we could also read /etc/passwd and look for the home based on // the process' UID return "" }