Skip to content

Commit

Permalink
Revamp dotenv CLI
Browse files Browse the repository at this point in the history
This commit simplifies the code for the clap CLI.

It now takes a path instead of a filename. If no path is specified, then
the default is *.env* in the current directory. This change was made
to prevent directory traversal attacks.

- clap version updated
- converted from builder API to derive API
- **breaking**: uses `from_path` instead of `from_filename`
- **breaking**: defaults to */.env*, no longer traversing parent directories
- **breaking**: exits with code 2 instead of code 1 if the external command is omitted
- error messages updated
- example added
  • Loading branch information
allan2 committed Aug 27, 2024
1 parent e7f398b commit 4010bd3
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 41 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Unreleased]

### Changed

- **breaking**: dotenvy CLI uses `from_path` instead of `from_filename`
- **breaking**: dotenvy CLI defaults to *./.env*, no longer traversing parent directories.
- **breaking**: dotenvy CLI exits with code 2 instead of code 1 if the external command is omitted
- Fix doctests on windows not compiling ([PR #79](https://github.com/allan2/dotenvy/pull/79) by [vallentin](https://github.com/vallentin).
- MSRV updated to 1.68.0

Expand Down
2 changes: 1 addition & 1 deletion dotenv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ name = "dotenvy"
required-features = ["cli"]

[dependencies]
clap = { version = "4.3.11", optional = true }
clap = { version = "4.5.16", features = ["derive"], optional = true }

[dev-dependencies]
tempfile = "3.3.0"
Expand Down
16 changes: 16 additions & 0 deletions dotenv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@ fn main() {
}
```

Async:
```rs
use dotenvy::dotenv;
use std::env;

fn main() {
// load environment variables from .env file
dotenv().expect(".env file not found");

for (key, value) in env::vars() {
println!("{key}: {value}");
}
}
```


### Loading at compile time

The `dotenv!` macro provided by `dotenvy_macro` crate can be used.
Expand Down
89 changes: 50 additions & 39 deletions dotenv/src/bin/dotenvy.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
use clap::Arg;
use std::{os::unix::process::CommandExt, process};
//! A CLI tool that loads a *.env* file before running a command.
//!
//! # Example
//!
//! Given a file *env.txt* with body `FOO=bar`, running
//!
//! ```sh
//! dotenvy -f env.txt printenv FOO
//! ```
//!
//! will output `bar`.
use clap::{Parser, Subcommand};
use core::error;
use std::{os::unix::process::CommandExt, path::PathBuf, process};

macro_rules! die {
($fmt:expr) => ({
Expand All @@ -12,55 +24,54 @@ macro_rules! die {
});
}

fn make_command(name: &str, args: Vec<&str>) -> process::Command {
let mut command = process::Command::new(name);

fn mk_cmd(program: &str, args: &[String]) -> process::Command {
let mut cmd = process::Command::new(program);
for arg in args {
command.arg(arg);
cmd.arg(arg);
}
cmd
}

#[derive(Parser)]
#[command(
name = "dotenvy",
about = "Run a command using an environment loaded from a .env file",
arg_required_else_help = true,
allow_external_subcommands = true
)]
struct Cli {
#[arg(short, long, default_value = "./.env")]
file: PathBuf,
#[clap(subcommand)]
subcmd: Subcmd,
}

command
#[derive(Subcommand)]
enum Subcmd {
#[clap(external_subcommand)]
External(Vec<String>),
}

fn main() {
let matches = clap::Command::new("dotenvy")
.about("Run a command using the environment in a .env file")
.override_usage("dotenvy <COMMAND> [ARGS]...")
.allow_external_subcommands(true)
.arg_required_else_help(true)
.arg(
Arg::new("FILE")
.short('f')
.long("file")
.help("Use a specific .env file (defaults to .env)"),
)
.get_matches();
fn main() -> Result<(), Box<dyn error::Error>> {
let cli = Cli::parse();

match matches.get_one::<String>("FILE") {
None => dotenvy::dotenv(),
Some(file) => dotenvy::from_filename(file),
// load the file
if let Err(e) = dotenvy::from_path(&cli.file) {
die!("Failed to load {path}: {e}", path = cli.file.display());
}
.unwrap_or_else(|e| die!("error: failed to load environment: {}", e));

let mut command = match matches.subcommand() {
Some((name, matches)) => {
let args = matches
.get_many("")
.map(|v| v.copied().collect())
.unwrap_or(Vec::new());

make_command(name, args)
}
None => die!("error: missing required argument <COMMAND>"),
};
// prepare the command
let Subcmd::External(args) = cli.subcmd;
let (program, args) = args.split_first().unwrap();
let mut cmd = mk_cmd(program, args);

// run the command
if cfg!(target_os = "windows") {
match command.spawn().and_then(|mut child| child.wait()) {
match cmd.spawn().and_then(|mut child| child.wait()) {
Ok(status) => process::exit(status.code().unwrap_or(1)),
Err(error) => die!("fatal: {}", error),
Err(e) => die!("fatal: {e}"),
};
} else {
let error = command.exec();
die!("fatal: {}", error);
die!("fatal: {}", cmd.exec());
};
}

0 comments on commit 4010bd3

Please sign in to comment.