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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,32 @@ in `.cade`, `.env` files, and `call` output, `KEY=value` follows the variable's
normal mode, while `KEY:=value` forces a **hard replace** that ignores ambient
and parent layers.

### variable expansion

`.cade` directive arguments expand `${VAR}` references against the environment.
only the braced `${...}` form is recognized. you may also write `\$` for a literal `$`,
so `LIT=\${VAR}` keeps the text `${VAR}` unexpanded.
`hook` commands are **not** expanded by cade because the shell runs them and expands `${...}`
itself.

```
call deploy --token ${API_TOKEN}
load env ${ENVIRONMENT:-dev}.env
DATA_DIR=${XDG_DATA_HOME:-${HOME}/.local/share}/myapp
```

the syntax is bash-compatible: `${VAR}` substitutes `VAR` (empty when unset),
and all `${VAR:-default}` / `${VAR-default}` / `${VAR:+alt}` / `${VAR+alt}`
forms are supported, with `default`/`alt` themselves expandable. the default is
optional, so `${VAR:-}` simply substitutes `VAR`.
in `call` and `watch`, an expanded value is always a single argument: it is not
re-split on spaces, so `call deploy ${ARGS}` passes one argument even when `ARGS`
contains spaces.

expansion is resolved when a layer is loaded and cached with it, keyed on the
layer's files. changing a referenced variable does not re-expand until one of
those files changes (`watch` an extra file to force it).

## direnv compatibility

cade can read [direnv](https://direnv.net/) `.envrc` files, but does **not**
Expand Down
14 changes: 5 additions & 9 deletions src/cli/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub enum ParseError {
UnknownLoadable,
TooManyOptions,
TooFewOptions,
InvalidQuoting,
EmptyLine,
}

Expand All @@ -18,7 +17,6 @@ impl Display for ParseError {
Self::UnknownLoadable => f.write_str("unknown loadable"),
Self::TooManyOptions => f.write_str("too many options"),
Self::TooFewOptions => f.write_str("too few options"),
Self::InvalidQuoting => f.write_str("unbalanced quotes"),
Self::EmptyLine => f.write_str("empty line"),
}
}
Expand Down Expand Up @@ -64,12 +62,11 @@ impl FromStr for Keyword {
"pure" => Pure,
"disinherit" => Disinherit,
"call" => {
// split respecting shell quoting
let target = shlex::split(rest_raw).ok_or(ParseError::InvalidQuoting)?;
if target.is_empty() {
// kept raw, `${}` expansion then shlex tokenization will occur
if rest_raw.is_empty() {
return Err(ParseError::TooFewOptions);
}
Call(target)
Call(rest_raw.to_string())
}
"load" => {
let rest: Vec<&str> = rest_raw.split_whitespace().collect();
Expand Down Expand Up @@ -116,11 +113,10 @@ impl FromStr for Keyword {
Clear(vars)
}
"watch" => {
let files = shlex::split(rest_raw).ok_or(ParseError::InvalidQuoting)?;
if files.is_empty() {
if rest_raw.is_empty() {
return Err(ParseError::TooFewOptions);
}
Watch(files)
Watch(rest_raw.to_string())
}
"concat" => {
let vars: Vec<String> = rest_raw.split_whitespace().map(|s| s.to_owned()).collect();
Expand Down
22 changes: 15 additions & 7 deletions src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,15 @@ struct WatchState {

// Falls back to an implicit `load envrc` when a dir has no .cade.
fn config_keywords(dir: &Path) -> Result<Vec<Keyword>> {
if std::fs::exists(dir.join(".cade")).unwrap_or(false) {
read_cade(&dir.join(".cade")).context("reading cade file")
let mut keywords = if std::fs::exists(dir.join(".cade")).unwrap_or(false) {
read_cade(&dir.join(".cade")).context("reading cade file")?
} else {
Ok(vec![Keyword::Load(Loadable::Envrc(String::new()))])
vec![Keyword::Load(Loadable::Envrc(String::new()))]
};
for kw in &mut keywords {
crate::expand::expand_keyword(kw);
}
Ok(keywords)
}

// Reject session ids that could escape the snapshots dir when used as a path.
Expand Down Expand Up @@ -1734,7 +1738,7 @@ fn load_single_layer(
for (action_index, kw) in keywords.iter().enumerate() {
let act = match kw {
Pure => Ok(CadeAction::Purify),
Call(argv) => call(path, argv.clone())
Call(raw) => call(path, tokenize_args(raw)?)
.context("calling process")
.map(CadeAction::Environ),
Load(loadable) => {
Expand Down Expand Up @@ -1764,8 +1768,12 @@ fn load_single_layer(
Ok(layer)
}

fn tokenize_args(raw: &str) -> Result<Vec<String>> {
shlex::split(raw).ok_or_else(|| anyhow!("unbalanced quotes in `{raw}`"))
}

/// Determine which files a layer depends on
fn watched_files_for_keywords(dir: &Path, keywords: &[Keyword]) -> Vec<PathBuf> {
fn watched_files_for_keywords(dir: &Path, keywords: &[Keyword]) -> Result<Vec<PathBuf>> {
let mut files = vec![dir.join(".cade")];
for kw in keywords {
match kw {
Expand All @@ -1774,11 +1782,11 @@ fn watched_files_for_keywords(dir: &Path, keywords: &[Keyword]) -> Vec<PathBuf>
// real files lexically, so creating it trips the watcher
Keyword::Load(loadable) => files.extend(loadable.resolve(dir).watch),
// explicit user-declared dependencies
Keyword::Watch(ws) => files.extend(ws.iter().map(|w| dir.join(w))),
Keyword::Watch(raw) => files.extend(tokenize_args(raw)?.iter().map(|w| dir.join(w))),
_ => {}
}
}
files
Ok(files)
}

fn compute_layer_key(watched_files: &[PathBuf]) -> String {
Expand Down
2 changes: 1 addition & 1 deletion src/core/activation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ impl Cade {
let mut nix_store_paths: Vec<String> = Vec::new();

for (layer_count, (path, keywords)) in cade_files.iter().enumerate() {
let watch_files = watched_files_for_keywords(path, keywords);
let watch_files = watched_files_for_keywords(path, keywords)?;
all_watch_files.extend(watch_files.clone());

let token = compute_layer_key(&watch_files);
Expand Down
Loading
Loading