Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions crates/bashkit/src/builtins/paste.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,32 @@ impl Builtin for Paste {
sources.push(stdin.lines().map(|l| l.to_string()).collect());
}
} else {
// Every `-` reads the one stdin: in parallel mode each takes the
// next line in turn (`paste - -` joins pairs), with -s the first
// one takes it all.
let stdin_lines: Vec<String> = ctx
.stdin
.map(|s| s.lines().map(|l| l.to_string()).collect())
.unwrap_or_default();
let dashes = files.iter().filter(|f| *f == "-").count();
let mut dash_idx = 0;
for file in &files {
if file == "-" {
let lines = ctx
.stdin
.map(|s| s.lines().map(|l| l.to_string()).collect())
.unwrap_or_default();
let lines = if opts.serial {
if dash_idx == 0 {
stdin_lines.clone()
} else {
Vec::new()
}
} else {
stdin_lines
.iter()
.skip(dash_idx)
.step_by(dashes)
.cloned()
.collect()
};
dash_idx += 1;
sources.push(lines);
} else {
let path = if file.starts_with('/') {
Expand Down Expand Up @@ -422,6 +442,14 @@ mod tests {
assert!(result.stderr.contains("paste: -d requires an argument"));
}

#[tokio::test]
async fn test_paste_dashes_share_stdin() {
let result = run_paste(&["-", "-"], Some("1\n2\n3\n")).await;
assert_eq!(result.stdout, "1\t2\n3\t\n");
let result = run_paste(&["-s", "-", "-"], Some("1\n2\n")).await;
assert_eq!(result.stdout, "1\t2\n\n");
}

#[tokio::test]
async fn test_paste_stdin_dash() {
let result =
Expand Down
17 changes: 17 additions & 0 deletions crates/bashkit/src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8699,8 +8699,25 @@ impl Interpreter {
self.pending_fd_capture_depth += 1;
}

// Output the call's redirects will route must not stream from the
// body first (`f > out` printed `out` too), as for compounds.
let has_output_redirect = redirects.iter().any(|r| {
!matches!(
r.kind,
RedirectKind::Input | RedirectKind::HereDoc | RedirectKind::HereString
)
});
let saved_callback = if has_output_redirect {
self.output_callback.take()
} else {
None
};

// Execute function body. Always restore call state even on error.
let result = self.execute_command(&func_def.body).await;
if let Some(cb) = saved_callback {
self.output_callback = Some(cb);
}
if capture_pending_fd {
self.pending_fd_capture_depth = self.pending_fd_capture_depth.saturating_sub(1);
if result.is_err() {
Expand Down
9 changes: 6 additions & 3 deletions crates/bashkit/src/interpreter/redirection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,12 @@ impl Interpreter {
stdin = Some(format!("{}\n", content).into());
}
RedirectKind::HereDoc | RedirectKind::HereDocStrip => {
// << EOF / <<- EOF - use the heredoc content as stdin
let content = self.expand_word(&redirect.target).await?;
stdin = Some(content.into());
// << EOF / <<- EOF - use the heredoc content as stdin.
// `3<<EOF` feeds fd 3, which nothing here reads.
if matches!(redirect.fd, None | Some(0)) {
let content = self.expand_word(&redirect.target).await?;
stdin = Some(content.into());
}
}
RedirectKind::DupInput => {
// <&FD - if FD is a coproc read FD, consume next line
Expand Down
37 changes: 30 additions & 7 deletions crates/bashkit/src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,12 @@ impl<'a> Lexer<'a> {
Some(Token::And)
} else if self.peek_char() == Some('>') {
self.advance();
Some(Token::RedirectBoth)
if self.peek_char() == Some('>') {
self.advance();
Some(Token::RedirectBothAppend)
} else {
Some(Token::RedirectBoth)
}
} else {
Some(Token::Background)
}
Expand Down Expand Up @@ -347,7 +352,14 @@ impl<'a> Lexer<'a> {
// 2-char redirect operator (e.g. ">>", "<&", "<<") matter, so bound the
// lookahead — collecting all remaining input here made every
// digit-initial word O(n) and the whole lex O(n^2) (TM-DOS-024).
let input_remaining: String = self.chars.clone().take(4).collect();
// Re-injected text (the rest of a heredoc line) comes first.
let input_remaining: String = self
.reinject_buf
.iter()
.copied()
.chain(self.chars.clone())
.take(4)
.collect();

// Check patterns: "N>" "N>>" "N>&" "N<" "N<&"
if fd_str.len() == 1
Expand Down Expand Up @@ -425,6 +437,17 @@ impl<'a> Lexer<'a> {
}
let target_fd: i32 = target_str.parse().unwrap_or(0);
return Some(Token::DupFdIn(fd, target_fd));
} else if rest.starts_with("<<") && !rest.starts_with("<<<") {
// N<<EOF / N<<-EOF - here document on fd N
let fd: i32 = first_digit.to_digit(10).unwrap() as i32;
self.advance(); // consume digit
self.advance(); // consume <
self.advance(); // consume <
let strip = self.peek_char() == Some('-');
if strip {
self.advance();
}
return Some(Token::HereDocFd(fd, strip));
} else if rest.starts_with('<') && !rest.starts_with("<<") {
// N< - input redirect with fd
let fd: i32 = first_digit.to_digit(10).unwrap() as i32;
Expand Down Expand Up @@ -2238,13 +2261,13 @@ impl<'a> Lexer<'a> {

// Re-inject saved rest-of-line so subsequent tokens (pipes, commands, etc.)
// are visible to the parser. Add a newline so the tokenizer sees the line break.
// The line break always comes back: it ends the command even when
// nothing followed the delimiter (`cat <<A <<B`).
let rest_of_line_chars = rest_of_line.chars().count();
if !rest_of_line.is_empty() {
for ch in rest_of_line.chars() {
self.reinject_buf.push_back(ch);
}
self.reinject_buf.push_back('\n');
for ch in rest_of_line.chars() {
self.reinject_buf.push_back(ch);
}
self.reinject_buf.push_back('\n');

(content, rest_of_line_chars)
}
Expand Down
Loading
Loading