Skip to content

Commit 19141c3

Browse files
authored
fix(builtins): parse combined short flags in paste builtin (#1045)
## Summary - Fix `paste -sd,` being treated as a filename instead of combined flags `-s -d,` - Added `try_parse_combined_flags()` to iterate through combined flag characters - `s` sets serial mode, `d` consumes remainder as delimiter spec ## Test plan - [ ] `paste_combined_flags` — `printf "a\nb\nc\n" | paste -sd,` outputs `a,b,c` - [ ] `paste_combined_flags_serial_only` — `paste -s` works - [ ] `paste_separate_flags` — `paste -s -d ,` still works - [ ] Unit tests for `-sd,` and `-sd:` variants - [ ] All existing paste tests pass Closes #965
1 parent 7094ac8 commit 19141c3

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

crates/bashkit/src/builtins/paste.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ fn parse_paste_args(args: &[String]) -> (PasteOptions, Vec<String>) {
3333
opts.delimiters = parse_delim_spec(val);
3434
} else if p.flag("-s") {
3535
opts.serial = true;
36+
} else if try_parse_combined_flags(&mut p, &mut opts) {
37+
// handled combined flags like -sd,
3638
} else if let Some(arg) = p.positional() {
3739
files.push(arg.to_string());
3840
}
@@ -45,6 +47,51 @@ fn parse_paste_args(args: &[String]) -> (PasteOptions, Vec<String>) {
4547
(opts, files)
4648
}
4749

50+
/// Parse combined short flags like `-sd,` where `s` is a boolean flag
51+
/// and `d` takes the rest of the string as its value.
52+
fn try_parse_combined_flags(
53+
p: &mut super::arg_parser::ArgParser<'_>,
54+
opts: &mut PasteOptions,
55+
) -> bool {
56+
let arg = match p.current() {
57+
Some(a) if a.starts_with('-') && !a.starts_with("--") && a.len() > 2 => a,
58+
_ => return false,
59+
};
60+
61+
let chars: Vec<char> = arg[1..].chars().collect();
62+
let mut i = 0;
63+
let mut serial = false;
64+
let mut delimiters = None;
65+
66+
while i < chars.len() {
67+
match chars[i] {
68+
's' => {
69+
serial = true;
70+
i += 1;
71+
}
72+
'd' => {
73+
// 'd' consumes the rest as delimiter spec
74+
let rest: String = chars[i + 1..].iter().collect();
75+
if !rest.is_empty() {
76+
delimiters = Some(parse_delim_spec(&rest));
77+
}
78+
i = chars.len(); // consumed everything
79+
}
80+
_ => return false, // unknown flag char, bail out
81+
}
82+
}
83+
84+
// All chars parsed successfully — apply and advance
85+
if serial {
86+
opts.serial = true;
87+
}
88+
if let Some(d) = delimiters {
89+
opts.delimiters = d;
90+
}
91+
p.advance();
92+
true
93+
}
94+
4895
fn parse_delim_spec(spec: &str) -> Vec<char> {
4996
let mut delims = Vec::new();
5097
let mut chars = spec.chars();
@@ -300,6 +347,20 @@ mod tests {
300347
assert!(result.stderr.contains("paste:"));
301348
}
302349

350+
#[tokio::test]
351+
async fn test_paste_combined_sd_comma() {
352+
let result = run_paste(&["-sd,"], Some("a\nb\nc\n")).await;
353+
assert_eq!(result.exit_code, 0);
354+
assert_eq!(result.stdout, "a,b,c\n");
355+
}
356+
357+
#[tokio::test]
358+
async fn test_paste_combined_sd_colon() {
359+
let result = run_paste(&["-sd:"], Some("x\ny\nz\n")).await;
360+
assert_eq!(result.exit_code, 0);
361+
assert_eq!(result.stdout, "x:y:z\n");
362+
}
363+
303364
#[tokio::test]
304365
async fn test_paste_stdin_dash() {
305366
let result =
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
### paste_combined_flags
2+
# paste -sd, should work as -s -d ,
3+
printf "a\nb\nc\n" | paste -sd,
4+
### expect
5+
a,b,c
6+
### end
7+
8+
### paste_combined_flags_tab
9+
# paste -sd with tab delimiter (default-ish)
10+
printf "a\nb\nc\n" | paste -s
11+
### expect
12+
a b c
13+
### end
14+
15+
### paste_separate_flags
16+
# paste -s -d , should still work
17+
printf "a\nb\nc\n" | paste -s -d ,
18+
### expect
19+
a,b,c
20+
### end

0 commit comments

Comments
 (0)