Fork non-command nodes for background jobs, fix interactive flag in child processes, and add empty variable test for [ builtin

This commit is contained in:
2026-03-09 21:55:03 -04:00
parent ac429cbdf4
commit 85e5fc2875
3 changed files with 24 additions and 4 deletions

View File

@@ -250,7 +250,7 @@ mod tests {
"pre-mode-change", "post-mode-change", "pre-mode-change", "post-mode-change",
"on-history-open", "on-history-close", "on-history-select", "on-history-open", "on-history-close", "on-history-select",
"on-completion-start", "on-completion-cancel", "on-completion-select", "on-completion-start", "on-completion-cancel", "on-completion-select",
"on-exit", "on-exit"
]; ];
for kind in kinds { for kind in kinds {
test_input(format!("autocmd {kind} 'true'")).unwrap(); test_input(format!("autocmd {kind} 'true'")).unwrap();

View File

@@ -249,7 +249,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// may have moved it during resize/rewrap // may have moved it during resize/rewrap
readline.writer.update_t_cols(); readline.writer.update_t_cols();
readline.mark_dirty(); readline.mark_dirty();
} }
if JOB_DONE.swap(false, Ordering::SeqCst) { if JOB_DONE.swap(false, Ordering::SeqCst) {
// update the prompt so any job count escape sequences update dynamically // update the prompt so any job count escape sequences update dynamically

View File

@@ -767,8 +767,16 @@ impl Dispatcher {
self.job_stack.new_job(); self.job_stack.new_job();
if cmds.len() == 1 { if cmds.len() == 1 {
self.fg_job = !is_bg && self.interactive; self.fg_job = !is_bg && self.interactive;
let cmd = cmds.into_iter().next().unwrap(); let mut cmd = cmds.into_iter().next().unwrap();
self.dispatch_node(cmd)?; if is_bg && !matches!(cmd.class, NdRule::Command { .. }) {
self.run_fork(&cmd.get_command().map(|t| t.to_string()).unwrap_or_default(), |s| {
if let Err(e) = s.dispatch_node(cmd) {
e.print_error();
}
})?;
} else {
self.dispatch_node(cmd)?;
}
// Give the pipeline terminal control as soon as the first child // Give the pipeline terminal control as soon as the first child
// establishes the PGID, so later children (e.g. nvim) don't get // establishes the PGID, so later children (e.g. nvim) don't get
@@ -1103,6 +1111,7 @@ impl Dispatcher {
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0))); let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0)));
self.interactive = false;
f(self); f(self);
exit(state::get_status()) exit(state::get_status())
} }
@@ -1401,4 +1410,15 @@ mod tests {
assert_eq!(state::get_status(), 0); assert_eq!(state::get_status(), 0);
assert_eq!(g.read_output(), "yes\n"); assert_eq!(g.read_output(), "yes\n");
} }
#[test]
fn empty_var_in_test() {
let _g = TestGuard::new();
// POSIX specifies that a quoted unset variable expands to an empty string, so the shell actually sees `[ -n "" ]`, which returns false
test_input("[ -n \"$EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING\" ]").unwrap();
assert_eq!(state::get_status(), 1);
// Without quotes, word splitting causes an empty var to be removed entirely, so the shell actually sees `[ -n ]`, testing the value of ']', which returns true
test_input("[ -n $EMPTYVAR_PROBABLY_NOT_SET_TO_ANYTHING ]").unwrap();
assert_eq!(state::get_status(), 0);
}
} }