Skip to content
Open
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
4 changes: 2 additions & 2 deletions GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ set : 'set' setting eol

setting : 'allow-duplicate-recipes' boolean?
| 'allow-duplicate-variables' boolean?
| 'dotenv-filename' ':=' string
| 'dotenv-filename' ':=' string | string_list
| 'dotenv-load' boolean?
| 'dotenv-path' ':=' string
| 'dotenv-path' ':=' string | string_list
| 'dotenv-required' boolean?
| 'export' boolean?
| 'fallback' boolean?
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -999,10 +999,10 @@ foo:
|------|-------|---------|-------------|
| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |
| `allow-duplicate-variables` | boolean | `false` | Allow variables appearing later in a `justfile` to override earlier variables with the same name. |
| `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. |
| `dotenv-filename` | string or [string] | - | Load a `.env` file with a custom name, if present. If an array is supplied, files are loaded in order. |
| `dotenv-load` | boolean | `false` | Load a `.env` file, if present. |
| `dotenv-override` | boolean | `false` | Override existing environment variables with values from the `.env` file. |
| `dotenv-path` | string | - | Load a `.env` file from a custom path and error if not present. Overrides `dotenv-filename`. |
| `dotenv-path` | string or [string] | - | Load a `.env` file from a custom path and error if not present. If an array is supplied, files are loaded in order. Overrides `dotenv-filename`. |
| `dotenv-required` | boolean | `false` | Error if a `.env` file isn't found. |
| `export` | boolean | `false` | Export all variables as environment variables. |
| `fallback` | boolean | `false` | Search `justfile` in parent directory if the first recipe on the command line is not found. |
Expand Down
24 changes: 15 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub(crate) struct Config {
pub(crate) color: Color,
pub(crate) command_color: Option<ansi_term::Color>,
pub(crate) cygpath: PathBuf,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) dotenv_filename: Vec<String>,
pub(crate) dotenv_path: Vec<PathBuf>,
pub(crate) dry_run: bool,
pub(crate) dump_format: DumpFormat,
pub(crate) explain: bool,
Expand Down Expand Up @@ -214,17 +214,17 @@ impl Config {
.arg(
Arg::new(arg::DOTENV_FILENAME)
.long("dotenv-filename")
.action(ArgAction::Set)
.help("Search for environment file named <DOTENV-FILENAME> instead of `.env`")
.action(ArgAction::Append)
.help("Search for environment files with this name instead of `.env`. Can be used multiple times to load from multiple files in order.")
.conflicts_with(arg::DOTENV_PATH),
)
.arg(
Arg::new(arg::DOTENV_PATH)
.short('E')
.long("dotenv-path")
.action(ArgAction::Set)
.action(ArgAction::Append)
.value_parser(value_parser!(PathBuf))
.help("Load <DOTENV-PATH> as environment file instead of searching for one"),
.help("Load environment files at this path instead of searching. Can be used multiple times to load from multiple files in order."),
)
.arg(
Arg::new(arg::DRY_RUN)
Expand Down Expand Up @@ -785,9 +785,15 @@ impl Config {
.map(CommandColor::into),
cygpath: matches.get_one::<PathBuf>(arg::CYGPATH).unwrap().clone(),
dotenv_filename: matches
.get_one::<String>(arg::DOTENV_FILENAME)
.map(Into::into),
dotenv_path: matches.get_one::<PathBuf>(arg::DOTENV_PATH).map(Into::into),
.get_many::<String>(arg::DOTENV_FILENAME)
.unwrap_or_default()
.map(Into::into)
.collect(),
dotenv_path: matches
.get_many::<PathBuf>(arg::DOTENV_PATH)
.unwrap_or_default()
.map(Into::into)
.collect(),
dry_run: matches.get_flag(arg::DRY_RUN),
dump_format: matches
.get_one::<DumpFormat>(arg::DUMP_FORMAT)
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ pub(crate) use {
string_delimiter::StringDelimiter,
string_kind::StringKind,
string_literal::StringLiteral,
string_literal_or_array::StringLiteralOrArray,
subcommand::Subcommand,
suggestion::Suggestion,
table::Table,
Expand Down Expand Up @@ -263,6 +264,7 @@ mod source;
mod string_delimiter;
mod string_kind;
mod string_literal;
mod string_literal_or_array;
mod subcommand;
mod suggestion;
mod table;
Expand Down
79 changes: 54 additions & 25 deletions src/load_dotenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,63 @@ pub(crate) fn load_dotenv(
settings: &Settings,
working_directory: &Path,
) -> RunResult<'static, BTreeMap<String, String>> {
let dotenv_filename = config
.dotenv_filename
.as_ref()
.or(settings.dotenv_filename.as_ref());
let dotenv_filenames = if !config.dotenv_filename.is_empty() {
config.dotenv_filename.as_slice()
} else {
settings.dotenv_filename.as_slice()
};

let dotenv_path = config
.dotenv_path
.as_ref()
.or(settings.dotenv_path.as_ref());
let dotenv_paths = if !config.dotenv_path.is_empty() {
config.dotenv_path.as_slice()
} else {
settings.dotenv_path.as_slice()
};

if !settings.dotenv_load
&& !settings.dotenv_override
&& !settings.dotenv_required
&& dotenv_filename.is_none()
&& dotenv_path.is_none()
&& dotenv_filenames.is_empty()
&& dotenv_paths.is_empty()
{
return Ok(BTreeMap::new());
}

if let Some(path) = dotenv_path {
let path = working_directory.join(path);
if path.is_file() {
return load_from_file(&path, settings);
if !dotenv_paths.is_empty() {
let present_paths = dotenv_paths
.iter()
.map(|path| working_directory.join(path))
.filter(|path| path.is_file())
.collect::<Vec<_>>();

if !present_paths.is_empty() {
return load_from_files(&present_paths, settings);
}
}

let filename = dotenv_filename.map_or(".env", |s| s.as_str());
let filenames = if dotenv_filenames.is_empty() {
vec![".env"]
} else {
dotenv_filenames
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
};

for directory in working_directory.ancestors() {
let path = directory.join(filename);
if path.is_file() {
return load_from_file(&path, settings);
let present_filenames = filenames
.iter()
.filter_map(|filename| {
let filename = directory.join(filename);
if filename.is_file() {
Some(filename)
} else {
None
}
})
.collect::<Vec<_>>();

if !present_filenames.is_empty() {
return load_from_files(&present_filenames, settings);
}
}

Expand All @@ -47,17 +72,21 @@ pub(crate) fn load_dotenv(
}
}

fn load_from_file(
path: &Path,
fn load_from_files(
paths: &[PathBuf],
settings: &Settings,
) -> RunResult<'static, BTreeMap<String, String>> {
let iter = dotenvy::from_path_iter(path)?;
let mut dotenv = BTreeMap::new();
for result in iter {
let (key, value) = result?;
if settings.dotenv_override || env::var_os(&key).is_none() {
dotenv.insert(key, value);

for path in paths {
let iter = dotenvy::from_path_iter(path)?;
for result in iter {
let (key, value) = result?;
if settings.dotenv_override || env::var_os(&key).is_none() {
dotenv.insert(key, value);
}
}
}

Ok(dotenv)
}
14 changes: 10 additions & 4 deletions src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,10 +323,16 @@ impl<'src> Node<'src> for Set<'src> {
set.push_mut(Tree::string(&argument.cooked));
}
}
Setting::DotenvFilename(value)
| Setting::DotenvPath(value)
| Setting::Tempdir(value)
| Setting::WorkingDirectory(value) => {
Setting::DotenvFilename(value) | Setting::DotenvPath(value) => match value {
StringLiteralOrArray::Single(string_literal) => {
set.push_mut(Tree::string(&string_literal.cooked))
}
StringLiteralOrArray::Multiple(string_literals) => set.push_mut(Tree::list(
string_literals.iter().map(|s| Tree::string(&s.cooked)),
)),
},

Setting::Tempdir(value) | Setting::WorkingDirectory(value) => {
set.push_mut(Tree::string(&value.cooked));
}
}
Expand Down
25 changes: 23 additions & 2 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1139,8 +1139,10 @@ impl<'run, 'src> Parser<'run, 'src> {
self.expect(ColonEquals)?;

let set_value = match keyword {
Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?)),
Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?)),
Keyword::DotenvFilename => Some(Setting::DotenvFilename(
self.parse_string_literal_or_array()?,
)),
Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal_or_array()?)),
Keyword::ScriptInterpreter => Some(Setting::ScriptInterpreter(self.parse_interpreter()?)),
Keyword::Shell => Some(Setting::Shell(self.parse_interpreter()?)),
Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?)),
Expand Down Expand Up @@ -1244,6 +1246,25 @@ impl<'run, 'src> Parser<'run, 'src> {
Ok(Some((token.unwrap(), attributes.into_keys().collect())))
}
}

fn parse_string_literal_or_array(&mut self) -> CompileResult<'src, StringLiteralOrArray<'src>> {
if self.accepted(BracketL)? {
let mut literals = Vec::new();

while !self.next_is(BracketR) {
literals.push(self.parse_string_literal()?);

if !self.accepted(Comma)? {
break;
}
}

self.expect(BracketR)?;
Ok(StringLiteralOrArray::Multiple(literals))
} else {
Ok(StringLiteralOrArray::Single(self.parse_string_literal()?))
}
}
}

#[cfg(test)]
Expand Down
14 changes: 8 additions & 6 deletions src/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ use super::*;
pub(crate) enum Setting<'src> {
AllowDuplicateRecipes(bool),
AllowDuplicateVariables(bool),
DotenvFilename(StringLiteral<'src>),
DotenvFilename(StringLiteralOrArray<'src>),
DotenvLoad(bool),
DotenvOverride(bool),
DotenvPath(StringLiteral<'src>),
DotenvPath(StringLiteralOrArray<'src>),
DotenvRequired(bool),
Export(bool),
Fallback(bool),
Expand Down Expand Up @@ -43,10 +43,12 @@ impl Display for Setting<'_> {
Self::ScriptInterpreter(shell) | Self::Shell(shell) | Self::WindowsShell(shell) => {
write!(f, "[{shell}]")
}
Self::DotenvFilename(value)
| Self::DotenvPath(value)
| Self::Tempdir(value)
| Self::WorkingDirectory(value) => {

Self::DotenvFilename(value) | Self::DotenvPath(value) => {
write!(f, "{}", value)
}

Self::Tempdir(value) | Self::WorkingDirectory(value) => {
write!(f, "{value}")
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool,
pub(crate) allow_duplicate_variables: bool,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_filename: Vec<String>,
pub(crate) dotenv_load: bool,
pub(crate) dotenv_override: bool,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) dotenv_path: Vec<PathBuf>,
pub(crate) dotenv_required: bool,
pub(crate) export: bool,
pub(crate) fallback: bool,
Expand Down Expand Up @@ -42,14 +42,14 @@ impl<'src> Settings<'src> {
Setting::AllowDuplicateVariables(allow_duplicate_variables) => {
settings.allow_duplicate_variables = allow_duplicate_variables;
}
Setting::DotenvFilename(filename) => {
settings.dotenv_filename = Some(filename.cooked);
Setting::DotenvFilename(filenames) => {
settings.dotenv_filename = filenames.cooked();
}
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = dotenv_load;
}
Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path.cooked));
Setting::DotenvPath(paths) => {
settings.dotenv_path = paths.cooked().into_iter().map(Into::into).collect();
}
Setting::DotenvOverride(dotenv_overrride) => {
settings.dotenv_override = dotenv_overrride;
Expand Down
34 changes: 34 additions & 0 deletions src/string_literal_or_array.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use super::*;

#[derive(Debug, Clone)]
pub(crate) enum StringLiteralOrArray<'src> {
Single(StringLiteral<'src>),
Multiple(Vec<StringLiteral<'src>>),
}

impl<'src> StringLiteralOrArray<'src> {
pub(crate) fn cooked(&self) -> Vec<String> {
match self {
Self::Single(lit) => vec![lit.cooked.clone()],
Self::Multiple(lits) => lits.iter().map(|lit| lit.cooked.clone()).collect(),
}
}
}

impl Display for StringLiteralOrArray<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::Single(lit) => write!(f, "{lit}"),
Self::Multiple(lits) => {
write!(f, "[")?;
for (i, lit) in lits.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{lit}")?;
}
write!(f, "]")
}
}
}
}
Loading
Loading