From 2124b7854537af71398e9165e595640cd62f4891 Mon Sep 17 00:00:00 2001 From: gsc Date: Mon, 26 Oct 2026 15:56:08 -0700 Subject: [PATCH 1/5] docs(loop): name looprc in config parsing comment [[tasks/harmony-2793]] --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 8f70bb7..767bfcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -50,7 +50,7 @@ fn main() -> Result<()> { std::process::exit(0); } - // Parse the .meta file + // Parse the .looprc file let absolute_path = std::env::current_dir()?.join(&config_path); if cli.verbose { From 64d86b73a91b97e9372c7789c41f5bf7d5625666 Mon Sep 17 00:00:00 2001 From: gsc Date: Mon, 26 Oct 2026 18:14:15 -0700 Subject: [PATCH 2/5] fix(loop): escape argument errors [[tasks/harmony-2793]] --- Cargo.toml | 2 ++ src/main.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 12743a5..f3750e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,8 @@ anyhow = "1.0" clap = { version = "4.3", features = ["derive"] } colored = "2.0" loop_lib = { path = "../loop_lib" } +# Any 0.x, as for loop_lib +meta_core = { path = "../meta_core", version = "<1" } rayon = "1.7" serde_json = "1.0" walkdir = "2.3" diff --git a/src/main.rs b/src/main.rs index 767bfcd..e2ae10d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,7 +41,7 @@ struct Cli { } fn main() -> Result<()> { - let cli = Cli::parse(); + let cli = parse_cli(); let config_path = cli.config.unwrap_or_else(|| PathBuf::from(".looprc")); @@ -87,3 +87,28 @@ fn main() -> Result<()> { Ok(()) } + +/// Parse the command line. Clap quotes rejected values as given, so an error +/// is shown from the escaped arguments: the same error, with values that +/// can't move the cursor or forge a line. +fn parse_cli() -> Cli { + use clap::error::ErrorKind; + Cli::try_parse().unwrap_or_else(|e| match e.kind() { + ErrorKind::DisplayHelp + | ErrorKind::DisplayVersion + | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => e.exit(), + _ => { + let shown = std::env::args_os() + .map(|arg| meta_core::display::escape(&arg.to_string_lossy()).into_owned()); + match Cli::try_parse_from(shown) { + Err(shown) => shown.exit(), + Ok(_) => Cli::command() + .error( + ErrorKind::InvalidValue, + "an argument contains a control character", + ) + .exit(), + } + } + }) +} From 8ae86d385aa12e6cfa60fea7e513057ded21801d Mon Sep 17 00:00:00 2001 From: gsc Date: Tue, 27 Oct 2026 14:52:23 -0700 Subject: [PATCH 3/5] fix(loop): escape the verbose config path [[tasks/harmony-2793]] --- src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index e2ae10d..91d2ebd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,7 +55,10 @@ fn main() -> Result<()> { if cli.verbose { println!("{}", "Verbose mode enabled".green()); - println!("\nResolved config file path: {}", absolute_path.display()); + println!( + "\nResolved config file path: {}", + meta_core::display::escape_path(&absolute_path) + ); println!("Executing command: {}", display_command(&cli.command)); } From 69d1479036b7ed77ef36bb47ddf0e2dffbbf3cde Mon Sep 17 00:00:00 2001 From: gsc Date: Tue, 27 Oct 2026 15:10:22 -0700 Subject: [PATCH 4/5] fix(loop): escape the verbose command echo [[tasks/harmony-2793]] --- src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 91d2ebd..a8f5bdf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -59,7 +59,10 @@ fn main() -> Result<()> { "\nResolved config file path: {}", meta_core::display::escape_path(&absolute_path) ); - println!("Executing command: {}", display_command(&cli.command)); + println!( + "Executing command: {}", + meta_core::display::escape(&display_command(&cli.command)) + ); } let mut config = if config_path.exists() { From a29035fc89646f4cd8d6f6f35ab87044983e5cd9 Mon Sep 17 00:00:00 2001 From: gsc Date: Tue, 27 Oct 2026 19:57:24 -0700 Subject: [PATCH 5/5] fix(cli): clap's own error for invalid UTF-8 arguments Its message doesn't echo the value, so the escaping fallback isn't needed. Implements [[tasks/harmony-2793]] --- src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index a8f5bdf..e6dcd82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,9 +100,11 @@ fn main() -> Result<()> { fn parse_cli() -> Cli { use clap::error::ErrorKind; Cli::try_parse().unwrap_or_else(|e| match e.kind() { + // clap's invalid UTF-8 error doesn't echo the value ErrorKind::DisplayHelp | ErrorKind::DisplayVersion - | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => e.exit(), + | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand + | ErrorKind::InvalidUtf8 => e.exit(), _ => { let shown = std::env::args_os() .map(|arg| meta_core::display::escape(&arg.to_string_lossy()).into_owned());