summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config/import.rs100
-rw-r--r--src/config/mod.rs14
2 files changed, 72 insertions, 42 deletions
diff --git a/src/config/import.rs b/src/config/import.rs
index 4602705..a5b2a63 100644
--- a/src/config/import.rs
+++ b/src/config/import.rs
@@ -3,10 +3,7 @@
//!
//! * `env:NAME`: an environment variable;
//! * `file:PATH`: a file's contents;
-//! * `cmd:COMMAND`: the standard output of a shell command (`sh -c`);
-//! * `template:TEXT`: text with importers in it, `${env:NAME}`, `${file:PATH}` or
-//! `${cmd:COMMAND}`, each replaced by its content (`$$` is a `$`; a placeholder ends
-//! at the first `}`), e.g. `template:node-${env:HOSTNAME}`.
+//! * `cmd:COMMAND`: the standard output of a shell command (`sh -c`).
//!
//! The content is text (trailing line breaks removed), converted like a value written
//! on the command line; with `+json` (`env+json:NAME`, …) it is parsed as JSON instead.
@@ -14,9 +11,12 @@
//! On the command line, a value with one of these prefixes is imported; `literal:`
//! passes the rest as is. In configuration files, an importer is a table:
//! `{ env = "NAME" }`, `{ file = "PATH" }`, `{ command = "COMMAND" }` (or an argument
-//! list, run without a shell), `{ template = "TEXT" }`, each with an optional
-//! `format = "json"`. Relative paths are relative to the file, and commands run in its
-//! directory, also inside a template.
+//! list, run without a shell), each with an optional `format = "json"`.
+//!
+//! Any other text value may hold placeholders, `${env:NAME}`, `${file:PATH}` or
+//! `${cmd:COMMAND}`, each replaced by its content as text (`$$` is a `$`; a placeholder
+//! ends at the first `}`), e.g. `node-${env:HOSTNAME}`. Relative paths are relative to
+//! the file, and commands run in its directory, in placeholders too.
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -42,6 +42,7 @@ pub(crate) enum Source {
argv: Vec<String>,
dir: Option<PathBuf>,
},
+ /// Text with placeholders (`interpolated`).
Template {
template: String,
dir: Option<PathBuf>,
@@ -67,12 +68,12 @@ pub(crate) fn parse_argument(raw: &str) -> Raw<'_> {
return Raw::Literal(rest);
}
let Some((prefix, rest)) = raw.split_once(':') else {
- return Raw::Literal(raw);
+ return literal_or_interpolated(raw);
};
let (kind, format) = match prefix.split_once('+') {
None => (prefix, Format::Text),
Some((kind, "json")) => (kind, Format::Json),
- Some(_) => return Raw::Literal(raw),
+ Some(_) => return literal_or_interpolated(raw),
};
let source = match kind {
"env" => Source::Env(rest.to_owned()),
@@ -81,21 +82,36 @@ pub(crate) fn parse_argument(raw: &str) -> Raw<'_> {
command: rest.to_owned(),
dir: None,
},
- "template" => Source::Template {
- template: rest.to_owned(),
- dir: None,
- },
- _ => return Raw::Literal(raw),
+ _ => return literal_or_interpolated(raw),
};
Raw::Import(Importer { source, format })
}
+fn literal_or_interpolated(raw: &str) -> Raw<'_> {
+ match interpolated(raw, None) {
+ Some(importer) => Raw::Import(importer),
+ None => Raw::Literal(raw),
+ }
+}
+
+/// `text` as an importer if it holds a `$` (a placeholder, `$$`, or a mistake that
+/// expanding it reports), read in `dir`; None if it is plain text.
+pub(crate) fn interpolated(text: &str, dir: Option<&Path>) -> Option<Importer> {
+ text.contains('$').then(|| Importer {
+ source: Source::Template {
+ template: text.to_owned(),
+ dir: dir.map(Path::to_owned),
+ },
+ format: Format::Text,
+ })
+}
+
/// Reads an importer table from a file in `dir`; None if `object` isn't one.
pub(crate) fn parse_table(
object: &JsonMap<String, JsonValue>,
dir: &Path,
) -> Result<Option<Importer>> {
- let sources = ["env", "file", "command", "template"]
+ let sources = ["env", "file", "command"]
.into_iter()
.filter(|key| object.contains_key(*key))
.count();
@@ -103,11 +119,11 @@ pub(crate) fn parse_table(
return Ok(None);
}
if sources > 1 {
- bail!("an importer takes exactly one of env, file, command and template");
+ bail!("an importer takes exactly one of env, file and command");
}
if let Some(unknown) = object
.keys()
- .find(|key| !["env", "file", "command", "template", "format"].contains(&key.as_str()))
+ .find(|key| !["env", "file", "command", "format"].contains(&key.as_str()))
{
bail!("unknown importer key {unknown:?}");
}
@@ -127,11 +143,6 @@ pub(crate) fn parse_table(
Source::Env(text("env")?)
} else if object.contains_key("file") {
Source::File(dir.join(text("file")?))
- } else if object.contains_key("template") {
- Source::Template {
- template: text("template")?,
- dir: Some(dir.to_owned()),
- }
} else {
match &object["command"] {
JsonValue::String(command) => Source::Shell {
@@ -170,7 +181,7 @@ impl Importer {
Source::File(path) => format!("file{format}:{}", path.display()),
Source::Shell { command, .. } => format!("cmd{format}:{command}"),
Source::Argv { argv, .. } => format!("cmd{format}:{}", argv.join(" ")),
- Source::Template { template, .. } => format!("template{format}:{template}"),
+ Source::Template { template, .. } => template.clone(),
}
}
@@ -211,7 +222,7 @@ pub(crate) enum Imported {
Json(JsonValue),
}
-/// A template with its placeholders replaced by their content.
+/// Text with its placeholders replaced by their content.
fn expand(template: &str, dir: Option<&Path>) -> Result<String> {
let mut out = String::new();
let mut rest = template;
@@ -238,13 +249,11 @@ fn expand(template: &str, dir: Option<&Path>) -> Result<String> {
}
}
Source::Shell { dir: run_in, .. } => *run_in = dir.map(Path::to_owned),
- Source::Template { .. } => bail!("templates don't nest: {placeholder:?}"),
+ Source::Template { .. } => bail!("placeholders don't nest: {placeholder:?}"),
Source::Env(_) | Source::Argv { .. } => {}
}
if importer.format != Format::Text {
- bail!(
- "placeholder {placeholder:?} must be text; +json applies to the whole template"
- );
+ bail!("placeholder {placeholder:?} must be text (+json isn't for placeholders)");
}
match importer.fetch()? {
Imported::Text(text) => out.push_str(&text),
@@ -252,8 +261,11 @@ fn expand(template: &str, dir: Option<&Path>) -> Result<String> {
}
rest = &after[end + 1..];
} else {
- out.push('$');
- rest = after;
+ // Most likely a shell-style `$NAME` that would silently stay as it is.
+ bail!(
+ "a `$` in {template:?} that starts no placeholder: write `$$` for a `$`, or \
+ e.g. `${{env:NAME}}` for a variable"
+ );
}
}
out.push_str(rest);
@@ -308,8 +320,11 @@ mod tests {
})
);
assert_eq!(parse_argument("env+yaml:X"), Raw::Literal("env+yaml:X"));
+ // A `$` makes it interpolated; expanding it then says what's wrong.
+ assert!(matches!(parse_argument("a $5 price"), Raw::Import(_)));
+ assert_eq!(parse_argument("literal:a $5"), Raw::Literal("a $5"));
assert_eq!(
- parse_argument("template:node-${env:HOST}"),
+ parse_argument("node-${env:HOST}"),
Raw::Import(Importer {
source: Source::Template {
template: "node-${env:HOST}".into(),
@@ -318,12 +333,23 @@ mod tests {
format: Format::Text
})
);
+ // A placeholder in a value that looks like a prefix is interpolated too.
+ assert!(matches!(
+ parse_argument("tls://${env:HOST}:9651"),
+ Raw::Import(_)
+ ));
+ assert_eq!(
+ parse_argument("literal:${env:HOST}"),
+ Raw::Literal("${env:HOST}")
+ );
}
#[test]
fn templates() {
let expanded = |template: &str| expand(template, None);
- assert_eq!(expanded("a-${cmd:echo b}-c $$5 $x").unwrap(), "a-b-c $5 $x");
+ assert_eq!(expanded("a-${cmd:echo b}-c $$5").unwrap(), "a-b-c $5");
+ assert!(expanded("$HOME/x").is_err());
+ assert!(expanded("a $5 price").is_err());
assert_eq!(
expanded("${cmd:printf one}${cmd:printf two}").unwrap(),
"onetwo"
@@ -331,16 +357,6 @@ mod tests {
assert!(expanded("${cmd:echo b").is_err());
assert!(expanded("${plain}").is_err());
assert!(expanded("${env+json:X}").is_err());
- assert!(expanded("${template:x}").is_err());
-
- let json = Importer {
- source: Source::Template {
- template: "[\"${cmd:printf a}\", 1]".into(),
- dir: None,
- },
- format: Format::Json,
- };
- assert_eq!(json.fetch().unwrap(), Imported::Json(json!(["a", 1])));
}
#[test]
diff --git a/src/config/mod.rs b/src/config/mod.rs
index c8bfa9f..fd8547d 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -662,6 +662,13 @@ fn resolve_file_value(
)),
None => bail!("a table here must be an importer (env, file or command)"),
},
+ JsonValue::String(text) => match import::interpolated(&text, Some(dir)) {
+ Some(importer) => Ok((
+ from_import(spec.kind, &importer)?,
+ vec![importer.describe()],
+ )),
+ None => Ok((JsonValue::String(text), Vec::new())),
+ },
JsonValue::Array(items) if spec.kind == Kind::List => {
let mut imports = Vec::new();
let mut values = Vec::new();
@@ -677,6 +684,13 @@ fn resolve_file_value(
}
imports.push(importer.describe());
}
+ JsonValue::String(text) => match import::interpolated(&text, Some(dir)) {
+ Some(importer) => {
+ values.push(from_import(Kind::Text, &importer)?);
+ imports.push(importer.describe());
+ }
+ None => values.push(JsonValue::String(text)),
+ },
other => values.push(other),
}
}