diff --git a/crates/bashkit/src/builtins/paste.rs b/crates/bashkit/src/builtins/paste.rs index c5a65fb3..026f6606 100644 --- a/crates/bashkit/src/builtins/paste.rs +++ b/crates/bashkit/src/builtins/paste.rs @@ -33,6 +33,8 @@ fn parse_paste_args(args: &[String]) -> (PasteOptions, Vec) { opts.delimiters = parse_delim_spec(val); } else if p.flag("-s") { opts.serial = true; + } else if try_parse_combined_flags(&mut p, &mut opts) { + // handled combined flags like -sd, } else if let Some(arg) = p.positional() { files.push(arg.to_string()); } @@ -45,6 +47,51 @@ fn parse_paste_args(args: &[String]) -> (PasteOptions, Vec) { (opts, files) } +/// Parse combined short flags like `-sd,` where `s` is a boolean flag +/// and `d` takes the rest of the string as its value. +fn try_parse_combined_flags( + p: &mut super::arg_parser::ArgParser<'_>, + opts: &mut PasteOptions, +) -> bool { + let arg = match p.current() { + Some(a) if a.starts_with('-') && !a.starts_with("--") && a.len() > 2 => a, + _ => return false, + }; + + let chars: Vec = arg[1..].chars().collect(); + let mut i = 0; + let mut serial = false; + let mut delimiters = None; + + while i < chars.len() { + match chars[i] { + 's' => { + serial = true; + i += 1; + } + 'd' => { + // 'd' consumes the rest as delimiter spec + let rest: String = chars[i + 1..].iter().collect(); + if !rest.is_empty() { + delimiters = Some(parse_delim_spec(&rest)); + } + i = chars.len(); // consumed everything + } + _ => return false, // unknown flag char, bail out + } + } + + // All chars parsed successfully — apply and advance + if serial { + opts.serial = true; + } + if let Some(d) = delimiters { + opts.delimiters = d; + } + p.advance(); + true +} + fn parse_delim_spec(spec: &str) -> Vec { let mut delims = Vec::new(); let mut chars = spec.chars(); @@ -300,6 +347,20 @@ mod tests { assert!(result.stderr.contains("paste:")); } + #[tokio::test] + async fn test_paste_combined_sd_comma() { + let result = run_paste(&["-sd,"], Some("a\nb\nc\n")).await; + assert_eq!(result.exit_code, 0); + assert_eq!(result.stdout, "a,b,c\n"); + } + + #[tokio::test] + async fn test_paste_combined_sd_colon() { + let result = run_paste(&["-sd:"], Some("x\ny\nz\n")).await; + assert_eq!(result.exit_code, 0); + assert_eq!(result.stdout, "x:y:z\n"); + } + #[tokio::test] async fn test_paste_stdin_dash() { let result = diff --git a/crates/bashkit/tests/spec_cases/bash/paste-flags.test.sh b/crates/bashkit/tests/spec_cases/bash/paste-flags.test.sh new file mode 100644 index 00000000..e7e23bd1 --- /dev/null +++ b/crates/bashkit/tests/spec_cases/bash/paste-flags.test.sh @@ -0,0 +1,20 @@ +### paste_combined_flags +# paste -sd, should work as -s -d , +printf "a\nb\nc\n" | paste -sd, +### expect +a,b,c +### end + +### paste_combined_flags_tab +# paste -sd with tab delimiter (default-ish) +printf "a\nb\nc\n" | paste -s +### expect +a b c +### end + +### paste_separate_flags +# paste -s -d , should still work +printf "a\nb\nc\n" | paste -s -d , +### expect +a,b,c +### end