Skip to content

Shell completions #3220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct BuildCommand {
short = 'f',
long = "from",
alias = "file",
value_hint = clap::ValueHint::AnyPath,
)]
pub app_source: Option<PathBuf>,

Expand Down
3 changes: 2 additions & 1 deletion src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ pub struct DoctorCommand {
name = APP_MANIFEST_FILE_OPT,
short = 'f',
long = "from",
alias = "file"
alias = "file",
value_hint = clap::ValueHint::AnyPath,
)]
pub app_source: Option<PathBuf>,
}
Expand Down
141 changes: 141 additions & 0 deletions src/commands/maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ pub enum MaintenanceCommands {
GenerateReference(GenerateReference),
/// Generate JSON schema for application manifest.
GenerateManifestSchema(GenerateSchema),
/// Generate a `completely` file which can then be processed into shell completions.
GenerateShellCompletions(GenerateCompletions),
}

impl MaintenanceCommands {
pub async fn run(&self, app: clap::Command<'_>) -> anyhow::Result<()> {
match self {
MaintenanceCommands::GenerateReference(cmd) => cmd.run(app).await,
MaintenanceCommands::GenerateManifestSchema(cmd) => cmd.run().await,
MaintenanceCommands::GenerateShellCompletions(cmd) => cmd.run(app).await,
}
}
}
Expand Down Expand Up @@ -58,3 +61,141 @@ fn write(output: &Option<PathBuf>, text: &str) -> anyhow::Result<()> {
}
Ok(())
}

#[derive(Parser, Debug)]
pub struct GenerateCompletions {
/// The file to which to generate the completions. If omitted, it is generated to stdout.
#[clap(short = 'o')]
pub output: Option<PathBuf>,
}

impl GenerateCompletions {
async fn run(&self, cmd: clap::Command<'_>) -> anyhow::Result<()> {
let writer: &mut dyn std::io::Write = match &self.output {
None => &mut std::io::stdout(),
Some(path) => &mut std::fs::File::create(path).unwrap(),
};

generate_completely_yaml(&cmd, writer);

Ok(())
}
}

fn generate_completely_yaml(cmd: &clap::Command, buf: &mut dyn std::io::Write) {
let mut completion_map = serde_json::value::Map::new();

let subcommands = visible_subcommands(cmd);

insert_array(
&mut completion_map,
cmd.get_name(),
subcommands.iter().map(|sc| sc.get_name()),
);

for subcmd in subcommands {
append_subcommand(&mut completion_map, subcmd, &format!("{} ", cmd.get_name()));
}

let j = serde_json::Value::Object(completion_map);
serde_json::to_writer_pretty(buf, &j).unwrap();
}

fn append_subcommand(
completion_map: &mut serde_json::value::Map<String, serde_json::Value>,
subcmd: &clap::Command<'_>,
prefix: &str,
) {
let key = format!("{}{}", prefix, subcmd.get_name());

let subsubcmds = visible_subcommands(subcmd);

let positionals = subcmd
.get_arguments()
.filter(|a| a.is_positional())
.map(|a| hint(&key, a).to_owned())
.filter(|h| !h.is_empty());
let subsubcmd_names = subsubcmds.iter().map(|c| c.get_name().to_owned());
let flags = subcmd
.get_arguments()
.filter(|a| !a.is_hide_set())
.flat_map(long_and_short);
let subcmd_options = positionals.chain(subsubcmd_names).chain(flags);

insert_array(completion_map, &key, subcmd_options);

for arg in subcmd.get_arguments() {
// We have already done positionals - this is for `cmd*--flag` arrays
if arg.is_positional() || !arg.is_takes_value_set() {
continue;
}

let hint = hint(&key, arg);
for flag in long_and_short(arg) {
let key = format!("{key}*{flag}");
insert_array(completion_map, &key, std::iter::once(hint));
}
}

for subsubcmd in &subsubcmds {
append_subcommand(completion_map, subsubcmd, &format!("{key} "));
}
}

fn hint(full_cmd: &str, arg: &clap::Arg<'_>) -> &'static str {
match arg.get_value_hint() {
clap::ValueHint::AnyPath => "<file>",
clap::ValueHint::FilePath => "<file>",
clap::ValueHint::DirPath => "<directory>",
_ => custom_hint(full_cmd, arg),
}
}

fn custom_hint(full_cmd: &str, arg: &clap::Arg<'_>) -> &'static str {
let arg_name = arg.get_long();

match (full_cmd, arg_name) {
// ("spin build", Some("component-id")) - no existing cmd. We'd ideally want a way to infer app path too
("spin new", Some("template")) => "$(spin templates list --format names-only 2>/dev/null)",
("spin plugins uninstall", None) => {
"$(spin plugins list --installed --format names-only 2>/dev/null)"
}
("spin plugins upgrade", None) => {
"$(spin plugins list --installed --format names-only 2>/dev/null)"
}
("spin templates uninstall", None) => {
"$(spin templates list --format names-only 2>/dev/null)"
}
// ("spin up", Some("component-id")) - no existing cmd. We'd ideally want a way to infer app path too
_ => "",
}
}

fn visible_subcommands<'a, 'b>(cmd: &'a clap::Command<'b>) -> Vec<&'a clap::Command<'b>> {
cmd.get_subcommands()
.filter(|sc| !sc.is_hide_set())
.collect()
}

fn insert_array<T: Into<String>>(
map: &mut serde_json::value::Map<String, serde_json::Value>,
key: impl Into<String>,
values: impl Iterator<Item = T>,
) {
let key = key.into();
let values = values
.map(|s| serde_json::Value::String(s.into()))
.collect();
map.insert(key, values);
}

fn long_and_short(arg: &clap::Arg<'_>) -> Vec<String> {
let mut result = vec![];
if let Some(c) = arg.get_short() {
result.push(format!("-{c}"));
}
if let Some(s) = arg.get_long() {
result.push(format!("--{s}"));
}
result
}
5 changes: 3 additions & 2 deletions src/commands/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub struct TemplateNewCommandCore {

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

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

/// An optional argument that allows to skip prompts for the manifest file
Expand Down Expand Up @@ -96,6 +96,7 @@ pub struct AddCommand {
name = APP_MANIFEST_FILE_OPT,
short = 'f',
long = "file",
value_hint = clap::ValueHint::AnyPath,
)]
pub app: Option<PathBuf>,
}
Expand Down
14 changes: 14 additions & 0 deletions src/commands/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ pub struct Install {
long = "file",
conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT,
conflicts_with = PLUGIN_NAME_OPT,
value_hint = clap::ValueHint::FilePath,
)]
pub local_manifest_src: Option<PathBuf>,

Expand Down Expand Up @@ -199,6 +200,7 @@ pub struct Upgrade {
short = 'f',
long = "file",
conflicts_with = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT,
value_hint = clap::ValueHint::AnyPath,
)]
pub local_manifest_src: Option<PathBuf>,

Expand Down Expand Up @@ -627,6 +629,7 @@ pub struct List {
pub enum ListFormat {
Plain,
Json,
NamesOnly,
}

impl List {
Expand All @@ -650,6 +653,7 @@ impl List {
match self.format {
ListFormat::Plain => Self::print_plain(&plugins),
ListFormat::Json => Self::print_json(&plugins),
ListFormat::NamesOnly => Self::print_names_only(&plugins),
}
}

Expand Down Expand Up @@ -686,6 +690,16 @@ impl List {
println!("{json_text}");
Ok(())
}

fn print_names_only(plugins: &[PluginDescriptor]) -> anyhow::Result<()> {
let names: std::collections::HashSet<_> = plugins.iter().map(|p| &p.name).collect();

for name in names {
println!("{name}");
}

Ok(())
}
}

/// Search for plugins by name.
Expand Down
5 changes: 3 additions & 2 deletions src/commands/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct Push {
short = 'f',
long = "from",
alias = "file",
value_hint = clap::ValueHint::AnyPath,
)]
pub app_source: Option<PathBuf>,

Expand Down Expand Up @@ -68,7 +69,7 @@ pub struct Push {
pub reference: String,

/// Cache directory for downloaded registry data.
#[clap(long)]
#[clap(long, value_hint = clap::ValueHint::DirPath)]
pub cache_dir: Option<PathBuf>,

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

/// Cache directory for downloaded registry data.
#[clap(long)]
#[clap(long, value_hint = clap::ValueHint::DirPath)]
pub cache_dir: Option<PathBuf>,
}

Expand Down
9 changes: 9 additions & 0 deletions src/commands/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ pub struct Install {
long = "dir",
conflicts_with = INSTALL_FROM_GIT_OPT,
conflicts_with = INSTALL_FROM_TAR_OPT,
value_hint = clap::ValueHint::DirPath,
)]
pub dir: Option<PathBuf>,

Expand Down Expand Up @@ -490,6 +491,7 @@ pub struct List {
#[derive(ValueEnum, Clone, Debug)]
pub enum ListFormat {
Table,
NamesOnly,
Json,
}

Expand All @@ -508,6 +510,7 @@ impl List {
prompt_install_default_templates(&template_manager).await?;
}
ListFormat::Table => self.print_templates_table(&list_results),
ListFormat::NamesOnly => self.print_templates_plain(&list_results),
ListFormat::Json => self.print_templates_json(&list_results)?,
};

Expand Down Expand Up @@ -565,6 +568,12 @@ impl List {
}
}

fn print_templates_plain(&self, list_results: &ListResults) {
for template in &list_results.templates {
println!("{}", template.id());
}
}

fn print_templates_json(&self, list_results: &ListResults) -> anyhow::Result<()> {
let json_vals: Vec<_> = list_results
.templates
Expand Down
6 changes: 4 additions & 2 deletions src/commands/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub struct UpCommand {
short = 'f',
long = "from",
group = "source",
value_hint = clap::ValueHint::AnyPath, // it accepts other things, but this is a good hint
)]
pub app_source: Option<String>,

Expand All @@ -66,6 +67,7 @@ pub struct UpCommand {
long = "from-file",
alias = "file",
group = "source",
value_hint = clap::ValueHint::AnyPath,
)]
pub file_source: Option<PathBuf>,

Expand Down Expand Up @@ -93,11 +95,11 @@ pub struct UpCommand {
pub env: Vec<(String, String)>,

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

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

/// For local apps with directory mounts and no excluded files, mount them directly instead of using a temporary
Expand Down
1 change: 1 addition & 0 deletions src/commands/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub struct WatchCommand {
short = 'f',
long = "from",
alias = "file",
value_hint = clap::ValueHint::AnyPath,
)]
pub app_source: Option<PathBuf>,

Expand Down
Loading