Skip to content
Merged
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
13 changes: 13 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ pub struct Spec<'a> {
pub license: Option<&'a str>,
/// Source repository URL for generated references and integrations.
pub repository: Option<&'a str>,
/// A tera template turning a command path into a link to the code implementing it.
///
/// Rendered with `path` bound to the command's words joined by `/`, so a docs
/// generator can put a "view source" link on every command page. Distinct from
/// [`Self::repository`], which is the plain project URL: a deep link with a
/// `{{path}}` placeholder cannot be turned back into one without knowing a
/// particular forge's URL layout.
pub source_code_link_template: Option<&'a str>,
/// The oldest `usage` that can read this spec, when the CLI says.
///
/// Written first, before anything a `usage` too old to understand would choke on — which is
Expand Down Expand Up @@ -556,6 +564,7 @@ impl<'a> SpecView<'a> {
author: self.base.author,
license: self.base.license,
repository: self.base.repository,
source_code_link_template: self.base.source_code_link_template,
min_usage_version: self.base.min_usage_version,
about: self.base.about,
long_about: self.base.long_about,
Expand Down Expand Up @@ -586,6 +595,7 @@ impl Spec<'_> {
author: None,
license: None,
repository: None,
source_code_link_template: None,
min_usage_version: None,
about: None,
long_about: None,
Expand Down Expand Up @@ -1283,6 +1293,9 @@ impl Spec<'_> {
if let Some(repository) = self.repository {
prop(out, "repository", repository)?;
}
if let Some(template) = self.source_code_link_template {
prop(out, "source_code_link_template", template)?;
}
// A description may be given on the spec or on its root command — a derive
// naturally has one doc comment and no reason to care which field it lands
// in — so either is written, the spec's first.
Expand Down
15 changes: 0 additions & 15 deletions cli/assets/usage-extra.usage.kdl

This file was deleted.

17 changes: 17 additions & 0 deletions cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ mod sponsors;
bin = "usage",
version,
min_usage_version = "4.0",
repository = "https://github.com/jdx/usage",
// The command path is not the file path: command names are hyphenated where the files
// that implement them are snake_case, a command with subcommands lives in its directory's
// `mod.rs`, and the four shell commands are all served by a single `shell.rs`.
//
// Unindented, because a raw string keeps every leading space it is given and only the
// `{%-`/`-%}` markers take any back — so indenting to match the attribute would be
// trusting each line to be surrounded by them.
source_code_link_template = r#"{%- set path = path | replace(from='-', to='_') -%}
{%- if cmd.subcommands | length > 0 -%}
{%- set path = path ~ "/mod.rs" -%}
{%- elif path in ["bash", "fish", "powershell", "zsh"] -%}
{%- set path = "shell.rs" -%}
{%- else -%}
{%- set path = path ~ ".rs" -%}
{%- endif -%}
https://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}"#,
usage = "Usage: usage <COMMAND>\n usage --completions <COMPLETIONS>\n usage --usage-spec",
// Every flag `usage` accepts is one it declares, so an unrecognised one is a mistake and
// saying so beats offering it to a positional — `usage lint --nope f.kdl` would otherwise
Expand Down
5 changes: 3 additions & 2 deletions cli/src/usage_spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use miette::Result;
pub(crate) fn generate() -> Result<()> {
// The declaration *is* the spec: the same tables that parsed the command line print it,
// with no bridge and no second model in between. `effect` in particular is now declared
// on each command rather than patched in here afterwards, since clap could not say it.
// on each command rather than patched in here afterwards, since clap could not say it,
// and `repository` and `source_code_link_template` used to be appended here from a
// checked-in KDL fragment for the same reason — the derive can say both now.
println!("// @generated by usage-cli from its own parse tables");
println!("{}", Cli::to_kdl().trim());
println!("{}", include_str!("../assets/usage-extra.usage.kdl").trim());

Ok(())
}
Expand Down
17 changes: 2 additions & 15 deletions cli/usage.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ min_usage_version "4.0"
name usage
bin usage
version "5.1.0"
repository "https://github.com/jdx/usage"
source_code_link_template "{%- set path = path | replace(from='-', to='_') -%}\n{%- if cmd.subcommands | length > 0 -%}\n{%- set path = path ~ \"/mod.rs\" -%}\n{%- elif path in [\"bash\", \"fish\", \"powershell\", \"zsh\"] -%}\n{%- set path = \"shell.rs\" -%}\n{%- else -%}\n{%- set path = path ~ \".rs\" -%}\n{%- endif -%}\nhttps://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}"
about "CLI for working with usage-based CLIs"
usage "Usage: usage <COMMAND>\n usage --completions <COMPLETIONS>\n usage --usage-spec"
unknown_flags error
Expand Down Expand Up @@ -256,18 +258,3 @@ cmd zsh help="Execute a shell script using zsh" unknown_flags=value {
long_help "Arguments to pass to script\n\nAnything `usage` does not recognise is a value rather than a mistake, which is what\nlets a shebang script take flags of its own."
}
}
repository "https://github.com/jdx/usage"
// The command path is not the file path: command names are hyphenated where the files
// that implement them are snake_case, a command with subcommands lives in its directory's
// `mod.rs`, and the four shell commands are all served by a single `shell.rs`.
source_code_link_template #"""
{%- set path = path | replace(from='-', to='_') -%}
{%- if cmd.subcommands | length > 0 -%}
{%- set path = path ~ "/mod.rs" -%}
{%- elif path in ["bash", "fish", "powershell", "zsh"] -%}
{%- set path = "shell.rs" -%}
{%- else -%}
{%- set path = path ~ ".rs" -%}
{%- endif -%}
https://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}
"""#
1 change: 1 addition & 0 deletions conformance/src/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ pub fn build_spec(spec: &Spec) -> &'static usage_argv::spec::Spec<'static> {
author: opt(&spec.author),
license: opt(&spec.license),
repository: opt(&spec.repository),
source_code_link_template: opt(&spec.source_code_link_template),
min_usage_version: opt(&spec.min_usage_version),
about: opt(&spec.about),
long_about: opt(&spec.about_long),
Expand Down
65 changes: 65 additions & 0 deletions conformance/tests/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,3 +680,68 @@ fn examples_survive_the_round_trip_from_a_typed_declaration() {
};
assert_eq!(deploy.environment.as_deref(), Some("prod"));
}

#[derive(Args)]
struct Go {
/// Something to do it to.
value: Option<String>,
}

#[derive(Subcommands)]
#[allow(dead_code)]
enum LinkedCommands {
/// Go somewhere.
Go(Go),
}

#[derive(Cli)]
#[usage(
bin = "linked",
repository = "https://github.com/jdx/usage",
source_code_link_template = r#"{%- set path = path | replace(from='-', to='_') -%}
{%- if cmd.subcommands | length > 0 -%}
{%- set path = path ~ "/mod.rs" -%}
{%- else -%}
{%- set path = path ~ ".rs" -%}
{%- endif -%}
https://github.com/jdx/usage/blob/main/cli/src/cli/{{path}}"#
)]
#[allow(dead_code)]
struct Linked {
#[usage(subcommand)]
command: LinkedCommands,
}

#[test]
fn the_source_code_link_template_survives_the_typed_spec() {
// A fourth. `usage` itself declared this in a KDL fragment appended to its own emitted
// spec, because the derive had no word for it — the one thing left keeping a hand-written
// second model beside the declaration. It reaches markdown, where it becomes the "view
// source" link on every command page, so a derive that drops it silently loses a link on
// every page of every CLI that wanted one.
let kdl = Linked::to_kdl();
let spec: LibSpec = kdl.parse().expect("valid spec");

// Newlines make it through: the writer escapes them into one quoted string rather than
// emitting a `#"""` block, and the template means nothing if its lines run together.
let template = spec
.source_code_link_template
.as_deref()
.expect("the template reached the typed spec");
assert_eq!(template.lines().count(), 7, "{template:?}");
assert!(template.starts_with("{%- set path"), "{template:?}");
assert!(template.ends_with("/cli/src/cli/{{path}}"), "{template:?}");
assert_eq!(
spec.repository.as_deref(),
Some("https://github.com/jdx/usage")
);

// And it renders, which is the only reason to carry it.
let go = spec.cmd.subcommands.get("go").expect("go");
let renderer = usage::docs::markdown::MarkdownRenderer::new(spec.clone());
let page = renderer.render_cmd(go).expect("a page for go");
assert!(
page.contains("https://github.com/jdx/usage/blob/main/cli/src/cli/go.rs"),
"{page}"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ name ex
bin ex
version "1.2.3"
long_version "1.2.3\ncommit abc123"
source_code_link_template "{%- if cmd.subcommands | length > 0 -%}\n{%- set path = path ~ \"/mod.rs\" -%}\n{%- endif -%}\nhttps://example.com/blob/main/src/{{path}}"
about "does things"
long_about "Does things, at length."
usage "Usage: ex <COMMAND>\n ex --version \"quoted\""
Expand Down
20 changes: 20 additions & 0 deletions conformance/tests/spec_roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,14 @@ static SPEC: Spec = Spec {
author: None,
license: None,
repository: None,
// Multi-line and full of quotes, because that is what a real one looks like and the
// writer has to escape it into a single KDL string rather than a `#"""` block.
source_code_link_template: Some(concat!(
"{%- if cmd.subcommands | length > 0 -%}\n",
"{%- set path = path ~ \"/mod.rs\" -%}\n",
"{%- endif -%}\n",
"https://example.com/blob/main/src/{{path}}",
)),
min_usage_version: None,
about: Some("does things"),
long_about: Some("Does things, at length."),
Expand Down Expand Up @@ -356,6 +364,15 @@ fn the_program_itself_survives() {
spec.usage,
"Usage: ex <COMMAND>\n ex --version \"quoted\""
);
let template = spec
.source_code_link_template
.as_deref()
.expect("the link template survived");
assert_eq!(template.lines().count(), 4, "{template:?}");
assert!(
template.contains("{%- set path = path ~ \"/mod.rs\" -%}"),
"{template:?}"
);
}

#[test]
Expand Down Expand Up @@ -768,6 +785,7 @@ fn a_declared_completer_becomes_a_run_the_reference_can_read() {
author: None,
license: None,
repository: None,
source_code_link_template: None,
min_usage_version: None,
about: None,
long_about: None,
Expand Down Expand Up @@ -906,6 +924,7 @@ fn two_commands_can_mean_different_things_by_one_name() {
author: None,
license: None,
repository: None,
source_code_link_template: None,
min_usage_version: None,
about: None,
long_about: None,
Expand Down Expand Up @@ -964,6 +983,7 @@ fn two_commands_can_mean_different_things_by_one_name() {
author: None,
license: None,
repository: None,
source_code_link_template: None,
min_usage_version: None,
about: None,
long_about: None,
Expand Down
2 changes: 2 additions & 0 deletions derive/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ pub fn emit(cli: &Cli) -> TokenStream {
let author = option_expr(cli.author.as_ref());
let license = option_expr(cli.license.as_ref());
let repository = option_expr(cli.repository.as_ref());
let source_code_link_template = option_expr(cli.source_code_link_template.as_ref());
let about = cli
.about_attr
.as_ref()
Expand Down Expand Up @@ -776,6 +777,7 @@ pub fn emit(cli: &Cli) -> TokenStream {
author: #author,
license: #license,
repository: #repository,
source_code_link_template: #source_code_link_template,
min_usage_version: #min_usage_version,
about: #about,
long_about: #long_about,
Expand Down
4 changes: 3 additions & 1 deletion derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@
//! spelling the CLI declares for itself. clap refuses that collision by panicking at startup;
//! here the declaration simply wins and the other spelling still answers.
//!
//! On the struct itself: `bin`, `version`, `long_version`, `author`, `license`, `repository`, `about`,
//! On the struct itself: `bin`, `version`, `long_version`, `author`, `license`, `repository`,
//! `source_code_link_template` — a tera template rendered with the command path as `path`,
//! which generated markdown turns into a "view source" link — `about`,
//! `long_about`, `before_help`, `after_help`,
//! clap-compatible `visible_alias(es)`, hidden `alias(es)`, and `hide` may stay on an
//! `Args` struct and are inherited by every subcommand variant that mounts it —
Expand Down
35 changes: 27 additions & 8 deletions derive/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ pub struct Cli {
pub author: Option<proc_macro2::TokenStream>,
pub license: Option<proc_macro2::TokenStream>,
pub repository: Option<proc_macro2::TokenStream>,
/// A tera template turning a command path into a link to the code implementing it.
///
/// Kept as tokens for the same reason as the three above: these run to several lines of
/// tera, and an adopter should be able to name a `const` rather than inline one.
pub source_code_link_template: Option<proc_macro2::TokenStream>,
/// Whether a flag-like token that names no flag is a value or an error. Unset
/// means the spec's default, which is `value`.
pub unknown_flags: Option<String>,
Expand Down Expand Up @@ -678,6 +683,7 @@ impl Cli {
author: None,
license: None,
repository: None,
source_code_link_template: None,
unknown_flags: None,
default_subcommand: None,
multicall: false,
Expand Down Expand Up @@ -869,6 +875,9 @@ impl Cli {
"author" => cli.author = Some(metadata_expr(&meta)?),
"license" => cli.license = Some(metadata_expr(&meta)?),
"repository" => cli.repository = Some(metadata_expr(&meta)?),
"source_code_link_template" => {
cli.source_code_link_template = Some(metadata_expr(&meta)?)
}
"before_help" => cli.before_help = Some(metadata_expr(&meta)?),
"next_help_heading" => cli.next_help_heading = Some(string_value(&meta)?),
"before_long_help" => cli.before_long_help = Some(metadata_expr(&meta)?),
Expand Down Expand Up @@ -960,7 +969,7 @@ impl Cli {
path,
format!(
"unknown option `{other}` on a struct; usage::Cli takes \
`name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `long_version`, `long_version_spec`, `author`, `license`, `repository`, `usage`, `alias`, `alias_hidden`, `visible_alias`, `hide`, `deprecated`, `deprecated_warn_at`, `deprecated_remove_at`, `verbatim_doc_comment`, `unknown_flags`, \
`name`, `name_spec`, `bin`, `bin_spec`, `version`, `version_spec`, `long_version`, `long_version_spec`, `author`, `license`, `repository`, `source_code_link_template`, `usage`, `alias`, `alias_hidden`, `visible_alias`, `hide`, `deprecated`, `deprecated_warn_at`, `deprecated_remove_at`, `verbatim_doc_comment`, `unknown_flags`, \
`default_subcommand`, `multicall`, `no_binary_name`, `arg_required_else_help`, `disable_help_flag`, `disable_help_subcommand`, `disable_version_flag`, `dont_delimit_trailing_values`, `args_override_self`, `subcommand_negates_reqs`, `args_conflicts_with_subcommands`, `subcommand_precedence_over_arg`, `allow_missing_positional`, \
`next_help_heading`, `subcommand_help_heading`, `next_line_help`, `flatten_help`, `term_width`, `max_term_width`, \
`subcommand_value_name`, `restart_token`, `mount`, `example` and \
Expand Down Expand Up @@ -1149,22 +1158,27 @@ impl Cli {
one claim about the whole emitted spec, and only the root emits one",
));
}
// Package metadata describes the emitted spec, not one command. Like
// `min_usage_version`, these values have no nested KDL location and would
// otherwise be accepted and silently dropped by `emit_args`.
// Package metadata, and the link template that names where the code lives,
// describe the emitted spec rather than one command. Like `min_usage_version`,
// these values have no nested KDL location and would otherwise be accepted and
// silently dropped by `emit_args`.
if let Some(name) = [
("author", self.author.is_some()),
("license", self.license.is_some()),
("repository", self.repository.is_some()),
(
"source_code_link_template",
self.source_code_link_template.is_some(),
),
]
.into_iter()
.find_map(|(name, present)| present.then_some(name))
{
return Err(self.misplaced(
ident,
format!(
"`{name}` belongs on the root, where `#[derive(Cli)]` is: package \
metadata describes the whole emitted spec, not one command"
"`{name}` belongs on the root, where `#[derive(Cli)]` is: it \
describes the whole emitted spec, not one command"
),
));
}
Expand Down Expand Up @@ -6642,8 +6656,13 @@ mod tests {
}

#[test]
fn package_metadata_belongs_on_the_root() {
for attribute in ["author", "license", "repository"] {
fn spec_level_metadata_belongs_on_the_root() {
for attribute in [
"author",
"license",
"repository",
"source_code_link_template",
] {
let body = format!(
r#"
#[usage({attribute} = "value")]
Expand Down
3 changes: 2 additions & 1 deletion docs/rust/clap-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ the Rust declaration, not only from generated KDL, wherever the bridge column sa
## Usage extensions

These are not clap compatibility gaps. usage additionally supports `mount`,
`restart_token`, `default_subcommand`, command and flag `effect`, Nushell completions,
`restart_token`, `default_subcommand`, command and flag `effect`,
`source_code_link_template`, Nushell completions,
and a language-neutral conformance corpus. clap cannot express those properties, so
a clap-generated spec cannot carry them without an overlay.

Expand Down
Loading