Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dotenv-filename and dotenv-path to in-justfile settings #1692

Merged
Merged
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
Loading