From 00013ba8b0ab90ae5e420175cfee88f91bb6001b Mon Sep 17 00:00:00 2001 From: harehare Date: Thu, 15 Oct 2026 23:14:32 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(run):=20align?= =?UTF-8?q?=20CLI=20flags=20with=20jq=20and=20move=20actions=20to=20subcom?= =?UTF-8?q?mands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `-c` is now the short form of `--compact` (jq compatible); `--count` is long-only - `--arg NAME VALUE` sets a named string argument, `--args VALUES...` sets positional arguments (replaces `--args NAME VALUE` and `--argv`) - `--list` is now `mq list` - `--clear-cache` is now `mq cache clear` BREAKING CHANGE: `-c` no longer means `--count`, `--args NAME VALUE` is now `--arg`, `--argv` is now `--args`, `--list` is now `mq list`, and `--clear-cache` is now `mq cache clear`. --- README.md | 2 +- crates/mq-lang/src/engine.rs | 2 +- crates/mq-lang/src/mqc.rs | 4 +- crates/mq-lang/src/mqc/tests.rs | 2 +- crates/mq-run/src/cli.rs | 190 ++++++++++++------ crates/mq-run/src/cli/mqc.rs | 2 +- crates/mq-run/tests/batch_modes_tests.rs | 8 +- crates/mq-run/tests/integration_tests.rs | 6 +- crates/mq-run/tests/mqc_tests.rs | 10 +- crates/mq-wasm/src/script.rs | 2 +- .../src/reference/modules_and_imports.md | 6 +- docs/books/src/start/compiled-programs.md | 2 +- docs/books/src/start/external_subcommands.md | 4 +- scripts/bump_version.sh | 10 +- 14 files changed, 159 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index 1f6d15a26..8d9d97f19 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 list` to see all available subcommands (built-in and external). ## External Subcommands diff --git a/crates/mq-lang/src/engine.rs b/crates/mq-lang/src/engine.rs index eaaf719f0..d95380351 100644 --- a/crates/mq-lang/src/engine.rs +++ b/crates/mq-lang/src/engine.rs @@ -591,7 +591,7 @@ impl Engine { /// Clears all HTTP module cache including versioned modules and lock files. /// - /// Use this when `--clear-cache` is passed on the CLI to wipe everything. + /// Use this for `mq cache clear` to wipe everything. pub fn clear_http_cache_all(&mut self) -> Result<(), crate::module::error::ModuleError> { let result = self.vm.module_loader.clear_http_cache_all(); #[cfg(not(feature = "debugger"))] 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..eaa5d97cf 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. @@ -504,12 +500,6 @@ struct ProgramArgs { #[arg(long = "refresh-modules", default_value_t = false)] refresh_modules: bool, - /// Remove all HTTP module cache including versioned (tagged) modules and lock files. - /// Use this to fully reset the cache when something goes wrong. - #[cfg(feature = "http-import")] - #[arg(long = "clear-cache", default_value_t = false)] - clear_cache: bool, - /// Disable the mq.lock integrity check for HTTP imports. /// By default a fetched URL's content is checked against mq.lock, and a mismatch is /// rejected unless --refresh-modules is also passed. @@ -608,9 +598,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 +775,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 +795,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 +867,14 @@ enum Commands { /// Start a debug adapter for mq #[cfg(feature = "debugger")] Dap, + /// List all available subcommands (built-in and external) + List, + /// Manage the HTTP module cache + #[cfg(feature = "http-import")] + Cache { + #[command(subcommand)] + action: CacheCommand, + }, /// Generate a shell completion script and print it to stdout Completion { /// Shell to generate the completion script for @@ -916,6 +914,15 @@ enum Commands { }, } +/// Actions of the `cache` subcommand. +#[cfg(feature = "http-import")] +#[derive(Debug, Subcommand)] +enum CacheCommand { + /// Remove all HTTP module cache including versioned (tagged) modules and lock files. + /// Use this to fully reset the cache when something goes wrong. + Clear, +} + /// Shell targets supported by the `completion` subcommand. #[derive(Clone, Debug, clap::ValueEnum)] enum CompletionShell { @@ -1093,8 +1100,15 @@ impl Cli { " {} - Compile a query to .mqc bytecode (experimental)", "compile".green() ), + format!( + " {} - List all available subcommands (built-in and external)", + "list".green() + ), ]; + #[cfg(feature = "http-import")] + output.push(format!(" {} - Manage the HTTP module cache", "cache".green())); + #[cfg(feature = "debugger")] output.push(format!(" {} - Start a debug adapter for mq", "dap".green())); @@ -1435,17 +1449,17 @@ impl Cli { let _ = writeln!(out, "\n{}", "Passing arguments to queries (ARGS):".bold().cyan()); 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, " mq -I null 'name' --args name Alice"); - let _ = writeln!(out, " mq -I null 'ARGS | .\"named\"' --args name Alice"); + let _ = writeln!(out, " mq -I null 'name' --arg name Alice"); + let _ = writeln!(out, " mq -I null 'ARGS | .\"named\"' --arg name Alice"); let _ = writeln!(out, " # => {{\"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 +1596,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,7 +1742,7 @@ impl Cli { } pub fn run(&self) -> miette::Result<()> { - if self.list { + if matches!(self.commands, Some(Commands::List)) { return self.list_commands(); } @@ -1844,6 +1858,14 @@ impl Cli { } #[cfg(feature = "debugger")] Some(Commands::Dap) => mq_dap::start().map_err(|e| miette!(e.to_string())), + Some(Commands::List) => unreachable!("handled before input validation"), + #[cfg(feature = "http-import")] + Some(Commands::Cache { + action: CacheCommand::Clear, + }) => { + let mut engine = self.create_engine()?; + engine.clear_http_cache_all().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"), @@ -1879,7 +1901,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 +1924,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 +1971,7 @@ impl Cli { } let positional: Vec = self - .argv + .args .as_deref() .unwrap_or(&[]) .iter() @@ -2001,9 +2023,7 @@ impl Cli { if let Some(path) = &self.input.program.lockfile_path { engine.set_lockfile_path(path.clone()); } - if self.input.program.clear_cache { - engine.clear_http_cache_all().map_err(|e| miette!(e.to_string()))?; - } else if self.input.program.refresh_modules { + if self.input.program.refresh_modules { engine.clear_http_cache().map_err(|e| miette!(e.to_string()))?; } } @@ -4032,6 +4052,54 @@ 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_list_and_cache_are_subcommands() { + assert!(matches!( + Cli::try_parse_from(["mq", "list"]).unwrap().commands, + Some(Commands::List) + )); + assert!(Cli::try_parse_from(["mq", "--list"]).is_err()); + assert!(Cli::try_parse_from(["mq", "--clear-cache"]).is_err()); + } + + #[cfg(feature = "http-import")] + #[test] + fn test_cache_clear_subcommand() { + let cli = Cli::try_parse_from(["mq", "cache", "clear"]).unwrap(); + assert!(matches!( + cli.commands, + Some(Commands::Cache { + action: CacheCommand::Clear + }) + )); + } + #[rstest] #[case::without_stack_trace(false)] #[case::with_stack_trace(true)] @@ -5504,7 +5572,7 @@ mod tests { #[test] fn test_list_commands() { let cli = Cli { - list: true, + commands: Some(Commands::List), ..Cli::default() }; @@ -6588,8 +6656,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 +6676,7 @@ mod tests { input_format: Some(InputFormat::Null), ..Default::default() }, - args, + arg, ..Default::default() }, output: OutputArgs { @@ -6617,7 +6685,7 @@ mod tests { ..Default::default() }, query: Some(query.to_string()), - argv, + args, ..Cli::default() }; @@ -6740,7 +6808,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 +6818,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 +6990,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 +7019,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 +7059,7 @@ mod tests { }, query: Some("self".to_string()), files: Some(vec![input_file]), - argv: None, + args: None, ..Cli::default() }; @@ -7029,7 +7097,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 +7111,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 +7149,7 @@ mod tests { }, query: Some(".h | len".to_string()), files: Some(vec![pattern]), - argv: None, + args: None, ..Cli::default() }; @@ -7123,7 +7191,7 @@ mod tests { }, query: Some(".h | len".to_string()), files: Some(vec![pattern1, pattern2]), - argv: None, + args: None, ..Cli::default() }; @@ -7146,7 +7214,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 +7226,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 +7246,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 +7259,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 +7272,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 +7304,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 +7312,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 +7324,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/crates/mq-wasm/src/script.rs b/crates/mq-wasm/src/script.rs index 20d785e73..e5ae5d77e 100644 --- a/crates/mq-wasm/src/script.rs +++ b/crates/mq-wasm/src/script.rs @@ -611,7 +611,7 @@ pub async fn clear_http_cache() -> Result<(), JsValue> { Ok(()) } -/// Removes all HTTP module cache and deletes `mq.lock` (matches `--clear-cache`). +/// Removes all HTTP module cache and deletes `mq.lock` (matches `mq cache clear`). #[wasm_bindgen(js_name=clearAllHttpCache)] pub async fn clear_all_http_cache() -> Result<(), JsValue> { #[cfg(feature = "opfs")] diff --git a/docs/books/src/reference/modules_and_imports.md b/docs/books/src/reference/modules_and_imports.md index d7fcdcaf6..9c32680da 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: @@ -426,8 +426,8 @@ Fetched modules are cached in `{system_cache_dir}/mq/` as `{md5(url)}.mq` files. - **Later use, content unchanged**: succeeds silently. - **Later use, content changed**: fails with an error explaining the mismatch. Re-run with `--refresh-modules` to accept the new content and update `mq.lock` (for a versioned/tagged - URL, whose cache `--refresh-modules` doesn't touch, use `--clear-cache` instead). -- `--refresh-modules` / `--clear-cache` also drop the corresponding entries from `mq.lock` + URL, whose cache `--refresh-modules` doesn't touch, use `mq cache clear` instead). +- `--refresh-modules` / `mq cache clear` also drop the corresponding entries from `mq.lock` (mutable-ref entries only, and all entries respectively), matching their disk-cache behavior. - Pass `--no-lockfile` to disable the check entirely (no file is read or written). - Pass `--lockfile ` to use a different location instead of `./mq.lock`. Missing parent 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..09a79a9c2 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 `list` subcommand to see all available subcommands: ```sh -mq --list +mq list ``` 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:" From 9ba7596840e8701a597796b68f6414bbd2b1d74c Mon Sep 17 00:00:00 2001 From: harehare Date: Thu, 15 Oct 2026 23:38:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(run):=20renam?= =?UTF-8?q?e=20`list`=20to=20`commands`,=20restore=20`--clear-cache`,=20un?= =?UTF-8?q?ify=20subcommand=20dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `mq list` is now `mq commands`. Descriptions come from the clap definition, external subcommands show their resolved path, and `--json` is added - `mq cache clear` is removed and `--clear-cache` is restored, so the cache is cleared at run time before the query executes - `run()` handles subcommands that take no query input (`commands`, `completion`, `help`, `compile`, `dap`) first, so query output flag validation no longer applies to them. `repl` skips output flag validation too BREAKING CHANGE: `mq list` is now `mq commands`, and `mq cache clear` is `--clear-cache` again. --- README.md | 2 +- crates/mq-lang/src/engine.rs | 2 +- crates/mq-run/src/cli.rs | 390 ++++++++++-------- crates/mq-wasm/src/script.rs | 2 +- .../src/reference/modules_and_imports.md | 4 +- docs/books/src/start/external_subcommands.md | 4 +- 6 files changed, 220 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index 8d9d97f19..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/engine.rs b/crates/mq-lang/src/engine.rs index d95380351..eaaf719f0 100644 --- a/crates/mq-lang/src/engine.rs +++ b/crates/mq-lang/src/engine.rs @@ -591,7 +591,7 @@ impl Engine { /// Clears all HTTP module cache including versioned modules and lock files. /// - /// Use this for `mq cache clear` to wipe everything. + /// Use this when `--clear-cache` is passed on the CLI to wipe everything. pub fn clear_http_cache_all(&mut self) -> Result<(), crate::module::error::ModuleError> { let result = self.vm.module_loader.clear_http_cache_all(); #[cfg(not(feature = "debugger"))] diff --git a/crates/mq-run/src/cli.rs b/crates/mq-run/src/cli.rs index eaa5d97cf..e285bbc2f 100644 --- a/crates/mq-run/src/cli.rs +++ b/crates/mq-run/src/cli.rs @@ -500,6 +500,12 @@ struct ProgramArgs { #[arg(long = "refresh-modules", default_value_t = false)] refresh_modules: bool, + /// Remove all HTTP module cache including versioned (tagged) modules and lock files. + /// Use this to fully reset the cache when something goes wrong. + #[cfg(feature = "http-import")] + #[arg(long = "clear-cache", default_value_t = false)] + clear_cache: bool, + /// Disable the mq.lock integrity check for HTTP imports. /// By default a fetched URL's content is checked against mq.lock, and a mismatch is /// rejected unless --refresh-modules is also passed. @@ -867,13 +873,12 @@ enum Commands { /// Start a debug adapter for mq #[cfg(feature = "debugger")] Dap, - /// List all available subcommands (built-in and external) - List, - /// Manage the HTTP module cache - #[cfg(feature = "http-import")] - Cache { - #[command(subcommand)] - action: CacheCommand, + /// 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 { @@ -914,15 +919,6 @@ enum Commands { }, } -/// Actions of the `cache` subcommand. -#[cfg(feature = "http-import")] -#[derive(Debug, Subcommand)] -enum CacheCommand { - /// Remove all HTTP module cache including versioned (tagged) modules and lock files. - /// Use this to fully reset the cache when something goes wrong. - Clear, -} - /// Shell targets supported by the `completion` subcommand. #[derive(Clone, Debug, clap::ValueEnum)] enum CompletionShell { @@ -959,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(); @@ -1084,43 +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() - ), - format!( - " {} - List all available subcommands (built-in and external)", - "list".green() - ), - ]; - - #[cfg(feature = "http-import")] - output.push(format!(" {} - Manage the HTTP module cache", "cache".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!(" {: miette::Result<()> { - if matches!(self.commands, Some(Commands::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()) @@ -1756,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")); } @@ -1828,72 +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::List) => unreachable!("handled before input validation"), - #[cfg(feature = "http-import")] - Some(Commands::Cache { - action: CacheCommand::Clear, - }) => { - let mut engine = self.create_engine()?; - engine.clear_http_cache_all().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 { @@ -2023,7 +2054,9 @@ impl Cli { if let Some(path) = &self.input.program.lockfile_path { engine.set_lockfile_path(path.clone()); } - if self.input.program.refresh_modules { + if self.input.program.clear_cache { + engine.clear_http_cache_all().map_err(|e| miette!(e.to_string()))?; + } else if self.input.program.refresh_modules { engine.clear_http_cache().map_err(|e| miette!(e.to_string()))?; } } @@ -4079,25 +4112,28 @@ mod tests { } #[test] - fn test_list_and_cache_are_subcommands() { + fn test_actions_are_subcommands_not_flags() { assert!(matches!( - Cli::try_parse_from(["mq", "list"]).unwrap().commands, - Some(Commands::List) + 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()); - assert!(Cli::try_parse_from(["mq", "--clear-cache"]).is_err()); } #[cfg(feature = "http-import")] #[test] - fn test_cache_clear_subcommand() { - let cli = Cli::try_parse_from(["mq", "cache", "clear"]).unwrap(); - assert!(matches!( - cli.commands, - Some(Commands::Cache { - action: CacheCommand::Clear - }) - )); + 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] @@ -5570,9 +5606,9 @@ mod tests { } #[test] - fn test_list_commands() { + fn test_run_commands() { let cli = Cli { - commands: Some(Commands::List), + commands: Some(Commands::Subcommands { json: false }), ..Cli::default() }; diff --git a/crates/mq-wasm/src/script.rs b/crates/mq-wasm/src/script.rs index e5ae5d77e..20d785e73 100644 --- a/crates/mq-wasm/src/script.rs +++ b/crates/mq-wasm/src/script.rs @@ -611,7 +611,7 @@ pub async fn clear_http_cache() -> Result<(), JsValue> { Ok(()) } -/// Removes all HTTP module cache and deletes `mq.lock` (matches `mq cache clear`). +/// Removes all HTTP module cache and deletes `mq.lock` (matches `--clear-cache`). #[wasm_bindgen(js_name=clearAllHttpCache)] pub async fn clear_all_http_cache() -> Result<(), JsValue> { #[cfg(feature = "opfs")] diff --git a/docs/books/src/reference/modules_and_imports.md b/docs/books/src/reference/modules_and_imports.md index 9c32680da..937e20e74 100644 --- a/docs/books/src/reference/modules_and_imports.md +++ b/docs/books/src/reference/modules_and_imports.md @@ -426,8 +426,8 @@ Fetched modules are cached in `{system_cache_dir}/mq/` as `{md5(url)}.mq` files. - **Later use, content unchanged**: succeeds silently. - **Later use, content changed**: fails with an error explaining the mismatch. Re-run with `--refresh-modules` to accept the new content and update `mq.lock` (for a versioned/tagged - URL, whose cache `--refresh-modules` doesn't touch, use `mq cache clear` instead). -- `--refresh-modules` / `mq cache clear` also drop the corresponding entries from `mq.lock` + URL, whose cache `--refresh-modules` doesn't touch, use `--clear-cache` instead). +- `--refresh-modules` / `--clear-cache` also drop the corresponding entries from `mq.lock` (mutable-ref entries only, and all entries respectively), matching their disk-cache behavior. - Pass `--no-lockfile` to disable the check entirely (no file is read or written). - Pass `--lockfile ` to use a different location instead of `./mq.lock`. Missing parent diff --git a/docs/books/src/start/external_subcommands.md b/docs/books/src/start/external_subcommands.md index 09a79a9c2..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` subcommand 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.