THIS REPOSITORY IS DEPRECATED. It illustrates the Generated CLI path to a custom
get-dotenvCLI, which has been superseded by theget-dotenvplugin host. Please see theget-dotenvdocumentation for more info!
This repository demonstrates how to leverage the rich feature set of the get-dotenv CLI within your own project.
This is a template repository based on my TypeScript NPM Package Template, so if you are starting from scratch, cloning this template is a great place to start!
Good code is configuration-driven. A simple and effective way to manage configuration is to use environment variables managed in dotenv files that look like this:
FEE=fie
FOE=fum
When we need to manage different configurations for multiple environments, this can rapidly get out of hand... especially when some of our configurations are secrets that should never be pushed to a code repository!
get-dotenv solves this problem by allowing you to segregate your variables into multiple dotenv files can be loaded into process.env as required. It even supports the dynamic generation or overriding of environment variables based on your own logic!
get-dotenv also provides an extensible CLI that allows you to do the same thing from the command line. This enables all kinds of powerful automation and orchestration scenarios.
This repository demonstrates how to extend the get-dotenv CLI with new commands that wrap functions from your own project.
To get started, clone this repository and run npm install.
The basic structure of the repository mirrors my TypeScript NPM Package Template. See that README for more info.
Find the following files:
└─ get-dotenv-child
├─ .env.local.template
└─ environments
├─ .env.dev.local.template
└─ .env.test.local.template
Copy each of these files and remove the .template extension from the copy. You should now have:
└─ get-dotenv-child
├─ .env.local
├─ .env.local.template
└─ environments
├─ .env.dev.local
├─ .env.dev.local.template
└─ .env.test.local
└─ .env.test.local.template
The resulting .local files contain "secrets" for the purpose of this demo, and are gitignored.
P.S. Like those neat directory trees? Try dirtree!
The TypeScript NPM Package Template exposes a single function foo that logs a message to the console.
This repository extends the base get-dotenv CLI with a new command foo that calls the foo function from the template.
To see this in action, run the following commands:
# Builds the project.
npm run build
# Creates a local symlink so you can call the CLI without extra gymnastics.
npm link
# Display the CLI help.
getdotenvchild -hYou'll see that the base CLI offers a lot of options for managing environment variables. At the bottom, you'll see this:
Commands:
cmd execute shell command string (default command)
foo [options] Wraps the foo function into a CLI command.
help [command] display help for command
Now run these commands:
getdotenvchild foo
# foo global public
getdotenvchild -e dev foo
# foo dev public
getdotenvchild -e test foo
# foo test public
getdotenvchild foo -t '$SECRET'
# foo test secretThe first three commands pulled the a default environment variable (PUBLIC) from different contexts and passed it to the foo function. The last command overrode the default input with a secret value (the SECRET variable).
You aren't just restricted to custom commands. You can also use the base CLI to execute any shell command. For example:
getdotenvchild -e dev cmd echo %DYNAMIC%
# dynamic dev public (a dynamically generated variable, more on that later)
# cmd is the default command, so you can also just...
getdotenvchild -e dev echo %DYNAMIC%
# dynamic dev publicFinally, an NPM script may need to do something on whatever environment is passed into it. You'll have to pass the environment after the script invocation, so the syntax above won't work. Instead, you can use the -c flag to pass a command string:
getdotenvchild -c "echo %DYNAMIC%" -e dev
# dynamic dev publicYou would then articulate your script in package.json like this:
{
"scripts": {
"foo": "getdotenvchild -c \"echo %DYNAMIC%\""
}
}... and you'd execute it like this:
npm run foo -- -e dev # on windows
npm run foo --- -e dev # on linuxBut if you are really smart, you'll install @antfu/ni, which eliminates all kinds of cross-platform nonsense, and you can just do this:
nr foo -e devAll the activity described above is driven by the following files:
└─ get-dotenv-child
├─ .env
├─ .env.dynamic.js
├─ .env.local
├─ environments
│ ├─ .env.dev
│ ├─ .env.dev.local
│ ├─ .env.test
│ └─ .env.test.local
├─ getdotenv.config.json
└─ src
└─ cli
└─ getdotenvchild
├─ fooCommand.ts
└─ index.ts
P.S. Like those neat directory trees? Try dirtree!
All of the files beginning with .env are dotenv files that look like this:
FEE=fie
FOE=fum
.env comtains global public variables that apply to all environment and may be pushed to the git repository.
Those ending in .local contain secrets and should not be pushed to the git repository. This is supported by an entry in .gitignore.
Those with an environment name following .env (e.g. .env.dev, env.dev.local) contain environment-specific values, which augment or override any defined in the global files.
These files may have a different naming convention and be located in any directory; this is specified in the Options section below.
The structure of the CLI and its package configuration follows the same conventions as the underlying template; see that documentation for more info.
The difference here is that this project's CLI uses the get-dotenv CLI as its base and extends it with a new command, foo.
The plumbing requires some familiarity with the commander library but is otherwise very simple. It is fully explained in the comments on two source files in the src/cli directory.
See Positional & Passthrough Options below for one key gotcha.
There are really three sets of options at work here:
-
The
GetDotenvOptionsobject passed togetDotenvthat tells the engine what to load and how. Unless you are callinggetDotenvprogrammatically, you don't need to worry about this. -
The
GetDotenvCliGenerateOptionsobject passed to your CLI that sets the default configuration for thegetDotenvoptions object and also some other stuff. See below for more info. -
The options passed to the CLI at the command line, which can override many the options set above. We'll cover these below as well.
Default options for your CLI can be set in three places, in reverse order of precedence:
-
A
getdotenv.config.jsonfile in the root of your CLI project. Think of these as the global defaults for your CLI. They ship with your package and are the same for everyone. -
Arguments passed to the
generateGetDotenvClifunction in your CLI'sindex.tsfile. These can override values from your globalgetdotenv.config.jsonfile, but the main purpose is to define anyloggerobject andpreHookorpostHookfunctions, which won't fit in a JSON file. -
When your CLI is installed in another project, the author can override your CLI defaults (except for the
logger,preHook, andpostHookfunctions) setting options in a localgetdotenv.config.jsonfile.
As described in A Quick Demo, your CLI can execute arbitrary shell commands, and can thus call itself. When you do this, any options set and variables loaded by the the parent instance are passed down to the child instance.
To avoid repeating myself, the table below also calls out options that can be passed programmatically to the getDotenv function. In this case, there is no "global" getdotenv.config.json file, only (optionally) the one in the root of the package that is calling the function.
| Option | Type | Description | Set Where? | Default Value |
|---|---|---|---|---|
alias |
string |
Cli alias. Should align with the bin property in package.json. |
getdotenv.config.jsongenerateGetDotenvCli |
'getdotenv' |
debug |
boolean | undefined |
Logs CLI internals when true. | getdotenv.config.jsongenerateGetDotenvCli-d, --debug-D, --debug-off |
undefined |
defaultEnv |
string | undefined |
Default target environment (used if env is not provided). |
getdotenv.config.jsongetDotenvgenerateGetDotenvCli--default-env <string> |
undefined |
description |
string |
Cli description (appears in CLI help). | getdotenv.config.jsongenerateGetDotenvCli |
'Base CLI.' |
dotenvToken |
string |
Filename token indicating a dotenv file. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli--dotenv-token <string> |
'.env' |
dynamicPath |
string | undefined |
Path to JS module default-exporting an object keyed to dynamic variable functions. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli--dynamic-path <string> |
undefined |
env |
string | undefined |
Target environment (dotenv expanded). | getDotenv-e, --env <string> |
undefined |
excludeAll |
Exclude all dotenv variables from loading. | -a, --exclude-all-A, --exclude-all-off |
false |
|
excludeDynamic |
boolean | undefined |
Exclude dynamic variables from loading. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli-z, --exclude-dynamic-Z, --exclude-dynamic-off |
false |
excludeEnv |
boolean | undefined |
Exclude environment-specific variables from loading. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli-n, --exclude-env-N, --exclude-env-off |
false |
excludeGlobal |
boolean | undefined |
Exclude global variables from loading. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli-g, --exclude-global-G, --exclude-global-off |
false |
excludePrivate |
boolean | undefined |
Exclude private variables from loading. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli-r, --exclude-private-R, --exclude-private-off |
false |
excludePublic |
boolean | undefined |
Exclude public variables from loading. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli-u, --exclude-public-U, --exclude-public-off |
false |
importMetaUrl |
string |
import.meta.url value from the module that calls generateGetDotenvCli. |
generateGetDotenvCli |
undefined |
loadProcess |
boolean | undefined |
Load dotenv variables to process.env. |
getdotenv.config.jsongetDotenvgenerateGetDotenvCli-p, --load-process-P, --load-process-off |
false |
log |
boolean | undefined |
Log loaded dotenv variables to logger. |
getdotenv.config.jsongetDotenvgenerateGetDotenvCli-l, --log-L, --log-off |
false |
logger |
typeof console |
A logger object that implements the console interface. |
getDotenvgenerateGetDotenvCli |
console |
outputPath |
string | undefined |
If populated, writes consolidated dotenv file to this path (dotenv expanded). | getdotenv.config.jsongetDotenvgenerateGetDotenvCli-o, --output-path <string> |
undefined |
paths |
string |
A delimited string of paths to dotenv files. **When passed to getDotenv this should be a string[]. |
getdotenv.config.jsongetDotenvgenerateGetDotenvCli--paths <string> |
'./' |
pathsDelimiter |
string |
A delimiter string with which to split paths. Only used if pathsDelimiterPattern is not provided. |
getdotenv.config.jsongenerateGetDotenvCli--paths-delimiter <string> |
' ' |
pathsDelimiterPattern |
string | undefined |
A regular expression pattern with which to split paths. Supersedes pathsDelimiter. |
getdotenv.config.jsongenerateGetDotenvCli--paths-delimiter-pattern <string> |
undefined |
preHook |
# |
A function that mutates inbound options & executes side effects within the getDotenv context before executing CLI commands. |
generateGetDotenvCli |
undefined |
privateToken |
string |
Filename token indicating private variables. | getdotenv.config.jsongetDotenvgenerateGetDotenvCli--private-token <string> |
'local' |
postHook |
# |
A function that executes side effects within the getDotenv context after executing CLI commands. |
generateGetDotenvCli |
undefined |
shell |
string | boolean | undefined |
If falsy, Execa will execute commands as Javascript. If true, Execa will execute commands in your OS default shell. Finally, your can specify a shell string. |
getdotenv.config.jsongenerateGetDotenvCli-s, --shell [string]-S, --shell-off |
true |
vars |
string | undefined |
A delimited string of key-value pairs declaratively specifying variables & values to be loaded in addition to any dotenv files (dotenv expanded). When passed to getDotenv this should be a Record<string, string>. |
getdotenv.config.jsongetDotenvgenerateGetDotenvCli-v, --vars <string> |
undefined |
varsAssignor |
string |
A string with which to split keys from values in vars. Only used if varsDelimiterPattern is not provided. |
getdotenv.config.jsongenerateGetDotenvCli--vars-assignor <string> |
'=' |
varsAssignorPattern |
string | undefined |
A regular expression pattern with which to split variable names from values in vars. Supersedes varsAssignor. |
getdotenv.config.jsongenerateGetDotenvCli--vars-assignor-pattern <string> |
undefined |
varsDelimiter |
string |
A string with which to split vars into key-value pairs. Only used if varsDelimiterPattern is not provided. |
getdotenv.config.jsongenerateGetDotenvCli--vars-delimiter <string> |
' ' |
varsDelimiterPattern |
string | undefined |
A regular expression pattern with which to split vars into key-value pairs. Supersedes varsDelimiter. |
getdotenv.config.jsongenerateGetDotenvCli--vars-delimiter-pattern <string> |
undefined |
It won't have escaped your notice that this is a TypeScript project. And generally speaking—ts-node aside—you can't run TypeScript directly. You have to compile it first.
So that's the gotcha. If you want to run your CLI, here are your choices (substituting your own project nomenclature as needed):
- Run it locally. You'll need to know the path to the compiled file.
# compile the project
npm run build
# view the cli help
node dist/getdotenvchild.cli.mjs -h- Link it locally. You can run it from anywhere on your system, but you need a local clone.
# compile the project
npm run build
# link the project
npm link
# view the cli help
getdotenvchild -h
# unlink when done
npm uninstall -g @karmaniverous/get-dotenv-child- Install it locally. You can run it from inside the project where you installed it.
# compile the project
npm run build
# publish the project
npm run release
# install the package in some other project
npm install @karmaniverous/get-dotenv-child
# view the cli help
npx getdotenvchild -h- Install it globally. You can run it from anywhere on your system.
# compile the project
npm run build
# publish the project
npm run release
# install the package globally
npm install -g @karmaniverous/get-dotenv-child
# view the cli help
getdotenvchild -hIf you examine the fooCommand file, you'll see that I employed dotenvExpandFromProcessEnv to expand the target option against process.env.
Why didn't I just use dotenvExpandFromProcessEnv as the input parser for the target option?
Great question! 🤣 Here's what that would look like:
// The default value '$PUBLIC' is a placeholder for a value loaded via dotenv.
.option(
'-t, --target <string>',
'the target to foo',
dotenvExpandFromProcessEnv,
'$PUBLIC',
)It turns out that commander default option values are not subjected to the provided parsing function. So the configured default value ('$PUBLIC') would get passed to your function logic without ever getting parsed.
Ok, so why not just parse the defaut value right there in the option configuration?
Another great question! Here's what that would look like:
// The default value '$PUBLIC' is a placeholder for a value loaded via dotenv.
.option(
'-t, --target <string>',
'the target to foo',
dotenvExpandFromProcessEnv('$PUBLIC'),
)That won't work either, because commander will wind up calling dotenvExpandFromProcessEnv before it runs getDotenv, therefore before process.env is populated with your dotenv variables.
So if you intend to expand your options, it makes sense to do so in your action step, which runs after getDotenv has populated process.env. If you like, you can expand the entire options object at once using dotenvExpandAll
The get-dotenv CLI is based on the commander library, which supports a rich combination of commands, options, arguments, and subcommands.
For example:
$> getdotenv -l foo -b bar bazIn the above example, getdotenv is the root command, and -l is a flag (a boolean option) against that command. foo is a subcommand; -b bar is a string option against the foo subcommand; and baz is an argument to the foo subcommand.
By default, the following command line would produce exactly the same execution:
$> getdotenv foo -l -b bar bazThis works so long as the foo subcommand does not also have a -l flag. When you're in charge of your entire CLI (and when your CLI is simple), this isn't hard to arrange.
However, when you're building a child CLI, you inherit whatever options & arguments the parent CLI has. This can make it difficult to predict the command line that will be passed to your child CLI. So commander provides the enablePositionalOptions and passThroughOptions features, which constrain the CLI so that options & arguments can only be used adjacent to their parent command/subcommand.
The get-dotenv parent CLI has a lot of options, so it's a good idea to enable these features in any command you append to it. You can see an example of this in fooCommand
See more great templates & tools on my GitHub Profile!