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
17 changes: 11 additions & 6 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ impl CompleteWord {
.as_ref()
.is_some_and(|rt| prev_token == Some(rt.as_str()));

let mut has_explicit_choices = false;
let mut choices = if ctoken == "-" {
let shorts = self.complete_short_flag_names(&parsed.available_flags, "");
let longs = self.complete_long_flag_names(&parsed.available_flags, "");
Expand All @@ -120,14 +121,18 @@ impl CompleteWord {
// but before flag_awaiting_value (since restart clears pending flag values)
let mut choices = vec![];
if let Some(arg) = parsed.cmd.args.first() {
has_explicit_choices = arg.choices.is_some();
choices.extend(self.complete_arg(&ctx, spec, &parsed.cmd, arg, &ctoken)?);
}
choices
} else if let Some(flag) = parsed.flag_awaiting_value.first() {
self.complete_arg(&ctx, spec, &parsed.cmd, flag.arg.as_ref().unwrap(), &ctoken)?
let arg = flag.arg.as_ref().unwrap();
has_explicit_choices = arg.choices.is_some();
self.complete_arg(&ctx, spec, &parsed.cmd, arg, &ctoken)?
} else {
let mut choices = vec![];
if let Some(arg) = parsed.cmd.args.get(parsed.args.len()) {
has_explicit_choices = arg.choices.is_some();
choices.extend(self.complete_arg(&ctx, spec, &parsed.cmd, arg, &ctoken)?);
}
if !parsed.cmd.subcommands.is_empty() {
Expand All @@ -153,7 +158,7 @@ impl CompleteWord {
choices
};
// Fallback to file completions if nothing is known about this argument and it's not a flag
if choices.is_empty() && !ctoken.starts_with('-') {
if choices.is_empty() && !ctoken.starts_with('-') && !has_explicit_choices {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let files = self.complete_path(&cwd, &ctoken, |_| true);
choices = files.into_iter().map(|n| (n, String::new())).collect();
Expand Down Expand Up @@ -266,10 +271,10 @@ impl CompleteWord {
}

if let Some(choices) = &arg.choices {
return Ok(choices
.choices
.iter()
.map(|c| (c.clone(), String::new()))
let values = choices.values();
return Ok(values
.into_iter()
.map(|c| (c, String::new()))
.filter(|(c, _)| c.starts_with(ctoken))
.collect());
}
Expand Down
26 changes: 26 additions & 0 deletions cli/tests/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,32 @@ fn complete_word_choices() {
.stdout("bash\nelvish\nfish\nnu\nxonsh\nzsh\npwsh\n");
}

#[test]
fn complete_word_choices_from_env() {
cmd("env-choices.usage.kdl", Some("fish"))
.env("DEPLOY_ENVS", "foo,bar baz")
.args(["--", "--env", ""])
.assert()
.success()
.stdout("foo\nbar\nbaz\n");
}

#[test]
fn complete_word_choices_from_env_unset_returns_empty() {
cmd("env-choices.usage.kdl", Some("fish"))
.env_remove("DEPLOY_ENVS")
.args(["--", "--env", ""])
.assert()
.success()
.stdout("");
}

#[test]
fn complete_word_default_subcommand_choices_do_not_block_root_file_fallback() {
assert_cmd("default-subcommand-root-fallback.usage.kdl", &["--", "C"])
.stdout(contains("Cargo.toml"));
}

#[test]
fn complete_word_shebang() {
assert_cmd("example.sh", &["--", "-"])
Expand Down
4 changes: 4 additions & 0 deletions docs/spec/reference/arg.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ arg "<shell>" {
choices "bash" "zsh" "fish" // <shell> must be one of the choices
}

arg "<env>" {
choices env="DEPLOY_ENVS" // values from $DEPLOY_ENVS, split on commas and/or whitespace
}

arg "<file>" long_help="longer help for --help (as oppoosed to -h)"

// double-dash behavior
Expand Down
4 changes: 4 additions & 0 deletions docs/spec/reference/flag.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ flag "--shell <shell>" {
choices "bash" "zsh" "fish" // <shell> must be one of the choices
}

flag "--env <env>" {
choices env="DEPLOY_ENVS" // values from $DEPLOY_ENVS, split on commas and/or whitespace
}

flag "--file <file>" long_help="longer help for --help (as opposed to -h)"
// this is equivalent to the above but preferred when a lot of space is needed
flag "--file <file>" {
Expand Down
8 changes: 8 additions & 0 deletions examples/default-subcommand-root-fallback.usage.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
arg "<target>"
default_subcommand "run"

cmd "run" {
arg "<task>" {
choices "task-a" "task-b"
}
}
3 changes: 3 additions & 0 deletions examples/env-choices.usage.kdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flag "--env <env>" {
choices env="DEPLOY_ENVS"
}
10 changes: 8 additions & 2 deletions lib/src/docs/cli/templates/spec_template_long.tera
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ Arguments:
{{ help | indent(width=2) }}
{%- endif %}
{%- endif %}
{%- if arg.choices %}
{%- if arg.choices and arg.choices.choices %}
[possible values: {{ arg.choices.choices | join(sep=", ") }}]
{%- endif %}
{%- if arg.choices and arg.choices.env %}
[choices env: {{ arg.choices.env }}]
{%- endif %}
{%- if arg.env %}
[env: {{ arg.env }}]
{%- endif %}
Expand All @@ -65,9 +68,12 @@ Flags:
{{ help | indent(width=2) }}
{%- endif %}
{%- endif %}
{%- if flag.arg.choices %}
{%- if flag.arg.choices and flag.arg.choices.choices %}
[possible values: {{ flag.arg.choices.choices | join(sep=", ") }}]
{%- endif %}
{%- if flag.arg.choices and flag.arg.choices.env %}
[choices env: {{ flag.arg.choices.env }}]
{%- endif %}
{%- if flag.env %}
[env: {{ flag.env }}]
{%- endif %}
Expand Down
6 changes: 4 additions & 2 deletions lib/src/docs/cli/templates/spec_template_short.tera
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Arguments:
{%- for arg in cmd.args %}
{{ arg.usage | trim }}
{%- if arg.help %} {{ arg.help }}{%- endif %}
{%- if arg.choices %} [{{ arg.choices.choices | join(sep=", ") }}]{%- endif %}
{%- if arg.choices and arg.choices.choices %} [{{ arg.choices.choices | join(sep=", ") }}]{%- endif %}
{%- if arg.choices and arg.choices.env %} [choices env: {{ arg.choices.env }}]{%- endif %}
{%- if arg.env %} [env: {{ arg.env }}]{%- endif %}
{%- if arg.default %} (default: {{ arg.default | join(sep=", ") }}){%- endif %}
{%- endfor %}
Expand All @@ -33,7 +34,8 @@ Flags:
{{ flag.usage | trim }}
{%- if flag.aliases %} [aliases: {{ flag.aliases | join(sep=", ") }}]{% endif %}
{%- if flag.help %} {{ flag.help }}{%- endif %}
{%- if flag.arg.choices %} [{{ flag.arg.choices.choices | join(sep=", ") }}]{%- endif %}
{%- if flag.arg.choices and flag.arg.choices.choices %} [{{ flag.arg.choices.choices | join(sep=", ") }}]{%- endif %}
{%- if flag.arg.choices and flag.arg.choices.env %} [choices env: {{ flag.arg.choices.env }}]{%- endif %}
{%- if flag.env %} [env: {{ flag.env }}]{%- endif %}
{%- endfor %}
{%- endif -%}
6 changes: 5 additions & 1 deletion lib/src/docs/markdown/templates/arg_template.md.tera
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

{{ arg.help_md | escape_md }}
{%- endif %}
{%- if arg.choices %}
{%- if arg.choices and arg.choices.choices %}

**Choices:**
{% for choice in arg.choices.choices %}
- `{{ choice }}`
{%- endfor %}
{%- endif %}
{%- if arg.choices and arg.choices.env %}

**Choices Environment Variable:** `{{ arg.choices.env }}`
{%- endif %}
{%- if arg.default | length > 0 %}

**Default:** {% for d in arg.default %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}
Expand Down
6 changes: 5 additions & 1 deletion lib/src/docs/markdown/templates/flag_template.md.tera
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

{{ flag.help_md | escape_md }}
{%- endif %}
{%- if flag.arg.choices %}
{%- if flag.arg.choices and flag.arg.choices.choices %}

**Choices:**
{% for choice in flag.arg.choices.choices %}
- `{{ choice }}`
{%- endfor %}
{%- endif %}
{%- if flag.arg.choices and flag.arg.choices.env %}

**Choices Environment Variable:** `{{ flag.arg.choices.env }}`
{%- endif %}
{%- if flag.default | length > 0 %}

**Default:** {% for d in flag.default %}`{{ d }}`{% if not loop.last %}, {% endif %}{% endfor %}
Expand Down
Loading
Loading