implemented 'type' and 'wait' builtins

fixed some tcsetpgrp() misbehavior

fixed not being able to redirect stderr from builtins
This commit is contained in:
2026-03-01 17:14:48 -05:00
parent 84aed128d6
commit 2ea44c55e9
38 changed files with 922 additions and 635 deletions

7
Cargo.lock generated
View File

@@ -533,6 +533,12 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "semver" name = "semver"
version = "1.0.27" version = "1.0.27"
@@ -596,6 +602,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"rand", "rand",
"regex", "regex",
"scopeguard",
"serde_json", "serde_json",
"tempfile", "tempfile",
"unicode-segmentation", "unicode-segmentation",

View File

@@ -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"] } nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] }
rand = "0.10.0" rand = "0.10.0"
regex = "1.11.1" regex = "1.11.1"
scopeguard = "1.2.0"
serde_json = "1.0.149" serde_json = "1.0.149"
unicode-segmentation = "1.12.0" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0" unicode-width = "0.2.0"

View File

@@ -1,17 +1,14 @@
use ariadne::Fmt; use ariadne::Fmt;
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, next_color}, libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state::{self, read_logic, write_logic}, state::{self, read_logic, write_logic},
}; };
use super::setup_builtin; pub fn alias(node: Node) -> ShResult<()> {
pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -20,8 +17,8 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // 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 { let Some((name, body)) = arg.split_once('=') else {
return Err(ShErr::at(ShErrKind::SyntaxErr, span, "alias: Expected an assignment in alias args")); 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); state::set_status(0);
Ok(()) Ok(())
} }
pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn unalias(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -62,8 +59,8 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // Display the environment variables

View File

@@ -1,13 +1,9 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use ariadne::Span;
use crate::{ 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<OptSpec> { fn arr_op_optspec() -> Vec<OptSpec> {
vec![ vec![
OptSpec { OptSpec {
@@ -44,7 +40,7 @@ impl Default for ArrOpOpts {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum End { Front, Back } 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 { let NdRule::Command {
assignments: _, assignments: _,
argv, 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 (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?; let arr_op_opts = get_arr_op_opts(opts)?;
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
let mut status = 0; let mut status = 0;
@@ -85,7 +81,7 @@ fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End
Ok(()) 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, 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 (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let _arr_op_opts = get_arr_op_opts(opts)?; let _arr_op_opts = get_arr_op_opts(opts)?;
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
let Some((name, _)) = argv.next() else { 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(()) Ok(())
} }
pub fn arr_pop(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn arr_pop(node: Node) -> ShResult<()> {
arr_pop_inner(node, io_stack, job, End::Back) arr_pop_inner(node, End::Back)
} }
pub fn arr_fpop(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn arr_fpop(node: Node) -> ShResult<()> {
arr_pop_inner(node, io_stack, job, End::Front) arr_pop_inner(node, End::Front)
} }
pub fn arr_push(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn arr_push(node: Node) -> ShResult<()> {
arr_push_inner(node, io_stack, job, End::Back) arr_push_inner(node, End::Back)
} }
pub fn arr_fpush(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn arr_fpush(node: Node) -> ShResult<()> {
arr_push_inner(node, io_stack, job, End::Front) 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 { let NdRule::Command {
assignments: _, assignments: _,
argv, 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 (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?; let arr_op_opts = get_arr_op_opts(opts)?;
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
for (arg, _) in argv { for (arg, _) in argv {
write_vars(|v| -> ShResult<()> { write_vars(|v| -> ShResult<()> {

View File

@@ -2,16 +2,13 @@ use ariadne::Fmt;
use yansi::Color; use yansi::Color;
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, next_color}, libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
state::{self}, state::{self},
}; };
use super::setup_builtin; pub fn cd(node: Node) -> ShResult<()> {
pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span(); let span = node.get_span();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -22,8 +19,8 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
}; };
let cd_span = argv.first().unwrap().span.clone(); let cd_span = argv.first().unwrap().span.clone();
let (argv, _) = setup_builtin(Some(argv), job, None)?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() { let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() {
(PathBuf::from(arg),Some(span)) (PathBuf::from(arg),Some(span))

View File

@@ -2,12 +2,10 @@ use bitflags::bitflags;
use nix::{libc::STDOUT_FILENO, unistd::write}; use nix::{libc::STDOUT_FILENO, unistd::write};
use crate::{ use crate::{
builtin::setup_builtin,
getopt::{Opt, OptSpec, get_opts_from_tokens}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
procio::{IoStack, borrow_fd}, procio::borrow_fd,
readline::complete::{BashCompSpec, CompContext, CompSpec}, readline::complete::{BashCompSpec, CompContext, CompSpec},
state::{self, read_meta, write_meta}, state::{self, read_meta, write_meta},
}; };
@@ -149,7 +147,7 @@ pub struct CompOpts {
pub opt_flags: CompOptFlags, 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, 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 (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(opts)?; let comp_opts = get_comp_opts(opts)?;
let (argv, _) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if comp_opts.flags.contains(CompFlags::PRINT) { if comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() { if argv.is_empty() {
@@ -219,7 +217,7 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
Ok(()) 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 _blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, 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 (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
let prefix = argv.clone().into_iter().nth(1).unwrap_or_default(); let prefix = argv.clone().into_iter().nth(1).unwrap_or_default();
let comp_opts = get_comp_opts(opts)?; 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); let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);

View File

@@ -5,11 +5,9 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use yansi::Color; use yansi::Color;
use crate::{ use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, next_color}, libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node, lex::Span}, parse::{NdRule, Node, execute::prepare_argv, lex::Span},
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state::{self, read_meta, write_meta}, state::{self, read_meta, write_meta},
}; };
@@ -103,7 +101,7 @@ fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult<StackIdx> {
} }
} }
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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -113,8 +111,8 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut dir = None; let mut dir = None;
let mut rotate_idx = None; let mut rotate_idx = None;
@@ -184,7 +182,7 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
Ok(()) 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -194,8 +192,8 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut remove_idx = None; let mut remove_idx = None;
let mut no_cd = false; let mut no_cd = false;
@@ -276,7 +274,7 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(()) 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -286,8 +284,8 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut abbreviate_home = true; let mut abbreviate_home = true;
let mut one_per_line = false; let mut one_per_line = false;

View File

@@ -1,12 +1,10 @@
use crate::{ use crate::{
builtin::setup_builtin,
expand::expand_prompt, expand::expand_prompt,
getopt::{Opt, OptSpec, get_opts_from_tokens}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state, 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -51,8 +49,8 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
assert!(!argv.is_empty()); assert!(!argv.is_empty());
let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?;
let flags = get_echo_flags(opts).blame(blame)?; let flags = get_echo_flags(opts).blame(blame)?;
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let output_channel = if flags.contains(EchoFlags::USE_STDERR) { let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
borrow_fd(STDERR_FILENO) borrow_fd(STDERR_FILENO)

View File

@@ -1,13 +1,10 @@
use crate::{ use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node, execute::exec_input}, parse::{NdRule, Node, execute::{exec_input, prepare_argv}},
procio::IoStack,
state, state,
}; };
pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn eval(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -16,8 +13,8 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() unreachable!()
}; };
let (expanded_argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut expanded_argv = prepare_argv(argv)?;
let expanded_argv = expanded_argv.unwrap(); if !expanded_argv.is_empty() { expanded_argv.remove(0); }
if expanded_argv.is_empty() { if expanded_argv.is_empty() {
state::set_status(0); state::set_status(0);
@@ -30,5 +27,5 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
exec_input(joined_argv, None, false) exec_input(joined_argv, None, false, Some("eval".into()))
} }

View File

@@ -1,15 +1,12 @@
use nix::{errno::Errno, unistd::execvpe}; use nix::{errno::Errno, unistd::execvpe};
use crate::{ use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, execute::ExecArgs}, parse::{NdRule, Node, execute::{ExecArgs, prepare_argv}},
procio::IoStack,
state, 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 { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -18,13 +15,8 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh
unreachable!() unreachable!()
}; };
let (expanded_argv, guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut expanded_argv = prepare_argv(argv)?;
let expanded_argv = expanded_argv.unwrap(); if !expanded_argv.is_empty() { expanded_argv.remove(0); }
if let Some(g) = guard {
// Persist redirections so they affect the entire shell,
// not just this command call
g.persist()
}
if expanded_argv.is_empty() { if expanded_argv.is_empty() {
state::set_status(0); 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(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => Err( Errno::ENOENT => Err(
ShErr::new(ShErrKind::CmdNotFound, span.clone()) ShErr::new(ShErrKind::NotFound, span.clone())
.labeled(span, format!("exec: command not found: {}", cmd_str)) .labeled(span, format!("exec: command not found: {}", cmd_str))
), ),
_ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))), _ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))),

73
src/builtin/intro.rs Normal file
View File

@@ -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::<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()
&& 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(())
}

View File

@@ -1,22 +1,20 @@
use ariadne::Fmt; use ariadne::Fmt;
use crate::{ use crate::{
jobs::{JobBldr, JobCmdFlags, JobID}, jobs::{JobCmdFlags, JobID, wait_bg},
libsh::error::{ShErr, ShErrKind, ShResult, next_color}, libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node, lex::Span}, parse::{NdRule, Node, execute::prepare_argv, lex::Span},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state::{self, read_jobs, write_jobs}, state::{self, read_jobs, write_jobs},
}; };
use super::setup_builtin;
pub enum JobBehavior { pub enum JobBehavior {
Foregound, Foregound,
Background, 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 blame = node.get_span().clone();
let cmd_tk = node.get_command(); let cmd_tk = node.get_command();
let cmd_span = cmd_tk.unwrap().span.clone(); 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!() unreachable!()
}; };
let (argv, _) = setup_builtin(Some(argv), job, None)?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) { if read_jobs(|j| j.get_fg().is_some()) {
@@ -84,7 +82,12 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
if arg.starts_with('%') { if arg.starts_with('%') {
let arg = arg.strip_prefix('%').unwrap(); let arg = arg.strip_prefix('%').unwrap();
if arg.chars().all(|ch| ch.is_ascii_digit()) { if arg.chars().all(|ch| ch.is_ascii_digit()) {
Ok(arg.parse::<usize>().unwrap()) let num = arg.parse::<usize>().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 { } else {
let result = write_jobs(|j| { let result = write_jobs(|j| {
let query_result = j.query(JobID::Command(arg.into())); let query_result = j.query(JobID::Command(arg.into()));
@@ -119,7 +122,7 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
} }
} }
pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn jobs(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -128,8 +131,8 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut flags = JobCmdFlags::empty(); let mut flags = JobCmdFlags::empty();
for (arg, span) in argv { for (arg, span) in argv {
@@ -158,7 +161,45 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(()) 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::<i32>().unwrap())))
} else {
Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?))
}
})
.collect::<ShResult<Vec<JobID>>>()?;
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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -168,8 +209,8 @@ pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {

View File

@@ -5,7 +5,7 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::{ 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)] #[derive(Debug, Clone)]
@@ -195,8 +195,6 @@ impl MapNode {
} }
} }
use super::setup_builtin;
fn map_opts_spec() -> [OptSpec; 6] { fn map_opts_spec() -> [OptSpec; 6] {
[ [
OptSpec { 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 { let NdRule::Command {
assignments: _, assignments: _,
argv, 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 (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?;
let map_opts = get_map_opts(opts); let map_opts = get_map_opts(opts);
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
if !argv.is_empty() { if !argv.is_empty() {
argv.remove(0); // remove "map" command from argv argv.remove(0); // remove "map" command from argv
} }

View File

@@ -1,14 +1,5 @@
use nix::unistd::Pid;
use crate::{ use crate::{
jobs::{ChildProc, JobBldr},
libsh::error::ShResult, libsh::error::ShResult,
parse::{
Redir,
execute::prepare_argv,
lex::{Span, Tk},
},
procio::{IoStack, RedirGuard},
state, state,
}; };
@@ -32,77 +23,15 @@ pub mod varcmds;
pub mod zoltraak; pub mod zoltraak;
pub mod map; pub mod map;
pub mod arrops; 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", "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "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<Vec<(String, Span)>>, Option<RedirGuard>)>;
pub fn setup_builtin(
argv: Option<Vec<Tk>>,
job: &mut JobBldr,
io_mode: Option<(&mut IoStack, Vec<Redir>)>,
) -> 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<()> { pub fn true_builtin() -> ShResult<()> {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -1,25 +1,20 @@
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state, state,
}; };
use super::setup_builtin; pub fn pwd(node: Node) -> ShResult<()> {
pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv: _,
} = node.class } = node.class
else { else {
unreachable!() unreachable!()
}; };
let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?;
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string(); let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string();

View File

@@ -6,12 +6,10 @@ use nix::{
}; };
use crate::{ use crate::{
builtin::setup_builtin,
getopt::{Opt, OptSpec, get_opts_from_tokens}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
procio::{IoStack, borrow_fd}, procio::borrow_fd,
readline::term::RawModeGuard, readline::term::RawModeGuard,
state::{self, VarFlags, VarKind, read_vars, write_vars}, state::{self, VarFlags, VarKind, read_vars, write_vars},
}; };
@@ -63,7 +61,7 @@ pub struct ReadOpts {
flags: ReadFlags, 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, 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 (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?;
let read_opts = get_read_flags(opts).blame(blame.clone())?; let read_opts = get_read_flags(opts).blame(blame.clone())?;
let (argv, _) = setup_builtin(Some(argv), job, None).blame(blame.clone())?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if let Some(prompt) = read_opts.prompt { if let Some(prompt) = read_opts.prompt {
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;

View File

@@ -1,13 +1,10 @@
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
state::{self, write_vars}, state::{self, write_vars},
}; };
use super::setup_builtin; pub fn shift(node: Node) -> ShResult<()> {
pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -16,8 +13,8 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv, _) = setup_builtin(Some(argv), job, None)?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if let Some((arg, span)) = argv.next() { if let Some((arg, span)) = argv.next() {

View File

@@ -1,15 +1,12 @@
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::{ShResult, ShResultExt}, libsh::error::{ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state::{self, write_shopts}, state::{self, write_shopts},
}; };
use super::setup_builtin; pub fn shopt(node: Node) -> ShResult<()> {
pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -18,8 +15,8 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if argv.is_empty() { if argv.is_empty() {
let mut output = write_shopts(|s| s.display_opts())?; let mut output = write_shopts(|s| s.display_opts())?;

View File

@@ -1,14 +1,11 @@
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
state::{self, source_file}, state::{self, source_file},
}; };
use super::setup_builtin; pub fn source(node: Node) -> ShResult<()> {
pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -17,8 +14,8 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv, _) = setup_builtin(Some(argv), job, None)?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
for (arg, span) in argv { for (arg, span) in argv {
let path = PathBuf::from(arg); let path = PathBuf::from(arg);

View File

@@ -7,11 +7,9 @@ use nix::{
}; };
use crate::{ use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state::{self, read_logic, write_logic}, 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 { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -123,8 +121,8 @@ pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if argv.is_empty() { if argv.is_empty() {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);

View File

@@ -1,15 +1,12 @@
use crate::{ use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, lex::split_tk_at}, parse::{NdRule, Node, execute::prepare_argv, lex::split_tk_at},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
state::{self, VarFlags, VarKind, read_vars, write_vars}, state::{self, VarFlags, VarKind, read_vars, write_vars},
}; };
use super::setup_builtin; pub fn readonly(node: Node) -> ShResult<()> {
pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -18,8 +15,6 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
unreachable!() unreachable!()
}; };
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
// Remove "readonly" from argv // Remove "readonly" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &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(()) 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 blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
@@ -71,8 +66,8 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() unreachable!()
}; };
let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
if argv.is_empty() { if argv.is_empty() {
return Err(ShErr::at(ShErrKind::SyntaxErr, blame, "unset: Expected at least one argument")); 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(()) Ok(())
} }
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn export(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -98,8 +93,6 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
unreachable!() unreachable!()
}; };
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
// Remove "export" from argv // Remove "export" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &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(()) Ok(())
} }
pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn local(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -143,8 +136,6 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() unreachable!()
}; };
let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?;
// Remove "local" from argv // Remove "local" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] };

View File

@@ -2,15 +2,12 @@ use std::os::unix::fs::OpenOptionsExt;
use crate::{ use crate::{
getopt::{Opt, OptSpec, get_opts_from_tokens}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::borrow_fd,
}; };
use super::setup_builtin;
bitflags! { bitflags! {
#[derive(Clone,Copy,Debug,PartialEq,Eq)] #[derive(Clone,Copy,Debug,PartialEq,Eq)]
struct ZoltFlags: u32 { struct ZoltFlags: u32 {
@@ -29,7 +26,7 @@ bitflags! {
/// The file given as an argument is completely destroyed. The command works by /// 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 /// 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. /// 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 { let NdRule::Command {
assignments: _, assignments: _,
argv, 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 mut argv = prepare_argv(argv)?;
let argv = argv.unwrap(); if !argv.is_empty() { argv.remove(0); }
for (arg, span) in argv { for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {

View File

@@ -856,7 +856,7 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
let mut io_stack = IoStack::new(); let mut io_stack = IoStack::new();
io_stack.push_frame(io_frame); 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(); e.print_error();
exit(1); exit(1);
} }
@@ -887,7 +887,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
io_stack.push_frame(cmd_sub_io_frame); 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(); e.print_error();
unsafe { libc::_exit(1) }; unsafe { libc::_exit(1) };
} }
@@ -2092,7 +2092,7 @@ pub fn expand_aliases(
} }
if let Some(alias) = log_tab.get_alias(&raw_tk) { 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); expanded_this_iter.push(raw_tk);
} }
} }

View File

@@ -1,12 +1,15 @@
use scopeguard::defer;
use crate::{ use crate::{
libsh::{ libsh::{
error::ShResult, error::{ShErr, ShErrKind, ShResult},
sys::TTY_FILENO,
term::{Style, Styled}, term::{Style, Styled},
}, },
prelude::*, prelude::*,
procio::{IoMode, borrow_fd}, procio::{IoMode, borrow_fd},
signal::{disable_reaping, enable_reaping}, 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; pub const SIG_EXIT_OFFSET: i32 = 128;
@@ -55,6 +58,21 @@ impl fmt::Display for DisplayWaitStatus {
} }
} }
pub fn code_from_status(stat: &WtStat) -> Option<i32> {
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)] #[derive(Clone, Debug)]
pub enum JobID { pub enum JobID {
Pgid(Pid), Pgid(Pid),
@@ -200,7 +218,7 @@ impl JobTab {
} }
fn prune_jobs(&mut self) { fn prune_jobs(&mut self) {
while let Some(job) = self.jobs.last() { while let Some(job) = self.jobs.last() {
if job.is_none() { if job.is_none() || job.as_ref().unwrap().is_done() {
self.jobs.pop(); self.jobs.pop();
} else { } else {
break; break;
@@ -215,6 +233,7 @@ impl JobTab {
self.next_open_pos() self.next_open_pos()
}; };
job.set_tabid(tab_pos); job.set_tabid(tab_pos);
let last_pid = job.children().last().map(|c| c.pid());
self.order.push(tab_pos); self.order.push(tab_pos);
if !silent { if !silent {
write( write(
@@ -227,6 +246,11 @@ impl JobTab {
} else { } else {
self.jobs[tab_pos] = Some(job); 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) Ok(tab_pos)
} }
pub fn order(&self) -> &[usize] { 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> { pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> {
match identifier { match identifier {
// Match by process group ID // Match by process group ID
@@ -315,6 +358,17 @@ impl JobTab {
} }
Ok(()) 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<Job> { pub fn remove_job(&mut self, id: JobID) -> Option<Job> {
let tabid = self.query(id).map(|job| job.tabid().unwrap()); let tabid = self.query(id).map(|job| job.tabid().unwrap());
if let Some(tabid) = tabid { if let Some(tabid) = tabid {
@@ -560,6 +614,12 @@ impl Job {
pub fn children_mut(&mut self) -> &mut Vec<ChildProc> { pub fn children_mut(&mut self) -> &mut Vec<ChildProc> {
&mut self.children &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<()> { pub fn killpg(&mut self, sig: Signal) -> ShResult<()> {
let stat = match sig { let stat = match sig {
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), 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_count = symbol.len() + id.to_string().len() + 3;
let padding = " ".repeat(padding_count); 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() { for (i, cmd) in self.get_cmds().iter().enumerate() {
let pid = if pids || init { let pid = if pids || init {
let mut pid = self.get_pids().get(i).unwrap().to_string(); let mut pid = self.get_pids().get(i).unwrap().to_string();
@@ -676,8 +738,12 @@ impl Job {
}, },
_ => stat_line.styled(Style::Cyan), _ => 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 { if i != self.get_cmds().len() - 1 {
stat_line = format!("{} |", stat_line); stat_line.push_str(" |");
} }
let stat_final = if long { let stat_final = if long {
@@ -698,7 +764,7 @@ impl Job {
} }
pub fn term_ctlr() -> Pid { 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 /// Calls attach_tty() on the shell's process group to retake control of the
@@ -712,6 +778,55 @@ pub fn take_term() -> ShResult<()> {
Ok(()) 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 /// Waits on the current foreground job and updates the shell's last status code
pub fn wait_fg(job: Job) -> ShResult<()> { pub fn wait_fg(job: Job) -> ShResult<()> {
if job.children().is_empty() { if job.children().is_empty() {
@@ -721,23 +836,22 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
let mut was_stopped = false; let mut was_stopped = false;
attach_tty(job.pgid())?; attach_tty(job.pgid())?;
disable_reaping(); disable_reaping();
defer! {
enable_reaping();
}
let statuses = write_jobs(|j| j.new_fg(job))?; let statuses = write_jobs(|j| j.new_fg(job))?;
for status in statuses { for status in statuses {
code = code_from_status(&status).unwrap_or(0);
match status { match status {
WtStat::Exited(_, exit_code) => { WtStat::Stopped(_, _) => {
code = exit_code;
}
WtStat::Stopped(_, sig) => {
was_stopped = true; was_stopped = true;
write_jobs(|j| j.fg_to_bg(status))?; write_jobs(|j| j.fg_to_bg(status))?;
code = SIG_EXIT_OFFSET + sig as i32;
} }
WtStat::Signaled(_, sig, _) => { WtStat::Signaled(_, sig, _) => {
if sig == Signal::SIGTSTP { if sig == Signal::SIGTSTP {
was_stopped = true; was_stopped = true;
write_jobs(|j| j.fg_to_bg(status))?; write_jobs(|j| j.fg_to_bg(status))?;
} }
code = SIG_EXIT_OFFSET + sig as i32;
} }
_ => { /* Do nothing */ } _ => { /* Do nothing */ }
} }
@@ -750,7 +864,6 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
} }
take_term()?; take_term()?;
set_status(code); set_status(code);
enable_reaping();
Ok(()) Ok(())
} }
@@ -766,7 +879,7 @@ pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> {
pub fn attach_tty(pgid: Pid) -> ShResult<()> { pub fn attach_tty(pgid: Pid) -> ShResult<()> {
// If we aren't attached to a terminal, the pgid already controls it, or the // If we aren't attached to a terminal, the pgid already controls it, or the
// process group does not exist Then return ok // 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(()); 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))?; 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( pthread_sigmask(
SigmaskHow::SIG_SETMASK, SigmaskHow::SIG_SETMASK,
@@ -794,7 +907,7 @@ pub fn attach_tty(pgid: Pid) -> ShResult<()> {
match result { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(_e) => { Err(_e) => {
tcsetpgrp(borrow_fd(0), getpgrp())?; tcsetpgrp(borrow_fd(*TTY_FILENO), getpgrp())?;
Ok(()) Ok(())
} }
} }

View File

@@ -5,6 +5,7 @@ use ariadne::Color;
use ariadne::{Report, ReportKind}; use ariadne::{Report, ReportKind};
use rand::TryRng; use rand::TryRng;
use crate::procio::RedirGuard;
use crate::{ use crate::{
libsh::term::{Style, Styled}, libsh::term::{Style, Styled},
parse::lex::{Span, SpanSource}, parse::lex::{Span, SpanSource},
@@ -44,7 +45,7 @@ impl ColorRng {
pub fn last_color(&mut self) -> Color { pub fn last_color(&mut self) -> Color {
if let Some(color) = self.last_color.take() { if let Some(color) = self.last_color.take() {
return color; color
} else { } else {
let color = self.next().unwrap_or(Color::White); let color = self.next().unwrap_or(Color::White);
self.last_color = Some(color); self.last_color = Some(color);
@@ -78,6 +79,10 @@ pub fn last_color() -> Color {
COLOR_RNG.with(|rng| rng.borrow_mut().last_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 { pub trait ShResultExt {
fn blame(self, span: Span) -> Self; fn blame(self, span: Span) -> Self;
fn try_blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self;
@@ -154,15 +159,25 @@ pub struct ShErr {
src_span: Option<Span>, src_span: Option<Span>,
labels: Vec<ariadne::Label<Span>>, labels: Vec<ariadne::Label<Span>>,
sources: Vec<SpanSource>, sources: Vec<SpanSource>,
notes: Vec<String> notes: Vec<String>,
/// 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<RedirGuard>
} }
impl ShErr { impl ShErr {
pub fn new(kind: ShErrKind, span: Span) -> Self { 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<String>) -> Self { pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> 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<String>) -> Self { pub fn at(kind: ShErrKind, span: Span, msg: impl Into<String>) -> Self {
let color = last_color(); // use last_color to ensure the same color is used for the label and the message given 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)) self.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg))
} }
pub fn blame(self, span: Span) -> Self { pub fn blame(self, span: Span) -> Self {
let ShErr { kind, src_span: _, labels, sources, notes } = self; let ShErr { kind, src_span: _, labels, sources, notes, io_guards } = self;
Self { kind, src_span: Some(span), labels, sources, notes } Self { kind, src_span: Some(span), labels, sources, notes, io_guards }
} }
pub fn try_blame(self, span: Span) -> Self { pub fn try_blame(self, span: Span) -> Self {
match 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 _ => self
} }
} }
@@ -197,23 +212,23 @@ impl ShErr {
self self
} }
pub fn with_label(self, source: SpanSource, label: ariadne::Label<Span>) -> Self { pub fn with_label(self, source: SpanSource, label: ariadne::Label<Span>) -> 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); sources.push(source);
labels.push(label); 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<Span>)>) -> Self { pub fn with_context(self, ctx: VecDeque<(SpanSource, ariadne::Label<Span>)>) -> 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 { for (src, label) in ctx {
sources.push(src); sources.push(src);
labels.push(label); 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<String>) -> Self { pub fn with_note(self, note: impl Into<String>) -> 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()); 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<Report<'_, Span>> { pub fn build_report(&self) -> Option<Report<'_, Span>> {
let span = self.src_span.as_ref()?; let span = self.src_span.as_ref()?;
@@ -313,8 +328,7 @@ pub enum ShErrKind {
ResourceLimitExceeded, ResourceLimitExceeded,
BadPermission, BadPermission,
Errno(Errno), Errno(Errno),
FileNotFound, NotFound,
CmdNotFound,
ReadlineErr, ReadlineErr,
// Not really errors, more like internal signals // Not really errors, more like internal signals
@@ -339,8 +353,7 @@ impl Display for ShErrKind {
Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::ResourceLimitExceeded => "Resource Limit Exceeded",
Self::BadPermission => "Bad Permissions", Self::BadPermission => "Bad Permissions",
Self::Errno(e) => &format!("Errno: {}", e.desc()), Self::Errno(e) => &format!("Errno: {}", e.desc()),
Self::FileNotFound => "File not found", Self::NotFound => "Not Found",
Self::CmdNotFound => "Command not found",
Self::CleanExit(_) => "", Self::CleanExit(_) => "",
Self::FuncReturn(_) => "Syntax Error", Self::FuncReturn(_) => "Syntax Error",
Self::LoopContinue(_) => "Syntax Error", Self::LoopContinue(_) => "Syntax Error",

215
src/libsh/guards.rs Normal file
View File

@@ -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<Option<Termios>> = 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<Vec<(String, Span)>>) -> impl Drop {
let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::<Vec<_>>());
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<String>,
) -> scopeguard::ScopeGuard<HashSet<String>, impl FnOnce(HashSet<String>)> {
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<F: FnOnce() -> 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, R>(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, &current)
.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<Termios>,
}
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();
}
}
}

View File

@@ -1,5 +1,6 @@
pub mod error; pub mod error;
pub mod flog; pub mod flog;
pub mod guards;
pub mod sys; pub mod sys;
pub mod term; pub mod term;
pub mod utils; pub mod utils;

View File

@@ -1,52 +1,7 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use termios::{LocalFlags, Termios};
use crate::prelude::*; use crate::prelude::*;
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| { pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty") open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty")
}); });
#[derive(Debug)]
pub struct TermiosGuard {
saved_termios: Option<Termios>,
}
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();
}
}
}

View File

@@ -28,7 +28,7 @@ use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::unistd::read; use nix::unistd::read;
use crate::builtin::trap::TrapTarget; 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::libsh::sys::TTY_FILENO;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
@@ -112,7 +112,7 @@ fn main() -> ExitCode {
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false) exec_input(cmd, None, false, None)
} else { } else {
shed_interactive() shed_interactive()
} { } {
@@ -120,8 +120,7 @@ fn main() -> ExitCode {
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) 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}"); eprintln!("shed: error running EXIT trap: {e}");
} }
@@ -131,6 +130,7 @@ fn main() -> ExitCode {
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
let path_raw = path.to_string_lossy().to_string();
if !path.is_file() { if !path.is_file() {
eprintln!("shed: Failed to open input file: {}", path.display()); eprintln!("shed: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
@@ -156,7 +156,7 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) 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<()> { fn shed_interactive() -> ShResult<()> {
@@ -186,6 +186,7 @@ fn shed_interactive() -> ShResult<()> {
m.try_rehash_commands(); m.try_rehash_commands();
m.try_rehash_cwd_listing(); m.try_rehash_cwd_listing();
}); });
error::clear_color();
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
@@ -265,7 +266,7 @@ fn shed_interactive() -> ShResult<()> {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); 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("<stdin>".into()))) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);

View File

@@ -7,12 +7,13 @@ use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ 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}, expand::{expand_aliases, glob_to_regex},
jobs::{ChildProc, JobStack, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
guards::{scope_guard, var_ctx_guard},
utils::RedirVecUtils, utils::RedirVecUtils,
}, },
prelude::*, prelude::*,
@@ -66,48 +67,6 @@ pub fn is_in_path(name: &str) -> bool {
} }
} }
pub struct ScopeGuard;
impl ScopeGuard {
pub fn exclusive_scope(args: Option<Vec<(String, Span)>>) -> Self {
let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::<Vec<_>>());
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 <command> <args>'
/// or for-loop variables
pub struct VarCtxGuard {
vars: HashSet<String>,
}
impl VarCtxGuard {
pub fn new(vars: HashSet<String>) -> 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 { pub enum AssignBehavior {
Export, Export,
Set, Set,
@@ -151,7 +110,7 @@ impl ExecArgs {
} }
} }
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -> ShResult<()> { pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool, source_name: Option<String>) -> ShResult<()> {
let log_tab = read_logic(|l| l.clone()); let log_tab = read_logic(|l| l.clone());
let input = expand_aliases(input, HashSet::new(), &log_tab); let input = expand_aliases(input, HashSet::new(), &log_tab);
let lex_flags = if interactive { let lex_flags = if interactive {
@@ -159,7 +118,8 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
} else { } else {
super::lex::LexFlags::empty() super::lex::LexFlags::empty()
}; };
let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags); let source_name = source_name.unwrap_or("<stdin>".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() { if let Err(errors) = parser.parse_src() {
for error in errors { for error in errors {
error.print_error(); error.print_error();
@@ -169,7 +129,7 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
let nodes = parser.extract_nodes(); 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 { if let Some(mut stack) = io_stack {
dispatcher.io_stack.extend(stack.drain(..)); dispatcher.io_stack.extend(stack.drain(..));
} }
@@ -179,7 +139,7 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
&& let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) && let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error))
{ {
let saved_status = state::get_status(); let saved_status = state::get_status();
exec_input(trap, None, false)?; exec_input(trap, None, false, Some(source_name))?;
state::set_status(saved_status); state::set_status(saved_status);
} }
@@ -189,16 +149,18 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
pub struct Dispatcher { pub struct Dispatcher {
nodes: VecDeque<Node>, nodes: VecDeque<Node>,
interactive: bool, interactive: bool,
source_name: String,
pub io_stack: IoStack, pub io_stack: IoStack,
pub job_stack: JobStack, pub job_stack: JobStack,
} }
impl Dispatcher { impl Dispatcher {
pub fn new(nodes: Vec<Node>, interactive: bool) -> Self { pub fn new(nodes: Vec<Node>, interactive: bool, source_name: String) -> Self {
let nodes = VecDeque::from(nodes); let nodes = VecDeque::from(nodes);
Self { Self {
nodes, nodes,
interactive, interactive,
source_name,
io_stack: IoStack::new(), io_stack: IoStack::new(),
job_stack: JobStack::new(), job_stack: JobStack::new(),
} }
@@ -244,7 +206,7 @@ impl Dispatcher {
let stack = IoStack { let stack = IoStack {
stack: self.io_stack.clone(), 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 { } else {
self.exec_cmd(node) self.exec_cmd(node)
} }
@@ -310,7 +272,7 @@ impl Dispatcher {
return Ok(()); 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 write_logic(|l| l.insert_func(name, func)); // Store the AST
Ok(()) Ok(())
} }
@@ -319,6 +281,7 @@ impl Dispatcher {
let NdRule::Command { assignments, argv } = subsh.class else { let NdRule::Command { assignments, argv } = subsh.class else {
unreachable!() unreachable!()
}; };
let name = self.source_name.clone();
self.run_fork("anonymous_subshell", |s| { self.run_fork("anonymous_subshell", |s| {
if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) {
@@ -337,7 +300,7 @@ impl Dispatcher {
let subsh = argv.remove(0); let subsh = argv.remove(0);
let subsh_body = subsh.0.to_string(); 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(); e.print_error();
}; };
}) })
@@ -371,7 +334,7 @@ impl Dispatcher {
let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?; let env_vars = self.set_assignments(assignments, AssignBehavior::Export)?;
let func_name = argv.remove(0).to_string(); 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); self.io_stack.append_to_frame(func.redirs);
@@ -379,7 +342,7 @@ impl Dispatcher {
let argv = prepare_argv(argv).try_blame(blame.clone())?; 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 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().propagate_context(func_ctx);
func_body.body_mut().flags = func.flags; func_body.body_mut().flags = func.flags;
@@ -412,7 +375,7 @@ impl Dispatcher {
let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(brc_grp.redirs); 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<()> { let brc_grp_logic = |s: &mut Self| -> ShResult<()> {
for node in body { for node in body {
let blame = node.get_span(); let blame = node.get_span();
@@ -430,7 +393,7 @@ impl Dispatcher {
} }
}) })
} else { } 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<()> { 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); let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(case_stmt.redirs); 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 case_logic = |s: &mut Self| -> ShResult<()> {
let exp_pattern = pattern.clone().expand()?; let exp_pattern = pattern.clone().expand()?;
@@ -484,7 +447,7 @@ impl Dispatcher {
} }
}) })
} else { } 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<()> { 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); let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(loop_stmt.redirs); 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 loop_logic = |s: &mut Self| -> ShResult<()> {
let CondNode { cond, body } = cond_node; let CondNode { cond, body } = cond_node;
@@ -547,7 +510,7 @@ impl Dispatcher {
} }
}) })
} else { } 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<()> { fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> {
@@ -571,14 +534,14 @@ impl Dispatcher {
}; };
self.io_stack.append_to_frame(for_stmt.redirs); 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<()> { let for_logic = |s: &mut Self| -> ShResult<()> {
// Expand all array variables // Expand all array variables
let arr: Vec<String> = to_expanded_strings(arr)?; let arr: Vec<String> = to_expanded_strings(arr)?;
let vars: Vec<String> = to_expanded_strings(vars)?; let vars: Vec<String> = 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()) { 'outer: for chunk in arr.chunks(vars.len()) {
let empty = String::new(); let empty = String::new();
@@ -594,7 +557,7 @@ impl Dispatcher {
VarFlags::NONE, VarFlags::NONE,
) )
})?; })?;
for_guard.vars.insert(var.to_string()); for_guard.insert(var.to_string());
} }
for node in body.clone() { for node in body.clone() {
@@ -625,7 +588,7 @@ impl Dispatcher {
} }
}) })
} else { } 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<()> { 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); let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(if_stmt.redirs); 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 if_logic = |s: &mut Self| -> ShResult<()> {
let mut matched = false; let mut matched = false;
@@ -682,7 +645,7 @@ impl Dispatcher {
} }
}) })
} else { } 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<()> { fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> {
@@ -696,6 +659,9 @@ impl Dispatcher {
// Zip the commands and their respective pipes into an iterator // Zip the commands and their respective pipes into an iterator
let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); 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 { for ((rpipe, wpipe), mut cmd) in pipes_and_cmds {
if let Some(pipe) = rpipe { if let Some(pipe) = rpipe {
self.io_stack.push_to_frame(pipe); self.io_stack.push_to_frame(pipe);
@@ -716,9 +682,18 @@ impl Dispatcher {
cmd.flags |= NdFlags::FORK_BUILTINS; cmd.flags |= NdFlags::FORK_BUILTINS;
} }
self.dispatch_node(cmd)?; 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 job = self.job_stack.finalize_job().unwrap();
let is_bg = pipeline.flags.contains(NdFlags::BACKGROUND);
dispatch_job(job, is_bg)?; dispatch_job(job, is_bg)?;
Ok(()) Ok(())
} }
@@ -757,10 +732,9 @@ impl Dispatcher {
unreachable!() unreachable!()
}; };
let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; 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(); // Handle builtin/command recursion before redirect/job setup
let io_stack_mut = &mut self.io_stack;
if cmd_raw.as_str() == "builtin" { if cmd_raw.as_str() == "builtin" {
*argv = argv *argv = argv
.iter_mut() .iter_mut()
@@ -779,43 +753,71 @@ impl Dispatcher {
} }
return self.exec_cmd(cmd); 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() { let result = match cmd_raw.as_str() {
"echo" => echo(cmd, io_stack_mut, curr_job_mut), "echo" => echo(cmd),
"cd" => cd(cmd, curr_job_mut), "cd" => cd(cmd),
"export" => export(cmd, io_stack_mut, curr_job_mut), "export" => export(cmd),
"local" => local(cmd, io_stack_mut, curr_job_mut), "local" => local(cmd),
"pwd" => pwd(cmd, io_stack_mut, curr_job_mut), "pwd" => pwd(cmd),
"source" => source(cmd, curr_job_mut), "source" => source(cmd),
"shift" => shift(cmd, curr_job_mut), "shift" => shift(cmd),
"fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), "fg" => continue_job(cmd, JobBehavior::Foregound),
"bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), "bg" => continue_job(cmd, JobBehavior::Background),
"disown" => disown(cmd, io_stack_mut, curr_job_mut), "disown" => disown(cmd),
"jobs" => jobs(cmd, io_stack_mut, curr_job_mut), "jobs" => jobs(cmd),
"alias" => alias(cmd, io_stack_mut, curr_job_mut), "alias" => alias(cmd),
"unalias" => unalias(cmd, io_stack_mut, curr_job_mut), "unalias" => unalias(cmd),
"return" => flowctl(cmd, ShErrKind::FuncReturn(0)), "return" => flowctl(cmd, ShErrKind::FuncReturn(0)),
"break" => flowctl(cmd, ShErrKind::LoopBreak(0)), "break" => flowctl(cmd, ShErrKind::LoopBreak(0)),
"continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)),
"exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)),
"zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), "zoltraak" => zoltraak(cmd),
"shopt" => shopt(cmd, io_stack_mut, curr_job_mut), "shopt" => shopt(cmd),
"read" => read_builtin(cmd, io_stack_mut, curr_job_mut), "read" => read_builtin(cmd),
"trap" => trap(cmd, io_stack_mut, curr_job_mut), "trap" => trap(cmd),
"pushd" => pushd(cmd, io_stack_mut, curr_job_mut), "pushd" => pushd(cmd),
"popd" => popd(cmd, io_stack_mut, curr_job_mut), "popd" => popd(cmd),
"dirs" => dirs(cmd, io_stack_mut, curr_job_mut), "dirs" => dirs(cmd),
"exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), "eval" => eval::eval(cmd),
"eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), "readonly" => readonly(cmd),
"readonly" => readonly(cmd, io_stack_mut, curr_job_mut), "unset" => unset(cmd),
"unset" => unset(cmd, io_stack_mut, curr_job_mut), "complete" => complete_builtin(cmd),
"complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut), "compgen" => compgen_builtin(cmd),
"compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut), "map" => map::map(cmd),
"map" => map::map(cmd, io_stack_mut, curr_job_mut), "pop" => arr_pop(cmd),
"pop" => arr_pop(cmd, io_stack_mut, curr_job_mut), "fpop" => arr_fpop(cmd),
"fpop" => arr_fpop(cmd, io_stack_mut, curr_job_mut), "push" => arr_push(cmd),
"push" => arr_push(cmd, io_stack_mut, curr_job_mut), "fpush" => arr_fpush(cmd),
"fpush" => arr_fpush(cmd, io_stack_mut, curr_job_mut), "rotate" => arr_rotate(cmd),
"rotate" => arr_rotate(cmd, io_stack_mut, curr_job_mut), "wait" => jobctl::wait(cmd),
"type" => intro::type_builtin(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
@@ -828,7 +830,7 @@ impl Dispatcher {
}; };
if let Err(e) = result { if let Err(e) = result {
Err(e.with_context(context)) Err(e.with_context(context).with_redirs(redir_guard))
} else { } else {
Ok(()) Ok(())
} }
@@ -861,8 +863,21 @@ impl Dispatcher {
let exec_args = ExecArgs::new(argv).blame(blame)?; let exec_args = ExecArgs::new(argv).blame(blame)?;
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let job = self.job_stack.curr_job_mut().unwrap(); let job = self.job_stack.curr_job_mut().unwrap();
let existing_pgid = job.pgid();
let child_logic = |pgid: Option<Pid>| -> ! {
// 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 cmd = &exec_args.cmd.0;
let span = exec_args.cmd.1; let span = exec_args.cmd.1;
@@ -872,7 +887,7 @@ impl Dispatcher {
let cmd_str = cmd.to_str().unwrap().to_string(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => { Errno::ENOENT => {
ShErr::new(ShErrKind::CmdNotFound, span.clone()) ShErr::new(ShErrKind::NotFound, span.clone())
.labeled(span, format!("{cmd_str}: command not found")) .labeled(span, format!("{cmd_str}: command not found"))
.with_context(context) .with_context(context)
.print_error(); .print_error();
@@ -887,11 +902,11 @@ impl Dispatcher {
}; };
if no_fork { if no_fork {
child_logic(); child_logic(existing_pgid);
} }
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => child_logic(), ForkResult::Child => child_logic(existing_pgid),
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
// Close proc sub pipe fds - the child has inherited them // Close proc sub pipe fds - the child has inherited them
// and will access them via /proc/self/fd/N. Keeping 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 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 pgid
} else { } else {
job.set_pgid(child); job.set_pgid(child);
@@ -918,15 +933,18 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { 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()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
let _ = setpgid(Pid::from_raw(0), existing_pgid.unwrap_or(Pid::from_raw(0)));
crate::signal::reset_signals();
f(self); f(self);
exit(state::get_status()) exit(state::get_status())
} }
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
write_jobs(|j| j.drain_registered_fds()); write_jobs(|j| j.drain_registered_fds());
let job = self.job_stack.curr_job_mut().unwrap(); 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 pgid
} else { } else {
job.set_pgid(child); job.set_pgid(child);

View File

@@ -100,6 +100,9 @@ impl Span {
pub fn new(range: Range<usize>, source: Arc<String>) -> Self { pub fn new(range: Range<usize>, source: Arc<String>) -> Self {
let source = SpanSource { name: "<stdin>".into(), content: source }; let source = SpanSource { name: "<stdin>".into(), content: source };
Span { range, source } Span { range, source }
}
pub fn from_span_source(range: Range<usize>, source: SpanSource) -> Self {
Span { range, source }
} }
pub fn rename(&mut self, name: String) { pub fn rename(&mut self, name: String) {
self.source.name = name; self.source.name = name;
@@ -108,6 +111,12 @@ impl Span {
self.source.name = name; self.source.name = name;
self 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 /// Slice the source string at the wrapped range
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.source.content[self.range().start..self.range().end] &self.source.content[self.range().start..self.range().end]
@@ -234,7 +243,9 @@ bitflags! {
pub struct LexStream { pub struct LexStream {
source: Arc<String>, source: Arc<String>,
pub cursor: usize, pub cursor: usize,
pub name: String,
quote_state: QuoteState, quote_state: QuoteState,
brc_grp_depth: usize,
brc_grp_start: Option<usize>, brc_grp_start: Option<usize>,
flags: LexFlags, flags: LexFlags,
} }
@@ -243,23 +254,21 @@ bitflags! {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct LexFlags: u32 { pub struct LexFlags: u32 {
/// The lexer is operating in interactive mode /// The lexer is operating in interactive mode
const INTERACTIVE = 0b000000001; const INTERACTIVE = 0b0000000001;
/// Allow unfinished input /// Allow unfinished input
const LEX_UNFINISHED = 0b000000010; const LEX_UNFINISHED = 0b0000000010;
/// The next string-type token is a command name /// 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 /// We are in a quotation, so quoting rules apply
const IN_QUOTE = 0b000001000; const IN_QUOTE = 0b0000001000;
/// Only lex strings; used in expansions /// Only lex strings; used in expansions
const RAW = 0b000010000; const RAW = 0b0000010000;
/// The lexer has not produced any tokens yet /// The lexer has not produced any tokens yet
const FRESH = 0b000010000; const FRESH = 0b0000100000;
/// The lexer has no more tokens to produce /// The lexer has no more tokens to produce
const STALE = 0b000100000; const STALE = 0b0001000000;
/// The lexer's cursor is in a brace group const EXPECTING_IN = 0b0010000000;
const IN_BRC_GRP = 0b001000000; const IN_CASE = 0b0100000000;
const EXPECTING_IN = 0b010000000;
const IN_CASE = 0b100000000;
} }
} }
@@ -269,8 +278,10 @@ impl LexStream {
Self { Self {
flags, flags,
source, source,
name: "<stdin>".into(),
cursor: 0, cursor: 0,
quote_state: QuoteState::default(), quote_state: QuoteState::default(),
brc_grp_depth: 0,
brc_grp_start: None, brc_grp_start: None,
} }
} }
@@ -296,18 +307,25 @@ impl LexStream {
}; };
self.source.get(start..end) 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> { pub fn slice_from_cursor(&self) -> Option<&str> {
self.slice(self.cursor..) self.slice(self.cursor..)
} }
pub fn in_brc_grp(&self) -> bool { 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) { pub fn enter_brc_grp(&mut self) {
if is { if self.brc_grp_depth == 0 {
self.flags |= LexFlags::IN_BRC_GRP;
self.brc_grp_start = Some(self.cursor); 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; self.brc_grp_start = None;
} }
} }
@@ -627,7 +645,7 @@ impl LexStream {
pos += 1; pos += 1;
let mut tk = self.get_token(self.cursor..pos, TkRule::BraceGrpStart); let mut tk = self.get_token(self.cursor..pos, TkRule::BraceGrpStart);
tk.flags |= TkFlags::IS_CMD; tk.flags |= TkFlags::IS_CMD;
self.set_in_brc_grp(true); self.enter_brc_grp();
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
self.cursor = pos; self.cursor = pos;
@@ -636,7 +654,7 @@ impl LexStream {
'}' if pos == self.cursor && self.in_brc_grp() => { '}' if pos == self.cursor && self.in_brc_grp() => {
pos += 1; pos += 1;
let tk = self.get_token(self.cursor..pos, TkRule::BraceGrpEnd); 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.set_next_is_cmd(true);
self.cursor = pos; self.cursor = pos;
return Ok(tk); return Ok(tk);
@@ -731,7 +749,8 @@ impl LexStream {
Ok(new_tk) Ok(new_tk)
} }
pub fn get_token(&self, range: Range<usize>, class: TkRule) -> Tk { pub fn get_token(&self, range: Range<usize>, 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) Tk::new(class, span)
} }
} }

View File

@@ -44,6 +44,7 @@ macro_rules! try_match {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ParsedSrc { pub struct ParsedSrc {
pub src: Arc<String>, pub src: Arc<String>,
pub name: String,
pub ast: Ast, pub ast: Ast,
pub lex_flags: LexFlags, pub lex_flags: LexFlags,
pub context: LabelCtx, pub context: LabelCtx,
@@ -53,11 +54,16 @@ impl ParsedSrc {
pub fn new(src: Arc<String>) -> Self { pub fn new(src: Arc<String>) -> Self {
Self { Self {
src, src,
name: "<stdin>".into(),
ast: Ast::new(vec![]), ast: Ast::new(vec![]),
lex_flags: LexFlags::empty(), lex_flags: LexFlags::empty(),
context: VecDeque::new(), 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 { pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
self.lex_flags = flags; self.lex_flags = flags;
self self
@@ -68,7 +74,7 @@ impl ParsedSrc {
} }
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> { pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
let mut tokens = 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 { match lex_result {
Ok(token) => tokens.push(token), Ok(token) => tokens.push(token),
Err(error) => return Err(vec![error]), Err(error) => return Err(vec![error]),
@@ -244,9 +250,9 @@ impl Node {
unreachable!() unreachable!()
}; };
Span::new( Span::from_span_source(
first_tk.span.range().start..last_tk.span.range().end, first_tk.span.range().start..last_tk.span.range().end,
first_tk.span.get_source(), first_tk.span.span_source().clone(),
) )
} }
} }

View File

@@ -154,39 +154,19 @@ impl<R: Read> IoBuf<R> {
} }
} }
pub struct RedirGuard(IoFrame); pub use crate::libsh::guards::RedirGuard;
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();
}
}
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and /// A struct wrapping three fildescs representing `stdin`, `stdout`, and
/// `stderr` respectively /// `stderr` respectively
#[derive(Debug, Clone)] #[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 /// A single stack frame used with the IoStack
/// Each stack frame represents the redirections of a single command /// Each stack frame represents the redirections of a single command
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct IoFrame { pub struct IoFrame {
pub redirs: Vec<Redir>, pub redirs: Vec<Redir>,
saved_io: Option<IoGroup>, pub(crate) saved_io: Option<IoGroup>,
} }
impl<'e> IoFrame { impl<'e> IoFrame {
@@ -241,7 +221,7 @@ impl<'e> IoFrame {
let src_fd = io_mode.src_fd(); let src_fd = io_mode.src_fd();
dup2(src_fd, tgt_fd)?; dup2(src_fd, tgt_fd)?;
} }
Ok(RedirGuard(self)) Ok(RedirGuard::new(self))
} }
pub fn restore(&mut self) -> ShResult<()> { pub fn restore(&mut self) -> ShResult<()> {
if let Some(saved) = self.saved_io.take() { if let Some(saved) = self.saved_io.take() {

View File

@@ -8,10 +8,11 @@ use crate::{
builtin::complete::{CompFlags, CompOptFlags, CompOpts}, builtin::complete::{CompFlags, CompOptFlags, CompOpts},
libsh::{ libsh::{
error::ShResult, error::ShResult,
guards::var_ctx_guard,
utils::TkVecUtils, utils::TkVecUtils,
}, },
parse::{ parse::{
execute::{VarCtxGuard, exec_input}, execute::exec_input,
lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped},
}, },
readline::{ readline::{
@@ -341,7 +342,7 @@ impl BashCompSpec {
] { ] {
vars_to_unset.insert(var.to_string()); vars_to_unset.insert(var.to_string());
} }
let _guard = VarCtxGuard::new(vars_to_unset); let _guard = var_ctx_guard(vars_to_unset);
let CompContext { let CompContext {
words, words,
@@ -391,7 +392,7 @@ impl BashCompSpec {
"{} {cmd_name} {cword_str} {pword_str}", "{} {cmd_name} {cword_str} {pword_str}",
self.function.as_ref().unwrap() 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()) Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default())
} }
@@ -532,7 +533,7 @@ impl Completer {
(before_cursor, after_cursor) (before_cursor, after_cursor)
} }
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) { pub fn get_subtoken_completion(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
let annotated = annotate_input_recursive(line); let annotated = annotate_input_recursive(line);
let mut ctx = vec![markers::NULL]; let mut ctx = vec![markers::NULL];
let mut last_priority = 0; let mut last_priority = 0;
@@ -776,20 +777,19 @@ impl Completer {
// Use marker-based context detection for sub-token awareness (e.g. VAR_SUB // 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 // inside a token). Run this before comp specs so variable completions take
// priority over programmable completion. // 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 marker_ctx.last() == Some(&markers::VAR_SUB)
if let Some(cur) = ctx.words.get(ctx.cword) { && let Some(cur) = ctx.words.get(ctx.cword) {
self.token_span.0 = token_start; self.token_span.0 = token_start;
let mut span = cur.span.clone(); let mut span = cur.span.clone();
span.set_range(token_start..self.token_span.1); span.set_range(token_start..self.token_span.1);
let raw_tk = span.as_str(); let raw_tk = span.as_str();
let candidates = complete_vars(raw_tk); let candidates = complete_vars(raw_tk);
if !candidates.is_empty() { if !candidates.is_empty() {
return Ok(CompResult::from_candidates(candidates)); return Ok(CompResult::from_candidates(candidates));
} }
} }
}
// Try programmable completion // Try programmable completion
match self.try_comp_spec(&ctx)? { match self.try_comp_spec(&ctx)? {

View File

@@ -785,6 +785,10 @@ impl LineBuf {
} }
(start, end) (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) { pub fn this_line(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number(); let line_no = self.cursor_line_number();
self.line_bounds(line_no) self.line_bounds(line_no)
@@ -2801,6 +2805,25 @@ impl LineBuf {
Verb::InsertChar(ch) => { Verb::InsertChar(ch) => {
self.insert_at_cursor(ch); self.insert_at_cursor(ch);
self.cursor.add(1); 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) => { Verb::Insert(string) => {
self.push_str(&string); self.push_str(&string);

View File

@@ -10,52 +10,24 @@ use nix::{
errno::Errno, errno::Errno,
libc::{self}, libc::{self},
poll::{self, PollFlags, PollTimeout}, poll::{self, PollFlags, PollTimeout},
sys::termios::{self, tcgetattr, tcsetattr},
unistd::isatty, unistd::isatty,
}; };
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform}; use vte::{Parser, Perform};
pub use crate::libsh::guards::{RawModeGuard, raw_mode};
use crate::{ use crate::{
libsh::{ libsh::error::{ShErr, ShErrKind, ShResult},
error::{ShErr, ShErrKind, ShResult},
sys::TTY_FILENO,
},
readline::keys::{KeyCode, ModKeys}, readline::keys::{KeyCode, ModKeys},
state::read_shopts, state::read_shopts,
}; };
use crate::{ use crate::{
procio::borrow_fd,
state::{read_meta, write_meta}, state::{read_meta, write_meta},
}; };
use super::keys::KeyEvent; 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 Row = u16;
pub type Col = 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<F: FnOnce() -> 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, R>(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 // PollReader - non-blocking key reader using vte parser
// ============================================================================ // ============================================================================

View File

@@ -57,7 +57,7 @@ pub fn check_signals() -> ShResult<()> {
let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 }; let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 };
let run_trap = |sig: Signal| -> ShResult<()> { let run_trap = |sig: Signal| -> ShResult<()> {
if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) { 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(()) 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) { extern "C" fn handle_signal(sig: libc::c_int) {
SIGNALS.fetch_or(1 << sig, Ordering::SeqCst); SIGNALS.fetch_or(1 << sig, Ordering::SeqCst);
} }

View File

@@ -20,7 +20,7 @@ use crate::{
}, },
parse::{ parse::{
ConjunctNode, NdRule, Node, ParsedSrc, ConjunctNode, NdRule, Node, ParsedSrc,
lex::{LexFlags, LexStream, Tk}, lex::{LexFlags, LexStream, Span, Tk},
}, },
prelude::*, prelude::*,
readline::{ readline::{
@@ -465,16 +465,31 @@ thread_local! {
pub static SHED: Shed = Shed::new(); 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 /// 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 /// 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)] #[derive(Clone, Debug)]
pub struct ShFunc(Node); pub struct ShFunc {
pub body: Node,
pub source: Span
}
impl ShFunc { 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()); let body = Self::extract_brc_grp_hack(src.extract_nodes());
Self(body) Self{ body, source }
} }
fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node { fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node {
// FIXME: find a better way to do this // FIXME: find a better way to do this
@@ -487,10 +502,10 @@ impl ShFunc {
*cmd *cmd
} }
pub fn body(&self) -> &Node { pub fn body(&self) -> &Node {
&self.0 &self.body
} }
pub fn body_mut(&mut self) -> &mut Node { pub fn body_mut(&mut self) -> &mut Node {
&mut self.0 &mut self.body
} }
} }
@@ -500,7 +515,7 @@ impl ShFunc {
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct LogTab { pub struct LogTab {
functions: HashMap<String, ShFunc>, functions: HashMap<String, ShFunc>,
aliases: HashMap<String, String>, aliases: HashMap<String, ShAlias>,
traps: HashMap<TrapTarget, String>, traps: HashMap<TrapTarget, String>,
} }
@@ -529,13 +544,13 @@ impl LogTab {
pub fn funcs(&self) -> &HashMap<String, ShFunc> { pub fn funcs(&self) -> &HashMap<String, ShFunc> {
&self.functions &self.functions
} }
pub fn aliases(&self) -> &HashMap<String, String> { pub fn aliases(&self) -> &HashMap<String, ShAlias> {
&self.aliases &self.aliases
} }
pub fn insert_alias(&mut self, name: &str, body: &str) { pub fn insert_alias(&mut self, name: &str, body: &str, source: Span) {
self.aliases.insert(name.into(), body.into()); self.aliases.insert(name.into(), ShAlias { body: body.into(), source });
} }
pub fn get_alias(&self, name: &str) -> Option<String> { pub fn get_alias(&self, name: &str) -> Option<ShAlias> {
self.aliases.get(name).cloned() self.aliases.get(name).cloned()
} }
pub fn remove_alias(&mut self, name: &str) { pub fn remove_alias(&mut self, name: &str) {
@@ -1140,6 +1155,29 @@ impl MetaTab {
pub fn remove_comp_spec(&mut self, cmd: &str) -> bool { pub fn remove_comp_spec(&mut self, cmd: &str) -> bool {
self.comp_specs.remove(cmd).is_some() self.comp_specs.remove(cmd).is_some()
} }
pub fn get_cmds_in_path() -> Vec<String> {
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) { pub fn try_rehash_commands(&mut self) {
let path = env::var("PATH").unwrap_or_default(); let path = env::var("PATH").unwrap_or_default();
let cwd = env::var("PWD").unwrap_or_default(); let cwd = env::var("PWD").unwrap_or_default();
@@ -1155,25 +1193,10 @@ impl MetaTab {
self.path_cache.clear(); self.path_cache.clear();
self.old_path = Some(path.clone()); self.old_path = Some(path.clone());
self.old_pwd = Some(cwd.clone()); self.old_pwd = Some(cwd.clone());
let paths = path.split(":").map(PathBuf::from); let cmds_in_path = Self::get_cmds_in_path();
for cmd in cmds_in_path {
for path in paths { self.path_cache.insert(cmd);
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());
}
}
}
}
if let Ok(entries) = Path::new(&cwd).read_dir() { if let Ok(entries) = Path::new(&cwd).read_dir() {
for entry in entries.flatten() { for entry in entries.flatten() {
let Ok(meta) = std::fs::metadata(entry.path()) else { 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<()> { 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 file = OpenOptions::new().read(true).open(path)?;
let mut buf = String::new(); let mut buf = String::new();
file.read_to_string(&mut buf)?; file.read_to_string(&mut buf)?;
exec_input(buf, None, false)?; exec_input(buf, None, false, Some(source_name))?;
Ok(()) Ok(())
} }