From 03566994397793e8cfaa0837c0d44b3e94b28f12 Mon Sep 17 00:00:00 2001 From: Lucas Pickering Date: Fri, 26 Jan 2024 07:27:30 -0500 Subject: [PATCH] Add oranda and build website --- .env-select.toml | 19 +- .github/workflows/web.yml | 98 ++++ .gitignore | 3 + CHANGELOG.md | 182 ++++++ README.md | 69 +-- USAGE.md | 542 ------------------ docs/.gitignore | 1 + docs/book.toml | 11 + docs/src/SUMMARY.md | 20 + docs/src/api/application.md | 19 + docs/src/api/profile.md | 53 ++ docs/src/api/shell_support.md | 9 + docs/src/api/value_source.md | 46 ++ docs/src/getting_started.md | 76 +++ docs/src/install.md | 3 + docs/src/introduction.md | 27 + docs/src/user_guide/inheritance.md | 110 ++++ docs/src/user_guide/key_concepts.md | 57 ++ .../setting_environment_variables.md | 114 ++++ docs/src/user_guide/side_effects.md | 115 ++++ docs/src/user_guide/troubleshooting.md | 30 + 21 files changed, 988 insertions(+), 616 deletions(-) create mode 100644 .github/workflows/web.yml delete mode 100644 USAGE.md create mode 100644 docs/.gitignore create mode 100644 docs/book.toml create mode 100644 docs/src/SUMMARY.md create mode 100644 docs/src/api/application.md create mode 100644 docs/src/api/profile.md create mode 100644 docs/src/api/shell_support.md create mode 100644 docs/src/api/value_source.md create mode 100644 docs/src/getting_started.md create mode 100644 docs/src/install.md create mode 100644 docs/src/introduction.md create mode 100644 docs/src/user_guide/inheritance.md create mode 100644 docs/src/user_guide/key_concepts.md create mode 100644 docs/src/user_guide/setting_environment_variables.md create mode 100644 docs/src/user_guide/side_effects.md create mode 100644 docs/src/user_guide/troubleshooting.md diff --git a/.env-select.toml b/.env-select.toml index c2a8ae5..3d8b604 100644 --- a/.env-select.toml +++ b/.env-select.toml @@ -1,13 +1,10 @@ -[applications.server.profiles.base] -variables = {PROTOCOL = "https"} -[applications.server.profiles.dev] -extends = ["base"] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.base1] +pre_export = [{setup = "echo base1 setup", teardown = "echo base1 teardown"}] -[applications.server.profiles.prd] -extends = ["base"] -[applications.server.profiles.prd.variables] -SERVICE1 = "prd" -SERVICE2 = "also-prd" -SERVICE3 = {type = "file", path = "creds.env", multiple = true} +[applications.server.profiles.base2] +pre_export = [{setup = "echo base2 setup", teardown = "echo base2 teardown"}] + +[applications.server.profiles.child] +extends = ["base1", "base2"] +pre_export = [{setup = "echo child setup", teardown = "echo child teardown"}] \ No newline at end of file diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml new file mode 100644 index 0000000..48b94eb --- /dev/null +++ b/.github/workflows/web.yml @@ -0,0 +1,98 @@ +# Workflow to build your docs with oranda (and mdbook) +# and deploy them to Github Pages +name: Web + +# We're going to push to the gh-pages branch, so we need that permission +permissions: + contents: write + +# What situations do we want to build docs in? +# All of these work independently and can be removed / commented out +# if you don't want oranda/mdbook running in that situation +on: + # Check that a PR didn't break docs! + # + # Note that the "Deploy to Github Pages" step won't run in this mode, + # so this won't have any side-effects. But it will tell you if a PR + # completely broke oranda/mdbook. Sadly we don't provide previews (yet)! + pull_request: + + # Whenever something gets pushed to main, update the docs! + # This is great for getting docs changes live without cutting a full release. + # + # Note that if you're using cargo-dist, this will "race" the Release workflow + # that actually builds the Github Release that oranda tries to read (and + # this will almost certainly complete first). As a result you will publish + # docs for the latest commit but the oranda landing page won't know about + # the latest release. The workflow_run trigger below will properly wait for + # cargo-dist, and so this half-published state will only last for ~10 minutes. + # + # If you only want docs to update with releases, disable this, or change it to + # a "release" branch. You can, of course, also manually trigger a workflow run + # when you want the docs to update. + push: + branches: + - master + + # Whenever a workflow called "Release" completes, update the docs! + # + # If you're using cargo-dist, this is recommended, as it will ensure that + # oranda always sees the latest release right when it's available. Note + # however that Github's UI is wonky when you use workflow_run, and won't + # show this workflow as part of any commit. You have to go to the "actions" + # tab for your repo to see this one running (the gh-pages deploy will also + # only show up there). + workflow_run: + workflows: ["Release"] + types: + - completed + +# Alright, let's do it! +jobs: + web: + name: Build and deploy site and docs + runs-on: ubuntu-latest + steps: + # Setup + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: dtolnay/rust-toolchain@stable + - uses: swatinem/rust-cache@v2 + + # If you use any mdbook plugins, here's the place to install them! + + # Install and run oranda (and mdbook) + # This will write all output to ./public/ (including copying mdbook's output to there) + - name: Install and run oranda + run: | + curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/oranda/releases/download/v0.6.1/oranda-installer.sh | sh + oranda build + + - name: Prepare HTML for link checking + # untitaker/hyperlink supports no site prefixes, move entire site into + # a subfolder + run: mkdir /tmp/public/ && cp -R public /tmp/public/oranda + + - name: Check HTML for broken internal links + uses: untitaker/hyperlink@0.1.29 + with: + args: /tmp/public/ + + # Deploy to our gh-pages branch (creating it if it doesn't exist) + # the "public" dir that oranda made above will become the root dir + # of this branch. + # + # Note that once the gh-pages branch exists, you must + # go into repo's settings > pages and set "deploy from branch: gh-pages" + # the other defaults work fine. + - name: Deploy to Github Pages + uses: JamesIves/github-pages-deploy-action@v4.4.1 + # ONLY if we're on main (so no PRs or feature branches allowed!) + if: ${{ github.ref == 'refs/heads/main' }} + with: + branch: gh-pages + # Gotta tell the action where to find oranda's output + folder: public + token: ${{ secrets.GITHUB_TOKEN }} + single-commit: true diff --git a/.gitignore b/.gitignore index ea8c4bf..4ce0859 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target + +# Generated by `oranda generate ci` +public/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 1712d7d..c7b4ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,3 +13,185 @@ ### Removed - [BREAKING] Removed Kubernetes value source + +## [0.11.0] - 2023-08-24 + +### Breaking Changes + +- Remove the concept of native commands + - The `command` value source is effectively gone, and the `shell` value source type has to renamed to `command` to replace the old one + - In other words, the `shell` type is gone and the `command` field for the `command` type now takes a string instead of a `string[]` + - Side effects can now only be shell commands (string literals) + - This is to reduce the overall complexity of the tool. I don't thi + nk there's a strong use case for native commands, where you can't just use shell commands + +### New Features + +- Add `cwd` option to `command` value source type, to force the command to execute in a particular directory +- Modifications to the `PATH` variable will be prepended to the existing value, rather than replacing it + - This special behavior is based on the variable name, and only applies to `PATH` + +## [0.10.0] - 2023-08-15 + +### Breaking Changes + +- Cascading config files are now only merged down to the profile level + +### New Features + +- Added side effects. See usage docs for more. Imperative environment configuration! + +### Other + +- Sourceable output from `es set` is now written to a temporary file instead of stdout. This difference is handled by the shell functions, so no change to behavior for users + +## [0.9.0] - 2023-07-31 + +### Breaking Changes + +- `es show` is now broken into sub-subcommands: `es show config` and `es show shell` +- Unknown keys in config will now be rejected + +### New Features + +- Add `--run-in-shell` flag to `es run` +- `es run` and `es set` no longer require an application name in the command; if not given, they will prompt, the same way they prompt for profile name + +### Other + +- Provide more context if the subprocess in `es run` fails + +## [0.8.0] - 2023-07-18 + +### New Features + +- Load multiple values from a single source with the `multiple` flag + - Supported for all value source types +- `file` value source, which loads value(s) from a file path (combine with `multiple = true` for maximum fun!) +- Support non-string primitives for simple literal values + - E.g. `VARIABLE1 = 123` or `VARIABLE2 = false` + - These values will simply be stringified before export, since shells only understand strings anyway + +### Other + +- Improve test coverage! + +## [0.7.0] - 2023-07-13 + +This should be the last release with major breaking changes. The config layout has changed dramatically in order to support planned (and unplanned) future features. + +### Breaking Changes + +- Removed `vars` config section. You can no longer provide mappings for single variables. Instead, define a set of profiles with single variables + - This feature didn't provide any additional functionality, it was just a slight convenience at the cost of complexity both for users and code +- Restructured profile config: + - Renamed `apps` field to `applications` + - Add new `profiles` and `variables` subfields + - Overall, this means `apps.app1.profile1.VARIABLE1` will now be `applications.app1.profiles.profile1.variables.VARIABLE1` + - This is more tedious, but allows for current and future features to fit into the config + +### New Features + +- Profile inheritance - profiles can now extend other profiles, eliminating the need to copy-paste a bunch of common content between profiles + +### Other + +- `es` shell function definitions now use the full path to the `env-select` binary rather than relying on `PATH` + - This eliminates the need to add `env-select` to the `PATH`, and also guarantees that the copy of `env-select` that is being executed by `es` is the one that generated that `es` definition in the first place + +## [0.6.2] - 2023-07-08 + +### Other + +- Fix binaries being built for the wrong architecture + +## [0.6.1] - 2023-07-08 + +### Other + +- Defer shell path loading until it's needed. This will enable env-select init on systems that don't have the specified shell present + +## [0.6.0] - 2023-07-08 + +I tried to fit all the foreseeable breaking changes into this release, there may be some more though. + +### Breaking Changes + +- Complex value sources (i.e. anything other than a simple string) now require the `type` field. E.g. `type = "literal"` or `type = "command"` + - As value sources get more intricate, options start to collide. This field makes it easy to disambiguate between source types, which allows them to have overlapping option names +- Rename `--shell-path` option _back_ to `--shell`, and it once again only requires a shell name, rather than a full path + - The full path for the shell will be grabbed via the `which` command now. This means whatever shell you use must be in your `PATH` +- Rename `command` value source type to `shell` + - The old `command` name is now used for commands that are executed natively + +### New Features + +- Add `run` subcommand, for one-off environment usage + - This runs a single command in the configured environment, rather than modifying the shell environment. Similar to `kubectl exec` or `poetry run` +- Add `command` value source type, which accepts an array of strings and executes a command natively, rather than via the shell +- Add `kubernetes` value source type, which executes a command in a kubernetes pod via `kubectl` +- Support complex literal values, enabling the `sensitive` option for literals + - This option probably isn't that useful, but now the field is supported globally +- Add a third level of verbosity (`-vvv`) to enable more granularity in log filtering + +### Other + +- Fix macOS x86 build in CI (the binary will appear on releases now) +- Add a bunch of tests + +## [0.5.0] - 2023-06-30 + +### New Features + +- Shell configuration can now be loaded from `env-select init` function. Add this to your shell startup script to load it automatically. See installation instructions for more info. +- `--shell-path` option allows you to override the `$SHELL` variable. This is rarely necessary, mostly useful for debugging. +- Print configured variables to stderr to give some feedback when running `es set` +- Add `sensitive` option to `command` value source, to mask data in information output +- Support additional verbosity level with `-vv` + +### Other + +- Dynamic commands are now executed within the scope of env-select . Env-select will run your shell as a subprocess to execute the command, rather than print out a templated string (e.g. `$(echo def)`) to invoke a subshell. This reduces the surface area for bugs, and opens up options new kinds of dynamic values. + +## [0.4.1] - 2023-06-25 + +### Other + +Fixed release process. Binaries for 0.4.0 are attached to this release + +## [0.4.0] - 2023-06-25 + +### Breaking Changes + +- Renamed binary from `es` to `env-select` (to facilitate shell plugins using `es`) +- Moved main functionality under `env-select set` subcommand + +### New Features + +- Added fish plugin +- Added `show` subcommand + +### Bug Fixes & Tweaks + +- Emit non-zero exit code for errors +- Print available variables and applications for bare `env-select set` or an invalid variable/application name + +## [0.3.0] - 2023-06-25 + +### New Features + +- Add `command` variant for values, allowing lazily evaluated commands instead of static values + +### Other + +- Add `aarch64-apple-darwin` to release build +- Upgrade to rust 1.67.1 + +## [0.2.0] - 2022-11-12 + +- Reorient schema around named profiles (breaking change) +- Fix terminal cursor disappearing after ctrl-c (#5) +- Allow passing profile name (or literal value, for single variables) as a cmd arg to skip interactive prompt (#3) +- Give profiles/variables consistent ordering in prompt (#2) +- Clean up error handling a bit +- Lots of doc improvements diff --git a/README.md b/README.md index b514800..6a96765 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,11 @@ ![license](https://img.shields.io/github/license/LucasPickering/env-select) [![crates.io version](https://img.shields.io/crates/v/env-select.svg)](https://crates.io/crates/env-select) +- [Home Page](https://env-select.lucaspickering.me) +- [Installation](https://env-select.lucaspickering.me/artifacts/) +- [Docs](https://env-select.lucaspickering.me/book/) +- [Changelog](https://env-select.lucaspickering.me/changelog/) + Easily switch between predefined values for arbitrary environment variables Features include (but are not limited to): - Interative prompts to select between variable profiles @@ -11,16 +16,7 @@ Easily switch between predefined values for arbitrary environment variables Feat - Modify your shell environment with `es set`, or run a one-off command in a modified environment with `es run` - Re-use common variables between profiles with inheritance -## Table of Contents - -- [Simple Example](#simple-example) -- [Installation](#installation) -- [Usage Guide](USAGE.md) -- [Disclaimer](#source-disclaimer) -- [Troubleshooting](#troubleshooting) -- [Bugs/Feedback](#bugsfeedback) - -## Simple Example +## Example ```toml # .env-select.toml @@ -44,65 +40,12 @@ SERVICE2=also-prd dev also-dev ``` -See the [Usage Guide](USAGE.md) for more detailed examples. - -## Installation - -### Brew - -```sh -brew install lucaspickering/tap/env-select -``` - -### Cargo - -```sh -cargo install env-select -``` - -### Configure Your Shell - -**This may not be necessary, depending on what shell you use and how you installed env-select.** The easiest way to check is to open a new shell and run `es help`. If it succeeds, you're good to go. If not, read on. - -Because env-select modifies your shell environment, it requires a wrapper function defined in the shell that can call the `env-select` binary and automatically apply its output. - -**All commands in this README/usage guide assume you have the appropriate shell configuration.** See [the disclaimer](#source-disclaimer) for why this is needed. - -#### Bash - -```sh -echo 'eval "$(env-select --shell bash init)"' >> ~/.bashrc -source ~/.bashrc # Run this in every existing shell -``` - -#### Zsh - -```sh -echo 'source <(env-select --shell zsh init)' >> ~/.zshrc -source ~/.zshrc # Run this in every existing shell -``` - -#### Fish - -```sh -echo 'env-select --shell fish init | source' >> ~/.config/fish/config.fish -source ~/.config/fish/config.fish # Run this in every existing shell -``` - -**Restart your shell (or `source `) after running the above command.** - ## `source` Disclaimer env-select runs as a subprocess to your shell (as all commands do), meaning it cannot modify your shell environment. To get around this, env-select will simply output shell commands that the shell plugins (or you) can then pipe to `source` (or `eval`) to modify your session. If you think piping stuff to `source` is dangerous and sPoOky, you're right. But consider the fact that at this point, you've already downloaded and executed a mystery binary on your machine. You should've already done your due diligence. -## Troubleshooting - -### `es: command not found` - -Make sure you've [configured your shell](#configure-your-shell) to load the `es` function automatically. - ## Bugs/Feedback If you find a bug or have a feature request, please [open an issue on GitHub](https://github.com/LucasPickering/env-select/issues/new). diff --git a/USAGE.md b/USAGE.md deleted file mode 100644 index cba0dca..0000000 --- a/USAGE.md +++ /dev/null @@ -1,542 +0,0 @@ -# Usage Guide - -## Table of Contents - -If viewing this [in GitHub](https://github.com/LucasPickering/env-select/blob/master/USAGE.md), use the Outline button in the top-right to view a table of contents. - -## Concepts - -env-select operates with a few different building blocks. From smallest to largest (rougly), they are: Application, Profile, Variable Mapping, Value Source and Side Effect. - -### Application - -An application is a group. "Application" in this case is a synonym for "use case" or "purpose". Each profile in an application accomplishes different versions of the same goal. - -```sh -# dev -SERVICE1=dev -SERVICE2=also-dev - -# prd -SERVICE1=prd -SERVICE2=also-prd -``` - -### Profile - -A profile is a set of variable mappings. - -```sh -SERVICE1=dev -SERVICE2=also-dev -``` - -### Variable Mapping - -A key mapped to a value source. Variables are selected as part of a profile. - -```sh -SERVICE1=dev -``` - -### Value Source - -A value source is a means of deriving a string for the shell. Typically this is just a literal string: `"abc"`, but it can also be a command that will be evaluated to a string at runtime. - -```sh -dev # Literal -$(echo prd) # Command -``` - -### Side Effect - -A side effect is a pairing of procedures: one to execute during environment, and one during teardown. These are used to perform environment configuration beyond environment variables. An example of a side effect is creating a file during setup, then deleting it during teardown. - -See [Side Effects usage](#side-effects) for more. - -## Usage - -First, define `.env-select.toml`. This is where you'll specify possible options for each variable. Here's an example: - -```toml -[applications.server.profiles.dev] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} - -[applications.server.profiles.prd] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} - -[applications.db.profiles.dev] -variables = {DATABASE = "dev", DB_USER = "root", DB_PASSWORD = "badpw"} -[applications.db.profiles.stg] -variables = {DATABASE = "stg", DB_USER = "root", DB_PASSWORD = "goodpw"} -[applications.db.profiles.prd] -variables = {DATABASE = "prd", DB_USER = "root", DB_PASSWORD = "greatpw"} -``` - -Now, you can easily switch between the defined values with `es`. - -### Select a set of variables - -In the config above, we've already predefined an application called `server`, which consists of two profiles, `dev` and `prd`. We can select between those profiles by providing the _application_ name. - -```sh -> es set server -❯ === dev === -SERVICE1=dev -SERVICE2=also-dev - - === prd === -SERVICE1=prd -SERVICE2=also-prd - -> echo $SERVICE1 $SERVICE2 -dev also-dev -``` - -If you know the name of the profile you want to select, you can skip the prompt by providing it directly to the command: - -```sh -> es set server dev -> echo $SERVICE1 $SERVICE2 -dev also-dev -``` - -### Run a single command - -If you want to run only a single command in the modified environment, rather than modify the entire shell, you can use `es run` instead of `es set`: - -```sh -# Select the profile to use for the `server` application, then run the command -> es run server -- echo $SERVICE1 $SERVICE2 -❯ === dev === -SERVICE1=dev -SERVICE2=also-dev - - === prd === -SERVICE1=prd -SERVICE2=also-prd - -dev also-dev -# You can also specify the profile name up front -> es run server dev -- echo $SERVICE1 $SERVICE2 -dev also-dev -# The surrounding environment is *not* modified -> echo $SERVICE1 $SERVICE2 - -``` - -`--` is required to delineate the arguments handled by `es` from the command being executed. The executed command is executed in your shell, so you can access shell features such as pipes and aliases. - -### Dynamic Values - -You can define variables whose values are provided dynamically, by specifying a command to execute rather than a static value. This allows you to provide values that can change over time, or secrets that you don't want appearing in the file. For example: - -```toml -[applications.db.profiles.dev.variables] -DATABASE = "dev" -DB_USER = "root" -DB_PASSWORD = {type = "command", command = "curl https://www.random.org/strings/?format=plain&len=10&num=1&loweralpha=on", sensitive = true} -``` - -When the `dev` profile is selected for the `db` app, the `DB_PASSWORD` value will be loaded from the file `password.txt`. The `sensitive` field is an _optional_ field that will mask the value in informational logging. - -The command is executed in the shell detected by env-select as your default (or the shell passed with `--shell`). - -### Multiple Values from a Single Source - -If you want to load multiple values from a single source, you can use the `multiple = true` flag. This will tell env-select to expect a mapping of `VARIABLE=value` as output from the value source, with one entry per line. Whitespace lines and anything preceded by a `#` will be ignored (this isthe standard `.env` file format). - -This means that **the key associated with this entry in the `variables` map will be ignored**. - -```toml -[applications.db.profiles.dev.variables] -DATABASE = "dev" -creds = {type = "file", path = "creds.env", multiple = true} -``` - -`creds.env`: - -```sh -DB_USER=root -DB_PASSWORD=hunter2 -``` - -`creds` will now be expanded into multiple variables: - -```sh -> es run db dev -- printenv -DATABASE=dev -DB_USER=root -DB_PASSWORD=hunter2 -``` - -Notice the `creds` key never appears in the environment; this is just a placeholder. You can use any key you want here. - -### Load Values from a File - -You can load one values from a file. - -```toml -[applications.db.profiles.dev.variables] -DATABASE = {type = "file", path = "database.txt"} -``` - -`database.txt`: - -``` -dev -``` - -```sh -> es run db dev -- printenv -DATABASE=dev -``` - -The file path will be relative to **the config file in which the path is defined**. For example, if you have two `.env-select.toml` files: - -```toml -# ~/code/.env-select.toml -[applications.server.profiles.dev.variables] -SERVICE1 = {type = "file", path = "service1.txt"} -``` - -```toml -# ~/.env-select.toml -[applications.server.profiles.dev.variables] -SERVICE2 = {type = "file", path = "service2.txt"} -``` - -In this scenario, `SERVICE1` will be loaded from `~/code/service1.txt` while `SERVICE2` will be loaded from `~/service2.txt`, **regardless of where env-select is invoked from**. - -This value source combines well with the `multiple` field to load `.env` files. [See here](#multiple-values-from-a-single-source). - -### PATH Variable - -If you want to modify the PATH variable, typically you just want to add to it, rather than replace it. Env-select will do this automatically, if the variable has the name `PATH`. - -```toml -[applications.server.profiles.dev.variables] -PATH = "~/.bin" -``` - -```sh -> echo $PATH -/bin:/usr/bin -> es run server dev -- printenv PATH -~/.bin:/bin:/usr/bin -``` - -### Side Effects - -Side effects allow you to configure your environment beyond simple environment variables, using imperative commands. Each side effects has two commands: setup and teardown. Additionally, there are two points at which side effects can execute: pre-export (before environment variables are exported) and post-export (with environment variables available). So there are four side effect stages in total (in their order of execution): - -- Pre-export setup -- Post-export setup -- Post-export teardown -- Pre-export teardown - -The meaning of "setup" and "teardown" varies based on what subcommand you're running: `es set` has no teardown stage, as its purpose is to leave the configured environment in place. Currently there is no way to tear down an `es set` environment (see [#37](https://github.com/LucasPickering/env-select/issues/37)). For `es run`, setup occurs before executing the given command, and teardown occurs after. - -While supplying both setup and teardown commands isn't required, it's best practice to revert whatever changes your setup command may have made. You should only omit the teardown function if your setup doesn't leave any lingering changes in the environment. - -#### Side Effect Examples - -Given this config: - -```toml -[applications.server.profiles.base] -# These commands *cannot* access the constructed environment -pre_export = [ - # Native commands - not executed through the shell - {setup = ["touch", "host.txt"], teardown = ["rm", "-f", "host.txt"]} -] -# These commands can use the constructed environment -post_export = [ - # Shell command - no teardown needed because the above command handles it - {setup = "echo https://$SERVICE1 > host.txt"} -] - - -[applications.server.profiles.dev] -extends = ["base"] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} - -[applications.server.profiles.prd] -extends = ["base"] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} -``` - -This will execute in the followingn order for `es set`: - -```sh -> es set server dev -# 1. Execute pre-export setup (host.txt is created) -# 2. Construct environment -# 3. Execute post-export setup (host URL is written to host.txt) -# 4. Environment is exported to your shell -> echo $SERVICE1 -dev -> cat host.txt -https://dev -``` - -And for `es run`: - -```sh -> es run server dev -- cat host.txt -# 1. Execute pre-export setup (host.txt is created) -# 2. Construct environment -# 3. Execute post-export setup (host URL is written to host.txt) -# 4. `cat host.txt` -https://dev -# 5. Execute post-export teardown (in this case, nothing) -# 6. Clear constructed environment variables -# 7. Execute pre-export teardown (host.txt is deleted) -> cat host.txt -cat: host.txt: No such file or directory -``` - -#### Side Effect Ordering - -Side effects are executed in their order of definition for setup, and the **reverse** order for teardown. This is to enable side effects that depend on each other; the dependents are torn down before the parents are. - -#### Side Effect Inheritance - -Inherited side effects are executed _before_ side effects defined in the selected profile during setup, and therefore _after_ during teardown. For profiles with multiple parents, the _left-most_ parent's side effects will execute first. - -An example of a config with inheritance: - -```toml -[applications.server.profiles.base1] -pre_export = [{setup = "echo base1"}] - -[applications.server.profiles.base2] -pre_export = [{setup = "echo base2"}] - -[applications.server.profiles.child] -extends = ["base1", "base2"] -pre_export = [{setup = "echo child"}] -``` - -And how the inheritance would resolve for the `child` profile: - -```toml -[applications.server.profiles.child] -pre_export = [ - {setup = "echo base1"}, - {setup = "echo base2"}, - {setup = "echo child"}, -] -``` - -## Configuration - -Configuration is defined in [TOML](https://toml.io/en/). Let's see this in action: - -```toml -[applications.server.profiles.variables.dev] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} -[applications.server.profiles.variables.prd] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} - -# This application has no profiles, but is still valid to configure -[applications.empty] - -# These profiles are big, so we can use full table syntax instead of the -# inline syntax. This is purely stylistic; you can make your inline -# tables as big as your heart desires. See https://toml.io/en/v1.0.0#table -[applications.big.profiles.prof1.variables] -VAR1 = "yes" -VAR2 = "yes" -VAR3 = "no" -VAR4 = "no" -VAR5 = "yes" - -[applications.big.profiles.prof2.variables] -VAR1 = "no" -VAR2 = "no" -VAR3 = "no" -VAR4 = "yes" -VAR5 = "no" -``` - -### Disjoint Profiles - -Profiles within an app can define differing sets of variables, like so: - -```toml -[applications.db.profiles.dev] -variables = {DATABASE = "dev", DB_USER = "root"} -[applications.db.profiles.stg] -variables = {DATABASE = "stg", DB_USER = "root", DB_PASSWORD = "goodpw"} -[applications.db.profiles.prd] -variables = {DATABASE = "prd", DB_USER = "root", DB_PASSWORD = "greatpw"} -``` - -The `dev` profile excludes the `DB_PASSWORD` variable. Beware though, whenever switch to the dev profile, it will simply not output a value for `DB_PASSWORD`. That means if you're switch from another profile, `DB_PASSWORD` will retain its old value! For this reason, it's generally best to define the same set of values for every profile in an app, and just use empty values as appropriate. - -### Cascading configs - -On every execution, env-select will scan the current directory for a file called `.env-select.toml` and parse it for a config. In addition to that, it will walk up the directory tree and check each ancestor directory tree for the same file. If multiple files are found, the results will be merged together, **down to the profile level only**. Lower config files having higher precedence. For example, if we execute `es set SERVICE1` in `~/code/`: - -```toml -# ~/code/.env-select.toml -[applications.server.profiles.dev] -variables = {SERVICE1 = "secret-dev-server", SERVICE2 = "another-secret-dev-server"} -[applications.server.profiles.stg] -variables = {SERVICE1 = "secret-stg-server", SERVICE2 = "another-secret-stg-server"} -``` - -```toml -# ~/.env-select.toml -[applications.server.profiles.dev] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} -[applications.server.profiles.prd] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} -``` - -then our resulting config, at execution time, will look like: - -```toml -# Note: this config never exists in the file system, only in memory during program execution - -# From ~/code/.env-select.toml (higher precedence) -[applications.server.profiles.dev] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} -[applications.server.profiles.prd] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} - -# From ~/.env-select.toml (no value in ~/code/.env-select.toml) -[applications.server.profiles.stg] -variables = {SERVICE1 = "secret-stg-server", SERVICE2 = "another-secret-stg-server"} -``` - -To see where env-select is loading configs from, and how they are being merged together, run the command with the `--verbose` (or `-v`) flag. - -## Profile Inheritance - -In addition to top-level merging of multiple config files, env-select also supports inheritance between profiles, via the `extends` field on a profile. For example: - -```toml -[applications.server.profiles.base] -variables = {PROTOCOL = "https"} -[applications.server.profiles.dev] -extends = ["base"] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} -[applications.server.profiles.prd] -extends = ["base"] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} -``` - -During execution, env-select will merge each profile with its parent(s): - -```sh -> es set server -❯ === base === -PROTOCOL=https - - === dev === -SERVICE1=dev -SERVICE2=also-dev -PROTOCOL=https - - === prd === -SERVICE1=prd -SERVICE2=also-prd -PROTOCOL=https -``` - -Note the `PROTOCOL` variable is available in `dev` and `prd`. The profile name given in `extends` is assumed to be a profile of the same application. To extend a profile from another application, use the format `application/profile`: - -```toml -[applications.common.profiles.base] -variables = {PROTOCOL = "https"} -[applications.server.profiles.dev] -extends = ["common/base"] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} -[applications.server.profiles.prd] -extends = ["common/base"] -variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} -``` - -#### Multiple Inheritance and Precedence - -Each profile can extend multiple parents. If two parents have conflicting values, the **left-most** parent has precedence: - -```toml -[applications.server.profiles.base1] -variables = {PROTOCOL = "https"} -[applications.server.profiles.base2] -variables = {PROTOCOL = "http"} -[applications.server.profiles.dev] -extends = ["base1", "base2"] -variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} -``` - -The value from `base1` is used: - -```sh -> es run server dev -- printenv PROTOCOL -https -``` - -Inheritance is applied recursively, meaning you can have arbitrarily large inheritance trees, **as long as there are no cycles**. - -## Configuration Reference - -### Value Source - -There are multiple types of value sources. The type used for a value source is determined by the `type` field. For example: - -```toml -# All of these profiles will generate the same exported value for GREETING - -# Literal shorthand - most common -[applications.example.profiles.shorthand.variables] -GREETING = "hello" - -# Literal expanded form - generally not needed -[applications.example.profiles.literal.variables] -GREETING = {type = "literal", value = "hello"} - -[applications.example.profiles.command.variables] -GREETING = {type = "command", command = "echo hello"} -``` - -#### Value Source Types - -| Value Source Type | Description | -| ----------------- | ----------------------- | -| `literal` | Literal static value | -| `file` | Load values from a file | -| `command` | Execute a shell command | - -#### Common Fields - -All value sources support the following common fields: - -| Option | Type | Default | Description | -| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------- | -| `multiple` | `boolean` | `false` | Load a `VARIABLE=value` mapping, instead of just a `value`; [see more](#multiple-values-from-a-single-source) | -| `sensitive` | `boolean` | `false` | Hide value in console output | - -#### Type-Specific Fields - -Each source type has its own set of available fields: - -| Value Source Type | Field | Type | Default | Description | -| ----------------- | --------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `literal` | `value` | `string` | **Required** | Static value to export | -| `file` | `path` | `string` | **Required** | Path to the file, relative to **the config file in which this is defined** | -| `command` | `command` | `string` | **Required** | Command to execute in a subshell; the output of the command will be exported | -| `command` | `cwd` | `string` | `null` | Directory from which to execute the command. Defaults to the directory from which `es` was invoked. Paths will be relative to the `.env-select.toml` file in which this command is defined. | - -## Shell Support - -env-select supports the following shells: - -- bash -- zsh -- fish - -If you use a different shell and would like support for it, please open an issue and I'll see what I can do! diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..7585238 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +book diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..e223e8d --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,11 @@ +[book] +authors = ["Lucas Pickering"] +description = "Easily switch between common values for arbitrary environment variables" +language = "en" +multilingual = false +src = "src" +title = "env-select" + +[output.html] +edit-url-template = "https://github.com/LucasPickering/env-select/edit/master/docs/{path}" +git-repository-url = "https://github.com/LucasPickering/env-select" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..1070e20 --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,20 @@ +# Summary + +- [Introduction](./introduction.md) +- [Install](./install.md) +- [Getting Started](./getting_started.md) + +# User Guide + +- [Key Concepts](./user_guide/key_concepts.md) +- [Setting Environment Variables](./user_guide/setting_environment_variables.md) +- [Inheritance & Cascading Configs](./user_guide/inheritance.md) +- [Side Effects](./user_guide/side_effects.md) +- [Troubleshooting](./user_guide/troubleshooting.md) + +# API Reference + +- [Application](./api/application.md) +- [Profile](./api/profile.md) +- [Value Source](./api/value_source.md) +- [Shell Support](./api/shell_support.md) diff --git a/docs/src/api/application.md b/docs/src/api/application.md new file mode 100644 index 0000000..407a1fd --- /dev/null +++ b/docs/src/api/application.md @@ -0,0 +1,19 @@ +# Application + +An application is the highest level of configuration resource in env-select. An application generally corresponds 1:1 with a deployed service or code repository. An application is essentially just a container for profiles. Here are some example application configurations: + +```toml +[applications.server.profiles.variables.dev] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.variables.prd] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} + +# This application has no profiles, but is still valid to configure +[applications.empty] +``` + +## Fields + +| Field | Type | Purpose | +| ---------- | ------- | --------------------------------------------------------- | +| `profiles` | `table` | Name:profile mapping for all profiles of this application | diff --git a/docs/src/api/profile.md b/docs/src/api/profile.md new file mode 100644 index 0000000..2cb1ad5 --- /dev/null +++ b/docs/src/api/profile.md @@ -0,0 +1,53 @@ +# Profile + +A profile is a collection of variable mappings and side effects. It generally maps to a single environment for a deployed application. Here are some example profiles: + +```toml +[applications.server.profiles.variables.dev] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.variables.prd] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} + +# This application has no profiles, but is still valid to configure +[applications.empty] + +# These profiles are big, so we can use full table syntax instead of the +# inline syntax. This is purely stylistic; you can make your inline +# tables as big as your heart desires. See https://toml.io/en/v1.0.0#table +[applications.big.profiles.prof1.variables] +VAR1 = "yes" +VAR2 = "yes" +VAR3 = "no" +VAR4 = "no" +VAR5 = "yes" + +[applications.big.profiles.prof2.variables] +VAR1 = "no" +VAR2 = "no" +VAR3 = "no" +VAR4 = "yes" +VAR5 = "no" +``` + +## Disjoint Profiles + +Profiles within an app can define differing sets of variables, like so: + +```toml +[applications.db.profiles.dev] +variables = {DATABASE = "dev", DB_USER = "root"} +[applications.db.profiles.stg] +variables = {DATABASE = "stg", DB_USER = "root", DB_PASSWORD = "goodpw"} +[applications.db.profiles.prd] +variables = {DATABASE = "prd", DB_USER = "root", DB_PASSWORD = "greatpw"} +``` + +The `dev` profile excludes the `DB_PASSWORD` variable. Beware though, whenever you switch to the dev profile, it will simply not output a value for `DB_PASSWORD`. That means if you switch from another profile, `DB_PASSWORD` will retain its old value! For this reason, it's generally best to define the same set of values for every profile in an app, and just use empty values as appropriate. + +## Fields + +| Field | Type | Purpose | +| ------------- | ------- | ------------------------------------------------ | +| `variables` | `table` | Variable:value mapping to export | +| `pre_export` | `array` | Side effects to run _before_ exporting variables | +| `post_export` | `array` | Side effects to run _after_ exporting variables | diff --git a/docs/src/api/shell_support.md b/docs/src/api/shell_support.md new file mode 100644 index 0000000..c76ee84 --- /dev/null +++ b/docs/src/api/shell_support.md @@ -0,0 +1,9 @@ +# Shell Support + +env-select supports the following shells: + +- bash +- zsh +- fish + +If you use a different shell and would like support for it, please open an issue and I'll see what I can do! diff --git a/docs/src/api/value_source.md b/docs/src/api/value_source.md new file mode 100644 index 0000000..399fb69 --- /dev/null +++ b/docs/src/api/value_source.md @@ -0,0 +1,46 @@ +# Value Source + +There are multiple types of value sources. The type used for a value source is determined by the `type` field. For example: + +```toml +# All of these profiles will generate the same exported value for GREETING + +# Literal shorthand - most common +[applications.example.profiles.shorthand.variables] +GREETING = "hello" + +# Literal expanded form - generally not needed +[applications.example.profiles.literal.variables] +GREETING = {type = "literal", value = "hello"} + +[applications.example.profiles.command.variables] +GREETING = {type = "command", command = "echo hello"} +``` + +## Value Source Types + +| Value Source Type | Description | +| ----------------- | ----------------------- | +| `literal` | Literal static value | +| `file` | Load values from a file | +| `command` | Execute a shell command | + +## Common Fields + +All value sources support the following common fields: + +| Option | Type | Default | Description | +| ----------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------- | +| `multiple` | `boolean` | `false` | Load a `VARIABLE=value` mapping, instead of just a `value`; [see more](#multiple-values-from-a-single-source) | +| `sensitive` | `boolean` | `false` | Hide value in console output | + +## Type-Specific Fields + +Each source type has its own set of available fields: + +| Value Source Type | Field | Type | Default | Description | +| ----------------- | --------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `literal` | `value` | `string` | **Required** | Static value to export | +| `file` | `path` | `string` | **Required** | Path to the file, relative to **the config file in which this is defined** | +| `command` | `command` | `string` | **Required** | Command to execute in a subshell; the output of the command will be exported | +| `command` | `cwd` | `string` | `null` | Directory from which to execute the command. Defaults to the directory from which `es` was invoked. Paths will be relative to the `.env-select.toml` file in which this command is defined. | diff --git a/docs/src/getting_started.md b/docs/src/getting_started.md new file mode 100644 index 0000000..5d14a01 --- /dev/null +++ b/docs/src/getting_started.md @@ -0,0 +1,76 @@ +# Getting Started + +## Create the configuration file + +Once you've [installed env-select](/artifacts), setup is easy. All configuration is defined in the [TOML](https://toml.io/en/) format. Create a file called `.env-select.toml` to hold your configuration. This file will apply to to your current directory **and all descendent directories**. In other words, any time you run the `es` command, it will search _up_ the directory tree the `.env-select.toml` file. + +Here's an example `.env-select.toml` file: + +```toml +[applications.server.profiles.dev] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} + +[applications.server.profiles.prd] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} + +[applications.db.profiles.dev] +variables = {DATABASE = "dev", DB_USER = "root", DB_PASSWORD = "badpw"} +[applications.db.profiles.stg] +variables = {DATABASE = "stg", DB_USER = "root", DB_PASSWORD = "goodpw"} +[applications.db.profiles.prd] +variables = {DATABASE = "prd", DB_USER = "root", DB_PASSWORD = "greatpw"} +``` + +Now, you can easily switch between the defined values with `es`. + +## Select a set of variables + +In the config above, we've already predefined an application called `server`, which consists of two profiles, `dev` and `prd`. We can select between those profiles by providing the _application_ name. + +```sh +> es set server +❯ === dev === +SERVICE1=dev +SERVICE2=also-dev + + === prd === +SERVICE1=prd +SERVICE2=also-prd + +> echo $SERVICE1 $SERVICE2 +dev also-dev +``` + +If you know the name of the profile you want to select, you can skip the prompt by providing it directly to the command: + +```sh +> es set server dev +> echo $SERVICE1 $SERVICE2 +dev also-dev +``` + +## Run a single command + +If you want to run only a single command in the modified environment, rather than modify the entire shell, you can use `es run` instead of `es set`: + +```sh +# Select the profile to use for the `server` application, then run the command +> es run server -- echo $SERVICE1 $SERVICE2 +❯ === dev === +SERVICE1=dev +SERVICE2=also-dev + + === prd === +SERVICE1=prd +SERVICE2=also-prd + +dev also-dev +# You can also specify the profile name up front +> es run server dev -- echo $SERVICE1 $SERVICE2 +dev also-dev +# The surrounding environment is *not* modified +> echo $SERVICE1 $SERVICE2 + +``` + +`--` is required to delineate the arguments handled by `es` from the command being executed. The executed command is executed in your shell, so you can access shell features such as pipes and aliases. diff --git a/docs/src/install.md b/docs/src/install.md new file mode 100644 index 0000000..02dbec9 --- /dev/null +++ b/docs/src/install.md @@ -0,0 +1,3 @@ +# Install + +See [installation instructions](/artifacts) diff --git a/docs/src/introduction.md b/docs/src/introduction.md new file mode 100644 index 0000000..0670713 --- /dev/null +++ b/docs/src/introduction.md @@ -0,0 +1,27 @@ +# Introduction + +env-select is a command line tool that makes it easy to define reusable shell environments. Its most common use case (but not only) is to manage deployed environments when working with webapps. For example, if you have a webapp that has local, staging, and production environments, you can use env-select to set environment variables corresponding to each environment: + +```toml +[applications.my_webapp.profiles.local.variables] +PROTOCOL = "http" +HOST = "localhost" +PORT = 3000 + +[applications.my_webapp.profiles.staging.variables] +PROTOCOL = "https" +HOST = "staging.my.webapp" +PORT = 443 + +[applications.my_webapp.profiles.production.variables] +PROTOCOL = "https" +HOST = "production.my.webapp" +PORT = 443 +``` + +env-select integrates with your shell to make it easy to configure environments. There are two possible ways to use env-select's environments: + +- `es set` - Export values to modify your current shell +- `es run` - Run a single command under the environment, without modifying your shell + +Read on to [Getting Started](./getting_started.md) to learn how to use env-select! diff --git a/docs/src/user_guide/inheritance.md b/docs/src/user_guide/inheritance.md new file mode 100644 index 0000000..b1399c0 --- /dev/null +++ b/docs/src/user_guide/inheritance.md @@ -0,0 +1,110 @@ +# Inheritance & Cascading Configs + +env-select supports two different features that enable sharing configuration: cascading configs and profile inheritance. Cascading configs automatically combines multiple `.env-select.toml` files into one config, while profile inheritance allows you to explicitly re-use variable mappings and side effects from other profiles. + +## Cascading configs + +On every execution, env-select will scan the current directory for a file called `.env-select.toml` and parse it for a config. In addition to that, it will walk up the directory tree and check each ancestor directory tree for the same file. If multiple files are found, the results will be merged together, **down to the profile level only**. Lower config files having higher precedence. For example, if we execute `es set SERVICE1` in `~/code/`: + +```toml +# ~/code/.env-select.toml +[applications.server.profiles.dev] +variables = {SERVICE1 = "secret-dev-server", SERVICE2 = "another-secret-dev-server"} +[applications.server.profiles.stg] +variables = {SERVICE1 = "secret-stg-server", SERVICE2 = "another-secret-stg-server"} +``` + +```toml +# ~/.env-select.toml +[applications.server.profiles.dev] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.prd] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} +``` + +then our resulting config, at execution time, will look like: + +```toml +# Note: this config never exists in the file system, only in memory during program execution + +# From ~/code/.env-select.toml (higher precedence) +[applications.server.profiles.dev] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.prd] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} + +# From ~/.env-select.toml (no value in ~/code/.env-select.toml) +[applications.server.profiles.stg] +variables = {SERVICE1 = "secret-stg-server", SERVICE2 = "another-secret-stg-server"} +``` + +To see where env-select is loading configs from, and how they are being merged together, run the command with the `--verbose` (or `-v`) flag. + +## Profile Inheritance + +In addition to top-level merging of multiple config files, env-select also supports inheritance between profiles, via the `extends` field on a profile. For example: + +```toml +[applications.server.profiles.base] +variables = {PROTOCOL = "https"} +[applications.server.profiles.dev] +extends = ["base"] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.prd] +extends = ["base"] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} +``` + +During execution, env-select will merge each profile with its parent(s): + +```sh +> es set server +❯ === base === +PROTOCOL=https + + === dev === +SERVICE1=dev +SERVICE2=also-dev +PROTOCOL=https + + === prd === +SERVICE1=prd +SERVICE2=also-prd +PROTOCOL=https +``` + +The profile name given in `extends` is assumed to be a profile of the same application. To extend a profile from another application, use the format `application/profile`: + +```toml +[applications.common.profiles.base] +variables = {PROTOCOL = "https"} +[applications.server.profiles.dev] +extends = ["common/base"] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +[applications.server.profiles.prd] +extends = ["common/base"] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} +``` + +### Multiple Inheritance and Precedence + +Each profile can extend multiple parents. If two parents have conflicting values, the **left-most** parent has precedence: + +```toml +[applications.server.profiles.base1] +variables = {PROTOCOL = "https"} +[applications.server.profiles.base2] +variables = {PROTOCOL = "http"} +[applications.server.profiles.dev] +extends = ["base1", "base2"] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} +``` + +The value from `base1` is used: + +```sh +> es run server dev -- printenv PROTOCOL +https +``` + +Inheritance is applied recursively, meaning you can have arbitrarily large inheritance trees, **as long as there are no cycles**. diff --git a/docs/src/user_guide/key_concepts.md b/docs/src/user_guide/key_concepts.md new file mode 100644 index 0000000..a108033 --- /dev/null +++ b/docs/src/user_guide/key_concepts.md @@ -0,0 +1,57 @@ +# Key Concepts + +env-select operates with a few different building blocks. From largest to smallest (rougly), they are: [Application](../api/application.md), [Profile](../api/profile.md), [Variable Mapping](../api/variable_mapping.md), [Value Source](../api/value_source.md) and [Side Effect](../api/side_effect.md). + +## Application + +An application is a group. "Application" in this case is a synonym for "use case" or "purpose". Each profile in an application accomplishes different versions of the same goal. Applications tend to map one-to-one to services or code repositories, but don't necessarily have to. + +```sh +# dev +SERVICE1=dev +SERVICE2=also-dev + +# prd +SERVICE1=prd +SERVICE2=also-prd +``` + +See the [API reference](../api/application.md) for more. + +## Profile + +A profile is a set of variable mappings. + +```sh +SERVICE1=dev +SERVICE2=also-dev +``` + +See the [API reference](../api/profile.md) for more. + +## Variable Mapping + +A key mapped to a value source. Variables are selected as part of a profile. + +```sh +SERVICE1=dev +``` + +See the [API reference](../api/variable_mapping.md) for more. + +## Value Source + +A value source is a means of deriving a string for the shell. Typically this is just a literal string: `"abc"`, but it can also be a command that will be evaluated to a string at runtime. + +```sh +dev # Literal +$(echo prd) # Command +``` + +See the [API reference](../api/value_source.md) for more. + +## Side Effect + +A side effect is a pairing of procedures: one to execute during environment, and one during teardown. These are used to perform environment configuration beyond environment variables. An example of a side effect is creating a file during setup, then deleting it during teardown. + +See the [API reference](../api/side_effect.md) and [user guide](./side_effects.md) for more. diff --git a/docs/src/user_guide/setting_environment_variables.md b/docs/src/user_guide/setting_environment_variables.md new file mode 100644 index 0000000..662e016 --- /dev/null +++ b/docs/src/user_guide/setting_environment_variables.md @@ -0,0 +1,114 @@ +# Setting Environment Variables + +The primary purpose of env-select is to configure environment variables. The most common way to do this is to provide static values: + +```toml +[applications.server.profiles.dev] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} + +[applications.server.profiles.prd] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} +``` + +## Dynamic Values + +If your values are not statically known, there are several ways to load dynamic values. Fore more detailed information on each option, see [the API reference](../api/value_source.md). + +### Shell Command + +You can specify a shell command to generate a value. This allows you to provide values that can change over time, or secrets that you don't want appearing in the file. For example: + +```toml +[applications.db.profiles.dev.variables] +DATABASE = "dev" +DB_USER = "root" +DB_PASSWORD = {type = "command", command = "curl https://www.random.org/strings/?format=plain&len=10&num=1&loweralpha=on", sensitive = true} +``` + +When the `dev` profile is selected for the `db` app, the `DB_PASSWORD` value will be randomly generated from a URL. The `sensitive` field is an _optional_ field that will mask the value in informational logging. + +The command is executed in the shell detected by env-select as your default (or the shell passed with `--shell`). + +### File + +You can also load values from a file: + +```toml +[applications.db.profiles.dev.variables] +DATABASE = {type = "file", path = "database.txt"} +``` + +`database.txt`: + +``` +dev +``` + +```sh +> es run db dev -- printenv +DATABASE=dev +``` + +The file path will be relative to **the config file in which the path is defined**. For example, if you have two `.env-select.toml` files: + +```toml +# ~/code/.env-select.toml +[applications.server.profiles.dev.variables] +SERVICE1 = {type = "file", path = "service1.txt"} +``` + +```toml +# ~/.env-select.toml +[applications.server.profiles.dev.variables] +SERVICE2 = {type = "file", path = "service2.txt"} +``` + +In this scenario, `SERVICE1` will be loaded from `~/code/service1.txt` while `SERVICE2` will be loaded from `~/service2.txt`, **regardless of where env-select is invoked from**. + +This value source combines well with the `multiple` field to load `.env` files (see next section). + +## Multiple Values from a Single Source + +If you want to load multiple values from a single source, you can use the `multiple = true` flag. This will tell env-select to expect a mapping of `VARIABLE=value` as output from the value source, with one entry per line. Whitespace lines and anything preceded by a `#` will be ignored (this is the standard `.env` file format). + +This means that **the key associated with this entry in the `variables` map will be ignored**. + +```toml +[applications.db.profiles.dev.variables] +DATABASE = "dev" +creds = {type = "file", path = "creds.env", multiple = true} +``` + +`creds.env`: + +```sh +DB_USER=root +DB_PASSWORD=hunter2 +``` + +`creds` will now be expanded into multiple variables: + +```sh +> es run db dev -- printenv +DATABASE=dev +DB_USER=root +DB_PASSWORD=hunter2 +``` + +Notice the `creds` key never appears in the environment; this is just a placeholder. You can use any key you want here. + +## Adding to the PATH Variable + +If you want to modify the `PATH` variable, typically you just want to add to it, rather than replace it. Because of this, env-select will treat the variable `PATH` specially. + +```toml +[applications.server.profiles.dev.variables] +PATH = "~/.bin" +``` + +```sh +> printenv PATH +/bin:/usr/bin +> es run server dev -- printenv PATH +~/.bin:/bin:/usr/bin +``` diff --git a/docs/src/user_guide/side_effects.md b/docs/src/user_guide/side_effects.md new file mode 100644 index 0000000..9763637 --- /dev/null +++ b/docs/src/user_guide/side_effects.md @@ -0,0 +1,115 @@ +# Side Effects + +Side effects allow you to configure your environment beyond simple environment variables, using imperative commands. Each side effects has two commands: setup and teardown. Additionally, there are two points at which side effects can execute: pre-export (before environment variables are exported) and post-export (with environment variables available). So there are four side effect stages in total (in their order of execution): + +- Pre-export setup +- Post-export setup +- Post-export teardown +- Pre-export teardown + +The meaning of "setup" and "teardown" varies based on what subcommand you're running: `es set` has no teardown stage, as its purpose is to leave the configured environment in place. Currently there is no way to tear down an `es set` environment (see [#37](https://github.com/LucasPickering/env-select/issues/37)). For `es run`, setup occurs before executing the given command, and teardown occurs after. + +While supplying both setup and teardown commands isn't required, it's best practice to revert whatever changes your setup command may have made. You should only omit the teardown function if your setup doesn't leave any lingering changes in the environment. + +## Examples + +Given this config: + +```toml +[applications.server.profiles.base] +# These commands *cannot* access the constructed environment +pre_export = [ + # Native commands - not executed through the shell + {setup = ["touch", "host.txt"], teardown = ["rm", "-f", "host.txt"]} +] +# These commands can use the constructed environment +post_export = [ + # Shell command - no teardown needed because the above command handles it + {setup = "echo https://$SERVICE1 > host.txt"} +] + + +[applications.server.profiles.dev] +extends = ["base"] +variables = {SERVICE1 = "dev", SERVICE2 = "also-dev"} + +[applications.server.profiles.prd] +extends = ["base"] +variables = {SERVICE1 = "prd", SERVICE2 = "also-prd"} +``` + +This will execute in the followingn order for `es set`: + +```sh +> es set server dev +# 1. Execute pre-export setup (host.txt is created) +# 2. Construct environment +# 3. Execute post-export setup (host URL is written to host.txt) +# 4. Environment is exported to your shell +> echo $SERVICE1 +dev +> cat host.txt +https://dev +``` + +And for `es run`: + +```sh +> es run server dev -- cat host.txt +# 1. Execute pre-export setup (host.txt is created) +# 2. Construct environment +# 3. Execute post-export setup (host URL is written to host.txt) +# 4. `cat host.txt` +https://dev +# 5. Execute post-export teardown (in this case, nothing) +# 6. Clear constructed environment variables +# 7. Execute pre-export teardown (host.txt is deleted) +> cat host.txt +cat: host.txt: No such file or directory +``` + +## Ordering + +Side effects are executed in their order of definition for setup, and the **reverse** order for teardown. This is to enable side effects that depend on each other; the dependents are torn down before the parents are. + +## Inheritance + +Inherited side effects are executed _before_ side effects defined in the selected profile during setup, and therefore _after_ during teardown. For profiles with multiple parents, the _left-most_ parent's side effects will execute first. + +An example of a config with inheritance: + +```toml +[applications.server.profiles.base1] +pre_export = [{setup = "echo base1 setup", teardown = "echo base1 teardown"}] + +[applications.server.profiles.base2] +pre_export = [{setup = "echo base2 setup", teardown = "echo base2 teardown"}] + +[applications.server.profiles.child] +extends = ["base1", "base2"] +pre_export = [{setup = "echo child setup", teardown = "echo child teardown"}] +``` + +And how the inheritance would resolve for the `child` profile: + +```toml +[applications.server.profiles.child] +pre_export = [ + {setup = "echo base1 setup", teardown = "echo base1 teardown"}, + {setup = "echo base2 setup", teardown = "echo base2 teardown"}, + {setup = "echo child setup", teardown = "echo child teardown"}, +] +``` + +Here's the order of command execution: + +```sh +> es run server child -- echo hello +base1 setup +base2 setup +child setup +hello +child teardown +base2 teardown +base1 teardown +``` diff --git a/docs/src/user_guide/troubleshooting.md b/docs/src/user_guide/troubleshooting.md new file mode 100644 index 0000000..86c9355 --- /dev/null +++ b/docs/src/user_guide/troubleshooting.md @@ -0,0 +1,30 @@ +# Troubleshooting + +## `es: command not found` + +Because env-select modifies your shell environment, it requires a wrapper function defined in the shell that can call the `env-select` binary and automatically apply its output. + +This error indicates the `es` shell function has not been loaded. Generally it should be installed by the installer, but depending on what shell you use and how you installed env-select, it may be missing. If so, follow the steps for your shell: + +#### Bash + +```sh +echo 'eval "$(env-select --shell bash init)"' >> ~/.bashrc +source ~/.bashrc # Run this in every existing shell +``` + +#### Zsh + +```sh +echo 'source <(env-select --shell zsh init)' >> ~/.zshrc +source ~/.zshrc # Run this in every existing shell +``` + +#### Fish + +```sh +echo 'env-select --shell fish init | source' >> ~/.config/fish/config.fish +source ~/.config/fish/config.fish # Run this in every existing shell +``` + +**Restart your shell (or `source `) after running the above command.**