diff --git a/cli-engine/src/cli.rs b/cli-engine/src/cli.rs index 773d599..b058464 100644 --- a/cli-engine/src/cli.rs +++ b/cli-engine/src/cli.rs @@ -1864,11 +1864,15 @@ impl Cli { apply_pagination_flags(&mut middleware, &command.spec, leaf); let args = command_args_from_matches(leaf, &command.spec, false); let user_args = command_args_from_matches(leaf, &command.spec, true); - let pagination_command = command - .spec - .pagination - .is_some() - .then(|| pagination_command_base(&command_path, &command.spec, &user_args, &flags)); + let pagination_command = command.spec.pagination.is_some().then(|| { + pagination_command_base( + &self.config.name, + &command_path, + &command.spec, + &user_args, + &flags, + ) + }); if let Err(err) = self.run_pre_run(&mut middleware, &command_path, &args) { return self.finish_run(render_cli_error(&middleware, &err, &self.config.app_id)); } @@ -2571,9 +2575,12 @@ fn apply_pagination_flags(middleware: &mut Middleware, spec: &CommandSpec, leaf: } /// Replays a paginating command's own explicit args, plus the global -/// `--filter`/`--expr`/`--fields` flags, as `--flag value` text — the base a -/// "view the next page" [`crate::NextAction`] is built from once the -/// response's [`crate::PaginationMeta`] is known. +/// `--filter`/`--expr`/`--fields` flags, as `--flag value` text, prefixed +/// with the CLI's binary name — the base a "view the next page" +/// [`crate::NextAction`] is built from once the response's +/// [`crate::PaginationMeta`] is known. Leading with the binary name keeps the +/// suggested command copy-pastable rather than a fragment starting at the +/// noun/verb path. /// /// `--filter`/`--expr`/`--fields` sit in the same output pipeline as /// pagination itself (filter -> paginate -> expr -> fields) and change what @@ -2594,12 +2601,16 @@ fn apply_pagination_flags(middleware: &mut Middleware, spec: &CommandSpec, leaf: /// those are added by the caller once it knows the /// next page's offset. fn pagination_command_base( + binary_name: &str, command_path: &str, spec: &CommandSpec, user_args: &crate::middleware::ValueMap, flags: &GlobalFlags, ) -> String { - let mut parts = vec![command_path.replace(':', " ")]; + let mut parts = vec![ + quote_pagination_value(binary_name), + command_path.replace(':', " "), + ]; for arg in &spec.args { let id = arg.get_id().as_str(); if let Some(value) = user_args.get(id) { diff --git a/cli-engine/tests/pagination.rs b/cli-engine/tests/pagination.rs index 5727e68..c65ebb1 100644 --- a/cli-engine/tests/pagination.rs +++ b/cli-engine/tests/pagination.rs @@ -98,7 +98,7 @@ async fn default_limit_applies_when_neither_flag_is_passed() { ); assert_eq!( rendered["next_actions"][0]["command"], - "list --limit 2 --offset 2" + "my-cli list --limit 2 --offset 2" ); assert!(rendered.get("metadata").is_none(), "{}", output.rendered); } @@ -131,7 +131,7 @@ async fn explicit_limit_and_offset_override_the_default_and_expose_pagination() ); assert_eq!( rendered["next_actions"][0]["command"], - "list --limit 2 --offset 3" + "my-cli list --limit 2 --offset 3" ); assert_eq!( rendered["next_actions"][0]["description"], @@ -184,7 +184,7 @@ async fn next_page_action_replays_other_flags_the_user_passed() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - "list --status active --limit 2 --offset 2" + "my-cli list --status active --limit 2 --offset 2" ); } @@ -214,7 +214,29 @@ async fn next_page_action_quotes_values_with_whitespace() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - "list --status \"in review\" --limit 2 --offset 2" + "my-cli list --status \"in review\" --limit 2 --offset 2" + ); +} + +#[tokio::test] +async fn next_page_action_quotes_a_binary_name_with_whitespace() { + let mut cli = Cli::new(CliConfig::new("my cli", "Dev tooling", "my-cli")); + cli.add_command(RuntimeCommandSpec::new( + CommandSpec::new("list", "List things") + .no_auth(true) + .with_pagination(PaginationConfig { + default_limit: 2, + ..PaginationConfig::default() + }), + async |_credential, _args| Ok(CommandResult::new(json!(items()))), + )); + + let output = cli.run(["my cli", "list", "--output", "json"]).await; + assert_eq!(output.exit_code, 0, "{}", output.rendered); + let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); + assert_eq!( + rendered["next_actions"][0]["command"], + "\"my cli\" list --limit 2 --offset 2" ); } @@ -250,7 +272,7 @@ async fn next_page_action_uses_the_real_long_flag_not_the_value_map_key() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - "list --sort-order asc --limit 2 --offset 2" + "my-cli list --sort-order asc --limit 2 --offset 2" ); } @@ -365,7 +387,7 @@ async fn human_output_shows_pagination_summary_and_next_steps() { output.rendered ); assert!( - output.rendered.contains("list --limit 2 --offset 2"), + output.rendered.contains("my-cli list --limit 2 --offset 2"), "{}", output.rendered ); @@ -433,7 +455,7 @@ async fn next_page_action_preserves_filter_expr_and_fields() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - "list --filter \"name != 'alpha'\" --expr \"sort_by(@, &name)\" --fields name --limit 2 --offset 2" + "my-cli list --filter \"name != 'alpha'\" --expr \"sort_by(@, &name)\" --fields name --limit 2 --offset 2" ); } @@ -463,7 +485,7 @@ async fn next_page_action_replays_a_set_false_flag_as_a_bare_switch() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - "list --no-cache --limit 2 --offset 2" + "my-cli list --no-cache --limit 2 --offset 2" ); } @@ -531,7 +553,7 @@ async fn next_page_action_replays_a_multi_value_arg_as_repeated_flags() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - "list --scope a --scope b --limit 2 --offset 2" + "my-cli list --scope a --scope b --limit 2 --offset 2" ); } @@ -607,7 +629,7 @@ async fn next_page_action_escapes_shell_metacharacters_and_expansions() { let rendered: serde_json::Value = serde_json::from_str(&output.rendered).expect("valid json"); assert_eq!( rendered["next_actions"][0]["command"], - r#"list --status "a;\$(whoami)\`x\`\"y\"\\z" --limit 2 --offset 2"# + r#"my-cli list --status "a;\$(whoami)\`x\`\"y\"\\z" --limit 2 --offset 2"# ); }