fixed heredocs using the same expansion pathway as regular strings

implemented backtick command subs

deferred heredoc expansion until redir time instead of parse time

implemented "$*" expansions

function defs like 'func   ()  { }' now parse correctly

fixed conjunctions short circuiting instead of skipping
This commit is contained in:
2026-03-15 00:01:33 -04:00
parent 9bd9c66b92
commit 101d8434f8
8 changed files with 278 additions and 84 deletions

View File

@@ -444,45 +444,9 @@ impl TryFrom<Tk> for RedirBldr {
let span = tk.span.clone();
if tk.flags.contains(TkFlags::IS_HEREDOC) {
let flags = tk.flags;
let mut heredoc_body = if flags.contains(TkFlags::LIT_HEREDOC) {
tk.as_str().to_string()
} else {
tk.expand()?.get_words().first().map(|s| s.as_str()).unwrap_or_default().to_string()
};
if flags.contains(TkFlags::TAB_HEREDOC) {
let lines = heredoc_body.lines();
let mut min_tabs = usize::MAX;
for line in lines {
if line.is_empty() { continue; }
let line_len = line.len();
let after_strip = line.trim_start_matches('\t').len();
let delta = line_len - after_strip;
min_tabs = min_tabs.min(delta);
}
if min_tabs == usize::MAX {
// let's avoid possibly allocating a string with 18 quintillion tabs
min_tabs = 0;
}
if min_tabs > 0 {
let stripped = heredoc_body.lines()
.fold(vec![], |mut acc, ln| {
if ln.is_empty() {
acc.push("");
return acc;
}
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
acc.push(stripped_ln);
acc
})
.join("\n");
heredoc_body = stripped + "\n";
}
}
Ok(RedirBldr {
io_mode: Some(IoMode::loaded_pipe(0, heredoc_body.as_bytes())?),
io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?),
class: Some(RedirType::HereDoc),
tgt_fd: Some(0),
span: Some(span)
@@ -921,13 +885,26 @@ impl ParseStream {
let mut node_tks: Vec<Tk> = vec![];
let body;
if !is_func_name(self.peek_tk()) {
// Two forms: "name()" as one token, or "name" followed by "()" as separate tokens
let spaced_form = !is_func_name(self.peek_tk())
&& self.peek_tk().is_some_and(|tk| tk.flags.contains(TkFlags::IS_CMD))
&& is_func_parens(self.tokens.get(1));
if !is_func_name(self.peek_tk()) && !spaced_form {
return Ok(None);
}
let name_tk = self.next_tk().unwrap();
node_tks.push(name_tk.clone());
let name = name_tk.clone();
let name_raw = name.to_string();
let name_raw = if spaced_form {
// Consume the "()" token
let parens_tk = self.next_tk().unwrap();
node_tks.push(parens_tk);
name.to_string()
} else {
name.to_string()
};
let mut src = name_tk.span.span_source().clone();
src.rename(name_raw.clone());
let color = next_color();
@@ -1155,7 +1132,7 @@ impl ParseStream {
.get_words()
.join(" ");
string.push('\n');
let io_mode = IoMode::loaded_pipe(redir_bldr.tgt_fd.unwrap_or(0), string.as_bytes())?;
let io_mode = IoMode::buffer(redir_bldr.tgt_fd.unwrap_or(0), string, redir_tk.flags)?;
Ok(redir_bldr.with_io_mode(io_mode).build())
}
_ => {
@@ -1958,6 +1935,12 @@ fn is_func_name(tk: Option<&Tk>) -> bool {
})
}
fn is_func_parens(tk: Option<&Tk>) -> bool {
tk.is_some_and(|tk| {
tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()"
})
}
/// Perform an operation on the child nodes of a given node
///
/// # Parameters