Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Crates.io Downloads](https://img.shields.io/crates/d/dotenv-cli?logo=Rust&color=blue)](https://crates.io/crates/dotenv-cli)
[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/mikegarde/dotenv-cli/total?logo=github&color=blue)](https://github.com/MikeGarde/dotenv-cli/releases)

A simple way to retrieve, update, or delete .env variables directly from the command line.
A simple way to retrieve, update, delete, or inject .env variables directly from the command line.

## Install

Expand Down Expand Up @@ -76,6 +76,27 @@ Delete a value from a .env file:
dotenv <key> --delete
```

### Running a Command

Load the variables from a `.env` file and run a command with them injected into
its environment. Everything after `--` is treated as the command to run:

```shell
dotenv -- npm run start
```

The child process inherits the current environment merged with the values from
the `.env` file. Variables already present in the environment take precedence
over values from the file, so an existing `export FOO=...` is not clobbered.

This respects `--file` and the `DOTENV_FILE` environment variable, so you can run against any file:

```shell
dotenv --file .env.production -- ./deploy.sh
```

The command's exit code is passed through, making it safe to chain in scripts and CI pipelines.

### Validating a File

Check that a .env file can be parsed without errors:
Expand All @@ -88,8 +109,11 @@ dotenv --validate --file .env.example

### RSA Key Pair

1. **Private Key:** Generate a new key using the `openssl` command. The private key is then stored in the .env file under the variable `RSA_KEY`.
2. **Public Key** The `dotenv` command, with the `--multiline` flag, retrieves the stored private key and pipes it back to openssl. `openssl` then generates a corresponding public key. This public key is stored in the `.env` file under the variable `RSA_PUB`.
1. **Private Key:** Generate a new key using the `openssl` command. The private key is then
stored in the .env file under the variable `RSA_KEY`.
2. **Public Key** The `dotenv` command, with the `--multiline` flag, retrieves the stored private
key and pipes it back to openssl. `openssl` then generates a corresponding public key. This
public key is stored in the `.env` file under the variable `RSA_PUB`.

```shell
openssl genpkey -algorithm RSA -outform PEM -pkeyopt rsa_keygen_bits:2048 2>/dev/null | dotenv RSA_KEY --set -
Expand All @@ -98,7 +122,9 @@ dotenv RSA_KEY -m | openssl rsa -pubout 2>/dev/null | dotenv RSA_PUB --set -

### App Version

This demonstrates two methods for updating the `APP_VERSION` in your `.env` file. The `sed` command is versatile and powerful, allowing for complex text manipulations. On the other hand, `dotenv` provides a more readable and straightforward syntax.
This demonstrates two methods for updating the `APP_VERSION` in your `.env` file. The `sed`
command is versatile and powerful, allowing for complex text manipulations. On the other
hand, `dotenv` provides a more readable and straightforward syntax.

```shell
NEW_VERSION=3.22.1
Expand Down Expand Up @@ -131,8 +157,8 @@ $ dotenv | jq 'to_entries | map(select(.key | startswith("DB_")))[] | "\(.key)=\

### JSON

By default multiple keys are returned as a JSON object. To return a single key as a JSON object, use the `--json` flag.
To not return a JSON object, use the `--no-json` flag.
By default, multiple keys are returned as a JSON object. To return a single key as a
JSON object, use the `--json` flag. To not return a JSON object, use the `--no-json` flag.

Return a .env file as JSON:

Expand Down Expand Up @@ -163,13 +189,11 @@ KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm

### Using DOTENV_FILE Environment Variable

You can define the `DOTENV_FILE` environment variable in your shell or script to specify the `.env` file to use, instead
of passing the `--file` option every time.
You can define the `DOTENV_FILE` environment variable in your shell or script to specify the
`.env` file to use, instead of passing the `--file` option every time. If the `--file` option
is provided, it will override the `DOTENV_FILE` environment variable.

```shell
export DOTENV_FILE=.env.example
dotenv <key>
```

This will use the `.env.example` file automatically. If the `--file` option is provided, it will override the
`DOTENV_FILE` environment variable.
2 changes: 2 additions & 0 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ tasks:
- task: uninstall
- npm install
- task: build:release
- mkdir -p vendor
- cp target/release/dotenv vendor/dotenv
- npm install -g

install:npm:repo:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.2.1",
"description": "Read and update dotenv files from the cli",
"bin": {
"dotenv": "./bin/dotenv"
"dotenv": "bin/dotenv"
},
"type": "module",
"files": [
Expand Down
7 changes: 7 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,11 @@ pub struct Cli {

#[arg(short, long, action = ArgAction::SetTrue, help = "Output extra debugging")]
pub debug: bool,

#[arg(
last = true,
value_name = "command",
help = "Run a command with the .env variables injected (e.g. `dotenv -- npm run start`)"
)]
pub command: Vec<String>,
}
2 changes: 1 addition & 1 deletion src/handlers/get_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub fn get_value(options: &Options) -> bool {
let env_object = options.env_object.as_ref().unwrap();
let mut all_found = true;

let keys: Vec<String> = if options.target_keys.is_empty() {
let keys: Vec<String> = if options.return_all_keys {
env_object.keys().cloned().collect()
} else {
options.target_keys.clone()
Expand Down
1 change: 1 addition & 0 deletions src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod delete_key;
pub mod get_value;
pub mod run_command;
pub mod set_value;
43 changes: 43 additions & 0 deletions src/handlers/run_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::process::Command;

use crate::env_object::EnvObject;

pub fn run_command(env_object: &EnvObject, command: &[String], debug: bool) -> i32 {
let program = &command[0];
let args = &command[1..];

let mut cmd = Command::new(program);
cmd.args(args);

for (key, env_val) in env_object.entries.iter() {
// Don't override variables already present in the environment.
if std::env::var_os(key).is_none() {
cmd.env(key, &env_val.value);
} else if debug {
eprintln!("Skipping {} (already set in the environment)", key);
}
}

if debug {
eprintln!("Running: {} {}", program, args.join(" "));
}

match cmd.status() {
Ok(status) => status.code().unwrap_or_else(|| {
// Terminated by a signal (Unix): mirror shells with 128 + signal.
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
status.signal().map(|s| 128 + s).unwrap_or(1)
}
#[cfg(not(unix))]
{
1
}
}),
Err(e) => {
eprintln!("dotenv: failed to run '{}': {}", program, e);
127
}
}
}
27 changes: 27 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,33 @@ fn run() -> Result<i32, Box<dyn std::error::Error>> {
return Ok(0);
}

// `dotenv -- <command> [args...]` runs a command with
// the .env variables injected into its environment.
if !cli.command.is_empty() {
if !cli.key.is_empty() || cli.set.is_some() || cli.delete || cli.json {
return Err(Box::new(RuleViolationError(
"Cannot combine a command (after `--`) with keys, --set, --delete, or --json"
.to_string(),
)));
}

let full_env_path_str = resolve_env_path(cli.file.clone())?;

if cli.debug {
eprintln!("File: {}", full_env_path_str);
eprintln!("Command: {:?}", cli.command);
}

let env_object = parse_env_file(&full_env_path_str)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;

return Ok(handlers::run_command::run_command(
&env_object,
&cli.command,
cli.debug,
));
}

let debug = cli.debug;
let multiline = cli.multiline;
let delete = cli.delete;
Expand Down
100 changes: 100 additions & 0 deletions tests/debug.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use assert_cmd::Command;
use predicates::prelude::*;
use std::path::Path;

fn bin() -> Command {
Command::cargo_bin("dotenv").unwrap()
}

fn env_path() -> String {
let here = Path::new(env!("CARGO_MANIFEST_DIR"));
here.join("tests/.env.test").to_string_lossy().to_string()
}

#[test]
fn debug_reports_keys_and_file() {
bin()
.arg("NAME")
.arg("--debug")
.arg("--file")
.arg(env_path())
.assert()
.success()
.stdout("dotenv-cli\n")
.stderr(
predicate::str::contains("Keys: [\"NAME\"]")
.and(predicate::str::contains(".env.test"))
.and(predicate::str::contains("Options assembled")),
);
}

#[test]
fn debug_reports_json_defaulting_and_wildcards() {
bin()
.arg("NESTED_*")
.arg("--debug")
.arg("--file")
.arg(env_path())
.assert()
.success()
.stderr(predicate::str::contains("Wildcard found"));
}

#[test]
fn debug_reports_json_defaulting_for_multiple_keys() {
bin()
.arg("NAME")
.arg("EMPTY")
.arg("--debug")
.arg("--file")
.arg(env_path())
.assert()
.success()
.stderr(predicate::str::contains(
"Key count (0 or >1) defaulting to JSON",
));
}

#[test]
fn debug_reports_file_during_validate() {
bin()
.arg("--validate")
.arg("--debug")
.arg("--file")
.arg(env_path())
.assert()
.success()
.stderr(predicate::str::contains("File: "));
}

#[test]
fn debug_reports_command_before_running_it() {
bin()
.arg("--debug")
.arg("--file")
.arg(env_path())
.arg("--")
.arg("true")
.assert()
.success()
.stderr(
predicate::str::contains("Command: [\"true\"]")
.and(predicate::str::contains("Running: true")),
);
}

#[test]
fn debug_reports_variables_skipped_because_already_set() {
bin()
.arg("--debug")
.arg("--file")
.arg(env_path())
.env("NAME", "from-the-shell")
.arg("--")
.arg("true")
.assert()
.success()
.stderr(predicate::str::contains(
"Skipping NAME (already set in the environment)",
));
}
Loading
Loading