From b777e6e3e4eb9871a17771d9732bda0774ee3764 Mon Sep 17 00:00:00 2001 From: gghez Date: Tue, 24 Mar 2026 03:51:46 +0100 Subject: [PATCH] fix(wget): detect -O - in trailing args absorbed by Clap When flags like --no-check-certificate precede -O -, Clap's trailing_var_arg absorbs all remaining tokens into the args vec, leaving the output field as None. This caused RTK to route through run() instead of run_stdout(), so the bare "-" was passed to wget as a positional URL argument. Scan trailing args for -O - / -O- / --output-document - patterns and strip them before routing to run_stdout(). Fixes #716 Signed-off-by: gghez --- CHANGELOG.md | 1 + src/main.rs | 8 ++- src/wget_cmd.rs | 146 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9e441b7..57ddc3c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes +* **wget:** fix `-O -` flag parsed as URL when preceded by other flags (e.g. `--no-check-certificate -O -`) * **ruby:** use `rails test` instead of `rake test` when positional file args are passed — `rake test` ignores positional files and only supports `TEST=path` ### Features diff --git a/src/main.rs b/src/main.rs index 0ff5124c..3f514f35 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1734,8 +1734,12 @@ fn main() -> Result<()> { } Commands::Wget { url, output, args } => { - if output.as_deref() == Some("-") { - wget_cmd::run_stdout(&url, &args, cli.verbose)?; + // Detect -O - in trailing args (Clap's trailing_var_arg may absorb + // -O - when other unknown flags precede it, e.g. --no-check-certificate -O -) + let stdout_in_args = wget_cmd::has_stdout_output_flag(&args); + if output.as_deref() == Some("-") || stdout_in_args { + let filtered: Vec = wget_cmd::strip_output_flag(&args); + wget_cmd::run_stdout(&url, &filtered, cli.verbose)?; } else { // Pass -O through to wget via args let mut all_args = Vec::new(); diff --git a/src/wget_cmd.rs b/src/wget_cmd.rs index 722f88f1..b41fc4a5 100644 --- a/src/wget_cmd.rs +++ b/src/wget_cmd.rs @@ -2,6 +2,56 @@ use crate::tracking; use crate::utils::resolved_command; use anyhow::{Context, Result}; +/// Check whether trailing args contain `-O -` / `-O-` / `--output-document -` +/// (stdout redirect that Clap's trailing_var_arg may have absorbed). +pub fn has_stdout_output_flag(args: &[String]) -> bool { + let mut i = 0; + while i < args.len() { + let a = args[i].as_str(); + // -O- (combined short form) + if a == "-O-" { + return true; + } + // -O - or -O — only stdout when value is "-" + if a == "-O" || a == "--output-document" { + if args.get(i + 1).map(|v| v.as_str()) == Some("-") { + return true; + } + i += 2; + continue; + } + // --output-document=- (long form with =) + if a == "--output-document=-" { + return true; + } + i += 1; + } + false +} + +/// Remove the `-O -` / `-O-` / `--output-document -` tokens from args, +/// returning the remaining arguments to pass through to wget. +pub fn strip_output_flag(args: &[String]) -> Vec { + let mut result = Vec::with_capacity(args.len()); + let mut i = 0; + while i < args.len() { + let a = args[i].as_str(); + if a == "-O-" || a == "--output-document=-" { + i += 1; + continue; + } + if a == "-O" || a == "--output-document" { + if args.get(i + 1).map(|v| v.as_str()) == Some("-") { + i += 2; + continue; + } + } + result.push(args[i].clone()); + i += 1; + } + result +} + /// Compact wget - strips progress bars, shows only result pub fn run(url: &str, args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); @@ -261,3 +311,99 @@ fn truncate_line(line: &str, max: usize) -> String { format!("{}...", t) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn s(v: &[&str]) -> Vec { + v.iter().map(|s| s.to_string()).collect() + } + + // --- has_stdout_output_flag --- + + #[test] + fn detect_dash_o_dash_separated() { + assert!(has_stdout_output_flag(&s(&["-O", "-"]))); + } + + #[test] + fn detect_dash_o_dash_combined() { + assert!(has_stdout_output_flag(&s(&["-O-"]))); + } + + #[test] + fn detect_long_form_separated() { + assert!(has_stdout_output_flag(&s(&["--output-document", "-"]))); + } + + #[test] + fn detect_long_form_equals() { + assert!(has_stdout_output_flag(&s(&["--output-document=-"]))); + } + + #[test] + fn detect_stdout_after_other_flags() { + assert!(has_stdout_output_flag(&s(&[ + "--no-check-certificate", + "-q", + "-O", + "-" + ]))); + } + + #[test] + fn no_detect_output_to_file() { + assert!(!has_stdout_output_flag(&s(&["-O", "file.html"]))); + } + + #[test] + fn no_detect_no_output_flag() { + assert!(!has_stdout_output_flag(&s(&["-q", "--no-check-certificate"]))); + } + + #[test] + fn no_detect_empty_args() { + assert!(!has_stdout_output_flag(&s(&[]))); + } + + #[test] + fn no_detect_dash_o_at_end_without_value() { + assert!(!has_stdout_output_flag(&s(&["-O"]))); + } + + // --- strip_output_flag --- + + #[test] + fn strip_dash_o_dash_separated() { + assert_eq!(strip_output_flag(&s(&["-q", "-O", "-"])), s(&["-q"])); + } + + #[test] + fn strip_dash_o_dash_combined() { + assert_eq!( + strip_output_flag(&s(&["--no-check-certificate", "-O-"])), + s(&["--no-check-certificate"]) + ); + } + + #[test] + fn strip_long_form_equals() { + assert_eq!( + strip_output_flag(&s(&["-q", "--output-document=-"])), + s(&["-q"]) + ); + } + + #[test] + fn strip_preserves_output_to_file() { + let args = s(&["-O", "file.html", "-q"]); + assert_eq!(strip_output_flag(&args), args); + } + + #[test] + fn strip_preserves_no_output_flag() { + let args = s(&["-q", "--no-check-certificate"]); + assert_eq!(strip_output_flag(&args), args); + } +}