Skip to content

Commit 82e0c4a

Browse files
committed
Shell completions via completely
Signed-off-by: itowlson <[email protected]>
1 parent daed2e3 commit 82e0c4a

File tree

9 files changed

+178
-7
lines changed

9 files changed

+178
-7
lines changed

src/commands/build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct BuildCommand {
2222
short = 'f',
2323
long = "from",
2424
alias = "file",
25+
value_hint = clap::ValueHint::AnyPath,
2526
)]
2627
pub app_source: Option<PathBuf>,
2728

src/commands/doctor.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ pub struct DoctorCommand {
1717
name = APP_MANIFEST_FILE_OPT,
1818
short = 'f',
1919
long = "from",
20-
alias = "file"
20+
alias = "file",
21+
value_hint = clap::ValueHint::AnyPath,
2122
)]
2223
pub app_source: Option<PathBuf>,
2324
}

src/commands/maintenance.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@ pub enum MaintenanceCommands {
99
GenerateReference(GenerateReference),
1010
/// Generate JSON schema for application manifest.
1111
GenerateManifestSchema(GenerateSchema),
12+
/// Generate a `completely` file which can then be processed into shell completions.
13+
GenerateShellCompletions(GenerateCompletions),
1214
}
1315

1416
impl MaintenanceCommands {
1517
pub async fn run(&self, app: clap::Command<'_>) -> anyhow::Result<()> {
1618
match self {
1719
MaintenanceCommands::GenerateReference(cmd) => cmd.run(app).await,
1820
MaintenanceCommands::GenerateManifestSchema(cmd) => cmd.run().await,
21+
MaintenanceCommands::GenerateShellCompletions(cmd) => cmd.run(app).await,
1922
}
2023
}
2124
}
@@ -58,3 +61,141 @@ fn write(output: &Option<PathBuf>, text: &str) -> anyhow::Result<()> {
5861
}
5962
Ok(())
6063
}
64+
65+
#[derive(Parser, Debug)]
66+
pub struct GenerateCompletions {
67+
/// The file to which to generate the completions. If omitted, it is generated to stdout.
68+
#[clap(short = 'o')]
69+
pub output: Option<PathBuf>,
70+
}
71+
72+
impl GenerateCompletions {
73+
async fn run(&self, cmd: clap::Command<'_>) -> anyhow::Result<()> {
74+
let writer: &mut dyn std::io::Write = match &self.output {
75+
None => &mut std::io::stdout(),
76+
Some(path) => &mut std::fs::File::create(path).unwrap(),
77+
};
78+
79+
generate_completely_yaml(&cmd, writer);
80+
81+
Ok(())
82+
}
83+
}
84+
85+
fn generate_completely_yaml(cmd: &clap::Command, buf: &mut dyn std::io::Write) {
86+
let mut completion_map = serde_json::value::Map::new();
87+
88+
let subcommands = visible_subcommands(cmd);
89+
90+
insert_array(
91+
&mut completion_map,
92+
cmd.get_name(),
93+
subcommands.iter().map(|sc| sc.get_name()),
94+
);
95+
96+
for subcmd in subcommands {
97+
append_subcommand(&mut completion_map, subcmd, &format!("{} ", cmd.get_name()));
98+
}
99+
100+
let j = serde_json::Value::Object(completion_map);
101+
serde_json::to_writer_pretty(buf, &j).unwrap();
102+
}
103+
104+
fn append_subcommand(
105+
completion_map: &mut serde_json::value::Map<String, serde_json::Value>,
106+
subcmd: &clap::Command<'_>,
107+
prefix: &str,
108+
) {
109+
let key = format!("{}{}", prefix, subcmd.get_name());
110+
111+
let subsubcmds = visible_subcommands(subcmd);
112+
113+
let positionals = subcmd
114+
.get_arguments()
115+
.filter(|a| a.is_positional())
116+
.map(|a| hint(&key, a).to_owned())
117+
.filter(|h| !h.is_empty());
118+
let subsubcmd_names = subsubcmds.iter().map(|c| c.get_name().to_owned());
119+
let flags = subcmd
120+
.get_arguments()
121+
.filter(|a| !a.is_hide_set())
122+
.flat_map(long_and_short);
123+
let subcmd_options = positionals.chain(subsubcmd_names).chain(flags);
124+
125+
insert_array(completion_map, &key, subcmd_options);
126+
127+
for arg in subcmd.get_arguments() {
128+
// We have already done positionals - this is for `cmd*--flag` arrays
129+
if arg.is_positional() || !arg.is_takes_value_set() {
130+
continue;
131+
}
132+
133+
let hint = hint(&key, arg);
134+
for flag in long_and_short(arg) {
135+
let key = format!("{key}*{flag}");
136+
insert_array(completion_map, &key, std::iter::once(hint));
137+
}
138+
}
139+
140+
for subsubcmd in &subsubcmds {
141+
append_subcommand(completion_map, subsubcmd, &format!("{key} "));
142+
}
143+
}
144+
145+
fn hint(full_cmd: &str, arg: &clap::Arg<'_>) -> &'static str {
146+
match arg.get_value_hint() {
147+
clap::ValueHint::AnyPath => "<file>",
148+
clap::ValueHint::FilePath => "<file>",
149+
clap::ValueHint::DirPath => "<directory>",
150+
_ => custom_hint(full_cmd, arg),
151+
}
152+
}
153+
154+
fn custom_hint(full_cmd: &str, arg: &clap::Arg<'_>) -> &'static str {
155+
let arg_name = arg.get_long();
156+
157+
match (full_cmd, arg_name) {
158+
// ("spin build", Some("component-id")) - no existing cmd. We'd ideally want a way to infer app path too
159+
("spin new", Some("template")) => "$(spin templates list --format names-only 2>/dev/null)",
160+
("spin plugins uninstall", None) => {
161+
"$(spin plugins list --installed --format names-only 2>/dev/null)"
162+
}
163+
("spin plugins upgrade", None) => {
164+
"$(spin plugins list --installed --format names-only 2>/dev/null)"
165+
}
166+
("spin templates uninstall", None) => {
167+
"$(spin templates list --format names-only 2>/dev/null)"
168+
}
169+
// ("spin up", Some("component-id")) - no existing cmd. We'd ideally want a way to infer app path too
170+
_ => "",
171+
}
172+
}
173+
174+
fn visible_subcommands<'a, 'b>(cmd: &'a clap::Command<'b>) -> Vec<&'a clap::Command<'b>> {
175+
cmd.get_subcommands()
176+
.filter(|sc| !sc.is_hide_set())
177+
.collect()
178+
}
179+
180+
fn insert_array<T: Into<String>>(
181+
map: &mut serde_json::value::Map<String, serde_json::Value>,
182+
key: impl Into<String>,
183+
values: impl Iterator<Item = T>,
184+
) {
185+
let key = key.into();
186+
let values = values
187+
.map(|s| serde_json::Value::String(s.into()))
188+
.collect();
189+
map.insert(key, values);
190+
}
191+
192+
fn long_and_short(arg: &clap::Arg<'_>) -> Vec<String> {
193+
let mut result = vec![];
194+
if let Some(c) = arg.get_short() {
195+
result.push(format!("-{c}"));
196+
}
197+
if let Some(s) = arg.get_long() {
198+
result.push(format!("--{s}"));
199+
}
200+
result
201+
}

src/commands/new.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub struct TemplateNewCommandCore {
4242

4343
/// The directory in which to create the new application or component.
4444
/// The default is the name argument.
45-
#[clap(short = 'o', long = "output", group = "location")]
45+
#[clap(short = 'o', long = "output", group = "location", value_hint = clap::ValueHint::DirPath)]
4646
pub output_path: Option<PathBuf>,
4747

4848
/// Create the new application or component in the current directory.
@@ -56,7 +56,7 @@ pub struct TemplateNewCommandCore {
5656
/// A TOML file which contains parameter values in name = "value" format.
5757
/// Parameters passed as CLI option overwrite parameters specified in the
5858
/// file.
59-
#[clap(long = "values-file")]
59+
#[clap(long = "values-file", value_hint = clap::ValueHint::FilePath)]
6060
pub values_file: Option<PathBuf>,
6161

6262
/// An optional argument that allows to skip prompts for the manifest file
@@ -96,6 +96,7 @@ pub struct AddCommand {
9696
name = APP_MANIFEST_FILE_OPT,
9797
short = 'f',
9898
long = "file",
99+
value_hint = clap::ValueHint::AnyPath,
99100
)]
100101
pub app: Option<PathBuf>,
101102
}

src/commands/plugins.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ pub struct Install {
7878
long = "file",
7979
conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT,
8080
conflicts_with = PLUGIN_NAME_OPT,
81+
value_hint = clap::ValueHint::FilePath,
8182
)]
8283
pub local_manifest_src: Option<PathBuf>,
8384

@@ -199,6 +200,7 @@ pub struct Upgrade {
199200
short = 'f',
200201
long = "file",
201202
conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT,
203+
value_hint = clap::ValueHint::AnyPath,
202204
)]
203205
pub local_manifest_src: Option<PathBuf>,
204206

@@ -627,6 +629,7 @@ pub struct List {
627629
pub enum ListFormat {
628630
Plain,
629631
Json,
632+
NamesOnly,
630633
}
631634

632635
impl List {
@@ -650,6 +653,7 @@ impl List {
650653
match self.format {
651654
ListFormat::Plain => Self::print_plain(&plugins),
652655
ListFormat::Json => Self::print_json(&plugins),
656+
ListFormat::NamesOnly => Self::print_names_only(&plugins),
653657
}
654658
}
655659

@@ -686,6 +690,16 @@ impl List {
686690
println!("{json_text}");
687691
Ok(())
688692
}
693+
694+
fn print_names_only(plugins: &[PluginDescriptor]) -> anyhow::Result<()> {
695+
let names: std::collections::HashSet<_> = plugins.iter().map(|p| &p.name).collect();
696+
697+
for name in names {
698+
println!("{name}");
699+
}
700+
701+
Ok(())
702+
}
689703
}
690704

691705
/// Search for plugins by name.

src/commands/registry.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub struct Push {
3737
short = 'f',
3838
long = "from",
3939
alias = "file",
40+
value_hint = clap::ValueHint::AnyPath,
4041
)]
4142
pub app_source: Option<PathBuf>,
4243

@@ -68,7 +69,7 @@ pub struct Push {
6869
pub reference: String,
6970

7071
/// Cache directory for downloaded registry data.
71-
#[clap(long)]
72+
#[clap(long, value_hint = clap::ValueHint::DirPath)]
7273
pub cache_dir: Option<PathBuf>,
7374

7475
/// Specifies the OCI image manifest annotations (in key=value format).
@@ -138,7 +139,7 @@ pub struct Pull {
138139
pub reference: String,
139140

140141
/// Cache directory for downloaded registry data.
141-
#[clap(long)]
142+
#[clap(long, value_hint = clap::ValueHint::DirPath)]
142143
pub cache_dir: Option<PathBuf>,
143144
}
144145

src/commands/templates.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ pub struct Install {
7979
long = "dir",
8080
conflicts_with = INSTALL_FROM_GIT_OPT,
8181
conflicts_with = INSTALL_FROM_TAR_OPT,
82+
value_hint = clap::ValueHint::DirPath,
8283
)]
8384
pub dir: Option<PathBuf>,
8485

@@ -490,6 +491,7 @@ pub struct List {
490491
#[derive(ValueEnum, Clone, Debug)]
491492
pub enum ListFormat {
492493
Table,
494+
NamesOnly,
493495
Json,
494496
}
495497

@@ -508,6 +510,7 @@ impl List {
508510
prompt_install_default_templates(&template_manager).await?;
509511
}
510512
ListFormat::Table => self.print_templates_table(&list_results),
513+
ListFormat::NamesOnly => self.print_templates_plain(&list_results),
511514
ListFormat::Json => self.print_templates_json(&list_results)?,
512515
};
513516

@@ -565,6 +568,12 @@ impl List {
565568
}
566569
}
567570

571+
fn print_templates_plain(&self, list_results: &ListResults) {
572+
for template in &list_results.templates {
573+
println!("{}", template.id());
574+
}
575+
}
576+
568577
fn print_templates_json(&self, list_results: &ListResults) -> anyhow::Result<()> {
569578
let json_vals: Vec<_> = list_results
570579
.templates

src/commands/up.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub struct UpCommand {
5555
short = 'f',
5656
long = "from",
5757
group = "source",
58+
value_hint = clap::ValueHint::AnyPath, // it accepts other things, but this is a good hint
5859
)]
5960
pub app_source: Option<String>,
6061

@@ -66,6 +67,7 @@ pub struct UpCommand {
6667
long = "from-file",
6768
alias = "file",
6869
group = "source",
70+
value_hint = clap::ValueHint::AnyPath,
6971
)]
7072
pub file_source: Option<PathBuf>,
7173

@@ -93,11 +95,11 @@ pub struct UpCommand {
9395
pub env: Vec<(String, String)>,
9496

9597
/// Temporary directory for the static assets of the components.
96-
#[clap(long = "temp", alias = "tmp")]
98+
#[clap(long = "temp", alias = "tmp", value_hint = clap::ValueHint::DirPath)]
9799
pub tmp: Option<PathBuf>,
98100

99101
/// Cache directory for downloaded components and assets.
100-
#[clap(long)]
102+
#[clap(long, value_hint = clap::ValueHint::DirPath)]
101103
pub cache_dir: Option<PathBuf>,
102104

103105
/// For local apps with directory mounts and no excluded files, mount them directly instead of using a temporary

src/commands/watch.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub struct WatchCommand {
4141
short = 'f',
4242
long = "from",
4343
alias = "file",
44+
value_hint = clap::ValueHint::AnyPath,
4445
)]
4546
pub app_source: Option<PathBuf>,
4647

0 commit comments

Comments
 (0)