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
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ fn run() -> Result<i32, Box<dyn std::error::Error>> {
let env_object = parse_env_file(&options.full_env_path)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;

// Wildcard expansion: `DB_*` → matched keys, automatically enables JSON output
// Wildcard expansion: `DB_*` -> matched keys, automatically enables JSON output.
// Read-only: a wildcard with --set/--delete is rejected by the qualifying rules.
if keys.len() == 1 && !options.no_json && options.target_keys[0].contains('*') {
if debug {
eprintln!("Wildcard found");
Expand Down
7 changes: 7 additions & 0 deletions src/qualifying_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,12 @@ pub fn qualifying_rules(opts: &Options) -> Result<(), RuleViolationError> {
"Must specify a single key when using --delete".to_string(),
));
}
// A wildcard names zero or more keys, so there is no single key to write to
// or remove. Rather than guess at which match was meant, refuse the write.
if (opts.action_set || opts.action_delete) && opts.target_keys.iter().any(|k| k.contains('*')) {
return Err(RuleViolationError(
"Cannot use a wildcard key with --set or --delete".to_string(),
));
}
Ok(())
}
55 changes: 55 additions & 0 deletions tests/wildcard.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use assert_cmd::Command;
use std::fs;
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;

fn bin() -> Command {
Command::cargo_bin("dotenv").unwrap()
Expand Down Expand Up @@ -87,3 +90,55 @@ fn no_json_disables_wildcard_expansion() {
.assert()
.failure();
}

const WRITABLE: &[u8] = b"# header\nDB_HOST=localhost\nAPP=1\nDB_USER=root\n";

fn writable_env() -> NamedTempFile {
let mut tmp = NamedTempFile::new().unwrap();
tmp.write_all(WRITABLE).unwrap();
tmp.flush().unwrap();
tmp
}

/// A wildcard names zero or more keys, so there is no single key to write to or
/// remove. Rather than guessing at which match was meant (or panicking when
/// there is none), the pattern is refused and the file is left untouched.
fn assert_write_rejected(pattern: &str, action: &[&str]) {
let tmp = writable_env();
bin()
.arg(pattern)
.args(action)
.arg("--file")
.arg(tmp.path())
.assert()
.code(1)
.stderr("Cannot use a wildcard key with --set or --delete\n");

assert_eq!(
fs::read(tmp.path()).unwrap(),
WRITABLE,
"{} {:?} must not modify the file",
pattern,
action
);
}

#[test]
fn wildcard_set_is_rejected() {
assert_write_rejected("DB_*", &["--set", "zzz"]);
}

#[test]
fn wildcard_set_matching_nothing_is_rejected() {
assert_write_rejected("ZZZ_*", &["--set", "zzz"]);
}

#[test]
fn wildcard_delete_is_rejected() {
assert_write_rejected("DB_*", &["--delete"]);
}

#[test]
fn wildcard_delete_matching_nothing_is_rejected() {
assert_write_rejected("ZZZ_*", &["--delete"]);
}
Loading