Skip to content

Commit

Permalink
Add dotenv-filename and dotenv-path settings (#1692)
Browse files Browse the repository at this point in the history
  • Loading branch information
ltfourrier authored Oct 12, 2023
1 parent d0c87c8 commit 812e1ea
Show file tree
Hide file tree
Showing 12 changed files with 233 additions and 89 deletions.
2 changes: 2 additions & 0 deletions GRAMMAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ assignment : NAME ':=' expression eol
export : 'export' assignment
setting : 'set' 'allow-duplicate-recipes' boolean?
| 'set' 'dotenv-filename' ':=' string
| 'set' 'dotenv-load' boolean?
| 'set' 'dotenv-path' ':=' string
| 'set' 'export' boolean?
| 'set' 'fallback' boolean?
| 'set' 'ignore-comments' boolean?
Expand Down
73 changes: 37 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Yay, all your tests passed!

- Wherever possible, errors are resolved statically. Unknown recipes and circular dependencies are reported before anything runs.

- `just` [loads `.env` files](#dotenv-integration), making it easy to populate environment variables.
- `just` [loads `.env` files](#dotenv-settings), making it easy to populate environment variables.

- Recipes can be [listed from the command line](#listing-available-recipes).

Expand Down Expand Up @@ -669,7 +669,9 @@ foo:
| Name | Value | Default | Description |
| ------------------------- | ------------------ | ------- |---------------------------------------------------------------------------------------------- |
| `allow-duplicate-recipes` | boolean | `false` | Allow recipes appearing later in a `justfile` to override earlier recipes with the same name. |
| `dotenv-filename` | string | - | Load a `.env` file with a custom name, if present. |
| `dotenv-load` | boolean | `false` | Load a `.env` file, if present. |
| `dotenv-path` | string | - | Load a `.env` file from a custom path, if present. Overrides `dotenv-filename`. |
| `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. |
| `ignore-comments` | boolean | `false` | Ignore recipe lines beginning with `#`. |
Expand Down Expand Up @@ -710,9 +712,41 @@ $ just foo
bar
```

#### Dotenv Load
#### Dotenv Settings

If `dotenv-load` is `true`, a `.env` file will be loaded if present. Defaults to `false`.
If `dotenv-load`, `dotenv-filename` or `dotenv-path` is set, `just` will load environment variables from a file.

If `dotenv-path` is set, `just` will look for a file at the given path.

Otherwise, `just` looks for a file named `.env` by default, unless `dotenv-filename` set, in which case the value of `dotenv-filename` is used. This file can be located in the same directory as your `justfile` or in a parent directory.

The loaded variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks.

For example, if your `.env` file contains:

```sh
# a comment, will be ignored
DATABASE_ADDRESS=localhost:6379
SERVER_PORT=1337
```

And your `justfile` contains:

```just
set dotenv-load
serve:
@echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```

`just serve` will output:

```sh
$ just serve
Starting server with database localhost:6379 on port 1337…
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```

#### Export

Expand Down Expand Up @@ -878,36 +912,6 @@ Available recipes:
test # test stuff
```

### Dotenv Integration

If [`dotenv-load`](#dotenv-load) is set, `just` will load environment variables from a file named `.env`. This file can be located in the same directory as your `justfile` or in a parent directory. These variables are environment variables, not `just` variables, and so must be accessed using `$VARIABLE_NAME` in recipes and backticks.

For example, if your `.env` file contains:

```sh
# a comment, will be ignored
DATABASE_ADDRESS=localhost:6379
SERVER_PORT=1337
```

And your `justfile` contains:

```just
set dotenv-load
serve:
@echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT…"
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```

`just serve` will output:

```sh
$ just serve
Starting server with database localhost:6379 on port 1337…
./server --database $DATABASE_ADDRESS --port $SERVER_PORT
```

### Variables and Substitution

Variables, strings, concatenation, path joining, and substitution using `{{…}}` are supported:
Expand Down Expand Up @@ -1528,9 +1532,6 @@ print_home_folder:
$ just
HOME is '/home/myuser'
```
#### Loading Environment Variables from a `.env` File

`just` will load environment variables from a `.env` file if [dotenv-load](#dotenv-load) is set. The variables in the file will be available as environment variables to the recipes. See [dotenv-integration](#dotenv-integration) for more information.

#### Setting `just` Variables from Environment Variables

Expand Down
4 changes: 0 additions & 4 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ alias t := test

alias c := check

bt := '0'

export RUST_BACKTRACE := bt

log := "warn"

export JUST_LOG := log
Expand Down
2 changes: 2 additions & 0 deletions src/keyword.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use super::*;
pub(crate) enum Keyword {
Alias,
AllowDuplicateRecipes,
DotenvFilename,
DotenvLoad,
DotenvPath,
Else,
Export,
Fallback,
Expand Down
25 changes: 14 additions & 11 deletions src/load_dotenv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ pub(crate) fn load_dotenv(
settings: &Settings,
working_directory: &Path,
) -> RunResult<'static, BTreeMap<String, String>> {
if !settings.dotenv_load.unwrap_or(false)
&& config.dotenv_filename.is_none()
&& config.dotenv_path.is_none()
{
let dotenv_filename = config
.dotenv_filename
.as_ref()
.or(settings.dotenv_filename.as_ref());

let dotenv_path = config
.dotenv_path
.as_ref()
.or(settings.dotenv_path.as_ref());

if !settings.dotenv_load.unwrap_or(false) && dotenv_filename.is_none() && dotenv_path.is_none() {
return Ok(BTreeMap::new());
}

if let Some(path) = &config.dotenv_path {
if let Some(path) = dotenv_path {
return load_from_file(path);
}

let filename = config
.dotenv_filename
.as_deref()
.unwrap_or(DEFAULT_DOTENV_FILENAME)
.to_owned();
let filename = dotenv_filename.map_or(DEFAULT_DOTENV_FILENAME, |s| s.as_str());

for directory in working_directory.ancestors() {
let path = directory.join(filename.as_str());
let path = directory.join(filename);
if path.is_file() {
return load_from_file(&path);
}
Expand Down
2 changes: 1 addition & 1 deletion src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ impl<'src> Node<'src> for Set<'src> {
set.push_mut(Tree::string(&argument.cooked));
}
}
Setting::Tempdir(value) => {
Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
set.push_mut(Tree::string(value));
}
}
Expand Down
64 changes: 31 additions & 33 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -780,21 +780,23 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {
self.presume_keyword(Keyword::Set)?;
let name = Name::from_identifier(self.presume(Identifier)?);
let lexeme = name.lexeme();
let Some(keyword) = Keyword::from_lexeme(lexeme) else {
return Err(name.error(CompileErrorKind::UnknownSetting {
setting: name.lexeme(),
}));
};

let set_bool: Option<Setting> = match Keyword::from_lexeme(lexeme) {
Some(kw) => match kw {
Keyword::AllowDuplicateRecipes => {
Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
}
Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
_ => None,
},
None => None,
let set_bool = match keyword {
Keyword::AllowDuplicateRecipes => {
Some(Setting::AllowDuplicateRecipes(self.parse_set_bool()?))
}
Keyword::DotenvLoad => Some(Setting::DotenvLoad(self.parse_set_bool()?)),
Keyword::Export => Some(Setting::Export(self.parse_set_bool()?)),
Keyword::Fallback => Some(Setting::Fallback(self.parse_set_bool()?)),
Keyword::IgnoreComments => Some(Setting::IgnoreComments(self.parse_set_bool()?)),
Keyword::PositionalArguments => Some(Setting::PositionalArguments(self.parse_set_bool()?)),
Keyword::WindowsPowershell => Some(Setting::WindowsPowerShell(self.parse_set_bool()?)),
_ => None,
};

if let Some(value) = set_bool {
Expand All @@ -803,26 +805,22 @@ impl<'tokens, 'src> Parser<'tokens, 'src> {

self.expect(ColonEquals)?;

if name.lexeme() == Keyword::Shell.lexeme() {
Ok(Set {
value: Setting::Shell(self.parse_shell()?),
name,
})
} else if name.lexeme() == Keyword::WindowsShell.lexeme() {
Ok(Set {
value: Setting::WindowsShell(self.parse_shell()?),
name,
})
} else if name.lexeme() == Keyword::Tempdir.lexeme() {
Ok(Set {
value: Setting::Tempdir(self.parse_string_literal()?.cooked),
name,
})
} else {
Err(name.error(CompileErrorKind::UnknownSetting {
setting: name.lexeme(),
}))
let set_value = match keyword {
Keyword::DotenvFilename => Some(Setting::DotenvFilename(self.parse_string_literal()?.cooked)),
Keyword::DotenvPath => Some(Setting::DotenvPath(self.parse_string_literal()?.cooked)),
Keyword::Shell => Some(Setting::Shell(self.parse_shell()?)),
Keyword::Tempdir => Some(Setting::Tempdir(self.parse_string_literal()?.cooked)),
Keyword::WindowsShell => Some(Setting::WindowsShell(self.parse_shell()?)),
_ => None,
};

if let Some(value) = set_value {
return Ok(Set { name, value });
}

Err(name.error(CompileErrorKind::UnknownSetting {
setting: name.lexeme(),
}))
}

/// Parse a shell setting value
Expand Down
6 changes: 4 additions & 2 deletions src/setting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use super::*;
#[derive(Debug, Clone)]
pub(crate) enum Setting<'src> {
AllowDuplicateRecipes(bool),
DotenvFilename(String),
DotenvLoad(bool),
DotenvPath(String),
Export(bool),
Fallback(bool),
IgnoreComments(bool),
Expand All @@ -25,8 +27,8 @@ impl<'src> Display for Setting<'src> {
| Setting::PositionalArguments(value)
| Setting::WindowsPowerShell(value) => write!(f, "{value}"),
Setting::Shell(shell) | Setting::WindowsShell(shell) => write!(f, "{shell}"),
Setting::Tempdir(tempdir) => {
write!(f, "{tempdir:?}")
Setting::DotenvFilename(value) | Setting::DotenvPath(value) | Setting::Tempdir(value) => {
write!(f, "{value:?}")
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ pub(crate) const WINDOWS_POWERSHELL_ARGS: &[&str] = &["-NoLogo", "-Command"];
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct Settings<'src> {
pub(crate) allow_duplicate_recipes: bool,
pub(crate) dotenv_filename: Option<String>,
pub(crate) dotenv_load: Option<bool>,
pub(crate) dotenv_path: Option<PathBuf>,
pub(crate) export: bool,
pub(crate) fallback: bool,
pub(crate) ignore_comments: bool,
Expand All @@ -29,9 +31,15 @@ impl<'src> Settings<'src> {
Setting::AllowDuplicateRecipes(allow_duplicate_recipes) => {
settings.allow_duplicate_recipes = allow_duplicate_recipes;
}
Setting::DotenvFilename(filename) => {
settings.dotenv_filename = Some(filename);
}
Setting::DotenvLoad(dotenv_load) => {
settings.dotenv_load = Some(dotenv_load);
}
Setting::DotenvPath(path) => {
settings.dotenv_path = Some(PathBuf::from(path));
}
Setting::Export(export) => {
settings.export = export;
}
Expand Down
Loading

0 comments on commit 812e1ea

Please sign in to comment.