diff --git a/README.md b/README.md index 1f6d15a26..1effb6779 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,7 @@ mq conv document.docx | mq -A 'section::section("Summary")' mq conv slides.pdf | mq view ``` -Run `mq --list` to see all available subcommands (built-in and external). +Run `mq commands` to see all available subcommands (built-in and external). ## External Subcommands diff --git a/crates/mq-lang/src/mqc.rs b/crates/mq-lang/src/mqc.rs index cf5404e21..ec15928eb 100644 --- a/crates/mq-lang/src/mqc.rs +++ b/crates/mq-lang/src/mqc.rs @@ -116,7 +116,7 @@ pub enum MqcError { #[diagnostic( code(mq::mqc::module_let_runtime_value), help( - "Module-level `let` values are computed once, when the .mqc file is compiled, so they cannot read --args/--argjson values. A module can't see them, so read \"{name}\" outside the module instead." + "Module-level `let` values are computed once, when the .mqc file is compiled, so they cannot read --arg/--argjson values. A module can't see them, so read \"{name}\" outside the module instead." ) )] ModuleLevelNotDefined { @@ -177,7 +177,7 @@ impl Mqc { &self.dependencies } - /// Engine globals the program reads at run time (e.g. `--args` values). + /// Engine globals the program reads at run time (e.g. `--arg` values). pub fn external_globals(&self) -> &[String] { &self.meta.external_globals } diff --git a/crates/mq-lang/src/mqc/tests.rs b/crates/mq-lang/src/mqc/tests.rs index ed8e3c27d..b917c4788 100644 --- a/crates/mq-lang/src/mqc/tests.rs +++ b/crates/mq-lang/src/mqc/tests.rs @@ -777,7 +777,7 @@ fn constant_expr() -> impl Strategy { ] } -/// Names that only exist at run time, such as `--args` values. +/// Names that only exist at run time, such as `--arg` values. fn runtime_name() -> impl Strategy { "rt_[a-z]{1,8}" } diff --git a/crates/mq-run/src/cli.rs b/crates/mq-run/src/cli.rs index 2b4bae0f6..e285bbc2f 100644 --- a/crates/mq-run/src/cli.rs +++ b/crates/mq-run/src/cli.rs @@ -102,10 +102,6 @@ pub struct Cli { #[clap(subcommand)] commands: Option, - /// List all available subcommands (built-in and external) - #[arg(long)] - list: bool, - /// Number of files to process before switching to parallel processing #[arg(short = 'P', long, default_value_t = 10)] parallel_threshold: usize, @@ -115,8 +111,8 @@ pub struct Cli { files: Option>, /// Positional string arguments, available as ARGS."positional" in queries. - #[arg(long = "argv", num_args = 0..)] - argv: Option>, + #[arg(long = "args", num_args = 0..)] + args: Option>, /// Maximum time in seconds allowed for query evaluation before aborting (e.g. 0.5, 5). /// Must be a finite positive number. No timeout by default. @@ -608,9 +604,9 @@ struct InputArgs { from_file: bool, /// Sets a named string argument. NAME is accessible directly in queries, and also - /// via ARGS."named" when --args or --argv is given. - #[arg(long, num_args = 2, value_names = ["NAME", "VALUE"], aliases = ["arg", "define"])] - args: Option>, + /// via ARGS."named" when --arg or --args is given. + #[arg(long, num_args = 2, value_names = ["NAME", "VALUE"], alias = "define")] + arg: Option>, /// Sets a named JSON argument. NAME is accessible directly in queries #[arg(long, num_args = 2, value_names = ["NAME", "JSON_VALUE"])] @@ -785,9 +781,9 @@ struct OutputArgs { #[arg(short = 'e', long = "exit-status", default_value_t = false)] exit_status: bool, - /// Output only the count of matching (non-None) results. Mirrors grep -c. + /// Output only the count of matching (non-None) results. /// With multiple files, prints "filename: N" per file and "total: N" at the end. - #[arg(short = 'c', long = "count", default_value_t = false, conflicts_with_all = ["update", "stream", "quiet"])] + #[arg(long = "count", default_value_t = false, conflicts_with_all = ["update", "stream", "quiet"])] count: bool, /// Skip the first N matching results before outputting. @@ -805,7 +801,7 @@ struct OutputArgs { no_position: bool, /// Print JSON on a single line, without pretty-printing. Only valid with -F json. - #[arg(long, default_value_t = false, conflicts_with = "quiet")] + #[arg(short = 'c', long, default_value_t = false, conflicts_with = "quiet")] compact: bool, /// Number of spaces per indent level in pretty-printed output (0-7), jq's @@ -877,6 +873,13 @@ enum Commands { /// Start a debug adapter for mq #[cfg(feature = "debugger")] Dap, + /// List built-in and external subcommands + #[command(name = "commands")] + Subcommands { + /// Print machine-readable JSON instead of formatted text + #[arg(long)] + json: bool, + }, /// Generate a shell completion script and print it to stdout Completion { /// Shell to generate the completion script for @@ -952,6 +955,17 @@ impl Cli { } } + /// Resolve `mq-NAME` in `~/.local/bin` first, then `PATH`. + fn external_command_path(name: &str) -> Option { + let command_bin = format!("mq-{}", name); + let local_path = Self::get_external_commands_dir().unwrap_or_default().join(&command_bin); + if local_path.exists() { + Some(local_path) + } else { + which(&command_bin).ok() + } + } + /// Find all external commands (mq-* files in ~/.local/bin and PATH) fn find_external_commands() -> Vec { let mut seen = std::collections::HashSet::new(); @@ -1077,36 +1091,62 @@ impl Cli { Ok(()) } - /// List all available subcommands (built-in and external) - fn list_commands(&self) -> miette::Result<()> { - let mut output = vec![ - format!("{}", "Built-in subcommands:".bold().cyan()), - format!( - " {} - Start a REPL session for interactive query execution", - "repl".green() - ), - format!( - " {} - Generate a shell completion script and print it to stdout", - "completion".green() - ), - format!( - " {} - Compile a query to .mqc bytecode (experimental)", - "compile".green() - ), - ]; + /// Print built-in subcommands (from the clap definition) and external `mq-*` commands. + fn run_commands(json: bool) -> miette::Result<()> { + let builtin: Vec<(String, String)> = Self::command() + .get_subcommands() + .filter(|command| !command.is_hide_set()) + .map(|command| { + let about = command.get_about().map(|about| about.to_string()).unwrap_or_default(); + let summary = about.split_once(". ").map_or(about.as_str(), |(first, _)| first); + ( + command.get_name().to_string(), + summary.trim_end_matches('.').to_string(), + ) + }) + .collect(); + let external: Vec<(String, Option)> = Self::find_external_commands() + .into_iter() + .map(|name| { + let path = Self::external_command_path(&name); + (name, path) + }) + .collect(); - #[cfg(feature = "debugger")] - output.push(format!(" {} - Start a debug adapter for mq", "dap".green())); + if json { + let value = serde_json::json!({ + "builtin": builtin + .iter() + .map(|(name, description)| serde_json::json!({ "name": name, "description": description })) + .collect::>(), + "external": external + .iter() + .map(|(name, path)| serde_json::json!({ "name": name, "path": path })) + .collect::>(), + }); + println!("{}", serde_json::to_string_pretty(&value).into_diagnostic()?); + return Ok(()); + } - let external_commands = Self::find_external_commands(); - if !external_commands.is_empty() { - output.push("".to_string()); + let width = builtin + .iter() + .map(|(name, _)| name.len()) + .chain(external.iter().map(|(name, _)| name.len())) + .max() + .unwrap_or(0); + let mut output = vec![format!("{}", "Built-in subcommands:".bold().cyan())]; + for (name, description) in &builtin { + output.push(format!(" {: {{\"name\": \"Alice\"}}\n"); let _ = writeln!( out, - " mq -I null 'ARGS | .\"positional\"' --argv x y z # must come after query and files" + " mq -I null 'ARGS | .\"positional\"' --args x y z # must come after query and files" ); let _ = writeln!(out, " # => [\"x\", \"y\", \"z\"]\n"); - let _ = writeln!(out, " mq -I null 'ARGS' file.md --args name Alice --argv x y z"); + let _ = writeln!(out, " mq -I null 'ARGS' file.md --arg name Alice --args x y z"); let _ = writeln!( out, " # => {{\"positional\": [\"x\",\"y\",\"z\"], \"named\": {{\"name\": \"Alice\"}}}}" @@ -1582,15 +1622,15 @@ impl Cli { let _ = writeln!(out, "\n## Passing arguments to queries (ARGS)\n"); let _ = writeln!( out, - "When `--args` or `--argv` is given, `ARGS = {{\"positional\": [...], \"named\": {{...}}}}`\n" + "When `--arg` or `--args` is given, `ARGS = {{\"positional\": [...], \"named\": {{...}}}}`\n" ); let _ = writeln!( out, - "```sh\nmq -I null 'name' --args name Alice\nmq -I null 'ARGS | .\"named\"' --args name Alice\n\ + "```sh\nmq -I null 'name' --arg name Alice\nmq -I null 'ARGS | .\"named\"' --arg name Alice\n\ # => {{\"name\": \"Alice\"}}\n\n\ - mq -I null 'ARGS | .\"positional\"' --argv x y z # must come after query and files\n\ + mq -I null 'ARGS | .\"positional\"' --args x y z # must come after query and files\n\ # => [\"x\", \"y\", \"z\"]\n\n\ - mq -I null 'ARGS' file.md --args name Alice --argv x y z\n\ + mq -I null 'ARGS' file.md --arg name Alice --args x y z\n\ # => {{\"positional\": [\"x\",\"y\",\"z\"], \"named\": {{\"name\": \"Alice\"}}}}\n```" ); @@ -1728,10 +1768,84 @@ impl Cli { } pub fn run(&self) -> miette::Result<()> { - if self.list { - return self.list_commands(); + if let Some(result) = self.run_standalone_command() { + return result; } + self.load_bytecode()?; + + if let Some(result) = self.run_external_command() { + return result; + } + + self.validate_csv_options()?; + self.validate_frontmatter_option()?; + + match &self.commands { + Some(Commands::Repl { files }) => return self.run_repl(files.as_deref()), + None if self.query.is_none() => return self.run_repl(None), + _ => {} + } + + self.validate_output_options()?; + self.run_query() + } + + /// Runs the subcommands that take no query or input document, so none of the query + /// input/output flag validation applies to them. + fn run_standalone_command(&self) -> Option> { + match self.commands.as_ref()? { + Commands::Subcommands { json } => Some(Self::run_commands(*json)), + Commands::Completion { shell } => Some(Self::generate_completion(shell)), + Commands::Help { name, json, markdown } => Some(Self::run_help(name.as_deref(), *json, *markdown)), + Commands::Compile { + query, + from_file, + output, + program, + } => { + if self.format.is_some() { + return Some(Err(miette!( + "-T/--format has no effect on `mq compile`; pass -I/--input-format after `compile` instead" + ))); + } + if self.input.program != ProgramArgs::default() || self.input.from_file { + return Some(Err(miette!( + "Pass compile options after `compile`: mq compile [OPTIONS] QUERY" + ))); + } + Some(Self::compile_bytecode(query, *from_file, output.as_deref(), program)) + } + #[cfg(feature = "debugger")] + Commands::Dap => Some(mq_dap::start().map_err(|e| miette!(e.to_string()))), + Commands::Repl { .. } => None, + } + } + + /// Runs `mq-NAME` when the query is the bare name of an external subcommand. + /// + /// clap parses `mq test arg1` as query="test", files=["arg1"], so this is detected here. + fn run_external_command(&self) -> Option> { + if self.input.from_file || self.commands.is_some() || self.bytecode.get().is_some() { + return None; + } + let query_value = self.query.as_ref()?; + if !query_value + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') + { + return None; + } + let command_path = Self::external_command_path(query_value)?; + let mut args = vec![query_value.clone()]; + if let Some(files) = &self.files { + args.extend(files.iter().map(|p| p.to_string_lossy().to_string())); + } + Some(self.execute_external_command(command_path, &args)) + } + + /// Checks the output flags against the resolved output format. + fn validate_output_options(&self) -> miette::Result<()> { if (self.output.before_context.is_some() || self.output.after_context.is_some() || self.output.context.is_some()) @@ -1742,60 +1856,6 @@ impl Cli { )); } - if let Some(Commands::Compile { - query, - from_file, - output, - program, - }) = &self.commands - { - if self.format.is_some() { - return Err(miette!( - "-T/--format has no effect on `mq compile`; pass -I/--input-format after `compile` instead" - )); - } - if self.input.program != ProgramArgs::default() || self.input.from_file { - return Err(miette!( - "Pass compile options after `compile`: mq compile [OPTIONS] QUERY" - )); - } - return Self::compile_bytecode(query, *from_file, output.as_deref(), program); - } - self.load_bytecode()?; - - // Check if query is actually an external subcommand - // This handles the case where clap parses "mq test arg1" as query="test", files=["arg1"] - if !self.input.from_file - && self.commands.is_none() - && self.bytecode.get().is_none() - && let Some(query_value) = &self.query - { - // Only treat as external command if query_value is a valid file name - if query_value - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') - { - let command_path = { - let command_bin = format!("mq-{}", query_value); - let command_path = Self::get_external_commands_dir().unwrap_or_default().join(&command_bin); - - if !command_path.exists() { - which(&command_bin).ok() - } else { - Some(command_path) - } - }; - - if let Some(command_path) = command_path { - let mut args = vec![query_value.clone()]; - if let Some(files) = &self.files { - args.extend(files.iter().map(|p| p.to_string_lossy().to_string())); - } - return self.execute_external_command(command_path, &args); - } - } - } - if !matches!(self.explicit_input_format(), Some(InputFormat::Markdown) | None) && self.output.update { return Err(miette!("The output format is not supported for the update option")); } @@ -1814,64 +1874,57 @@ impl Cli { return Err(miette!("--indent/--tab are only valid with -F json or -F xml")); } - self.validate_csv_options()?; - self.validate_frontmatter_option()?; - self.check_output_permitted()?; + self.check_output_permitted() + } - match &self.commands { - Some(Commands::Repl { files }) => { - let engine = self.create_engine()?; - let input = match files { - Some(files) if !files.is_empty() => { - let files = Self::expand_glob_patterns(files)?; - if files.iter().any(|f| Self::is_stdin_path(f)) { - return Err(miette!("`-` (stdin) is not supported by the repl command")); - } - let contents = self.read_files_content(&files)?; - let mut combined = Vec::new(); - for (file, content) in &contents { - combined.extend(self.resolve_input(file, content)?); - } - combined - } - _ => vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))], - }; - Self::new_repl(engine, input).run() + fn run_repl(&self, files: Option<&[PathBuf]>) -> miette::Result<()> { + let engine = self.create_engine()?; + let input = match files { + Some(files) if !files.is_empty() => { + let files = Self::expand_glob_patterns(files)?; + if files.iter().any(|f| Self::is_stdin_path(f)) { + return Err(miette!("`-` (stdin) is not supported by the repl command")); + } + let contents = self.read_files_content(&files)?; + let mut combined = Vec::new(); + for (file, content) in &contents { + combined.extend(self.resolve_input(file, content)?); + } + combined } - None if self.query.is_none() => { - let engine = self.create_engine()?; - Self::new_repl(engine, vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))]).run() + _ => vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))], + }; + Self::new_repl(engine, input).run() + } + + fn run_query(&self) -> miette::Result<()> { + #[cfg(feature = "watch")] + { + if self.input.watch { + return self.run_watch(); } - #[cfg(feature = "debugger")] - Some(Commands::Dap) => mq_dap::start().map_err(|e| miette!(e.to_string())), - Some(Commands::Completion { shell }) => Self::generate_completion(shell), - Some(Commands::Help { name, json, markdown }) => Self::run_help(name.as_deref(), *json, *markdown), - Some(Commands::Compile { .. }) => unreachable!("handled before input validation"), - #[cfg(feature = "watch")] - None if self.input.watch => self.run_watch(), - None => { - let result = self.execute_once(); - - // --exit-status / -e: exit with code 1 if no truthy value was - // produced. Mirrors jq's behaviour: false and null are falsy; - // everything else (including empty string, 0, [], {}) is truthy. - if self.output.exit_status { - let had_truthy = HAD_TRUTHY_OUTPUT.load(Ordering::Relaxed); - if !had_truthy { - result?; - std::process::exit(1); - } - } + } - // --diff: exit 1 if anything would change, for CI gating. - if self.output.diff && HAD_DIFF.load(Ordering::Relaxed) { - result?; - std::process::exit(1); - } + let result = self.execute_once(); - result + // --exit-status / -e: exit with code 1 if no truthy value was + // produced. Mirrors jq's behaviour: false and null are falsy; + // everything else (including empty string, 0, [], {}) is truthy. + if self.output.exit_status { + let had_truthy = HAD_TRUTHY_OUTPUT.load(Ordering::Relaxed); + if !had_truthy { + result?; + std::process::exit(1); } } + + // --diff: exit 1 if anything would change, for CI gating. + if self.output.diff && HAD_DIFF.load(Ordering::Relaxed) { + result?; + std::process::exit(1); + } + + result } fn create_engine(&self) -> miette::Result { @@ -1879,7 +1932,7 @@ impl Cli { let mut engine = mq_lang::DefaultEngine::default(); engine.set_io(Shared::new(sandboxed_io)); engine.load_builtin_module(); - // Empty defaults for stdin; `--args` etc. below may override them. + // Empty defaults for stdin; `--arg` etc. below may override them. Self::define_file_vars(&engine, None); if self.input.program.aggregate { @@ -1902,13 +1955,13 @@ impl Cli { } } - if self.input.args.is_some() - || self.argv.is_some() + if self.input.arg.is_some() + || self.args.is_some() || self.input.argjson.is_some() || self.input.slurp_file.is_some() { let mut named: DictMap = DictMap::default(); - if let Some(args) = &self.input.args { + if let Some(args) = &self.input.arg { for v in args.chunks(2) { engine.define_string_value(&v[0], &v[1]); named.insert( @@ -1949,7 +2002,7 @@ impl Cli { } let positional: Vec = self - .argv + .args .as_deref() .unwrap_or(&[]) .iter() @@ -4032,6 +4085,57 @@ mod tests { assert_eq!(cli.error_format, expected); } + #[rstest] + #[case::short(&["mq", "-c", "-F", "json", "self"])] + #[case::long(&["mq", "--compact", "-F", "json", "self"])] + fn test_compact_flag(#[case] args: &[&str]) { + let cli = Cli::try_parse_from(args).unwrap(); + assert!(cli.output.compact); + assert!(!cli.output.count); + } + + #[test] + fn test_count_has_no_short_flag() { + assert!(Cli::try_parse_from(["mq", "--count", "self"]).unwrap().output.count); + } + + #[test] + fn test_arg_and_args_flags() { + let cli = Cli::try_parse_from(["mq", "--arg", "name", "Alice", "self", "--args", "x", "y"]).unwrap(); + assert_eq!(cli.input.arg, Some(vec!["name".to_string(), "Alice".to_string()])); + assert_eq!(cli.args, Some(vec!["x".to_string(), "y".to_string()])); + } + + #[test] + fn test_argv_flag_is_removed() { + assert!(Cli::try_parse_from(["mq", "self", "--argv", "x"]).is_err()); + } + + #[test] + fn test_actions_are_subcommands_not_flags() { + assert!(matches!( + Cli::try_parse_from(["mq", "commands"]).unwrap().commands, + Some(Commands::Subcommands { json: false }) + )); + assert!(Cli::try_parse_from(["mq", "list"]).unwrap().commands.is_none()); + assert!(Cli::try_parse_from(["mq", "--list"]).is_err()); + } + + #[cfg(feature = "http-import")] + #[test] + fn test_clear_cache_flag() { + let cli = Cli::try_parse_from(["mq", "--clear-cache", "self"]).unwrap(); + assert!(cli.input.program.clear_cache); + } + + #[rstest] + #[case::completion(&["mq", "--compact", "completion", "bash"])] + #[case::help(&["mq", "--compact", "help", "map"])] + #[case::commands(&["mq", "--compact", "commands"])] + fn test_standalone_commands_ignore_output_flags(#[case] args: &[&str]) { + assert!(Cli::try_parse_from(args).unwrap().run().is_ok()); + } + #[rstest] #[case::without_stack_trace(false)] #[case::with_stack_trace(true)] @@ -5502,9 +5606,9 @@ mod tests { } #[test] - fn test_list_commands() { + fn test_run_commands() { let cli = Cli { - list: true, + commands: Some(Commands::Subcommands { json: false }), ..Cli::default() }; @@ -6588,8 +6692,8 @@ mod tests { )] fn test_args_and_data( #[case] suffix: &str, + #[case] arg: Option>, #[case] args: Option>, - #[case] argv: Option>, #[case] query: &str, #[case] expected: &str, ) { @@ -6608,7 +6712,7 @@ mod tests { input_format: Some(InputFormat::Null), ..Default::default() }, - args, + arg, ..Default::default() }, output: OutputArgs { @@ -6617,7 +6721,7 @@ mod tests { ..Default::default() }, query: Some(query.to_string()), - argv, + args, ..Cli::default() }; @@ -6740,7 +6844,7 @@ mod tests { input_format: Some(InputFormat::Null), ..Default::default() }, - args: Some(vec!["name".to_string(), "Alice".to_string()]), + arg: Some(vec!["name".to_string(), "Alice".to_string()]), argjson: Some(vec!["count".to_string(), "42".to_string()]), ..Default::default() }, @@ -6750,7 +6854,7 @@ mod tests { ..Default::default() }, query: Some(r#"ARGS | ."named""#.to_string()), - argv: Some(vec!["x".to_string()]), + args: Some(vec!["x".to_string()]), ..Cli::default() }; @@ -6922,7 +7026,7 @@ mod tests { fn test_set_file_vars_keeps_user_global_for_stdin() { let cli = Cli { input: InputArgs { - args: Some(vec!["__FILE__".to_string(), "custom".to_string()]), + arg: Some(vec!["__FILE__".to_string(), "custom".to_string()]), ..Default::default() }, ..Cli::default() @@ -6951,7 +7055,7 @@ mod tests { input_format: Some(InputFormat::Null), ..Default::default() }, - args: Some(vec!["name".to_string(), "Alice".to_string()]), + arg: Some(vec!["name".to_string(), "Alice".to_string()]), argjson: Some(vec!["count".to_string(), "42".to_string()]), slurp_file: Some(vec!["data".to_string(), data_file.to_string_lossy().to_string()]), ..Default::default() @@ -6991,7 +7095,7 @@ mod tests { }, query: Some("self".to_string()), files: Some(vec![input_file]), - argv: None, + args: None, ..Cli::default() }; @@ -7029,7 +7133,7 @@ mod tests { }, query: Some("self".to_string()), files: Some(vec![input.clone()]), - argv: None, + args: None, ..Cli::default() }; assert!(cli.run().is_ok()); @@ -7043,10 +7147,10 @@ mod tests { output: OutputArgs::default(), query: Some("self".to_string()), files: Some(vec![file1, file2]), - argv: None, + args: None, ..Cli::default() }; - assert!(cli.run().is_ok(), "multi-file run without --argv should succeed"); + assert!(cli.run().is_ok(), "multi-file run without --args should succeed"); } #[test] @@ -7081,7 +7185,7 @@ mod tests { }, query: Some(".h | len".to_string()), files: Some(vec![pattern]), - argv: None, + args: None, ..Cli::default() }; @@ -7123,7 +7227,7 @@ mod tests { }, query: Some(".h | len".to_string()), files: Some(vec![pattern1, pattern2]), - argv: None, + args: None, ..Cli::default() }; @@ -7146,7 +7250,7 @@ mod tests { output: OutputArgs::default(), query: Some("self".to_string()), files: Some(vec![PathBuf::from("/nonexistent/mq_glob_no_match_*.md")]), - argv: None, + args: None, ..Cli::default() }; @@ -7158,7 +7262,7 @@ mod tests { #[test] fn test_files_with_data_does_not_mix() { - // --argv values must not be treated as files, and files must not appear in ARGS + // --args values must not be treated as files, and files must not appear in ARGS let (_, input_file) = create_file("test_files_with_data.md", "# content"); let (_, output_file) = create_file("test_files_with_data_out.md", ""); let input_file_clone = input_file.clone(); @@ -7178,7 +7282,7 @@ mod tests { }, query: Some("ARGS".to_string()), files: Some(vec![input_file]), - argv: Some(vec!["alpha".to_string(), "beta".to_string()]), + args: Some(vec!["alpha".to_string(), "beta".to_string()]), ..Cli::default() }; @@ -7191,7 +7295,7 @@ mod tests { #[test] fn test_files_without_data_args_undefined() { - // Without --argv or --args, ARGS must be undefined (runtime error expected) + // Without --arg or --args, ARGS must be undefined (runtime error expected) let (_, input_file) = create_file("test_files_no_args_undefined.md", "# x"); let input_file_clone = input_file.clone(); @@ -7204,19 +7308,19 @@ mod tests { output: OutputArgs::default(), query: Some("ARGS".to_string()), files: Some(vec![input_file]), - argv: None, + args: None, ..Cli::default() }; assert!( cli.run().is_err(), - "ARGS should be undefined when neither --args nor --argv is given" + "ARGS should be undefined when neither --arg nor --args is given" ); } #[test] fn test_files_with_data_file_content_processed() { - // File content is still processed even when --argv is given + // File content is still processed even when --args is given let (_, input_file) = create_file("test_files_data_content.md", "# heading"); let (_, output_file) = create_file("test_files_data_content_out.md", ""); let input_file_clone = input_file.clone(); @@ -7236,7 +7340,7 @@ mod tests { }, query: Some("self".to_string()), files: Some(vec![input_file]), - argv: Some(vec!["x".to_string()]), + args: Some(vec!["x".to_string()]), ..Cli::default() }; @@ -7244,7 +7348,7 @@ mod tests { let result = fs::read_to_string(&output_file).expect("Failed to read output"); assert!( result.contains("heading"), - "file content should still be processed when --argv is given" + "file content should still be processed when --args is given" ); } @@ -7256,13 +7360,13 @@ mod tests { input_format: Some(InputFormat::Null), ..Default::default() }, - args: Some(vec!["name".to_string(), "Alice".to_string()]), + arg: Some(vec!["name".to_string(), "Alice".to_string()]), ..Default::default() }, query: Some("name".to_string()), ..Cli::default() }; - assert!(cli.run().is_ok(), "--args with a valid NAME VALUE pair should succeed"); + assert!(cli.run().is_ok(), "--arg with a valid NAME VALUE pair should succeed"); } #[cfg(feature = "watch")] diff --git a/crates/mq-run/src/cli/mqc.rs b/crates/mq-run/src/cli/mqc.rs index 7dd7ba4b0..911177897 100644 --- a/crates/mq-run/src/cli/mqc.rs +++ b/crates/mq-run/src/cli/mqc.rs @@ -163,7 +163,7 @@ impl Cli { /// builtin would not shadow it as it does when running the query from source. fn reject_builtin_named_globals(&self) -> miette::Result<()> { let options = [ - ("--args", &self.input.args), + ("--arg", &self.input.arg), ("--argjson", &self.input.argjson), ("--rawfile", &self.input.raw_file), ("--slurpfile", &self.input.slurp_file), diff --git a/crates/mq-run/tests/batch_modes_tests.rs b/crates/mq-run/tests/batch_modes_tests.rs index 9be4fc891..4ad915ae7 100644 --- a/crates/mq-run/tests/batch_modes_tests.rs +++ b/crates/mq-run/tests/batch_modes_tests.rs @@ -100,7 +100,7 @@ fn test_count_matches_single_file_counts(#[case] query: &str, #[case] with_csv: }; let counts: Vec = files .iter() - .map(|file| stdout(dir.path(), &["-c", query], &[file]).trim().parse().unwrap()) + .map(|file| stdout(dir.path(), &["--count", query], &[file]).trim().parse().unwrap()) .collect(); let mut expected: String = files .iter() @@ -109,7 +109,7 @@ fn test_count_matches_single_file_counts(#[case] query: &str, #[case] with_csv: .collect(); expected.push_str(&format!("total: {}\n", counts.iter().sum::())); - assert_eq!(stdout(dir.path(), &["-c", query], &files), expected); + assert_eq!(stdout(dir.path(), &["--count", query], &files), expected); } #[rstest] @@ -148,7 +148,7 @@ fn test_mqc_count_matches_single_file_counts() { .args(["compile", ".h", "-o", "query.mqc"]) .assert() .success(); - let output = stdout(dir.path(), &["-c", "query.mqc"], MARKDOWN); + let output = stdout(dir.path(), &["--count", "query.mqc"], MARKDOWN); let mut expected: String = MARKDOWN.iter().map(|file| format!("{file}: 2\n")).collect(); expected.push_str(&format!("total: {}\n", MARKDOWN.len() * 2)); assert_eq!(output, expected); @@ -156,7 +156,7 @@ fn test_mqc_count_matches_single_file_counts() { #[rstest] fn test_invalid_query_fails_in_every_mode( - #[values(&["-P", "1000"], &["-P", "0"], &["-c"], &["-S", r#""---""#])] mode: &[&str], + #[values(&["-P", "1000"], &["-P", "0"], &["--count"], &["-S", r#""---""#])] mode: &[&str], ) { let dir = fixture(); let assert = mq(dir.path()) diff --git a/crates/mq-run/tests/integration_tests.rs b/crates/mq-run/tests/integration_tests.rs index a58da0958..ea7d8c81a 100644 --- a/crates/mq-run/tests/integration_tests.rs +++ b/crates/mq-run/tests/integration_tests.rs @@ -200,7 +200,7 @@ fn test_compact_rejected_without_json_format() { "#) )] #[case::args( - vec!["--unbuffered", "--args", "val1", "test", "select(contains(val1))"], + vec!["--unbuffered", "--arg", "val1", "test", "select(contains(val1))"], "# **title**\n\n- test1\n- test2", Some("- test1\n- test2\n") )] @@ -1597,10 +1597,10 @@ fn test_count_stdin( } #[test] -fn test_count_short_flag() -> Result<(), Box> { +fn test_count_flag() -> Result<(), Box> { let mut cmd = cargo::cargo_bin_cmd!("mq"); cmd.arg("--unbuffered") - .arg("-c") + .arg("--count") .arg(".h2") .write_stdin("# h1\n\n## h2\n\n## h2b\n") .assert() diff --git a/crates/mq-run/tests/mqc_tests.rs b/crates/mq-run/tests/mqc_tests.rs index 00d1ddae8..8e3fff462 100644 --- a/crates/mq-run/tests/mqc_tests.rs +++ b/crates/mq-run/tests/mqc_tests.rs @@ -142,17 +142,17 @@ fn test_run_reads_args_at_run_time() { compile(dir.path(), r#"s"hello ${name}""#, &[]); let output = stdout( mq(dir.path()) - .args(["query.mqc", "-I", "null", "--args", "name", "mq"]) + .args(["query.mqc", "-I", "null", "--arg", "name", "mq"]) .assert(), ); assert_eq!(output.trim(), "hello mq"); } #[rstest] -#[case::args(&["--args", "len", "5"], "--args len")] +#[case::args(&["--arg", "len", "5"], "--arg len")] #[case::argjson(&["--argjson", "upcase", "1"], "--argjson upcase")] -#[case::coroutine_builtin(&["--args", "next", "x"], "--args next")] -#[case::later_pair(&["--args", "name", "mq", "--args", "len", "5"], "--args len")] +#[case::coroutine_builtin(&["--arg", "next", "x"], "--arg next")] +#[case::later_pair(&["--arg", "name", "mq", "--arg", "len", "5"], "--arg len")] fn test_run_rejects_globals_named_after_builtins(#[case] flags: &[&str], #[case] expected: &str) { let dir = TempDir::new().unwrap(); compile(dir.path(), "len", &[]); @@ -510,7 +510,7 @@ fn test_run_reads_runtime_names_outside_module(#[case] query: &str) { for value in ["hello", "world"] { let output = stdout( mq(dir.path()) - .args(["query.mqc", "-I", "null", "--args", "arg", value]) + .args(["query.mqc", "-I", "null", "--arg", "arg", value]) .assert(), ); assert_eq!(output.trim(), value); diff --git a/docs/books/src/reference/modules_and_imports.md b/docs/books/src/reference/modules_and_imports.md index d7fcdcaf6..937e20e74 100644 --- a/docs/books/src/reference/modules_and_imports.md +++ b/docs/books/src/reference/modules_and_imports.md @@ -42,7 +42,7 @@ A module, inline or imported, sees only: - Built-in functions. - Modules it imports itself, and inline modules defined earlier (by qualified name). -It does not see the query's own `let`, `var`, or `def` bindings, modules imported outside it, or runtime values such as `__FILE__` and `--args`. Referring to them is an error. +It does not see the query's own `let`, `var`, or `def` bindings, modules imported outside it, or runtime values such as `__FILE__` and `--arg`. Referring to them is an error. ```mq module csv_tools: diff --git a/docs/books/src/start/compiled-programs.md b/docs/books/src/start/compiled-programs.md index acfe250b2..f9b675cdf 100644 --- a/docs/books/src/start/compiled-programs.md +++ b/docs/books/src/start/compiled-programs.md @@ -37,7 +37,7 @@ Module-level `let` values are computed once, when the program is compiled. ## What is read at run time -- Values from `--args`, `--argjson`, `--rawfile`, `--slurpfile`, and `__FILE__`, outside modules. A module can't read them. A name that matches a builtin function, such as `--args len 5`, is rejected: the program resolved that name to the builtin when it was compiled. +- Values from `--arg`, `--argjson`, `--rawfile`, `--slurpfile`, and `__FILE__`, outside modules. A module can't read them. A name that matches a builtin function, such as `--args len 5`, is rejected: the program resolved that name to the builtin when it was compiled. - Permissions such as `--allow-read` and `--allow-net`. A compiled program gets no permission that the running `mq` does not grant. Permissions passed to `mq compile` apply only while module-level `let` values are computed, and are not saved. - Environment variables read with interpolation, such as `s"${$HOME}"` (with `--allow-env`). `mq compile` rejects a bare `$VAR`, and any environment read in a module-level `let`, because the value would be saved in the file. diff --git a/docs/books/src/start/external_subcommands.md b/docs/books/src/start/external_subcommands.md index 527a4b707..18fdf9a03 100644 --- a/docs/books/src/start/external_subcommands.md +++ b/docs/books/src/start/external_subcommands.md @@ -13,10 +13,10 @@ The first match found is used. ## Listing Available Subcommands -Use the `--list` flag to see all available subcommands: +Use the `commands` subcommand to see all available subcommands (add `--json` for machine-readable output): ```sh -mq --list +mq commands ``` This makes it easy to add your own tools and workflows to `mq` without modifying the core binary. diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index 0d0787bea..adbc51e54 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -37,14 +37,14 @@ echo "Bumping version: ${CURRENT_VERSION} -> ${MQ_VERSION}" # Update the workspace Cargo.toml tmpfile=$(mktemp) -"$MQ" -I raw --args version $MQ_VERSION 'import "bump_version" | bump_version::crates_version(self, version)' ../Cargo.toml > "$tmpfile" && mv "$tmpfile" ../Cargo.toml +"$MQ" -I raw --arg version $MQ_VERSION 'import "bump_version" | bump_version::crates_version(self, version)' ../Cargo.toml > "$tmpfile" && mv "$tmpfile" ../Cargo.toml # Update package.json files for dir in ../packages ../editors; do for package in "$dir"/*; do if [ -f "$package/package.json" ]; then tmpfile=$(mktemp) - "$MQ" -I raw --args version $MQ_VERSION 'import "bump_version" | bump_version::npm_version(self, version)' "$package/package.json" > "$tmpfile" && mv "$tmpfile" "$package/package.json" + "$MQ" -I raw --arg version $MQ_VERSION 'import "bump_version" | bump_version::npm_version(self, version)' "$package/package.json" > "$tmpfile" && mv "$tmpfile" "$package/package.json" fi done done @@ -53,15 +53,15 @@ done JETBRAINS_GRADLE_PROPERTIES="../editors/jetbrains/gradle.properties" if [ -f "$JETBRAINS_GRADLE_PROPERTIES" ]; then tmpfile=$(mktemp) - "$MQ" -I text --args version $MQ_VERSION 'import "bump_version" | bump_version::gradle_version(version)' "$JETBRAINS_GRADLE_PROPERTIES" > "$tmpfile" && mv "$tmpfile" "$JETBRAINS_GRADLE_PROPERTIES" + "$MQ" -I text --arg version $MQ_VERSION 'import "bump_version" | bump_version::gradle_version(version)' "$JETBRAINS_GRADLE_PROPERTIES" > "$tmpfile" && mv "$tmpfile" "$JETBRAINS_GRADLE_PROPERTIES" fi # Update README.md with the new version -"$MQ" -U --args VERSION $MQ_VERSION 'import "bump_version" | bump_version::code_block_version(VERSION)' $README > README.md.tmp \ +"$MQ" -U --arg VERSION $MQ_VERSION 'import "bump_version" | bump_version::code_block_version(VERSION)' $README > README.md.tmp \ && mv README.md.tmp $README # Update INSTALL_DOC.md with the new version -"$MQ" -U --args VERSION $MQ_VERSION 'import "bump_version" | bump_version::code_block_version(VERSION)' $INSTALL_DOC > INSTALL_DOC.md.tmp \ +"$MQ" -U --arg VERSION $MQ_VERSION 'import "bump_version" | bump_version::code_block_version(VERSION)' $INSTALL_DOC > INSTALL_DOC.md.tmp \ && mv INSTALL_DOC.md.tmp $INSTALL_DOC echo "Done. Review with 'git diff', then:"