Skip to content
Merged
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
68 changes: 59 additions & 9 deletions lib/src/spec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ impl Spec {
///
/// Automatically detects whether the file is:
/// - A `.kdl` or `.usage.kdl` file containing a raw spec
/// - A script file with embedded `# USAGE:` comments
/// - A script file with embedded `#USAGE` comments
///
/// If `bin` is not specified in the spec, it defaults to the filename.
#[must_use = "parsing result should be used"]
Expand All @@ -107,13 +107,12 @@ impl Spec {
}
/// Parse a spec from a script file's embedded USAGE comments.
///
/// Extracts the spec from comment lines starting with `# USAGE:` or `// USAGE:`.
/// Extracts the spec from comment lines marked with `#USAGE`, `//USAGE`,
/// `::USAGE`, or their `[USAGE]` variants.
/// If `bin` is not specified in the spec, it defaults to the filename.
#[must_use = "parsing result should be used"]
pub fn parse_script(file: &Path) -> Result<Spec, UsageErr> {
let raw = extract_usage_from_comments(&file::read_to_string(file)?);
let ctx = ParsingContext::new(file, &raw);
let mut spec = Self::parse(&ctx, &raw)?;
let mut spec = Self::parse_script_with_path(&file::read_to_string(file)?, file)?;
if spec.bin.is_empty() {
spec.bin = file
.file_name()
Expand All @@ -127,6 +126,24 @@ impl Spec {
Ok(spec)
}

/// Parse a spec from a script string's embedded USAGE comments.
///
/// Extracts the spec from comment lines marked with `#USAGE`, `//USAGE`,
/// `::USAGE`, or their `[USAGE]` variants. Unlike [`Self::parse_script`],
/// this function cannot infer `bin` or `name` from a filename. Relative
/// `include` paths are rejected because there is no source path to resolve
/// them against; absolute `include` paths remain supported.
#[must_use = "parsing result should be used"]
pub fn parse_script_str(input: &str) -> Result<Spec, UsageErr> {
Self::parse_script_with_path(input, Path::new(""))
Comment thread
jdx marked this conversation as resolved.
}

fn parse_script_with_path(input: &str, file: &Path) -> Result<Spec, UsageErr> {
let raw = extract_usage_from_comments(input);
let ctx = ParsingContext::new(file, &raw);
Self::parse(&ctx, &raw)
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
#[deprecated]
pub fn parse_spec(input: &str) -> Result<Spec, UsageErr> {
Self::parse(&Default::default(), input)
Expand Down Expand Up @@ -224,10 +241,12 @@ impl Spec {
.file
.parent()
.ok_or_else(|| {
ctx.build_err(
format!("cannot get parent of {}", ctx.file.display()),
node.span(),
)
let msg = if ctx.file.as_os_str().is_empty() {
"relative includes require a source file".to_string()
} else {
format!("cannot get parent of {}", ctx.file.display())
};
ctx.build_err(msg, node.span())
})?
.join(file),
false => file.to_path_buf(),
Expand Down Expand Up @@ -982,4 +1001,35 @@ example "demo --version"
);
assert!(output.contains("example \"demo --version\""));
}

#[test]
fn test_parse_script_str() {
let spec = Spec::parse_script_str(
r#"
#!/bin/bash
#USAGE bin "test"
#USAGE flag "--foo" help="test"
echo "hello"
"#,
)
.unwrap();

assert_eq!(spec.bin, "test");
assert_eq!(spec.name, "test");
assert_eq!(spec.cmd.flags.len(), 1);
assert_eq!(spec.cmd.flags[0].long, ["foo"]);
}

#[test]
fn test_parse_script_str_rejects_relative_includes() {
let err = Spec::parse_script_str(r#"#USAGE include file="relative.usage.kdl""#)
.expect_err("relative includes need a source path");

match err {
UsageErr::InvalidInput(msg, _, _) => {
assert_eq!(msg, "relative includes require a source file");
}
err => panic!("unexpected error: {err:?}"),
}
}
}