From 2ea44c55e9d6f68f85e5bbb776efccbd0dc6e7bd Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sun, 1 Mar 2026 17:14:48 -0500 Subject: [PATCH] implemented 'type' and 'wait' builtins fixed some tcsetpgrp() misbehavior fixed not being able to redirect stderr from builtins --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/builtin/alias.rs | 21 ++-- src/builtin/arrops.rs | 40 +++---- src/builtin/cd.rs | 11 +- src/builtin/complete.rs | 15 +-- src/builtin/dirstack.rs | 24 ++-- src/builtin/echo.rs | 12 +- src/builtin/eval.rs | 13 +-- src/builtin/exec.rs | 18 +-- src/builtin/intro.rs | 73 ++++++++++++ src/builtin/jobctl.rs | 71 +++++++++--- src/builtin/map.rs | 7 +- src/builtin/mod.rs | 77 +------------ src/builtin/pwd.rs | 11 +- src/builtin/read.rs | 12 +- src/builtin/shift.rs | 11 +- src/builtin/shopt.rs | 13 +-- src/builtin/source.rs | 11 +- src/builtin/trap.rs | 12 +- src/builtin/varcmds.rs | 25 ++--- src/builtin/zoltraak.rs | 13 +-- src/expand.rs | 6 +- src/jobs.rs | 145 +++++++++++++++++++++--- src/libsh/error.rs | 47 +++++--- src/libsh/guards.rs | 215 +++++++++++++++++++++++++++++++++++ src/libsh/mod.rs | 1 + src/libsh/sys.rs | 45 -------- src/main.rs | 13 ++- src/parse/execute.rs | 236 +++++++++++++++++++++------------------ src/parse/lex.rs | 59 ++++++---- src/parse/mod.rs | 12 +- src/procio.rs | 28 +---- src/readline/complete.rs | 34 +++--- src/readline/linebuf.rs | 23 ++++ src/readline/term.rs | 91 +-------------- src/signal.rs | 18 ++- src/state.rs | 86 +++++++++----- 38 files changed, 922 insertions(+), 635 deletions(-) create mode 100644 src/builtin/intro.rs create mode 100644 src/libsh/guards.rs diff --git a/Cargo.lock b/Cargo.lock index 57727d2..2d8a6b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -533,6 +533,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -596,6 +602,7 @@ dependencies = [ "pretty_assertions", "rand", "regex", + "scopeguard", "serde_json", "tempfile", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 6cfd2fe..0696c29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ log = "0.4.29" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } rand = "0.10.0" regex = "1.11.1" +scopeguard = "1.2.0" serde_json = "1.0.149" unicode-segmentation = "1.12.0" unicode-width = "0.2.0" diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index 025173d..d6cbe54 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -1,17 +1,14 @@ use ariadne::Fmt; use crate::{ - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, state::{self, read_logic, write_logic}, }; -use super::setup_builtin; - -pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn alias(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -20,8 +17,8 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if argv.is_empty() { // Display the environment variables @@ -46,14 +43,14 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< let Some((name, body)) = arg.split_once('=') else { return Err(ShErr::at(ShErrKind::SyntaxErr, span, "alias: Expected an assignment in alias args")); }; - write_logic(|l| l.insert_alias(name, body)); + write_logic(|l| l.insert_alias(name, body, span.clone())); } } state::set_status(0); Ok(()) } -pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn unalias(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -62,8 +59,8 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if argv.is_empty() { // Display the environment variables diff --git a/src/builtin/arrops.rs b/src/builtin/arrops.rs index a7d8efd..24f2109 100644 --- a/src/builtin/arrops.rs +++ b/src/builtin/arrops.rs @@ -1,13 +1,9 @@ use std::collections::VecDeque; -use ariadne::Span; - use crate::{ - getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state::{self, VarFlags, VarKind, read_vars, write_vars} + getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, procio::borrow_fd, state::{self, VarFlags, VarKind, write_vars} }; -use super::setup_builtin; - fn arr_op_optspec() -> Vec { vec![ OptSpec { @@ -44,7 +40,7 @@ impl Default for ArrOpOpts { #[derive(Clone, Copy)] enum End { Front, Back } -fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End) -> ShResult<()> { +fn arr_pop_inner(node: Node, end: End) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -55,8 +51,8 @@ fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; let arr_op_opts = get_arr_op_opts(opts)?; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let stdout = borrow_fd(STDOUT_FILENO); let mut status = 0; @@ -85,7 +81,7 @@ fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End Ok(()) } -fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End) -> ShResult<()> { +fn arr_push_inner(node: Node, end: End) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -97,8 +93,8 @@ fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: En let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; let _arr_op_opts = get_arr_op_opts(opts)?; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut argv = argv.into_iter(); let Some((name, _)) = argv.next() else { @@ -124,23 +120,23 @@ fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: En Ok(()) } -pub fn arr_pop(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - arr_pop_inner(node, io_stack, job, End::Back) +pub fn arr_pop(node: Node) -> ShResult<()> { + arr_pop_inner(node, End::Back) } -pub fn arr_fpop(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - arr_pop_inner(node, io_stack, job, End::Front) +pub fn arr_fpop(node: Node) -> ShResult<()> { + arr_pop_inner(node, End::Front) } -pub fn arr_push(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - arr_push_inner(node, io_stack, job, End::Back) +pub fn arr_push(node: Node) -> ShResult<()> { + arr_push_inner(node, End::Back) } -pub fn arr_fpush(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - arr_push_inner(node, io_stack, job, End::Front) +pub fn arr_fpush(node: Node) -> ShResult<()> { + arr_push_inner(node, End::Front) } -pub fn arr_rotate(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn arr_rotate(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -151,8 +147,8 @@ pub fn arr_rotate(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShRe let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; let arr_op_opts = get_arr_op_opts(opts)?; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } for (arg, _) in argv { write_vars(|v| -> ShResult<()> { diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index 784b8dc..3c164e8 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -2,16 +2,13 @@ use ariadne::Fmt; use yansi::Color; use crate::{ - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, state::{self}, }; -use super::setup_builtin; - -pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { +pub fn cd(node: Node) -> ShResult<()> { let span = node.get_span(); let NdRule::Command { assignments: _, @@ -22,8 +19,8 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { }; let cd_span = argv.first().unwrap().span.clone(); - let (argv, _) = setup_builtin(Some(argv), job, None)?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() { (PathBuf::from(arg),Some(span)) diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index d6acb44..1ff00f4 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -2,12 +2,10 @@ use bitflags::bitflags; use nix::{libc::STDOUT_FILENO, unistd::write}; use crate::{ - builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node}, - procio::{IoStack, borrow_fd}, + parse::{NdRule, Node, execute::prepare_argv}, + procio::borrow_fd, readline::complete::{BashCompSpec, CompContext, CompSpec}, state::{self, read_meta, write_meta}, }; @@ -149,7 +147,7 @@ pub struct CompOpts { pub opt_flags: CompOptFlags, } -pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn complete_builtin(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -168,8 +166,8 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) - let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?; let comp_opts = get_comp_opts(opts)?; - let (argv, _) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if comp_opts.flags.contains(CompFlags::PRINT) { if argv.is_empty() { @@ -219,7 +217,7 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) - Ok(()) } -pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn compgen_builtin(node: Node) -> ShResult<()> { let _blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -239,7 +237,6 @@ pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?; let prefix = argv.clone().into_iter().nth(1).unwrap_or_default(); let comp_opts = get_comp_opts(opts)?; - let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src); diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs index c11d8ce..a5e8fb7 100644 --- a/src/builtin/dirstack.rs +++ b/src/builtin/dirstack.rs @@ -5,11 +5,9 @@ use nix::{libc::STDOUT_FILENO, unistd::write}; use yansi::Color; use crate::{ - builtin::setup_builtin, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, - parse::{NdRule, Node, lex::Span}, - procio::{IoStack, borrow_fd}, + parse::{NdRule, Node, execute::prepare_argv, lex::Span}, + procio::borrow_fd, state::{self, read_meta, write_meta}, }; @@ -103,7 +101,7 @@ fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult { } } -pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn pushd(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -113,8 +111,8 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut dir = None; let mut rotate_idx = None; @@ -184,7 +182,7 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< Ok(()) } -pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn popd(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -194,8 +192,8 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut remove_idx = None; let mut no_cd = false; @@ -276,7 +274,7 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( Ok(()) } -pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn dirs(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -286,8 +284,8 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut abbreviate_home = true; let mut one_per_line = false; diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 2f5c4c9..7a8dbb4 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -1,12 +1,10 @@ use crate::{ - builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, state, }; @@ -39,7 +37,7 @@ bitflags! { } } -pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn echo(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -51,8 +49,8 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( assert!(!argv.is_empty()); let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?; let flags = get_echo_flags(opts).blame(blame)?; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let output_channel = if flags.contains(EchoFlags::USE_STDERR) { borrow_fd(STDERR_FILENO) diff --git a/src/builtin/eval.rs b/src/builtin/eval.rs index 37e26b3..d29fda4 100644 --- a/src/builtin/eval.rs +++ b/src/builtin/eval.rs @@ -1,13 +1,10 @@ use crate::{ - builtin::setup_builtin, - jobs::JobBldr, libsh::error::ShResult, - parse::{NdRule, Node, execute::exec_input}, - procio::IoStack, + parse::{NdRule, Node, execute::{exec_input, prepare_argv}}, state, }; -pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn eval(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -16,8 +13,8 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (expanded_argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let expanded_argv = expanded_argv.unwrap(); + let mut expanded_argv = prepare_argv(argv)?; + if !expanded_argv.is_empty() { expanded_argv.remove(0); } if expanded_argv.is_empty() { state::set_status(0); @@ -30,5 +27,5 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( .collect::>() .join(" "); - exec_input(joined_argv, None, false) + exec_input(joined_argv, None, false, Some("eval".into())) } diff --git a/src/builtin/exec.rs b/src/builtin/exec.rs index 981d9d5..5e3ce1e 100644 --- a/src/builtin/exec.rs +++ b/src/builtin/exec.rs @@ -1,15 +1,12 @@ use nix::{errno::Errno, unistd::execvpe}; use crate::{ - builtin::setup_builtin, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node, execute::ExecArgs}, - procio::IoStack, + parse::{NdRule, Node, execute::{ExecArgs, prepare_argv}}, state, }; -pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn exec_builtin(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -18,13 +15,8 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh unreachable!() }; - let (expanded_argv, guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let expanded_argv = expanded_argv.unwrap(); - if let Some(g) = guard { - // Persist redirections so they affect the entire shell, - // not just this command call - g.persist() - } + let mut expanded_argv = prepare_argv(argv)?; + if !expanded_argv.is_empty() { expanded_argv.remove(0); } if expanded_argv.is_empty() { state::set_status(0); @@ -42,7 +34,7 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh let cmd_str = cmd.to_str().unwrap().to_string(); match e { Errno::ENOENT => Err( - ShErr::new(ShErrKind::CmdNotFound, span.clone()) + ShErr::new(ShErrKind::NotFound, span.clone()) .labeled(span, format!("exec: command not found: {}", cmd_str)) ), _ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))), diff --git a/src/builtin/intro.rs b/src/builtin/intro.rs new file mode 100644 index 0000000..5447e03 --- /dev/null +++ b/src/builtin/intro.rs @@ -0,0 +1,73 @@ +use std::{env, os::unix::fs::PermissionsExt, path::Path}; + +use ariadne::{Fmt, Span}; + +use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::{NdRule, Node, execute::prepare_argv, lex::KEYWORDS}, state::{self, ShAlias, ShFunc, read_logic, read_vars}}; + +pub fn type_builtin(node: Node) -> ShResult<()> { + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } + + /* + * we have to check in the same order that the dispatcher checks this + * 1. function + * 2. builtin + * 3. command + */ + + 'outer: for (arg,span) in argv { + if let Some(func) = read_logic(|v| v.get_func(&arg)) { + let ShFunc { body: _, source } = func; + let (line, col) = source.line_and_col(); + let name = source.source().name(); + println!("{arg} is a function defined at {name}:{}:{}", line + 1, col + 1); + } else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) { + let ShAlias { body, source } = alias; + let (line, col) = source.line_and_col(); + let name = source.source().name(); + println!("{arg} is an alias for '{body}' defined at {name}:{}:{}", line + 1, col + 1); + } else if BUILTINS.contains(&arg.as_str()) { + println!("{arg} is a shell builtin"); + } else if KEYWORDS.contains(&arg.as_str()) { + println!("{arg} is a shell keyword"); + } else { + let path = env::var("PATH").unwrap_or_default(); + let paths = path.split(':') + .map(Path::new) + .collect::>(); + + for path in paths { + if let Ok(entries) = path.read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; + let is_exec = meta.permissions().mode() & 0o111 != 0; + + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + && name == arg { + println!("{arg} is {}", entry.path().display()); + continue 'outer; + } + } + } + } + + state::set_status(1); + return Err(ShErr::at(ShErrKind::NotFound, span, format!("'{}' is not a command, function, or alias", arg.fg(next_color())))); + } + } + + state::set_status(0); + Ok(()) +} diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index 4ef37d1..04c918b 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -1,22 +1,20 @@ use ariadne::Fmt; use crate::{ - jobs::{JobBldr, JobCmdFlags, JobID}, + jobs::{JobCmdFlags, JobID, wait_bg}, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, - parse::{NdRule, Node, lex::Span}, + parse::{NdRule, Node, execute::prepare_argv, lex::Span}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, state::{self, read_jobs, write_jobs}, }; -use super::setup_builtin; - pub enum JobBehavior { Foregound, Background, } -pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> { +pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> { let blame = node.get_span().clone(); let cmd_tk = node.get_command(); let cmd_span = cmd_tk.unwrap().span.clone(); @@ -32,8 +30,8 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR unreachable!() }; - let (argv, _) = setup_builtin(Some(argv), job, None)?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut argv = argv.into_iter(); if read_jobs(|j| j.get_fg().is_some()) { @@ -84,7 +82,12 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult { if arg.starts_with('%') { let arg = arg.strip_prefix('%').unwrap(); if arg.chars().all(|ch| ch.is_ascii_digit()) { - Ok(arg.parse::().unwrap()) + let num = arg.parse::().unwrap_or_default(); + if num == 0 { + Err(ShErr::at(ShErrKind::SyntaxErr, blame, format!("Invalid job id: {}", arg.fg(next_color())))) + } else { + Ok(num.saturating_sub(1)) + } } else { let result = write_jobs(|j| { let query_result = j.query(JobID::Command(arg.into())); @@ -119,7 +122,7 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult { } } -pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn jobs(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -128,8 +131,8 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut flags = JobCmdFlags::empty(); for (arg, span) in argv { @@ -158,7 +161,45 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( Ok(()) } -pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn wait(node: Node) -> ShResult<()> { + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } + if read_jobs(|j| j.curr_job().is_none()) { + state::set_status(0); + return Err(ShErr::at(ShErrKind::ExecFail, blame, "wait: No jobs found")); + } + let argv = argv.into_iter() + .map(|arg| { + if arg.0.as_str().chars().all(|ch| ch.is_ascii_digit()) { + Ok(JobID::Pid(Pid::from_raw(arg.0.parse::().unwrap()))) + } else { + Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?)) + } + }) + .collect::>>()?; + + if argv.is_empty() { + write_jobs(|j| j.wait_all_bg())?; + } else { + for arg in argv { + wait_bg(arg)?; + } + } + + // don't set status here, the status of the waited-on job should be the status of the wait builtin + Ok(()) +} + +pub fn disown(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -168,8 +209,8 @@ pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut argv = argv.into_iter(); let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { diff --git a/src/builtin/map.rs b/src/builtin/map.rs index 00c8bd1..a8cef63 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -5,7 +5,7 @@ use nix::{libc::STDOUT_FILENO, unistd::write}; use serde_json::{Map, Value}; use crate::{ - expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_tk, split_tk_at}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars} + expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_tk, split_tk_at}}, procio::borrow_fd, state::{self, read_vars, write_vars} }; #[derive(Debug, Clone)] @@ -195,8 +195,6 @@ impl MapNode { } } -use super::setup_builtin; - fn map_opts_spec() -> [OptSpec; 6] { [ OptSpec { @@ -243,7 +241,7 @@ bitflags! { } } -pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn map(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -254,7 +252,6 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() let (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?; let map_opts = get_map_opts(opts); - let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; if !argv.is_empty() { argv.remove(0); // remove "map" command from argv } diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index f55426e..6c7bf25 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -1,14 +1,5 @@ -use nix::unistd::Pid; - use crate::{ - jobs::{ChildProc, JobBldr}, libsh::error::ShResult, - parse::{ - Redir, - execute::prepare_argv, - lex::{Span, Tk}, - }, - procio::{IoStack, RedirGuard}, state, }; @@ -32,77 +23,15 @@ pub mod varcmds; pub mod zoltraak; pub mod map; pub mod arrops; +pub mod intro; -pub const BUILTINS: [&str; 41] = [ +pub const BUILTINS: [&str; 43] = [ "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", - "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate" + "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type" ]; -/// Sets up a builtin command -/// -/// Prepares a builtin for execution by processing arguments, setting up -/// redirections, and registering the command as a child process in the given -/// `JobBldr` -/// -/// # Parameters -/// * argv - The vector of raw argument tokens -/// * job - A mutable reference to a `JobBldr` -/// * io_mode - An optional 2-tuple consisting of a mutable reference to an -/// `IoStack` and a vector of `Redirs` -/// -/// # Behavior -/// * Cleans, expands, and word splits the arg vector -/// * Adds a new `ChildProc` to the job builder -/// * Performs redirections, if any. -/// -/// # Returns -/// * The processed arg vector -/// * The popped `IoFrame`, if any -/// -/// # Notes -/// * If redirections are given to this function, the caller must call -/// `IoFrame.restore()` on the returned `IoFrame` -/// * If redirections are given, the second field of the resulting tuple will -/// *always* be `Some()` -/// * If no redirections are given, the second field will *always* be `None` -type SetupReturns = ShResult<(Option>, Option)>; -pub fn setup_builtin( - argv: Option>, - job: &mut JobBldr, - io_mode: Option<(&mut IoStack, Vec)>, -) -> SetupReturns { - let mut argv = argv.map(|argv| prepare_argv(argv)).transpose()?; - - let child_pgid = if let Some(pgid) = job.pgid() { - pgid - } else { - job.set_pgid(Pid::this()); - Pid::this() - }; - let cmd_name = argv - .as_mut() - .and_then(|argv| { - if argv.is_empty() { - None - } else { - Some(argv.remove(0).0) - } - }).unwrap_or_else(|| String::new()); - let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?; - job.push_child(child); - - let guard = io_mode.map(|(io,rdrs)| { - io.append_to_frame(rdrs); - io.pop_frame().redirect() - }).transpose()?; - - // We return the io_frame because the caller needs to also call - // io_frame.restore() - Ok((argv, guard)) -} - pub fn true_builtin() -> ShResult<()> { state::set_status(0); Ok(()) diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index 6d3978a..2a60796 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -1,25 +1,20 @@ use crate::{ - jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, state, }; -use super::setup_builtin; - -pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn pwd(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, - argv, + argv: _, } = node.class else { unreachable!() }; - let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let stdout = borrow_fd(STDOUT_FILENO); let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string(); diff --git a/src/builtin/read.rs b/src/builtin/read.rs index a0d943c..f2a96cf 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -6,12 +6,10 @@ use nix::{ }; use crate::{ - builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, - parse::{NdRule, Node}, - procio::{IoStack, borrow_fd}, + parse::{NdRule, Node, execute::prepare_argv}, + procio::borrow_fd, readline::term::RawModeGuard, state::{self, VarFlags, VarKind, read_vars, write_vars}, }; @@ -63,7 +61,7 @@ pub struct ReadOpts { flags: ReadFlags, } -pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn read_builtin(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -75,8 +73,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?; let read_opts = get_read_flags(opts).blame(blame.clone())?; - let (argv, _) = setup_builtin(Some(argv), job, None).blame(blame.clone())?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if let Some(prompt) = read_opts.prompt { write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; diff --git a/src/builtin/shift.rs b/src/builtin/shift.rs index 373a381..4e385b1 100644 --- a/src/builtin/shift.rs +++ b/src/builtin/shift.rs @@ -1,13 +1,10 @@ use crate::{ - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, state::{self, write_vars}, }; -use super::setup_builtin; - -pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { +pub fn shift(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -16,8 +13,8 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { unreachable!() }; - let (argv, _) = setup_builtin(Some(argv), job, None)?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } let mut argv = argv.into_iter(); if let Some((arg, span)) = argv.next() { diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index 41945ff..ba7f8f1 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -1,15 +1,12 @@ use crate::{ - jobs::JobBldr, libsh::error::{ShResult, ShResultExt}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, state::{self, write_shopts}, }; -use super::setup_builtin; - -pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn shopt(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -18,8 +15,8 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if argv.is_empty() { let mut output = write_shopts(|s| s.display_opts())?; diff --git a/src/builtin/source.rs b/src/builtin/source.rs index 8c67949..2485a3e 100644 --- a/src/builtin/source.rs +++ b/src/builtin/source.rs @@ -1,14 +1,11 @@ use crate::{ - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, state::{self, source_file}, }; -use super::setup_builtin; - -pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { +pub fn source(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -17,8 +14,8 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { unreachable!() }; - let (argv, _) = setup_builtin(Some(argv), job, None)?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } for (arg, span) in argv { let path = PathBuf::from(arg); diff --git a/src/builtin/trap.rs b/src/builtin/trap.rs index b4bea83..5ec80d6 100644 --- a/src/builtin/trap.rs +++ b/src/builtin/trap.rs @@ -7,11 +7,9 @@ use nix::{ }; use crate::{ - builtin::setup_builtin, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node}, - procio::{IoStack, borrow_fd}, + parse::{NdRule, Node, execute::prepare_argv}, + procio::borrow_fd, state::{self, read_logic, write_logic}, }; @@ -114,7 +112,7 @@ impl Display for TrapTarget { } } -pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn trap(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -123,8 +121,8 @@ pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if argv.is_empty() { let stdout = borrow_fd(STDOUT_FILENO); diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs index d45a96d..d9cd4e8 100644 --- a/src/builtin/varcmds.rs +++ b/src/builtin/varcmds.rs @@ -1,15 +1,12 @@ use crate::{ - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node, lex::split_tk_at}, + parse::{NdRule, Node, execute::prepare_argv, lex::split_tk_at}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, state::{self, VarFlags, VarKind, read_vars, write_vars}, }; -use super::setup_builtin; - -pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn readonly(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -18,8 +15,6 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu unreachable!() }; - let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; - // Remove "readonly" from argv let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; @@ -61,7 +56,7 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu Ok(()) } -pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn unset(node: Node) -> ShResult<()> { let blame = node.get_span().clone(); let NdRule::Command { assignments: _, @@ -71,8 +66,8 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } if argv.is_empty() { return Err(ShErr::at(ShErrKind::SyntaxErr, blame, "unset: Expected at least one argument")); @@ -89,7 +84,7 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< Ok(()) } -pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn export(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -98,8 +93,6 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult unreachable!() }; - let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; - // Remove "export" from argv let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; @@ -134,7 +127,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult Ok(()) } -pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn local(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -143,8 +136,6 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; - // Remove "local" from argv let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index 5a3a969..076271c 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -2,15 +2,12 @@ use std::os::unix::fs::OpenOptionsExt; use crate::{ getopt::{Opt, OptSpec, get_opts_from_tokens}, - jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, - parse::{NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, prelude::*, - procio::{IoStack, borrow_fd}, + procio::borrow_fd, }; -use super::setup_builtin; - bitflags! { #[derive(Clone,Copy,Debug,PartialEq,Eq)] struct ZoltFlags: u32 { @@ -29,7 +26,7 @@ bitflags! { /// The file given as an argument is completely destroyed. The command works by /// shredding all of the data contained in the file, before truncating the /// length of the file to 0 to ensure that not even any metadata remains. -pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { +pub fn zoltraak(node: Node) -> ShResult<()> { let NdRule::Command { assignments: _, argv, @@ -106,8 +103,8 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu } } - let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; - let argv = argv.unwrap(); + let mut argv = prepare_argv(argv)?; + if !argv.is_empty() { argv.remove(0); } for (arg, span) in argv { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { diff --git a/src/expand.rs b/src/expand.rs index 78e045f..36562d7 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -856,7 +856,7 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult { let mut io_stack = IoStack::new(); io_stack.push_frame(io_frame); - if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) { + if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false, Some("process_sub".into())) { e.print_error(); exit(1); } @@ -887,7 +887,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { match unsafe { fork()? } { ForkResult::Child => { io_stack.push_frame(cmd_sub_io_frame); - if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false) { + if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false, Some("command_sub".into())) { e.print_error(); unsafe { libc::_exit(1) }; } @@ -2092,7 +2092,7 @@ pub fn expand_aliases( } if let Some(alias) = log_tab.get_alias(&raw_tk) { - result.replace_range(tk.span.range(), &alias); + result.replace_range(tk.span.range(), &alias.to_string()); expanded_this_iter.push(raw_tk); } } diff --git a/src/jobs.rs b/src/jobs.rs index 48c1f5a..1dc9b6f 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -1,12 +1,15 @@ +use scopeguard::defer; + use crate::{ libsh::{ - error::ShResult, + error::{ShErr, ShErrKind, ShResult}, + sys::TTY_FILENO, term::{Style, Styled}, }, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, - state::{self, set_status, write_jobs}, + state::{self, ShellParam, set_status, write_jobs, write_vars}, }; pub const SIG_EXIT_OFFSET: i32 = 128; @@ -55,6 +58,21 @@ impl fmt::Display for DisplayWaitStatus { } } +pub fn code_from_status(stat: &WtStat) -> Option { + match stat { + WtStat::Exited(_, exit_code) => { + Some(*exit_code) + } + WtStat::Stopped(_, sig) => { + Some(SIG_EXIT_OFFSET + *sig as i32) + } + WtStat::Signaled(_, sig, _) => { + Some(SIG_EXIT_OFFSET + *sig as i32) + } + _ => { None } + } +} + #[derive(Clone, Debug)] pub enum JobID { Pgid(Pid), @@ -200,7 +218,7 @@ impl JobTab { } fn prune_jobs(&mut self) { while let Some(job) = self.jobs.last() { - if job.is_none() { + if job.is_none() || job.as_ref().unwrap().is_done() { self.jobs.pop(); } else { break; @@ -215,6 +233,7 @@ impl JobTab { self.next_open_pos() }; job.set_tabid(tab_pos); + let last_pid = job.children().last().map(|c| c.pid()); self.order.push(tab_pos); if !silent { write( @@ -227,6 +246,11 @@ impl JobTab { } else { self.jobs[tab_pos] = Some(job); } + + if let Some(pid) = last_pid { + write_vars(|v| v.set_param(ShellParam::LastJob, &pid.to_string())) + } + Ok(tab_pos) } pub fn order(&self) -> &[usize] { @@ -257,6 +281,25 @@ impl JobTab { }), } } + pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { + let Some(job) = self.query_mut(id.clone()) else { + return Ok(()) + }; + match id { + JobID::Pid(pid) => { + let Some(child) = job.children_mut().iter_mut().find(|c| c.pid() == pid) else { + return Ok(()) + }; + child.set_stat(stat); + } + JobID::Pgid(_) | + JobID::TableID(_) | + JobID::Command(_) => { + job.set_stats(stat); + } + } + Ok(()) + } pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { match identifier { // Match by process group ID @@ -315,6 +358,17 @@ impl JobTab { } Ok(()) } + pub fn wait_all_bg(&mut self) -> ShResult<()> { + disable_reaping(); + defer! { + enable_reaping(); + } + for job in self.jobs.iter_mut() { + let Some(job) = job else { continue }; + job.wait_pgrp()?; + } + Ok(()) + } pub fn remove_job(&mut self, id: JobID) -> Option { let tabid = self.query(id).map(|job| job.tabid().unwrap()); if let Some(tabid) = tabid { @@ -560,6 +614,12 @@ impl Job { pub fn children_mut(&mut self) -> &mut Vec { &mut self.children } + pub fn is_done(&self) -> bool { + self + .children + .iter() + .all(|chld| chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true)) + } pub fn killpg(&mut self, sig: Signal) -> ShResult<()> { let stat = match sig { Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), @@ -653,7 +713,9 @@ impl Job { let padding_count = symbol.len() + id.to_string().len() + 3; let padding = " ".repeat(padding_count); - let mut output = format!("[{}]{}\t", id + 1, symbol); + let mut output = String::new(); + let id_box = format!("[{}]{}", id + 1, symbol); + output.push_str(&format!("{id_box}\t")); for (i, cmd) in self.get_cmds().iter().enumerate() { let pid = if pids || init { let mut pid = self.get_pids().get(i).unwrap().to_string(); @@ -676,8 +738,12 @@ impl Job { }, _ => stat_line.styled(Style::Cyan), }; + if i != 0 { + let padding = " ".repeat(id_box.len() - 1); + stat_line = format!("{padding}{}", stat_line); + } if i != self.get_cmds().len() - 1 { - stat_line = format!("{} |", stat_line); + stat_line.push_str(" |"); } let stat_final = if long { @@ -698,7 +764,7 @@ impl Job { } pub fn term_ctlr() -> Pid { - tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp()) + tcgetpgrp(borrow_fd(*TTY_FILENO)).unwrap_or(getpgrp()) } /// Calls attach_tty() on the shell's process group to retake control of the @@ -712,6 +778,55 @@ pub fn take_term() -> ShResult<()> { Ok(()) } +pub fn wait_bg(id: JobID) -> ShResult<()> { + disable_reaping(); + defer! { + enable_reaping(); + }; + match id { + JobID::Pid(pid) => { + let stat = loop { + match waitpid(pid, None) { + Ok(stat) => break stat, + Err(Errno::EINTR) => continue, // Retry on signal interruption + Err(Errno::ECHILD) => return Ok(()), // No such child, treat as already reaped + Err(e) => return Err(e.into()), + } + }; + write_jobs(|j| j.update_by_id(id, stat))?; + set_status(code_from_status(&stat).unwrap_or(0)); + } + _ => { + let Some(mut job) = write_jobs(|j| j.remove_job(id.clone())) else { + return Err(ShErr::simple(ShErrKind::ExecFail, format!("wait: No such job with id {:?}", id))); + }; + let statuses = job.wait_pgrp()?; + let mut was_stopped = false; + let mut code = 0; + for status in statuses { + code = code_from_status(&status).unwrap_or(0); + match status { + WtStat::Stopped(_, _) => { + was_stopped = true; + } + WtStat::Signaled(_, sig, _) => { + if sig == Signal::SIGTSTP { + was_stopped = true; + } + } + _ => { /* Do nothing */ } + } + } + + if was_stopped { + write_jobs(|j| j.insert_job(job, false))?; + } + set_status(code); + } + } + Ok(()) +} + /// Waits on the current foreground job and updates the shell's last status code pub fn wait_fg(job: Job) -> ShResult<()> { if job.children().is_empty() { @@ -721,23 +836,22 @@ pub fn wait_fg(job: Job) -> ShResult<()> { let mut was_stopped = false; attach_tty(job.pgid())?; disable_reaping(); + defer! { + enable_reaping(); + } let statuses = write_jobs(|j| j.new_fg(job))?; for status in statuses { + code = code_from_status(&status).unwrap_or(0); match status { - WtStat::Exited(_, exit_code) => { - code = exit_code; - } - WtStat::Stopped(_, sig) => { + WtStat::Stopped(_, _) => { was_stopped = true; write_jobs(|j| j.fg_to_bg(status))?; - code = SIG_EXIT_OFFSET + sig as i32; } WtStat::Signaled(_, sig, _) => { if sig == Signal::SIGTSTP { was_stopped = true; write_jobs(|j| j.fg_to_bg(status))?; } - code = SIG_EXIT_OFFSET + sig as i32; } _ => { /* Do nothing */ } } @@ -750,7 +864,6 @@ pub fn wait_fg(job: Job) -> ShResult<()> { } take_term()?; set_status(code); - enable_reaping(); Ok(()) } @@ -766,7 +879,7 @@ pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> { pub fn attach_tty(pgid: Pid) -> ShResult<()> { // If we aren't attached to a terminal, the pgid already controls it, or the // process group does not exist Then return ok - if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() { + if !isatty(*TTY_FILENO).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() { return Ok(()); } @@ -783,7 +896,7 @@ pub fn attach_tty(pgid: Pid) -> ShResult<()> { pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&new_mask), Some(&mut mask_bkup))?; - let result = tcsetpgrp(borrow_fd(0), pgid); + let result = tcsetpgrp(borrow_fd(*TTY_FILENO), pgid); pthread_sigmask( SigmaskHow::SIG_SETMASK, @@ -794,7 +907,7 @@ pub fn attach_tty(pgid: Pid) -> ShResult<()> { match result { Ok(_) => Ok(()), Err(_e) => { - tcsetpgrp(borrow_fd(0), getpgrp())?; + tcsetpgrp(borrow_fd(*TTY_FILENO), getpgrp())?; Ok(()) } } diff --git a/src/libsh/error.rs b/src/libsh/error.rs index c7bda1a..2bbb933 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -5,6 +5,7 @@ use ariadne::Color; use ariadne::{Report, ReportKind}; use rand::TryRng; +use crate::procio::RedirGuard; use crate::{ libsh::term::{Style, Styled}, parse::lex::{Span, SpanSource}, @@ -44,7 +45,7 @@ impl ColorRng { pub fn last_color(&mut self) -> Color { if let Some(color) = self.last_color.take() { - return color; + color } else { let color = self.next().unwrap_or(Color::White); self.last_color = Some(color); @@ -78,6 +79,10 @@ pub fn last_color() -> Color { COLOR_RNG.with(|rng| rng.borrow_mut().last_color()) } +pub fn clear_color() { + COLOR_RNG.with(|rng| rng.borrow_mut().last_color = None); +} + pub trait ShResultExt { fn blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self; @@ -154,15 +159,25 @@ pub struct ShErr { src_span: Option, labels: Vec>, sources: Vec, - notes: Vec + notes: Vec, + + /// If we propagate through a redirect boundary, we take ownership of + /// the RedirGuard(s) so that redirections stay alive until the error + /// is printed. Multiple guards can accumulate as the error bubbles + /// through nested redirect scopes. + io_guards: Vec } impl ShErr { pub fn new(kind: ShErrKind, span: Span) -> Self { - Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![] } + Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![], io_guards: vec![] } } pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { - Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()] } + Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] } + } + pub fn with_redirs(mut self, guard: RedirGuard) -> Self { + self.io_guards.push(guard); + self } pub fn at(kind: ShErrKind, span: Span, msg: impl Into) -> Self { let color = last_color(); // use last_color to ensure the same color is used for the label and the message given @@ -178,12 +193,12 @@ impl ShErr { self.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) } pub fn blame(self, span: Span) -> Self { - let ShErr { kind, src_span: _, labels, sources, notes } = self; - Self { kind, src_span: Some(span), labels, sources, notes } + let ShErr { kind, src_span: _, labels, sources, notes, io_guards } = self; + Self { kind, src_span: Some(span), labels, sources, notes, io_guards } } pub fn try_blame(self, span: Span) -> Self { match self { - ShErr { kind, src_span: None, labels, sources, notes } => Self { kind, src_span: Some(span), labels, sources, notes }, + ShErr { kind, src_span: None, labels, sources, notes, io_guards } => Self { kind, src_span: Some(span), labels, sources, notes, io_guards }, _ => self } } @@ -197,23 +212,23 @@ impl ShErr { self } pub fn with_label(self, source: SpanSource, label: ariadne::Label) -> Self { - let ShErr { kind, src_span, mut labels, mut sources, notes } = self; + let ShErr { kind, src_span, mut labels, mut sources, notes, io_guards } = self; sources.push(source); labels.push(label); - Self { kind, src_span, labels, sources, notes } + Self { kind, src_span, labels, sources, notes, io_guards } } pub fn with_context(self, ctx: VecDeque<(SpanSource, ariadne::Label)>) -> Self { - let ShErr { kind, src_span, mut labels, mut sources, notes } = self; + let ShErr { kind, src_span, mut labels, mut sources, notes, io_guards } = self; for (src, label) in ctx { sources.push(src); labels.push(label); } - Self { kind, src_span, labels, sources, notes } + Self { kind, src_span, labels, sources, notes, io_guards } } pub fn with_note(self, note: impl Into) -> Self { - let ShErr { kind, src_span, labels, sources, mut notes } = self; + let ShErr { kind, src_span, labels, sources, mut notes, io_guards } = self; notes.push(note.into()); - Self { kind, src_span, labels, sources, notes } + Self { kind, src_span, labels, sources, notes, io_guards } } pub fn build_report(&self) -> Option> { let span = self.src_span.as_ref()?; @@ -313,8 +328,7 @@ pub enum ShErrKind { ResourceLimitExceeded, BadPermission, Errno(Errno), - FileNotFound, - CmdNotFound, + NotFound, ReadlineErr, // Not really errors, more like internal signals @@ -339,8 +353,7 @@ impl Display for ShErrKind { Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::BadPermission => "Bad Permissions", Self::Errno(e) => &format!("Errno: {}", e.desc()), - Self::FileNotFound => "File not found", - Self::CmdNotFound => "Command not found", + Self::NotFound => "Not Found", Self::CleanExit(_) => "", Self::FuncReturn(_) => "Syntax Error", Self::LoopContinue(_) => "Syntax Error", diff --git a/src/libsh/guards.rs b/src/libsh/guards.rs new file mode 100644 index 0000000..2b60c0c --- /dev/null +++ b/src/libsh/guards.rs @@ -0,0 +1,215 @@ +use std::cell::RefCell; +use std::collections::HashSet; +use std::os::fd::{BorrowedFd, RawFd}; + +use nix::sys::termios::{self, LocalFlags, Termios, tcgetattr, tcsetattr}; +use nix::unistd::isatty; +use scopeguard::guard; + +thread_local! { + static ORIG_TERMIOS: RefCell> = const { RefCell::new(None) }; +} + +use crate::parse::lex::Span; +use crate::procio::{IoFrame, borrow_fd}; +use crate::readline::term::get_win_size; +use crate::state::write_vars; + +use super::sys::TTY_FILENO; + +// ============================================================================ +// ScopeGuard — RAII variable scope management +// ============================================================================ + +pub fn scope_guard(args: Option>) -> impl Drop { + let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::>()); + write_vars(|v| v.descend(argv)); + guard((), |_| { + write_vars(|v| v.ascend()); + }) +} + +pub fn shared_scope_guard() -> impl Drop { + write_vars(|v| v.descend(None)); + guard((), |_| { + write_vars(|v| v.ascend()); + }) +} + +// ============================================================================ +// VarCtxGuard — RAII variable context cleanup +// ============================================================================ + +pub fn var_ctx_guard( + vars: HashSet, +) -> scopeguard::ScopeGuard, impl FnOnce(HashSet)> { + guard(vars, |vars| { + write_vars(|v| { + for var in &vars { + v.unset_var(var).ok(); + } + }); + }) +} + +// ============================================================================ +// RedirGuard — RAII I/O redirection restoration +// ============================================================================ + +#[derive(Debug)] +pub struct RedirGuard(pub(crate) IoFrame); + +impl RedirGuard { + pub(crate) fn new(frame: IoFrame) -> Self { + Self(frame) + } + pub fn persist(mut self) { + use nix::unistd::close; + if let Some(saved) = self.0.saved_io.take() { + close(saved.0).ok(); + close(saved.1).ok(); + close(saved.2).ok(); + } + } +} + +impl Drop for RedirGuard { + fn drop(&mut self) { + self.0.restore().ok(); + } +} + +// ============================================================================ +// RawModeGuard — RAII terminal raw mode management +// ============================================================================ + +pub fn raw_mode() -> RawModeGuard { + let orig = termios::tcgetattr(unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }) + .expect("Failed to get terminal attributes"); + let mut raw = orig.clone(); + termios::cfmakeraw(&mut raw); + // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals + raw.local_flags |= termios::LocalFlags::ISIG; + // Keep OPOST enabled so \n is translated to \r\n on output + raw.output_flags |= termios::OutputFlags::OPOST; + termios::tcsetattr( + unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, + termios::SetArg::TCSANOW, + &raw, + ) + .expect("Failed to set terminal to raw mode"); + + let (_cols, _rows) = get_win_size(*TTY_FILENO); + + ORIG_TERMIOS.with(|cell| *cell.borrow_mut() = Some(orig.clone())); + + RawModeGuard { + orig, + fd: *TTY_FILENO, + } +} + +pub struct RawModeGuard { + orig: termios::Termios, + fd: RawFd, +} + +impl RawModeGuard { + /// Disable raw mode temporarily for a specific operation + pub fn disable_for R, R>(&self, func: F) -> R { + unsafe { + let fd = BorrowedFd::borrow_raw(self.fd); + // Temporarily restore the original termios + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &self.orig) + .expect("Failed to temporarily disable raw mode"); + + // Run the function + let result = func(); + + // Re-enable raw mode + let mut raw = self.orig.clone(); + termios::cfmakeraw(&mut raw); + // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals + raw.local_flags |= termios::LocalFlags::ISIG; + // Keep OPOST enabled so \n is translated to \r\n on output + raw.output_flags |= termios::OutputFlags::OPOST; + termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode"); + + result + } + } + + pub fn with_cooked_mode(f: F) -> R + where + F: FnOnce() -> R, + { + let current = tcgetattr(borrow_fd(*TTY_FILENO)).expect("Failed to get terminal attributes"); + let orig = ORIG_TERMIOS.with(|cell| cell.borrow().clone()) + .expect("with_cooked_mode called before raw_mode()"); + tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig) + .expect("Failed to restore cooked mode"); + let res = f(); + tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, ¤t) + .expect("Failed to restore raw mode"); + res + } +} + +impl Drop for RawModeGuard { + fn drop(&mut self) { + unsafe { + let _ = termios::tcsetattr( + BorrowedFd::borrow_raw(self.fd), + termios::SetArg::TCSANOW, + &self.orig, + ); + } + } +} + +// ============================================================================ +// TermiosGuard — RAII termios state management +// ============================================================================ + +#[derive(Debug)] +pub struct TermiosGuard { + saved_termios: Option, +} + +impl TermiosGuard { + pub fn new(new_termios: Termios) -> Self { + let mut new = Self { + saved_termios: None, + }; + + if isatty(*TTY_FILENO).unwrap() { + let current_termios = termios::tcgetattr(std::io::stdin()).unwrap(); + new.saved_termios = Some(current_termios); + + termios::tcsetattr( + std::io::stdin(), + nix::sys::termios::SetArg::TCSANOW, + &new_termios, + ) + .unwrap(); + } + + new + } +} + +impl Default for TermiosGuard { + fn default() -> Self { + let mut termios_val = termios::tcgetattr(std::io::stdin()).unwrap(); + termios_val.local_flags &= !LocalFlags::ECHOCTL; + Self::new(termios_val) + } +} + +impl Drop for TermiosGuard { + fn drop(&mut self) { + if let Some(saved) = &self.saved_termios { + termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap(); + } + } +} diff --git a/src/libsh/mod.rs b/src/libsh/mod.rs index 820e4ab..c9bab3c 100644 --- a/src/libsh/mod.rs +++ b/src/libsh/mod.rs @@ -1,5 +1,6 @@ pub mod error; pub mod flog; +pub mod guards; pub mod sys; pub mod term; pub mod utils; diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs index 0a3e440..e84cc6b 100644 --- a/src/libsh/sys.rs +++ b/src/libsh/sys.rs @@ -1,52 +1,7 @@ use std::sync::LazyLock; -use termios::{LocalFlags, Termios}; - use crate::prelude::*; pub static TTY_FILENO: LazyLock = LazyLock::new(|| { open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty") }); - -#[derive(Debug)] -pub struct TermiosGuard { - saved_termios: Option, -} - -impl TermiosGuard { - pub fn new(new_termios: Termios) -> Self { - let mut new = Self { - saved_termios: None, - }; - - if isatty(*TTY_FILENO).unwrap() { - let current_termios = termios::tcgetattr(std::io::stdin()).unwrap(); - new.saved_termios = Some(current_termios); - - termios::tcsetattr( - std::io::stdin(), - nix::sys::termios::SetArg::TCSANOW, - &new_termios, - ) - .unwrap(); - } - - new - } -} - -impl Default for TermiosGuard { - fn default() -> Self { - let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); - termios.local_flags &= !LocalFlags::ECHOCTL; - Self::new(termios) - } -} - -impl Drop for TermiosGuard { - fn drop(&mut self) { - if let Some(saved) = &self.saved_termios { - termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap(); - } - } -} diff --git a/src/main.rs b/src/main.rs index fd213c9..5e5516d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use nix::unistd::read; use crate::builtin::trap::TrapTarget; -use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +use crate::libsh::error::{self, ShErr, ShErrKind, ShResult}; use crate::libsh::sys::TTY_FILENO; use crate::parse::execute::exec_input; use crate::prelude::*; @@ -112,7 +112,7 @@ fn main() -> ExitCode { if let Err(e) = if let Some(path) = args.script { run_script(path, args.script_args) } else if let Some(cmd) = args.command { - exec_input(cmd, None, false) + exec_input(cmd, None, false, None) } else { shed_interactive() } { @@ -120,8 +120,7 @@ fn main() -> ExitCode { }; if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) - && let Err(e) = exec_input(trap, None, false) - { + && let Err(e) = exec_input(trap, None, false, Some("trap".into())) { eprintln!("shed: error running EXIT trap: {e}"); } @@ -131,6 +130,7 @@ fn main() -> ExitCode { fn run_script>(path: P, args: Vec) -> ShResult<()> { let path = path.as_ref(); + let path_raw = path.to_string_lossy().to_string(); if !path.is_file() { eprintln!("shed: Failed to open input file: {}", path.display()); QUIT_CODE.store(1, Ordering::SeqCst); @@ -156,7 +156,7 @@ fn run_script>(path: P, args: Vec) -> ShResult<()> { write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) } - exec_input(input, None, false) + exec_input(input, None, false, Some(path_raw)) } fn shed_interactive() -> ShResult<()> { @@ -186,6 +186,7 @@ fn shed_interactive() -> ShResult<()> { m.try_rehash_commands(); m.try_rehash_cwd_listing(); }); + error::clear_color(); // Handle any pending signals while signals_pending() { @@ -265,7 +266,7 @@ fn shed_interactive() -> ShResult<()> { Ok(ReadlineEvent::Line(input)) => { let start = Instant::now(); write_meta(|m| m.start_timer()); - if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) { + if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("".into()))) { match e.kind() { ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst); diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 0debbd8..c2f6e61 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -7,12 +7,13 @@ use ariadne::Fmt; use crate::{ builtin::{ - alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak + alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak }, expand::{expand_aliases, glob_to_regex}, - jobs::{ChildProc, JobStack, dispatch_job}, + jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, libsh::{ error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, + guards::{scope_guard, var_ctx_guard}, utils::RedirVecUtils, }, prelude::*, @@ -66,48 +67,6 @@ pub fn is_in_path(name: &str) -> bool { } } -pub struct ScopeGuard; - -impl ScopeGuard { - pub fn exclusive_scope(args: Option>) -> Self { - let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::>()); - write_vars(|v| v.descend(argv)); - Self - } - pub fn shared_scope() -> Self { - // used in environments that inherit from the parent, like subshells - write_vars(|v| v.descend(None)); - Self - } -} - -impl Drop for ScopeGuard { - fn drop(&mut self) { - write_vars(|v| v.ascend()); - } -} - -/// Used to throw away variables that exist in temporary contexts -/// such as 'VAR=value ' -/// or for-loop variables -pub struct VarCtxGuard { - vars: HashSet, -} -impl VarCtxGuard { - pub fn new(vars: HashSet) -> Self { - Self { vars } - } -} -impl Drop for VarCtxGuard { - fn drop(&mut self) { - write_vars(|v| { - for var in &self.vars { - v.unset_var(var).ok(); - } - }); - } -} - pub enum AssignBehavior { Export, Set, @@ -151,7 +110,7 @@ impl ExecArgs { } } -pub fn exec_input(input: String, io_stack: Option, interactive: bool) -> ShResult<()> { +pub fn exec_input(input: String, io_stack: Option, interactive: bool, source_name: Option) -> ShResult<()> { let log_tab = read_logic(|l| l.clone()); let input = expand_aliases(input, HashSet::new(), &log_tab); let lex_flags = if interactive { @@ -159,7 +118,8 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - } else { super::lex::LexFlags::empty() }; - let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags); + let source_name = source_name.unwrap_or("".into()); + let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags).with_name(source_name.clone()); if let Err(errors) = parser.parse_src() { for error in errors { error.print_error(); @@ -169,7 +129,7 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - let nodes = parser.extract_nodes(); - let mut dispatcher = Dispatcher::new(nodes, interactive); + let mut dispatcher = Dispatcher::new(nodes, interactive, source_name.clone()); if let Some(mut stack) = io_stack { dispatcher.io_stack.extend(stack.drain(..)); } @@ -179,7 +139,7 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - && let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) { let saved_status = state::get_status(); - exec_input(trap, None, false)?; + exec_input(trap, None, false, Some(source_name))?; state::set_status(saved_status); } @@ -189,16 +149,18 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - pub struct Dispatcher { nodes: VecDeque, interactive: bool, + source_name: String, pub io_stack: IoStack, pub job_stack: JobStack, } impl Dispatcher { - pub fn new(nodes: Vec, interactive: bool) -> Self { + pub fn new(nodes: Vec, interactive: bool, source_name: String) -> Self { let nodes = VecDeque::from(nodes); Self { nodes, interactive, + source_name, io_stack: IoStack::new(), job_stack: JobStack::new(), } @@ -244,7 +206,7 @@ impl Dispatcher { let stack = IoStack { stack: self.io_stack.clone(), }; - exec_input(format!("cd {dir}"), Some(stack), self.interactive) + exec_input(format!("cd {dir}"), Some(stack), self.interactive, Some(self.source_name.clone())) } else { self.exec_cmd(node) } @@ -310,7 +272,7 @@ impl Dispatcher { return Ok(()); } - let func = ShFunc::new(func_parser); + let func = ShFunc::new(func_parser,blame); write_logic(|l| l.insert_func(name, func)); // Store the AST Ok(()) } @@ -319,6 +281,7 @@ impl Dispatcher { let NdRule::Command { assignments, argv } = subsh.class else { unreachable!() }; + let name = self.source_name.clone(); self.run_fork("anonymous_subshell", |s| { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { @@ -337,7 +300,7 @@ impl Dispatcher { let subsh = argv.remove(0); let subsh_body = subsh.0.to_string(); - if let Err(e) = exec_input(subsh_body, None, s.interactive) { + if let Err(e) = exec_input(subsh_body, None, s.interactive, Some(name)) { e.print_error(); }; }) @@ -371,7 +334,7 @@ impl Dispatcher { let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; let func_name = argv.remove(0).to_string(); - let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); + let _var_guard = var_ctx_guard(env_vars.into_iter().collect()); self.io_stack.append_to_frame(func.redirs); @@ -379,7 +342,7 @@ impl Dispatcher { let argv = prepare_argv(argv).try_blame(blame.clone())?; let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { - let _guard = ScopeGuard::exclusive_scope(Some(argv)); + let _guard = scope_guard(Some(argv)); func_body.body_mut().propagate_context(func_ctx); func_body.body_mut().flags = func.flags; @@ -412,7 +375,7 @@ impl Dispatcher { let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(brc_grp.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let guard = self.io_stack.pop_frame().redirect()?; let brc_grp_logic = |s: &mut Self| -> ShResult<()> { for node in body { let blame = node.get_span(); @@ -430,7 +393,7 @@ impl Dispatcher { } }) } else { - brc_grp_logic(self) + brc_grp_logic(self).map_err(|e| e.with_redirs(guard)) } } fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { @@ -446,7 +409,7 @@ impl Dispatcher { let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(case_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let guard = self.io_stack.pop_frame().redirect()?; let case_logic = |s: &mut Self| -> ShResult<()> { let exp_pattern = pattern.clone().expand()?; @@ -484,7 +447,7 @@ impl Dispatcher { } }) } else { - case_logic(self).try_blame(blame) + case_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) } } fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { @@ -502,7 +465,7 @@ impl Dispatcher { let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(loop_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let guard = self.io_stack.pop_frame().redirect()?; let loop_logic = |s: &mut Self| -> ShResult<()> { let CondNode { cond, body } = cond_node; @@ -547,7 +510,7 @@ impl Dispatcher { } }) } else { - loop_logic(self).try_blame(blame) + loop_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) } } fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { @@ -571,14 +534,14 @@ impl Dispatcher { }; self.io_stack.append_to_frame(for_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let guard = self.io_stack.pop_frame().redirect()?; let for_logic = |s: &mut Self| -> ShResult<()> { // Expand all array variables let arr: Vec = to_expanded_strings(arr)?; let vars: Vec = to_expanded_strings(vars)?; - let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect()); + let mut for_guard = var_ctx_guard(vars.iter().map(|v| v.to_string()).collect()); 'outer: for chunk in arr.chunks(vars.len()) { let empty = String::new(); @@ -594,7 +557,7 @@ impl Dispatcher { VarFlags::NONE, ) })?; - for_guard.vars.insert(var.to_string()); + for_guard.insert(var.to_string()); } for node in body.clone() { @@ -625,7 +588,7 @@ impl Dispatcher { } }) } else { - for_logic(self).try_blame(blame) + for_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) } } fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { @@ -640,7 +603,7 @@ impl Dispatcher { let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(if_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let guard = self.io_stack.pop_frame().redirect()?; let if_logic = |s: &mut Self| -> ShResult<()> { let mut matched = false; @@ -682,7 +645,7 @@ impl Dispatcher { } }) } else { - if_logic(self).try_blame(blame) + if_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) } } fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { @@ -696,6 +659,9 @@ impl Dispatcher { // Zip the commands and their respective pipes into an iterator let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); + let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); + let mut tty_attached = false; + for ((rpipe, wpipe), mut cmd) in pipes_and_cmds { if let Some(pipe) = rpipe { self.io_stack.push_to_frame(pipe); @@ -716,9 +682,18 @@ impl Dispatcher { cmd.flags |= NdFlags::FORK_BUILTINS; } self.dispatch_node(cmd)?; + + // Give the pipeline terminal control as soon as the first child + // establishes the PGID, so later children (e.g. nvim) don't get + // SIGTTOU when they try to modify terminal attributes. + if !tty_attached && !is_bg { + if let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() { + attach_tty(pgid).ok(); + tty_attached = true; + } + } } let job = self.job_stack.finalize_job().unwrap(); - let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND); dispatch_job(job, is_bg)?; Ok(()) } @@ -757,10 +732,9 @@ impl Dispatcher { unreachable!() }; let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; - let _var_guard = VarCtxGuard::new(env_vars.into_iter().collect()); + let _var_guard = var_ctx_guard(env_vars.into_iter().collect()); - let curr_job_mut = self.job_stack.curr_job_mut().unwrap(); - let io_stack_mut = &mut self.io_stack; + // Handle builtin/command recursion before redirect/job setup if cmd_raw.as_str() == "builtin" { *argv = argv .iter_mut() @@ -779,43 +753,71 @@ impl Dispatcher { } return self.exec_cmd(cmd); } + + // Set up redirections here so we can attach the guard to propagated errors. + self.io_stack.append_to_frame(mem::take(&mut cmd.redirs)); + let redir_guard = self.io_stack.pop_frame().redirect()?; + + // Register ChildProc in current job + let job = self.job_stack.curr_job_mut().unwrap(); + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let child = ChildProc::new(Pid::this(), Some(&cmd_raw), Some(child_pgid))?; + job.push_child(child); + + // Handle exec specially — persist redirections before dispatch + if cmd_raw.as_str() == "exec" { + redir_guard.persist(); + let result = exec::exec_builtin(cmd); + return if let Err(e) = result { + Err(e.with_context(context)) + } else { + Ok(()) + }; + } + let result = match cmd_raw.as_str() { - "echo" => echo(cmd, io_stack_mut, curr_job_mut), - "cd" => cd(cmd, curr_job_mut), - "export" => export(cmd, io_stack_mut, curr_job_mut), - "local" => local(cmd, io_stack_mut, curr_job_mut), - "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), - "source" => source(cmd, curr_job_mut), - "shift" => shift(cmd, curr_job_mut), - "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), - "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), - "disown" => disown(cmd, io_stack_mut, curr_job_mut), - "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), - "alias" => alias(cmd, io_stack_mut, curr_job_mut), - "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), + "echo" => echo(cmd), + "cd" => cd(cmd), + "export" => export(cmd), + "local" => local(cmd), + "pwd" => pwd(cmd), + "source" => source(cmd), + "shift" => shift(cmd), + "fg" => continue_job(cmd, JobBehavior::Foregound), + "bg" => continue_job(cmd, JobBehavior::Background), + "disown" => disown(cmd), + "jobs" => jobs(cmd), + "alias" => alias(cmd), + "unalias" => unalias(cmd), "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), - "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), - "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), - "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), - "trap" => trap(cmd, io_stack_mut, curr_job_mut), - "pushd" => pushd(cmd, io_stack_mut, curr_job_mut), - "popd" => popd(cmd, io_stack_mut, curr_job_mut), - "dirs" => dirs(cmd, io_stack_mut, curr_job_mut), - "exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), - "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), - "readonly" => readonly(cmd, io_stack_mut, curr_job_mut), - "unset" => unset(cmd, io_stack_mut, curr_job_mut), - "complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut), - "compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut), - "map" => map::map(cmd, io_stack_mut, curr_job_mut), - "pop" => arr_pop(cmd, io_stack_mut, curr_job_mut), - "fpop" => arr_fpop(cmd, io_stack_mut, curr_job_mut), - "push" => arr_push(cmd, io_stack_mut, curr_job_mut), - "fpush" => arr_fpush(cmd, io_stack_mut, curr_job_mut), - "rotate" => arr_rotate(cmd, io_stack_mut, curr_job_mut), + "zoltraak" => zoltraak(cmd), + "shopt" => shopt(cmd), + "read" => read_builtin(cmd), + "trap" => trap(cmd), + "pushd" => pushd(cmd), + "popd" => popd(cmd), + "dirs" => dirs(cmd), + "eval" => eval::eval(cmd), + "readonly" => readonly(cmd), + "unset" => unset(cmd), + "complete" => complete_builtin(cmd), + "compgen" => compgen_builtin(cmd), + "map" => map::map(cmd), + "pop" => arr_pop(cmd), + "fpop" => arr_fpop(cmd), + "push" => arr_push(cmd), + "fpush" => arr_fpush(cmd), + "rotate" => arr_rotate(cmd), + "wait" => jobctl::wait(cmd), + "type" => intro::type_builtin(cmd), "true" | ":" => { state::set_status(0); Ok(()) @@ -828,7 +830,7 @@ impl Dispatcher { }; if let Err(e) = result { - Err(e.with_context(context)) + Err(e.with_context(context).with_redirs(redir_guard)) } else { Ok(()) } @@ -861,8 +863,21 @@ impl Dispatcher { let exec_args = ExecArgs::new(argv).blame(blame)?; let _guard = self.io_stack.pop_frame().redirect()?; let job = self.job_stack.curr_job_mut().unwrap(); + let existing_pgid = job.pgid(); + + let child_logic = |pgid: Option| -> ! { + // Put ourselves in the correct process group before exec. + // For the first child in a pipeline pgid is None, so we + // become our own group leader (setpgid(0,0)). For later + // children we join the leader's group. + let _ = setpgid(Pid::from_raw(0), pgid.unwrap_or(Pid::from_raw(0))); + + // Reset signal dispositions before exec. SIG_IGN is preserved + // across execvpe, so the shell's ignored SIGTTIN/SIGTTOU would + // leak into child processes and break programs like nvim that + // need default terminal-stop behavior. + crate::signal::reset_signals(); - let child_logic = || -> ! { let cmd = &exec_args.cmd.0; let span = exec_args.cmd.1; @@ -872,7 +887,7 @@ impl Dispatcher { let cmd_str = cmd.to_str().unwrap().to_string(); match e { Errno::ENOENT => { - ShErr::new(ShErrKind::CmdNotFound, span.clone()) + ShErr::new(ShErrKind::NotFound, span.clone()) .labeled(span, format!("{cmd_str}: command not found")) .with_context(context) .print_error(); @@ -887,11 +902,11 @@ impl Dispatcher { }; if no_fork { - child_logic(); + child_logic(existing_pgid); } match unsafe { fork()? } { - ForkResult::Child => child_logic(), + ForkResult::Child => child_logic(existing_pgid), ForkResult::Parent { child } => { // Close proc sub pipe fds - the child has inherited them // and will access them via /proc/self/fd/N. Keeping them @@ -900,7 +915,7 @@ impl Dispatcher { let cmd_name = exec_args.cmd.0.to_str().unwrap(); - let child_pgid = if let Some(pgid) = job.pgid() { + let child_pgid = if let Some(pgid) = existing_pgid { pgid } else { job.set_pgid(child); @@ -918,15 +933,18 @@ impl Dispatcher { Ok(()) } fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { + let existing_pgid = self.job_stack.curr_job_mut().unwrap().pgid(); match unsafe { fork()? } { ForkResult::Child => { + let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0))); + crate::signal::reset_signals(); f(self); exit(state::get_status()) } ForkResult::Parent { child } => { write_jobs(|j| j.drain_registered_fds()); let job = self.job_stack.curr_job_mut().unwrap(); - let child_pgid = if let Some(pgid) = job.pgid() { + let child_pgid = if let Some(pgid) = existing_pgid { pgid } else { job.set_pgid(child); diff --git a/src/parse/lex.rs b/src/parse/lex.rs index a6d80e2..6a5937f 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -100,6 +100,9 @@ impl Span { pub fn new(range: Range, source: Arc) -> Self { let source = SpanSource { name: "".into(), content: source }; Span { range, source } + } + pub fn from_span_source(range: Range, source: SpanSource) -> Self { + Span { range, source } } pub fn rename(&mut self, name: String) { self.source.name = name; @@ -108,6 +111,12 @@ impl Span { self.source.name = name; self } + pub fn line_and_col(&self) -> (usize,usize) { + let content = self.source.content(); + let source = ariadne::Source::from(content.as_str()); + let (_, line, col) = source.get_byte_line(self.range.start).unwrap(); + (line, col) + } /// Slice the source string at the wrapped range pub fn as_str(&self) -> &str { &self.source.content[self.range().start..self.range().end] @@ -234,7 +243,9 @@ bitflags! { pub struct LexStream { source: Arc, pub cursor: usize, + pub name: String, quote_state: QuoteState, + brc_grp_depth: usize, brc_grp_start: Option, flags: LexFlags, } @@ -243,23 +254,21 @@ bitflags! { #[derive(Debug, Clone, Copy)] pub struct LexFlags: u32 { /// The lexer is operating in interactive mode - const INTERACTIVE = 0b000000001; + const INTERACTIVE = 0b0000000001; /// Allow unfinished input - const LEX_UNFINISHED = 0b000000010; + const LEX_UNFINISHED = 0b0000000010; /// The next string-type token is a command name - const NEXT_IS_CMD = 0b000000100; + const NEXT_IS_CMD = 0b0000000100; /// We are in a quotation, so quoting rules apply - const IN_QUOTE = 0b000001000; + const IN_QUOTE = 0b0000001000; /// Only lex strings; used in expansions - const RAW = 0b000010000; + const RAW = 0b0000010000; /// The lexer has not produced any tokens yet - const FRESH = 0b000010000; + const FRESH = 0b0000100000; /// The lexer has no more tokens to produce - const STALE = 0b000100000; - /// The lexer's cursor is in a brace group - const IN_BRC_GRP = 0b001000000; - const EXPECTING_IN = 0b010000000; - const IN_CASE = 0b100000000; + const STALE = 0b0001000000; + const EXPECTING_IN = 0b0010000000; + const IN_CASE = 0b0100000000; } } @@ -269,8 +278,10 @@ impl LexStream { Self { flags, source, + name: "".into(), cursor: 0, quote_state: QuoteState::default(), + brc_grp_depth: 0, brc_grp_start: None, } } @@ -296,18 +307,25 @@ impl LexStream { }; self.source.get(start..end) } + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } pub fn slice_from_cursor(&self) -> Option<&str> { self.slice(self.cursor..) } pub fn in_brc_grp(&self) -> bool { - self.flags.contains(LexFlags::IN_BRC_GRP) + self.brc_grp_depth > 0 } - pub fn set_in_brc_grp(&mut self, is: bool) { - if is { - self.flags |= LexFlags::IN_BRC_GRP; + pub fn enter_brc_grp(&mut self) { + if self.brc_grp_depth == 0 { self.brc_grp_start = Some(self.cursor); - } else { - self.flags &= !LexFlags::IN_BRC_GRP; + } + self.brc_grp_depth += 1; + } + pub fn leave_brc_grp(&mut self) { + self.brc_grp_depth -= 1; + if self.brc_grp_depth == 0 { self.brc_grp_start = None; } } @@ -627,7 +645,7 @@ impl LexStream { pos += 1; let mut tk = self.get_token(self.cursor..pos, TkRule::BraceGrpStart); tk.flags |= TkFlags::IS_CMD; - self.set_in_brc_grp(true); + self.enter_brc_grp(); self.set_next_is_cmd(true); self.cursor = pos; @@ -636,7 +654,7 @@ impl LexStream { '}' if pos == self.cursor && self.in_brc_grp() => { pos += 1; let tk = self.get_token(self.cursor..pos, TkRule::BraceGrpEnd); - self.set_in_brc_grp(false); + self.leave_brc_grp(); self.set_next_is_cmd(true); self.cursor = pos; return Ok(tk); @@ -731,7 +749,8 @@ impl LexStream { Ok(new_tk) } pub fn get_token(&self, range: Range, class: TkRule) -> Tk { - let span = Span::new(range, self.source.clone()); + let mut span = Span::new(range, self.source.clone()); + span.rename(self.name.clone()); Tk::new(class, span) } } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 7f6b314..1a6a0e6 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -44,6 +44,7 @@ macro_rules! try_match { #[derive(Clone, Debug)] pub struct ParsedSrc { pub src: Arc, + pub name: String, pub ast: Ast, pub lex_flags: LexFlags, pub context: LabelCtx, @@ -53,11 +54,16 @@ impl ParsedSrc { pub fn new(src: Arc) -> Self { Self { src, + name: "".into(), ast: Ast::new(vec![]), lex_flags: LexFlags::empty(), context: VecDeque::new(), } } + pub fn with_name(mut self, name: String) -> Self { + self.name = name; + self + } pub fn with_lex_flags(mut self, flags: LexFlags) -> Self { self.lex_flags = flags; self @@ -68,7 +74,7 @@ impl ParsedSrc { } pub fn parse_src(&mut self) -> Result<(), Vec> { let mut tokens = vec![]; - for lex_result in LexStream::new(self.src.clone(), self.lex_flags) { + for lex_result in LexStream::new(self.src.clone(), self.lex_flags).with_name(self.name.clone()) { match lex_result { Ok(token) => tokens.push(token), Err(error) => return Err(vec![error]), @@ -244,9 +250,9 @@ impl Node { unreachable!() }; - Span::new( + Span::from_span_source( first_tk.span.range().start..last_tk.span.range().end, - first_tk.span.get_source(), + first_tk.span.span_source().clone(), ) } } diff --git a/src/procio.rs b/src/procio.rs index 25a0fc4..b526e44 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -154,39 +154,19 @@ impl IoBuf { } } -pub struct RedirGuard(IoFrame); - -impl RedirGuard { - pub fn persist(mut self) { - if let Some(saved) = self.0.saved_io.take() { - close(saved.0).ok(); - close(saved.1).ok(); - close(saved.2).ok(); - } - - // the guard is dropped here - // but since we took the saved fds - // the drop does not restore them - } -} - -impl Drop for RedirGuard { - fn drop(&mut self) { - self.0.restore().ok(); - } -} +pub use crate::libsh::guards::RedirGuard; /// A struct wrapping three fildescs representing `stdin`, `stdout`, and /// `stderr` respectively #[derive(Debug, Clone)] -pub struct IoGroup(RawFd, RawFd, RawFd); +pub struct IoGroup(pub(crate) RawFd, pub(crate) RawFd, pub(crate) RawFd); /// A single stack frame used with the IoStack /// Each stack frame represents the redirections of a single command #[derive(Default, Clone, Debug)] pub struct IoFrame { pub redirs: Vec, - saved_io: Option, + pub(crate) saved_io: Option, } impl<'e> IoFrame { @@ -241,7 +221,7 @@ impl<'e> IoFrame { let src_fd = io_mode.src_fd(); dup2(src_fd, tgt_fd)?; } - Ok(RedirGuard(self)) + Ok(RedirGuard::new(self)) } pub fn restore(&mut self) -> ShResult<()> { if let Some(saved) = self.saved_io.take() { diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 1efb8ab..4f0e706 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -8,10 +8,11 @@ use crate::{ builtin::complete::{CompFlags, CompOptFlags, CompOpts}, libsh::{ error::ShResult, + guards::var_ctx_guard, utils::TkVecUtils, }, parse::{ - execute::{VarCtxGuard, exec_input}, + execute::exec_input, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, }, readline::{ @@ -341,7 +342,7 @@ impl BashCompSpec { ] { vars_to_unset.insert(var.to_string()); } - let _guard = VarCtxGuard::new(vars_to_unset); + let _guard = var_ctx_guard(vars_to_unset); let CompContext { words, @@ -391,7 +392,7 @@ impl BashCompSpec { "{} {cmd_name} {cword_str} {pword_str}", self.function.as_ref().unwrap() ); - exec_input(input, None, false)?; + exec_input(input, None, false, Some("comp_function".into()))?; Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default()) } @@ -532,7 +533,7 @@ impl Completer { (before_cursor, after_cursor) } - pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec, usize) { + pub fn get_subtoken_completion(&self, line: &str, cursor_pos: usize) -> (Vec, usize) { let annotated = annotate_input_recursive(line); let mut ctx = vec![markers::NULL]; let mut last_priority = 0; @@ -776,20 +777,19 @@ impl Completer { // Use marker-based context detection for sub-token awareness (e.g. VAR_SUB // inside a token). Run this before comp specs so variable completions take // priority over programmable completion. - let (mut marker_ctx, token_start) = self.get_completion_context(&line, cursor_pos); + let (mut marker_ctx, token_start) = self.get_subtoken_completion(&line, cursor_pos); - if marker_ctx.last() == Some(&markers::VAR_SUB) { - if let Some(cur) = ctx.words.get(ctx.cword) { - self.token_span.0 = token_start; - let mut span = cur.span.clone(); - span.set_range(token_start..self.token_span.1); - let raw_tk = span.as_str(); - let candidates = complete_vars(raw_tk); - if !candidates.is_empty() { - return Ok(CompResult::from_candidates(candidates)); - } - } - } + if marker_ctx.last() == Some(&markers::VAR_SUB) + && let Some(cur) = ctx.words.get(ctx.cword) { + self.token_span.0 = token_start; + let mut span = cur.span.clone(); + span.set_range(token_start..self.token_span.1); + let raw_tk = span.as_str(); + let candidates = complete_vars(raw_tk); + if !candidates.is_empty() { + return Ok(CompResult::from_candidates(candidates)); + } + } // Try programmable completion match self.try_comp_spec(&ctx)? { diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index bc28b46..5ac0a29 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -785,6 +785,10 @@ impl LineBuf { } (start, end) } + pub fn this_line_content(&mut self) -> Option<&str> { + let (start,end) = self.this_line_exclusive(); + self.slice(start..end) + } pub fn this_line(&mut self) -> (usize, usize) { let line_no = self.cursor_line_number(); self.line_bounds(line_no) @@ -2801,6 +2805,25 @@ impl LineBuf { Verb::InsertChar(ch) => { self.insert_at_cursor(ch); self.cursor.add(1); + let before = self.auto_indent_level; + if read_shopts(|o| o.prompt.auto_indent) + && let Some(line_content) = self.this_line_content() { + match line_content.trim() { + "esac" | "done" | "fi" | "}" => { + self.calc_indent_level(); + if self.auto_indent_level < before { + let delta = before - self.auto_indent_level; + let line_start = self.start_of_line(); + for _ in 0..delta { + if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { + self.remove(line_start); + } + } + } + } + _ => { /* nothing to see here */ } + } + } } Verb::Insert(string) => { self.push_str(&string); diff --git a/src/readline/term.rs b/src/readline/term.rs index 89bcddc..eaea139 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -10,52 +10,24 @@ use nix::{ errno::Errno, libc::{self}, poll::{self, PollFlags, PollTimeout}, - sys::termios::{self, tcgetattr, tcsetattr}, unistd::isatty, }; use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use vte::{Parser, Perform}; +pub use crate::libsh::guards::{RawModeGuard, raw_mode}; use crate::{ - libsh::{ - error::{ShErr, ShErrKind, ShResult}, - sys::TTY_FILENO, - }, + libsh::error::{ShErr, ShErrKind, ShResult}, readline::keys::{KeyCode, ModKeys}, state::read_shopts, }; use crate::{ - procio::borrow_fd, state::{read_meta, write_meta}, }; use super::keys::KeyEvent; -pub fn raw_mode() -> RawModeGuard { - let orig = termios::tcgetattr(unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }) - .expect("Failed to get terminal attributes"); - let mut raw = orig.clone(); - termios::cfmakeraw(&mut raw); - // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals - raw.local_flags |= termios::LocalFlags::ISIG; - // Keep OPOST enabled so \n is translated to \r\n on output - raw.output_flags |= termios::OutputFlags::OPOST; - termios::tcsetattr( - unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, - termios::SetArg::TCSANOW, - &raw, - ) - .expect("Failed to set terminal to raw mode"); - - let (_cols, _rows) = get_win_size(*TTY_FILENO); - - RawModeGuard { - orig, - fd: *TTY_FILENO, - } -} - pub type Row = u16; pub type Col = u16; @@ -325,65 +297,6 @@ impl Read for TermBuffer { } } -pub struct RawModeGuard { - orig: termios::Termios, - fd: RawFd, -} - -impl RawModeGuard { - /// Disable raw mode temporarily for a specific operation - pub fn disable_for R, R>(&self, func: F) -> R { - unsafe { - let fd = BorrowedFd::borrow_raw(self.fd); - // Temporarily restore the original termios - termios::tcsetattr(fd, termios::SetArg::TCSANOW, &self.orig) - .expect("Failed to temporarily disable raw mode"); - - // Run the function - let result = func(); - - // Re-enable raw mode - let mut raw = self.orig.clone(); - termios::cfmakeraw(&mut raw); - // Keep ISIG enabled so Ctrl+C/Ctrl+Z still generate signals - raw.local_flags |= termios::LocalFlags::ISIG; - // Keep OPOST enabled so \n is translated to \r\n on output - raw.output_flags |= termios::OutputFlags::OPOST; - termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode"); - - result - } - } - - pub fn with_cooked_mode(f: F) -> R - where - F: FnOnce() -> R, - { - let raw = tcgetattr(borrow_fd(*TTY_FILENO)).expect("Failed to get terminal attributes"); - let mut cooked = raw.clone(); - cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO; - cooked.input_flags |= termios::InputFlags::ICRNL; - tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &cooked) - .expect("Failed to set cooked mode"); - let res = f(); - tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &raw) - .expect("Failed to restore raw mode"); - res - } -} - -impl Drop for RawModeGuard { - fn drop(&mut self) { - unsafe { - let _ = termios::tcsetattr( - BorrowedFd::borrow_raw(self.fd), - termios::SetArg::TCSANOW, - &self.orig, - ); - } - } -} - // ============================================================================ // PollReader - non-blocking key reader using vte parser // ============================================================================ diff --git a/src/signal.rs b/src/signal.rs index 7f5ff40..b468c6e 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -57,7 +57,7 @@ pub fn check_signals() -> ShResult<()> { let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 }; let run_trap = |sig: Signal| -> ShResult<()> { if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) { - exec_input(command, None, false)?; + exec_input(command, None, false, Some("trap".into()))?; } Ok(()) }; @@ -149,6 +149,22 @@ pub fn sig_setup() { } } +/// Reset all signal dispositions to SIG_DFL. +/// Called in child processes before exec so that the shell's custom +/// handlers and SIG_IGN dispositions don't leak into child programs. +pub fn reset_signals() { + let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); + unsafe { + for sig in Signal::iterator() { + // SIGKILL and SIGSTOP can't be caught/changed + if sig == Signal::SIGKILL || sig == Signal::SIGSTOP { + continue; + } + let _ = sigaction(sig, &default); + } + } +} + extern "C" fn handle_signal(sig: libc::c_int) { SIGNALS.fetch_or(1 << sig, Ordering::SeqCst); } diff --git a/src/state.rs b/src/state.rs index 0c701b1..c5a5e07 100644 --- a/src/state.rs +++ b/src/state.rs @@ -20,7 +20,7 @@ use crate::{ }, parse::{ ConjunctNode, NdRule, Node, ParsedSrc, - lex::{LexFlags, LexStream, Tk}, + lex::{LexFlags, LexStream, Span, Tk}, }, prelude::*, readline::{ @@ -465,16 +465,31 @@ thread_local! { pub static SHED: Shed = Shed::new(); } +#[derive(Clone, Debug)] +pub struct ShAlias { + pub body: String, + pub source: Span +} + +impl Display for ShAlias { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.body) + } +} + /// A shell function /// /// Wraps the BraceGrp Node that forms the body of the function, and provides some helper methods to extract it from the parse tree #[derive(Clone, Debug)] -pub struct ShFunc(Node); +pub struct ShFunc { + pub body: Node, + pub source: Span +} impl ShFunc { - pub fn new(mut src: ParsedSrc) -> Self { + pub fn new(mut src: ParsedSrc, source: Span) -> Self { let body = Self::extract_brc_grp_hack(src.extract_nodes()); - Self(body) + Self{ body, source } } fn extract_brc_grp_hack(mut tree: Vec) -> Node { // FIXME: find a better way to do this @@ -487,10 +502,10 @@ impl ShFunc { *cmd } pub fn body(&self) -> &Node { - &self.0 + &self.body } pub fn body_mut(&mut self) -> &mut Node { - &mut self.0 + &mut self.body } } @@ -500,7 +515,7 @@ impl ShFunc { #[derive(Default, Clone, Debug)] pub struct LogTab { functions: HashMap, - aliases: HashMap, + aliases: HashMap, traps: HashMap, } @@ -529,13 +544,13 @@ impl LogTab { pub fn funcs(&self) -> &HashMap { &self.functions } - pub fn aliases(&self) -> &HashMap { + pub fn aliases(&self) -> &HashMap { &self.aliases } - pub fn insert_alias(&mut self, name: &str, body: &str) { - self.aliases.insert(name.into(), body.into()); + pub fn insert_alias(&mut self, name: &str, body: &str, source: Span) { + self.aliases.insert(name.into(), ShAlias { body: body.into(), source }); } - pub fn get_alias(&self, name: &str) -> Option { + pub fn get_alias(&self, name: &str) -> Option { self.aliases.get(name).cloned() } pub fn remove_alias(&mut self, name: &str) { @@ -1140,6 +1155,29 @@ impl MetaTab { pub fn remove_comp_spec(&mut self, cmd: &str) -> bool { self.comp_specs.remove(cmd).is_some() } + pub fn get_cmds_in_path() -> Vec { + let path = env::var("PATH").unwrap_or_default(); + let paths = path.split(":").map(PathBuf::from); + let mut cmds = vec![]; + for path in paths { + if let Ok(entries) = path.read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; + let is_exec = meta.permissions().mode() & 0o111 != 0; + + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + { + cmds.push(name.to_string()); + } + } + } + } + cmds + } pub fn try_rehash_commands(&mut self) { let path = env::var("PATH").unwrap_or_default(); let cwd = env::var("PWD").unwrap_or_default(); @@ -1155,25 +1193,10 @@ impl MetaTab { self.path_cache.clear(); self.old_path = Some(path.clone()); self.old_pwd = Some(cwd.clone()); - let paths = path.split(":").map(PathBuf::from); - - for path in paths { - if let Ok(entries) = path.read_dir() { - for entry in entries.flatten() { - let Ok(meta) = std::fs::metadata(entry.path()) else { - continue; - }; - let is_exec = meta.permissions().mode() & 0o111 != 0; - - if meta.is_file() - && is_exec - && let Some(name) = entry.file_name().to_str() - { - self.path_cache.insert(name.to_string()); - } - } - } - } + let cmds_in_path = Self::get_cmds_in_path(); + for cmd in cmds_in_path { + self.path_cache.insert(cmd); + } if let Ok(entries) = Path::new(&cwd).read_dir() { for entry in entries.flatten() { let Ok(meta) = std::fs::metadata(entry.path()) else { @@ -1432,10 +1455,11 @@ pub fn source_rc() -> ShResult<()> { } pub fn source_file(path: PathBuf) -> ShResult<()> { + let source_name = path.to_string_lossy().to_string(); let mut file = OpenOptions::new().read(true).open(path)?; let mut buf = String::new(); file.read_to_string(&mut buf)?; - exec_input(buf, None, false)?; + exec_input(buf, None, false, Some(source_name))?; Ok(()) }