Skip to content
Open
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
6 changes: 6 additions & 0 deletions cli/src/cli/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ impl Exec {
for (key, val) in &parsed.as_env() {
cmd.env(key, val);
}
if let Some(trailing) = parsed.trailing_varargs_count() {
cmd.env(
"usage__varargs_idx",
self.args.len().saturating_sub(trailing).to_string(),
);
}

let result = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;

Expand Down
6 changes: 6 additions & 0 deletions cli/src/cli/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ impl Shell {
for (key, val) in &parsed.as_env() {
cmd.env(key, val);
}
if let Some(trailing) = parsed.trailing_varargs_count() {
cmd.env(
"usage__varargs_idx",
self.args.len().saturating_sub(trailing).to_string(),
);
}

let result = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;

Expand Down
27 changes: 27 additions & 0 deletions docs/cli/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,30 @@ by spaces. If an arg itself has a space, then it will have quotes around it. Thi
by [`shell_words::join()`](https://docs.rs/shell-words/latest/shell_words/fn.join.html). For now,
this is not customizable behavior. It would be possible to
support [alternatives](https://github.com/jdx/usage/issues/189) though.

## Trailing Varargs

Usage always sets the `usage__varargs_idx` environment variable on the subprocess. Its value is the
number of parsed args in `$@` that precede the trailing varargs. A script can `shift` by this amount
to isolate the varargs in `$@`. When there are no trailing varargs, the value equals the total number
of args, so `shift $usage__varargs_idx` empties `$@`:

```bash
#!/usr/bin/env -S usage bash
#USAGE flag "-v --verbose" help="Enable verbose output"
#USAGE arg "<file>" help="Input file"
#USAGE arg "[extra...]" help="Extra arguments passed through"

echo "file: $usage_file"
echo "extra: $usage_extra"

shift "$usage__varargs_idx"
echo "raw varargs in \$@: $@"
```

```bash
$ ./mycli --verbose input.txt foo bar baz
file: input.txt
extra: foo bar baz
raw varargs in $@: foo bar baz
```
200 changes: 200 additions & 0 deletions lib/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,23 @@ impl ParseOutput {
}
env
}

pub fn trailing_varargs_count(&self) -> Option<usize> {
if !self.errors.is_empty() {
return None;
}
Some(
self.args
.iter()
.rev()
.take_while(|(a, _)| a.var)
.filter_map(|(_, v)| match v {
ParseValue::MultiString(s) => Some(s.len()),
_ => None,
})
.sum(),
)
}
}

impl Display for ParseValue {
Expand Down Expand Up @@ -2146,4 +2163,187 @@ mod tests {
_ => panic!("Expected MultiString, got {:?}", args_val),
}
}

#[test]
fn test_trailing_varargs_count_basic() {
let spec: Spec = r#"
flag "--verbose"
arg "<file>"
arg "[extra...]"
"#
.parse()
.unwrap();
let input: Vec<String> = vec!["test", "--verbose", "myfile", "extra1", "extra2"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.trailing_varargs_count(), Some(2));
}

#[test]
fn test_trailing_varargs_count_no_varargs() {
let spec: Spec = r#"
flag "--verbose"
arg "<file>"
"#
.parse()
.unwrap();
let input: Vec<String> = vec!["test", "--verbose", "myfile"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.trailing_varargs_count(), Some(0));
}

#[test]
fn test_trailing_varargs_count_with_double_dash() {
let spec: Spec = r#"
flag "--verbose"
arg "<file>"
arg "[extra...]"
"#
.parse()
.unwrap();
let input: Vec<String> = vec!["test", "--verbose", "myfile", "--", "extra1", "extra2"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
assert_eq!(parsed.trailing_varargs_count(), Some(2));
}

#[test]
fn test_trailing_varargs_count_with_subcommand() {
let cmd = SpecCommand::builder()
.name("test")
.subcommand(
SpecCommand::builder()
.name("run")
.arg(SpecArg::builder().name("task").build())
.arg(
SpecArg::builder()
.name("args")
.required(false)
.var(true)
.build(),
)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input: Vec<String> = vec!["test", "run", "build", "arg1", "arg2"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
let env = parsed.as_env();
assert_eq!(env.get("usage_task").unwrap(), "build");
assert_eq!(env.get("usage_args").unwrap(), "arg1 arg2");
assert_eq!(parsed.trailing_varargs_count(), Some(2));
}

#[test]
fn test_trailing_varargs_count_multiple_groups() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("files")
.required(false)
.var(true)
.var_max(2)
.build(),
)
.arg(
SpecArg::builder()
.name("dirs")
.required(false)
.var(true)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input: Vec<String> = vec!["test", "f1", "f2", "d1", "d2", "d3"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
let env = parsed.as_env();
assert_eq!(env.get("usage_files").unwrap(), "f1 f2");
assert_eq!(env.get("usage_dirs").unwrap(), "d1 d2 d3");
assert_eq!(parsed.trailing_varargs_count(), Some(5));
}

#[test]
fn test_trailing_varargs_count_non_trailing_excluded() {
let cmd = SpecCommand::builder()
.name("test")
.arg(
SpecArg::builder()
.name("files")
.required(false)
.var(true)
.var_max(2)
.build(),
)
.arg(SpecArg::builder().name("output").build())
.arg(
SpecArg::builder()
.name("extra")
.required(false)
.var(true)
.build(),
)
.build();
let spec = Spec {
name: "test".to_string(),
bin: "test".to_string(),
cmd,
..Default::default()
};
let input: Vec<String> = vec!["test", "f1", "f2", "out", "e1", "e2"]
.into_iter()
.map(String::from)
.collect();
let parsed = parse(&spec, &input).unwrap();
let env = parsed.as_env();
assert_eq!(env.get("usage_files").unwrap(), "f1 f2");
assert_eq!(env.get("usage_output").unwrap(), "out");
assert_eq!(env.get("usage_extra").unwrap(), "e1 e2");
assert_eq!(parsed.trailing_varargs_count(), Some(2));
}

#[test]
fn test_trailing_varargs_count_non_multi_string() {
let cmd = SpecCommand::builder()
.name("test")
.arg(SpecArg::builder().name("extra").var(true).build())
.build();
let mut out = ParseOutput {
cmd: cmd.clone(),
cmds: vec![cmd],
args: IndexMap::new(),
flags: IndexMap::new(),
available_flags: BTreeMap::new(),
flag_awaiting_value: vec![],
errors: vec![],
};
out.args.insert(
Arc::new(SpecArg::builder().name("extra").var(true).build()),
ParseValue::String("single".to_string()),
);
assert_eq!(out.trailing_varargs_count(), Some(0));
}
}
Loading