From 8ad53f09b3ece96bee866c1d6268617bc3584725 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Tue, 12 Aug 2025 13:58:25 -0400 Subject: [PATCH] Added rustfmt.toml, formatted codebase --- rustfmt.toml | 7 + src/builtin/alias.rs | 168 +- src/builtin/cd.rs | 74 +- src/builtin/echo.rs | 127 +- src/builtin/export.rs | 67 +- src/builtin/flowctl.rs | 65 +- src/builtin/jobctl.rs | 311 +- src/builtin/mod.rs | 114 +- src/builtin/pwd.rs | 35 +- src/builtin/shift.rs | 51 +- src/builtin/shopt.rs | 51 +- src/builtin/source.rs | 64 +- src/builtin/test.rs | 587 ++-- src/builtin/zoltraak.rs | 301 +- src/expand.rs | 2706 +++++++++-------- src/fern.rs | 175 +- src/getopt.rs | 109 +- src/jobs.rs | 1293 ++++---- src/libsh/error.rs | 671 ++-- src/libsh/flog.rs | 73 +- src/libsh/mod.rs | 2 +- src/libsh/sys.rs | 109 +- src/libsh/term.rs | 250 +- src/libsh/utils.rs | 150 +- src/parse/execute.rs | 1243 ++++---- src/parse/lex.rs | 1596 +++++----- src/parse/mod.rs | 2810 +++++++++-------- src/prelude.rs | 61 +- src/procio.rs | 470 +-- src/prompt/highlight.rs | 1 + src/prompt/mod.rs | 40 +- src/prompt/readline/history.rs | 605 ++-- src/prompt/readline/keys.rs | 237 +- src/prompt/readline/layout.rs | 1 + src/prompt/readline/linebuf.rs | 5046 ++++++++++++++++--------------- src/prompt/readline/mod.rs | 671 ++-- src/prompt/readline/register.rs | 296 +- src/prompt/readline/term.rs | 1057 +++---- src/prompt/readline/vicmd.rs | 742 ++--- src/prompt/readline/vimode.rs | 3329 ++++++++++---------- src/shopt.rs | 1027 +++---- src/signal.rs | 255 +- src/state.rs | 703 +++-- src/tests/error.rs | 204 +- src/tests/expand.rs | 353 +-- src/tests/getopt.rs | 46 +- src/tests/lexer.rs | 42 +- src/tests/mod.rs | 62 +- src/tests/parser.rs | 222 +- src/tests/readline.rs | 855 +++--- src/tests/script.rs | 65 +- src/tests/term.rs | 40 +- 52 files changed, 15188 insertions(+), 14451 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d243aa0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +max_width = 100 +tab_spaces = 2 +edition = "2021" + +newline_style = "Unix" + +wrap_comments = true diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index d0414b2..2dd3d75 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -1,95 +1,103 @@ -use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, read_logic, write_logic}}; +use crate::{ + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, + state::{self, read_logic, write_logic}, +}; use super::setup_builtin; pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - if argv.is_empty() { - // Display the environment variables - let mut alias_output = read_logic(|l| { - l.aliases() - .iter() - .map(|ent| format!("{} = \"{}\"", ent.0, ent.1)) - .collect::>() - }); - alias_output.sort(); // Sort them alphabetically - let mut alias_output = alias_output.join("\n"); // Join them with newlines - alias_output.push('\n'); // Push a final newline + if argv.is_empty() { + // Display the environment variables + let mut alias_output = read_logic(|l| { + l.aliases() + .iter() + .map(|ent| format!("{} = \"{}\"", ent.0, ent.1)) + .collect::>() + }); + alias_output.sort(); // Sort them alphabetically + let mut alias_output = alias_output.join("\n"); // Join them with newlines + alias_output.push('\n'); // Push a final newline - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, alias_output.as_bytes())?; // Write it - } else { - for (arg,span) in argv { - if arg == "command" || arg == "builtin" { - return Err( - ShErr::full( - ShErrKind::ExecFail, - format!("alias: Cannot assign alias to reserved name '{arg}'"), - span - ) - ) - } + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, alias_output.as_bytes())?; // Write it + } else { + for (arg, span) in argv { + if arg == "command" || arg == "builtin" { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("alias: Cannot assign alias to reserved name '{arg}'"), + span, + )); + } - let Some((name,body)) = arg.split_once('=') else { - return Err( - ShErr::full( - ShErrKind::SyntaxErr, - "alias: Expected an assignment in alias args", - span - ) - ) - }; - write_logic(|l| l.insert_alias(name, body)); - } - } - io_frame.unwrap().restore()?; - state::set_status(0); - Ok(()) + let Some((name, body)) = arg.split_once('=') else { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + "alias: Expected an assignment in alias args", + span, + )); + }; + write_logic(|l| l.insert_alias(name, body)); + } + } + io_frame.unwrap().restore()?; + state::set_status(0); + Ok(()) } pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + if argv.is_empty() { + // Display the environment variables + let mut alias_output = read_logic(|l| { + l.aliases() + .iter() + .map(|ent| format!("{} = \"{}\"", ent.0, ent.1)) + .collect::>() + }); + alias_output.sort(); // Sort them alphabetically + let mut alias_output = alias_output.join("\n"); // Join them with newlines + alias_output.push('\n'); // Push a final newline - if argv.is_empty() { - // Display the environment variables - let mut alias_output = read_logic(|l| { - l.aliases() - .iter() - .map(|ent| format!("{} = \"{}\"", ent.0, ent.1)) - .collect::>() - }); - alias_output.sort(); // Sort them alphabetically - let mut alias_output = alias_output.join("\n"); // Join them with newlines - alias_output.push('\n'); // Push a final newline - - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, alias_output.as_bytes())?; // Write it - } else { - for (arg,span) in argv { - flog!(DEBUG, arg); - if read_logic(|l| l.get_alias(&arg)).is_none() { - return Err( - ShErr::full( - ShErrKind::SyntaxErr, - format!("unalias: alias '{arg}' not found"), - span - ) - ) - }; - write_logic(|l| l.remove_alias(&arg)) - } - } - io_frame.unwrap().restore()?; - state::set_status(0); - Ok(()) + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, alias_output.as_bytes())?; // Write it + } else { + for (arg, span) in argv { + flog!(DEBUG, arg); + if read_logic(|l| l.get_alias(&arg)).is_none() { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + format!("unalias: alias '{arg}' not found"), + span, + )); + }; + write_logic(|l| l.remove_alias(&arg)) + } + } + io_frame.unwrap().restore()?; + state::set_status(0); + Ok(()) } diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index b06fc7c..888819e 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -1,45 +1,51 @@ -use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, state::{self}}; +use crate::{ + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node}, + prelude::*, + state::{self}, +}; use super::setup_builtin; pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { - let span = node.get_span(); - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let span = node.get_span(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,_) = setup_builtin(argv,job,None)?; + let (argv, _) = setup_builtin(argv, job, None)?; - let new_dir = if let Some((arg,_)) = argv.into_iter().next() { - PathBuf::from(arg) - } else { - PathBuf::from(env::var("HOME").unwrap()) - }; + let new_dir = if let Some((arg, _)) = argv.into_iter().next() { + PathBuf::from(arg) + } else { + PathBuf::from(env::var("HOME").unwrap()) + }; - if !new_dir.exists() { - return Err( - ShErr::full( - ShErrKind::ExecFail, - format!("cd: No such file or directory '{}'",new_dir.display()), - span, - ) - ) - } + if !new_dir.exists() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("cd: No such file or directory '{}'", new_dir.display()), + span, + )); + } - if !new_dir.is_dir() { - return Err( - ShErr::full( - ShErrKind::ExecFail, - format!("cd: Not a directory '{}'",new_dir.display()), - span, - ) - ) - } + if !new_dir.is_dir() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("cd: Not a directory '{}'", new_dir.display()), + span, + )); + } - env::set_current_dir(new_dir).unwrap(); - let new_dir = env::current_dir().unwrap(); - env::set_var("PWD", new_dir); + env::set_current_dir(new_dir).unwrap(); + let new_dir = env::current_dir().unwrap(); + env::set_var("PWD", new_dir); - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 8a392dd..4f5d479 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -1,77 +1,90 @@ use std::sync::LazyLock; -use crate::{builtin::setup_builtin, getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state}; +use crate::{ + builtin::setup_builtin, + getopt::{get_opts_from_tokens, Opt, OptSet}, + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, + state, +}; -pub static ECHO_OPTS: LazyLock = LazyLock::new(|| {[ - Opt::Short('n'), - Opt::Short('E'), - Opt::Short('e'), - Opt::Short('r'), -].into()}); +pub static ECHO_OPTS: LazyLock = LazyLock::new(|| { + [ + Opt::Short('n'), + Opt::Short('E'), + Opt::Short('e'), + Opt::Short('r'), + ] + .into() +}); bitflags! { - pub struct EchoFlags: u32 { - const NO_NEWLINE = 0b000001; - const USE_STDERR = 0b000010; - const USE_ESCAPE = 0b000100; - } + pub struct EchoFlags: u32 { + const NO_NEWLINE = 0b000001; + const USE_STDERR = 0b000010; + const USE_ESCAPE = 0b000100; + } } pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; - assert!(!argv.is_empty()); - let (argv,opts) = get_opts_from_tokens(argv); - let flags = get_echo_flags(opts).blame(blame)?; - let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + assert!(!argv.is_empty()); + let (argv, opts) = get_opts_from_tokens(argv); + let flags = get_echo_flags(opts).blame(blame)?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let output_channel = if flags.contains(EchoFlags::USE_STDERR) { - borrow_fd(STDERR_FILENO) - } else { - borrow_fd(STDOUT_FILENO) - }; + let output_channel = if flags.contains(EchoFlags::USE_STDERR) { + borrow_fd(STDERR_FILENO) + } else { + borrow_fd(STDOUT_FILENO) + }; - let mut echo_output = argv.into_iter() - .map(|a| a.0) // Extract the String from the tuple of (String,Span) - .collect::>() - .join(" "); + let mut echo_output = argv + .into_iter() + .map(|a| a.0) // Extract the String from the tuple of (String,Span) + .collect::>() + .join(" "); - if !flags.contains(EchoFlags::NO_NEWLINE) { - echo_output.push('\n') - } + if !flags.contains(EchoFlags::NO_NEWLINE) { + echo_output.push('\n') + } - write(output_channel, echo_output.as_bytes())?; + write(output_channel, echo_output.as_bytes())?; - io_frame.unwrap().restore()?; - state::set_status(0); - Ok(()) + io_frame.unwrap().restore()?; + state::set_status(0); + Ok(()) } pub fn get_echo_flags(mut opts: Vec) -> ShResult { - let mut flags = EchoFlags::empty(); + let mut flags = EchoFlags::empty(); - while let Some(opt) = opts.pop() { - if !ECHO_OPTS.contains(&opt) { - return Err( - ShErr::simple( - ShErrKind::ExecFail, - format!("echo: Unexpected flag '{opt}'"), - ) - ) - } - let Opt::Short(opt) = opt else { - unreachable!() - }; + while let Some(opt) = opts.pop() { + if !ECHO_OPTS.contains(&opt) { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("echo: Unexpected flag '{opt}'"), + )); + } + let Opt::Short(opt) = opt else { unreachable!() }; - match opt { - 'n' => flags |= EchoFlags::NO_NEWLINE, - 'r' => flags |= EchoFlags::USE_STDERR, - 'e' => flags |= EchoFlags::USE_ESCAPE, - _ => unreachable!() - } - } + match opt { + 'n' => flags |= EchoFlags::NO_NEWLINE, + 'r' => flags |= EchoFlags::USE_STDERR, + 'e' => flags |= EchoFlags::USE_ESCAPE, + _ => unreachable!(), + } + } - Ok(flags) + Ok(flags) } diff --git a/src/builtin/export.rs b/src/builtin/export.rs index 6e1c4cd..d64bde4 100644 --- a/src/builtin/export.rs +++ b/src/builtin/export.rs @@ -1,35 +1,48 @@ -use crate::{jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, write_vars}}; +use crate::{ + jobs::JobBldr, + libsh::error::ShResult, + parse::{NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, + state::{self, write_vars}, +}; use super::setup_builtin; pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - if argv.is_empty() { - // Display the environment variables - let mut env_output = env::vars() - .map(|var| format!("{}={}",var.0,var.1)) // Get all of them, zip them into one string - .collect::>(); - env_output.sort(); // Sort them alphabetically - let mut env_output = env_output.join("\n"); // Join them with newlines - env_output.push('\n'); // Push a final newline + if argv.is_empty() { + // Display the environment variables + let mut env_output = env::vars() + .map(|var| format!("{}={}", var.0, var.1)) // Get all of them, zip them into one string + .collect::>(); + env_output.sort(); // Sort them alphabetically + let mut env_output = env_output.join("\n"); // Join them with newlines + env_output.push('\n'); // Push a final newline - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, env_output.as_bytes())?; // Write it - } else { - for (arg,_) in argv { - if let Some((var,val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, val, true)); // Export an assignment like 'foo=bar' - } else { - write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any - } - } - } - io_frame.unwrap().restore()?; - state::set_status(0); - Ok(()) + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, env_output.as_bytes())?; // Write it + } else { + for (arg, _) in argv { + if let Some((var, val)) = arg.split_once('=') { + write_vars(|v| v.set_var(var, val, true)); // Export an assignment like + // 'foo=bar' + } else { + write_vars(|v| v.export_var(&arg)); // Export an existing variable, if + // any + } + } + } + io_frame.unwrap().restore()?; + state::set_status(0); + Ok(()) } diff --git a/src/builtin/flowctl.rs b/src/builtin/flowctl.rs index f28a39b..590f000 100644 --- a/src/builtin/flowctl.rs +++ b/src/builtin/flowctl.rs @@ -1,39 +1,46 @@ -use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*}; +use crate::{ + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{execute::prepare_argv, NdRule, Node}, + prelude::*, +}; pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { - use ShErrKind::*; - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; - let mut code = 0; + use ShErrKind::*; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + let mut code = 0; - let mut argv = prepare_argv(argv)?; - let cmd = argv.remove(0).0; + let mut argv = prepare_argv(argv)?; + let cmd = argv.remove(0).0; - if !argv.is_empty() { - let (arg,span) = argv - .into_iter() - .next() - .unwrap(); + if !argv.is_empty() { + let (arg, span) = argv.into_iter().next().unwrap(); - let Ok(status) = arg.parse::() else { - return Err( - ShErr::full(ShErrKind::SyntaxErr, format!("{cmd}: Expected a number"), span) - ) - }; + let Ok(status) = arg.parse::() else { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + format!("{cmd}: Expected a number"), + span, + )); + }; - code = status; - } + code = status; + } - flog!(DEBUG,code); + flog!(DEBUG, code); - let kind = match kind { - LoopContinue(_) => LoopContinue(code), - LoopBreak(_) => LoopBreak(code), - FuncReturn(_) => FuncReturn(code), - CleanExit(_) => CleanExit(code), - _ => unreachable!() - }; + let kind = match kind { + LoopContinue(_) => LoopContinue(code), + LoopBreak(_) => LoopBreak(code), + FuncReturn(_) => FuncReturn(code), + CleanExit(_) => CleanExit(code), + _ => unreachable!(), + }; - Err(ShErr::simple(kind, "")) + Err(ShErr::simple(kind, "")) } diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index 8c82060..9b6165c 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -1,183 +1,182 @@ -use crate::{jobs::{JobBldr, JobCmdFlags, JobID}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::Span, NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, read_jobs, write_jobs}}; +use crate::{ + jobs::{JobBldr, JobCmdFlags, JobID}, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{lex::Span, NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, + state::{self, read_jobs, write_jobs}, +}; use super::setup_builtin; pub enum JobBehavior { - Foregound, - Background + Foregound, + Background, } pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> { - let blame = node.get_span().clone(); - let cmd = match behavior { - JobBehavior::Foregound => "fg", - JobBehavior::Background => "bg" - }; - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let blame = node.get_span().clone(); + let cmd = match behavior { + JobBehavior::Foregound => "fg", + JobBehavior::Background => "bg", + }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,_) = setup_builtin(argv, job, None)?; - let mut argv = argv.into_iter(); + let (argv, _) = setup_builtin(argv, job, None)?; + let mut argv = argv.into_iter(); - if read_jobs(|j| j.get_fg().is_some()) { - return Err( - ShErr::full( - ShErrKind::InternalErr, - format!("Somehow called '{}' with an existing foreground job",cmd), - blame - ) - ) - } + if read_jobs(|j| j.get_fg().is_some()) { + return Err(ShErr::full( + ShErrKind::InternalErr, + format!("Somehow called '{}' with an existing foreground job", cmd), + blame, + )); + } - let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { - id - } else { - return Err( - ShErr::full( - ShErrKind::ExecFail, - "No jobs found", - blame - ) - ) - }; + let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { + id + } else { + return Err(ShErr::full(ShErrKind::ExecFail, "No jobs found", blame)); + }; - let tabid = match argv.next() { - Some((arg,blame)) => parse_job_id(&arg, blame)?, - None => curr_job_id - }; + let tabid = match argv.next() { + Some((arg, blame)) => parse_job_id(&arg, blame)?, + None => curr_job_id, + }; - let mut job = write_jobs(|j| { - let id = JobID::TableID(tabid); - let query_result = j.query(id.clone()); - if query_result.is_some() { - Ok(j.remove_job(id.clone()).unwrap()) - } else { - Err( - ShErr::full( - ShErrKind::ExecFail, - format!("Job id `{}' not found", tabid), - blame - ) - ) - } - })?; + let mut job = write_jobs(|j| { + let id = JobID::TableID(tabid); + let query_result = j.query(id.clone()); + if query_result.is_some() { + Ok(j.remove_job(id.clone()).unwrap()) + } else { + Err(ShErr::full( + ShErrKind::ExecFail, + format!("Job id `{}' not found", tabid), + blame, + )) + } + })?; - job.killpg(Signal::SIGCONT)?; + job.killpg(Signal::SIGCONT)?; - match behavior { - JobBehavior::Foregound => { - write_jobs(|j| j.new_fg(job))?; - } - JobBehavior::Background => { - let job_order = read_jobs(|j| j.order().to_vec()); - write(borrow_fd(1), job.display(&job_order, JobCmdFlags::PIDS).as_bytes())?; - write_jobs(|j| j.insert_job(job, true))?; - } - } - state::set_status(0); - Ok(()) + match behavior { + JobBehavior::Foregound => { + write_jobs(|j| j.new_fg(job))?; + } + JobBehavior::Background => { + let job_order = read_jobs(|j| j.order().to_vec()); + write( + borrow_fd(1), + job.display(&job_order, JobCmdFlags::PIDS).as_bytes(), + )?; + write_jobs(|j| j.insert_job(job, true))?; + } + } + state::set_status(0); + Ok(()) } fn parse_job_id(arg: &str, blame: Span) -> ShResult { - if arg.starts_with('%') { - let arg = arg.strip_prefix('%').unwrap(); - if arg.chars().all(|ch| ch.is_ascii_digit()) { - Ok(arg.parse::().unwrap()) - } else { - let result = write_jobs(|j| { - let query_result = j.query(JobID::Command(arg.into())); - query_result.map(|job| job.tabid().unwrap()) - }); - match result { - Some(id) => Ok(id), - None => Err( - ShErr::full( - ShErrKind::InternalErr, - "Found a job but no table id in parse_job_id()", - blame - ) - ) - } - } - } else if arg.chars().all(|ch| ch.is_ascii_digit()) { - let result = write_jobs(|j| { - let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::().unwrap()))); - if let Some(job) = pgid_query_result { - return Some(job.tabid().unwrap()) - } + if arg.starts_with('%') { + let arg = arg.strip_prefix('%').unwrap(); + if arg.chars().all(|ch| ch.is_ascii_digit()) { + Ok(arg.parse::().unwrap()) + } else { + let result = write_jobs(|j| { + let query_result = j.query(JobID::Command(arg.into())); + query_result.map(|job| job.tabid().unwrap()) + }); + match result { + Some(id) => Ok(id), + None => Err(ShErr::full( + ShErrKind::InternalErr, + "Found a job but no table id in parse_job_id()", + blame, + )), + } + } + } else if arg.chars().all(|ch| ch.is_ascii_digit()) { + let result = write_jobs(|j| { + let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::().unwrap()))); + if let Some(job) = pgid_query_result { + return Some(job.tabid().unwrap()); + } - if arg.parse::().unwrap() > 0 { - let table_id_query_result = j.query(JobID::TableID(arg.parse::().unwrap())); - return table_id_query_result.map(|job| job.tabid().unwrap()); - } + if arg.parse::().unwrap() > 0 { + let table_id_query_result = j.query(JobID::TableID(arg.parse::().unwrap())); + return table_id_query_result.map(|job| job.tabid().unwrap()); + } - None - }); + None + }); - match result { - Some(id) => Ok(id), - None => Err( - ShErr::full( - ShErrKind::InternalErr, - "Found a job but no table id in parse_job_id()", - blame - ) - ) - } - } else { - Err( - ShErr::full( - ShErrKind::SyntaxErr, - format!("Invalid fd arg: {}", arg), - blame - ) - ) - } + match result { + Some(id) => Ok(id), + None => Err(ShErr::full( + ShErrKind::InternalErr, + "Found a job but no table id in parse_job_id()", + blame, + )), + } + } else { + Err(ShErr::full( + ShErrKind::SyntaxErr, + format!("Invalid fd arg: {}", arg), + blame, + )) + } } pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut flags = JobCmdFlags::empty(); - for (arg,span) in argv { - let mut chars = arg.chars().peekable(); - if chars.peek().is_none_or(|ch| *ch != '-') { - return Err( - ShErr::full( - ShErrKind::SyntaxErr, - "Invalid flag in jobs call", - span - ) - ) - } - chars.next(); - for ch in chars { - let flag = match ch { - 'l' => JobCmdFlags::LONG, - 'p' => JobCmdFlags::PIDS, - 'n' => JobCmdFlags::NEW_ONLY, - 'r' => JobCmdFlags::RUNNING, - 's' => JobCmdFlags::STOPPED, - _ => return Err( - ShErr::full( - ShErrKind::SyntaxErr, - "Invalid flag in jobs call", - span - ) - ) + let mut flags = JobCmdFlags::empty(); + for (arg, span) in argv { + let mut chars = arg.chars().peekable(); + if chars.peek().is_none_or(|ch| *ch != '-') { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + "Invalid flag in jobs call", + span, + )); + } + chars.next(); + for ch in chars { + let flag = match ch { + 'l' => JobCmdFlags::LONG, + 'p' => JobCmdFlags::PIDS, + 'n' => JobCmdFlags::NEW_ONLY, + 'r' => JobCmdFlags::RUNNING, + 's' => JobCmdFlags::STOPPED, + _ => { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + "Invalid flag in jobs call", + span, + )) + } + }; + flags |= flag + } + } + write_jobs(|j| j.print_jobs(flags))?; + io_frame.unwrap().restore()?; + state::set_status(0); - }; - flags |= flag - } - } - write_jobs(|j| j.print_jobs(flags))?; - io_frame.unwrap().restore()?; - state::set_status(0); - - Ok(()) + Ok(()) } diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index f4061a2..dbc044f 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -1,50 +1,45 @@ use nix::unistd::Pid; -use crate::{jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{execute::prepare_argv, lex::{Span, Tk}, Redir}, procio::{IoFrame, IoStack}}; +use crate::{ + jobs::{ChildProc, JobBldr}, + libsh::error::ShResult, + parse::{ + execute::prepare_argv, + lex::{Span, Tk}, + Redir, + }, + procio::{IoFrame, IoStack}, +}; -pub mod echo; -pub mod cd; -pub mod export; -pub mod pwd; -pub mod source; -pub mod shift; -pub mod jobctl; pub mod alias; +pub mod cd; +pub mod echo; +pub mod export; pub mod flowctl; -pub mod zoltraak; +pub mod jobctl; +pub mod pwd; +pub mod shift; pub mod shopt; -pub mod test; // [[ ]] thing +pub mod source; +pub mod test; +pub mod zoltraak; // [[ ]] thing -pub const BUILTINS: [&str;19] = [ - "echo", - "cd", - "export", - "pwd", - "source", - "shift", - "jobs", - "fg", - "bg", - "alias", - "unalias", - "return", - "break", - "continue", - "exit", - "zoltraak", - "shopt", - "builtin", - "command", +pub const BUILTINS: [&str; 19] = [ + "echo", "cd", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias", + "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", ]; /// 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` +/// 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` +/// * 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 @@ -56,36 +51,39 @@ pub const BUILTINS: [&str;19] = [ /// * 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 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<(Vec<(String,Span)>, Option)>; +type SetupReturns = ShResult<(Vec<(String, Span)>, Option)>; pub fn setup_builtin( - argv: Vec, - job: &mut JobBldr, - io_mode: Option<(&mut IoStack,Vec)>, + argv: Vec, + job: &mut JobBldr, + io_mode: Option<(&mut IoStack, Vec)>, ) -> SetupReturns { - let mut argv: Vec<(String,Span)> = prepare_argv(argv)?; + let mut argv: Vec<(String, Span)> = prepare_argv(argv)?; - let child_pgid = if let Some(pgid) = job.pgid() { - pgid - } else { - job.set_pgid(Pid::this()); - Pid::this() - }; - let cmd_name = argv.remove(0).0; - let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?; - job.push_child(child); + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(Pid::this()); + Pid::this() + }; + let cmd_name = argv.remove(0).0; + let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?; + job.push_child(child); - let io_frame = if let Some((io_stack,redirs)) = io_mode { - io_stack.append_to_frame(redirs); - let mut io_frame = io_stack.pop_frame(); - io_frame.redirect()?; - Some(io_frame) - } else { - None - }; + let io_frame = if let Some((io_stack, redirs)) = io_mode { + io_stack.append_to_frame(redirs); + let mut io_frame = io_stack.pop_frame(); + io_frame.redirect()?; + Some(io_frame) + } else { + None + }; - // We return the io_frame because the caller needs to also call io_frame.restore() - Ok((argv,io_frame)) + // We return the io_frame because the caller needs to also call + // io_frame.restore() + Ok((argv, io_frame)) } diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index 7925d69..ebf9525 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -1,21 +1,32 @@ -use crate::{jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state}; +use crate::{ + jobs::JobBldr, + libsh::error::ShResult, + parse::{NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, + state, +}; use super::setup_builtin; pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (_,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + let (_, io_frame) = setup_builtin(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(); - curr_dir.push('\n'); - write(stdout, curr_dir.as_bytes())?; + let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string(); + curr_dir.push('\n'); + write(stdout, curr_dir.as_bytes())?; - io_frame.unwrap().restore().unwrap(); - state::set_status(0); - Ok(()) + io_frame.unwrap().restore().unwrap(); + state::set_status(0); + Ok(()) } diff --git a/src/builtin/shift.rs b/src/builtin/shift.rs index 7127e82..931bbc1 100644 --- a/src/builtin/shift.rs +++ b/src/builtin/shift.rs @@ -1,30 +1,37 @@ -use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, state::{self, write_vars}}; +use crate::{ + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node}, + state::{self, write_vars}, +}; use super::setup_builtin; pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,_) = setup_builtin(argv, job, None)?; - let mut argv = argv.into_iter(); + let (argv, _) = setup_builtin(argv, job, None)?; + let mut argv = argv.into_iter(); - if let Some((arg,span)) = argv.next() { - let Ok(count) = arg.parse::() else { - return Err( - ShErr::full( - ShErrKind::ExecFail, - "Expected a number in shift args", - span - ) - ) - }; - for _ in 0..count { - write_vars(|v| v.fpop_arg()); - } - } + if let Some((arg, span)) = argv.next() { + let Ok(count) = arg.parse::() else { + return Err(ShErr::full( + ShErrKind::ExecFail, + "Expected a number in shift args", + span, + )); + }; + for _ in 0..count { + write_vars(|v| v.fpop_arg()); + } + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index 7974541..1ac37f8 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -1,30 +1,41 @@ -use crate::{jobs::JobBldr, libsh::error::{ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::write_shopts}; +use crate::{ + jobs::JobBldr, + libsh::error::{ShResult, ShResultExt}, + parse::{NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, + state::write_shopts, +}; use super::setup_builtin; pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut io_frame = io_frame.unwrap(); - io_frame.redirect()?; - for (arg,span) in argv { - let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { - continue - }; + let mut io_frame = io_frame.unwrap(); + io_frame.redirect()?; + for (arg, span) in argv { + let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { + continue; + }; - let output_channel = borrow_fd(STDOUT_FILENO); - output.push('\n'); + let output_channel = borrow_fd(STDOUT_FILENO); + output.push('\n'); - if let Err(e) = write(output_channel, output.as_bytes()) { - io_frame.restore()?; - return Err(e.into()) - } - } - io_frame.restore()?; + if let Err(e) = write(output_channel, output.as_bytes()) { + io_frame.restore()?; + return Err(e.into()); + } + } + io_frame.restore()?; - Ok(()) + Ok(()) } diff --git a/src/builtin/source.rs b/src/builtin/source.rs index a16ae1e..f3b3c88 100644 --- a/src/builtin/source.rs +++ b/src/builtin/source.rs @@ -1,37 +1,43 @@ -use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, state::{self, source_file}}; +use crate::{ + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node}, + prelude::*, + state::{self, source_file}, +}; use super::setup_builtin; pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv,_) = setup_builtin(argv, job, None)?; + let (argv, _) = setup_builtin(argv, job, None)?; - for (arg,span) in argv { - let path = PathBuf::from(arg); - if !path.exists() { - return Err( - ShErr::full( - ShErrKind::ExecFail, - format!("source: File '{}' not found",path.display()), - span - ) - ); - } - if !path.is_file() { - return Err( - ShErr::full( - ShErrKind::ExecFail, - format!("source: Given path '{}' is not a file",path.display()), - span - ) - ); - } - source_file(path)?; - } + for (arg, span) in argv { + let path = PathBuf::from(arg); + if !path.exists() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("source: File '{}' not found", path.display()), + span, + )); + } + if !path.is_file() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("source: Given path '{}' is not a file", path.display()), + span, + )); + } + source_file(path)?; + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } diff --git a/src/builtin/test.rs b/src/builtin/test.rs index 8d2407b..5788038 100644 --- a/src/builtin/test.rs +++ b/src/builtin/test.rs @@ -1,328 +1,321 @@ use std::{fs::metadata, path::PathBuf, str::FromStr}; -use nix::{sys::stat::{self, SFlag}, unistd::AccessFlags}; +use nix::{ + sys::stat::{self, SFlag}, + unistd::AccessFlags, +}; use regex::Regex; -use crate::{libsh::error::{ShErr, ShErrKind, ShResult},prelude::*, parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}}; +use crate::{ + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}, + prelude::*, +}; #[derive(Debug, Clone)] pub enum UnaryOp { - Exists, // -e - Directory, // -d - File, // -f - Symlink, // -h or -L - Readable, // -r - Writable, // -w - Executable, // -x - NonEmpty, // -s - NamedPipe, // -p - Socket, // -S - BlockSpecial, // -b - CharSpecial, // -c - Sticky, // -k - UIDOwner, // -O - GIDOwner, // -G - ModifiedSinceStatusChange, // -N - SetUID, // -u - SetGID, // -g - Terminal, // -t - NonNull, // -n - Null, // -z + Exists, // -e + Directory, // -d + File, // -f + Symlink, // -h or -L + Readable, // -r + Writable, // -w + Executable, // -x + NonEmpty, // -s + NamedPipe, // -p + Socket, // -S + BlockSpecial, // -b + CharSpecial, // -c + Sticky, // -k + UIDOwner, // -O + GIDOwner, // -G + ModifiedSinceStatusChange, // -N + SetUID, // -u + SetGID, // -g + Terminal, // -t + NonNull, // -n + Null, // -z } impl FromStr for UnaryOp { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "-e" => Ok(Self::Exists), - "-d" => Ok(Self::Directory), - "-f" => Ok(Self::File), - "-h" | "-L" => Ok(Self::Symlink), // -h or -L - "-r" => Ok(Self::Readable), - "-w" => Ok(Self::Writable), - "-x" => Ok(Self::Executable), - "-s" => Ok(Self::NonEmpty), - "-p" => Ok(Self::NamedPipe), - "-S" => Ok(Self::Socket), - "-b" => Ok(Self::BlockSpecial), - "-c" => Ok(Self::CharSpecial), - "-k" => Ok(Self::Sticky), - "-O" => Ok(Self::UIDOwner), - "-G" => Ok(Self::GIDOwner), - "-N" => Ok(Self::ModifiedSinceStatusChange), - "-u" => Ok(Self::SetUID), - "-g" => Ok(Self::SetGID), - "-t" => Ok(Self::Terminal), - "-n" => Ok(Self::NonNull), - "-z" => Ok(Self::Null), - _ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] }) - } - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "-e" => Ok(Self::Exists), + "-d" => Ok(Self::Directory), + "-f" => Ok(Self::File), + "-h" | "-L" => Ok(Self::Symlink), // -h or -L + "-r" => Ok(Self::Readable), + "-w" => Ok(Self::Writable), + "-x" => Ok(Self::Executable), + "-s" => Ok(Self::NonEmpty), + "-p" => Ok(Self::NamedPipe), + "-S" => Ok(Self::Socket), + "-b" => Ok(Self::BlockSpecial), + "-c" => Ok(Self::CharSpecial), + "-k" => Ok(Self::Sticky), + "-O" => Ok(Self::UIDOwner), + "-G" => Ok(Self::GIDOwner), + "-N" => Ok(Self::ModifiedSinceStatusChange), + "-u" => Ok(Self::SetUID), + "-g" => Ok(Self::SetGID), + "-t" => Ok(Self::Terminal), + "-n" => Ok(Self::NonNull), + "-z" => Ok(Self::Null), + _ => Err(ShErr::Simple { + kind: ShErrKind::SyntaxErr, + msg: "Invalid test operator".into(), + notes: vec![], + }), + } + } } #[derive(Debug, Clone)] pub enum TestOp { - Unary(UnaryOp), - StringEq, // == - StringNeq, // != - IntEq, // -eq - IntNeq, // -ne - IntGt, // -gt - IntLt, // -lt - IntGe, // -ge - IntLe, // -le - RegexMatch, // =~ + Unary(UnaryOp), + StringEq, // == + StringNeq, // != + IntEq, // -eq + IntNeq, // -ne + IntGt, // -gt + IntLt, // -lt + IntGe, // -ge + IntLe, // -le + RegexMatch, // =~ } impl FromStr for TestOp { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "==" => Ok(Self::StringEq), - "!=" => Ok(Self::StringNeq), - "=~" => Ok(Self::RegexMatch), - "-eq" => Ok(Self::IntEq), - "-ne" => Ok(Self::IntNeq), - "-gt" => Ok(Self::IntGt), - "-lt" => Ok(Self::IntLt), - "-ge" => Ok(Self::IntGe), - "-le" => Ok(Self::IntLe), - _ if TEST_UNARY_OPS.contains(&s) => { - Ok(Self::Unary(s.parse::()?)) - } - _ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] }) - } - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "==" => Ok(Self::StringEq), + "!=" => Ok(Self::StringNeq), + "=~" => Ok(Self::RegexMatch), + "-eq" => Ok(Self::IntEq), + "-ne" => Ok(Self::IntNeq), + "-gt" => Ok(Self::IntGt), + "-lt" => Ok(Self::IntLt), + "-ge" => Ok(Self::IntGe), + "-le" => Ok(Self::IntLe), + _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::()?)), + _ => Err(ShErr::Simple { + kind: ShErrKind::SyntaxErr, + msg: "Invalid test operator".into(), + notes: vec![], + }), + } + } } fn replace_posix_classes(pat: &str) -> String { - pat.replace("[[:alnum:]]", r"[A-Za-z0-9]") - .replace("[[:alpha:]]", r"[A-Za-z]") - .replace("[[:blank:]]", r"[ \t]") - .replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]") - .replace("[[:digit:]]", r"[0-9]") - .replace("[[:graph:]]", r"[!-~]") - .replace("[[:lower:]]", r"[a-z]") - .replace("[[:print:]]", r"[\x20-\x7E]") - .replace("[[:space:]]", r"[ \t\r\n\x0B\x0C]") // vertical tab (\x0B), form feed (\x0C) - .replace("[[:upper:]]", r"[A-Z]") - .replace("[[:xdigit:]]", r"[0-9A-Fa-f]") + pat + .replace("[[:alnum:]]", r"[A-Za-z0-9]") + .replace("[[:alpha:]]", r"[A-Za-z]") + .replace("[[:blank:]]", r"[ \t]") + .replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]") + .replace("[[:digit:]]", r"[0-9]") + .replace("[[:graph:]]", r"[!-~]") + .replace("[[:lower:]]", r"[a-z]") + .replace("[[:print:]]", r"[\x20-\x7E]") + .replace("[[:space:]]", r"[ \t\r\n\x0B\x0C]") // vertical tab (\x0B), form feed (\x0C) + .replace("[[:upper:]]", r"[A-Z]") + .replace("[[:xdigit:]]", r"[0-9A-Fa-f]") } pub fn double_bracket_test(node: Node) -> ShResult { - let err_span = node.get_span(); - let NdRule::Test { cases } = node.class else { - unreachable!() - }; - let mut last_result = false; - let mut conjunct_op: Option; + let err_span = node.get_span(); + let NdRule::Test { cases } = node.class else { + unreachable!() + }; + let mut last_result = false; + let mut conjunct_op: Option; - for case in cases { - let result = match case { - TestCase::Unary { operator, operand, conjunct } => { - let operand = operand.expand()?.get_words().join(" "); - conjunct_op = conjunct; - let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { - return Err( - ShErr::Full { kind: ShErrKind::SyntaxErr, msg: "Invalid unary operator".into(), notes: vec![], span: err_span } - ) - }; - match op { - UnaryOp::Exists => { - let path = PathBuf::from(operand.as_str()); - path.exists() - } - UnaryOp::Directory => { - let path = PathBuf::from(operand.as_str()); - if path.exists() { - path.metadata() - .unwrap() - .is_dir() - } else { - false - } - } - UnaryOp::File => { - let path = PathBuf::from(operand.as_str()); - if path.exists() { - path.metadata() - .unwrap() - .is_file() - } else { - false - } - } - UnaryOp::Symlink => { - let path = PathBuf::from(operand.as_str()); - if path.exists() { - path.metadata() - .unwrap() - .file_type() - .is_symlink() - } else { - false - } - } - UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(), - UnaryOp::Writable => nix::unistd::access(operand.as_str(), AccessFlags::W_OK).is_ok(), - UnaryOp::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(), - UnaryOp::NonEmpty => { - match metadata(operand.as_str()) { - Ok(meta) => meta.len() > 0, - Err(_) => false - } - } - UnaryOp::NamedPipe => { - match stat::stat(operand.as_str()) { - Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO), - Err(_) => false, - } - } - UnaryOp::Socket => { - match stat::stat(operand.as_str()) { - Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK), - Err(_) => false, - } - } - UnaryOp::BlockSpecial => { - match stat::stat(operand.as_str()) { - Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK), - Err(_) => false, - } - } - UnaryOp::CharSpecial => { - match stat::stat(operand.as_str()) { - Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR), - Err(_) => false, - } - } - UnaryOp::Sticky => { - match stat::stat(operand.as_str()) { - Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0, - Err(_) => false, - } - } - UnaryOp::UIDOwner => { - match stat::stat(operand.as_str()) { - Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(), - Err(_) => false, - } - } + for case in cases { + let result = match case { + TestCase::Unary { + operator, + operand, + conjunct, + } => { + let operand = operand.expand()?.get_words().join(" "); + conjunct_op = conjunct; + let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { + return Err(ShErr::Full { + kind: ShErrKind::SyntaxErr, + msg: "Invalid unary operator".into(), + notes: vec![], + span: err_span, + }); + }; + match op { + UnaryOp::Exists => { + let path = PathBuf::from(operand.as_str()); + path.exists() + } + UnaryOp::Directory => { + let path = PathBuf::from(operand.as_str()); + if path.exists() { + path.metadata().unwrap().is_dir() + } else { + false + } + } + UnaryOp::File => { + let path = PathBuf::from(operand.as_str()); + if path.exists() { + path.metadata().unwrap().is_file() + } else { + false + } + } + UnaryOp::Symlink => { + let path = PathBuf::from(operand.as_str()); + if path.exists() { + path.metadata().unwrap().file_type().is_symlink() + } else { + false + } + } + UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(), + UnaryOp::Writable => nix::unistd::access(operand.as_str(), AccessFlags::W_OK).is_ok(), + UnaryOp::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(), + UnaryOp::NonEmpty => match metadata(operand.as_str()) { + Ok(meta) => meta.len() > 0, + Err(_) => false, + }, + UnaryOp::NamedPipe => match stat::stat(operand.as_str()) { + Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO), + Err(_) => false, + }, + UnaryOp::Socket => match stat::stat(operand.as_str()) { + Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK), + Err(_) => false, + }, + UnaryOp::BlockSpecial => match stat::stat(operand.as_str()) { + Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK), + Err(_) => false, + }, + UnaryOp::CharSpecial => match stat::stat(operand.as_str()) { + Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR), + Err(_) => false, + }, + UnaryOp::Sticky => match stat::stat(operand.as_str()) { + Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0, + Err(_) => false, + }, + UnaryOp::UIDOwner => match stat::stat(operand.as_str()) { + Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(), + Err(_) => false, + }, - UnaryOp::GIDOwner => { - match stat::stat(operand.as_str()) { - Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(), - Err(_) => false, - } - } + UnaryOp::GIDOwner => match stat::stat(operand.as_str()) { + Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(), + Err(_) => false, + }, - UnaryOp::ModifiedSinceStatusChange => { - match stat::stat(operand.as_str()) { - Ok(stat) => stat.st_mtime > stat.st_ctime, - Err(_) => false, - } - } + UnaryOp::ModifiedSinceStatusChange => match stat::stat(operand.as_str()) { + Ok(stat) => stat.st_mtime > stat.st_ctime, + Err(_) => false, + }, - UnaryOp::SetUID => { - match stat::stat(operand.as_str()) { - Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0, - Err(_) => false, - } - } + UnaryOp::SetUID => match stat::stat(operand.as_str()) { + Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0, + Err(_) => false, + }, - UnaryOp::SetGID => { - match stat::stat(operand.as_str()) { - Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0, - Err(_) => false, - } - } + UnaryOp::SetGID => match stat::stat(operand.as_str()) { + Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0, + Err(_) => false, + }, - UnaryOp::Terminal => { - match operand.as_str().parse::() { - Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 }, - Err(_) => false, - } - } - UnaryOp::NonNull => !operand.is_empty(), - UnaryOp::Null => operand.is_empty(), - } - } - TestCase::Binary { lhs, operator, rhs, conjunct } => { - let lhs = lhs.expand()?.get_words().join(" "); - let rhs = rhs.expand()?.get_words().join(" "); - conjunct_op = conjunct; - let test_op = operator.as_str().parse::()?; - flog!(DEBUG, lhs); - flog!(DEBUG, rhs); - flog!(DEBUG, test_op); - match test_op { - TestOp::Unary(_) => { - return Err( - ShErr::Full { - kind: ShErrKind::SyntaxErr, - msg: "Expected a binary operator in this test call; found a unary operator".into(), - notes: vec![], - span: err_span - } - ) - } - TestOp::StringEq => rhs.trim() == lhs.trim(), - TestOp::StringNeq => rhs.trim() != lhs.trim(), - TestOp::IntNeq | - TestOp::IntGt | - TestOp::IntLt | - TestOp::IntGe | - TestOp::IntLe | - TestOp::IntEq => { - let err = ShErr::Full { - kind: ShErrKind::SyntaxErr, - msg: format!("Expected an integer with '{}' operator", operator.as_str()), - notes: vec![], - span: err_span.clone() - }; - let Ok(lhs) = lhs.trim().parse::() else { - return Err(err) - }; - let Ok(rhs) = rhs.trim().parse::() else { - return Err(err) - }; - match test_op { - TestOp::IntNeq => lhs != rhs, - TestOp::IntGt => lhs > rhs, - TestOp::IntLt => lhs < rhs, - TestOp::IntGe => lhs >= rhs, - TestOp::IntLe => lhs <= rhs, - TestOp::IntEq => lhs == rhs, - _ => unreachable!() - } - } - TestOp::RegexMatch => { - // FIXME: Imagine doing all of this in every single iteration of a loop - let cleaned = replace_posix_classes(&rhs); - let regex = Regex::new(&cleaned).unwrap(); - regex.is_match(&lhs) - } - } - } - }; - flog!(DEBUG, last_result); + UnaryOp::Terminal => match operand.as_str().parse::() { + Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 }, + Err(_) => false, + }, + UnaryOp::NonNull => !operand.is_empty(), + UnaryOp::Null => operand.is_empty(), + } + } + TestCase::Binary { + lhs, + operator, + rhs, + conjunct, + } => { + let lhs = lhs.expand()?.get_words().join(" "); + let rhs = rhs.expand()?.get_words().join(" "); + conjunct_op = conjunct; + let test_op = operator.as_str().parse::()?; + flog!(DEBUG, lhs); + flog!(DEBUG, rhs); + flog!(DEBUG, test_op); + match test_op { + TestOp::Unary(_) => { + return Err(ShErr::Full { + kind: ShErrKind::SyntaxErr, + msg: "Expected a binary operator in this test call; found a unary operator".into(), + notes: vec![], + span: err_span, + }) + } + TestOp::StringEq => rhs.trim() == lhs.trim(), + TestOp::StringNeq => rhs.trim() != lhs.trim(), + TestOp::IntNeq + | TestOp::IntGt + | TestOp::IntLt + | TestOp::IntGe + | TestOp::IntLe + | TestOp::IntEq => { + let err = ShErr::Full { + kind: ShErrKind::SyntaxErr, + msg: format!("Expected an integer with '{}' operator", operator.as_str()), + notes: vec![], + span: err_span.clone(), + }; + let Ok(lhs) = lhs.trim().parse::() else { + return Err(err); + }; + let Ok(rhs) = rhs.trim().parse::() else { + return Err(err); + }; + match test_op { + TestOp::IntNeq => lhs != rhs, + TestOp::IntGt => lhs > rhs, + TestOp::IntLt => lhs < rhs, + TestOp::IntGe => lhs >= rhs, + TestOp::IntLe => lhs <= rhs, + TestOp::IntEq => lhs == rhs, + _ => unreachable!(), + } + } + TestOp::RegexMatch => { + // FIXME: Imagine doing all of this in every single iteration of a loop + let cleaned = replace_posix_classes(&rhs); + let regex = Regex::new(&cleaned).unwrap(); + regex.is_match(&lhs) + } + } + } + }; + flog!(DEBUG, last_result); - if let Some(op) = conjunct_op { - match op { - ConjunctOp::And if !last_result => { - last_result = result; - break - } - ConjunctOp::Or if last_result => { - last_result = result; - break - } - _ => {} - } - } else { - last_result = result; - } - } - flog!(DEBUG, last_result); - Ok(last_result) + if let Some(op) = conjunct_op { + match op { + ConjunctOp::And if !last_result => { + last_result = result; + break; + } + ConjunctOp::Or if last_result => { + last_result = result; + break; + } + _ => {} + } + } else { + last_result = result; + } + } + flog!(DEBUG, last_result); + Ok(last_result) } diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index 5a378b5..fc53d22 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -1,190 +1,191 @@ use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock}; -use crate::{getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}}; +use crate::{ + getopt::{get_opts_from_tokens, Opt, OptSet}, + jobs::JobBldr, + libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, + parse::{NdRule, Node}, + prelude::*, + procio::{borrow_fd, IoStack}, +}; use super::setup_builtin; pub static ZOLTRAAK_OPTS: LazyLock = LazyLock::new(|| { - [ - Opt::Long("dry-run".into()), - Opt::Long("confirm".into()), - Opt::Long("no-preserve-root".into()), - Opt::Short('r'), - Opt::Short('f'), - Opt::Short('v') - ].into() + [ + Opt::Long("dry-run".into()), + Opt::Long("confirm".into()), + Opt::Long("no-preserve-root".into()), + Opt::Short('r'), + Opt::Short('f'), + Opt::Short('v'), + ] + .into() }); bitflags! { - #[derive(Clone,Copy,Debug,PartialEq,Eq)] - struct ZoltFlags: u32 { - const DRY = 0b000001; - const CONFIRM = 0b000010; - const NO_PRESERVE_ROOT = 0b000100; - const RECURSIVE = 0b001000; - const FORCE = 0b010000; - const VERBOSE = 0b100000; - } + #[derive(Clone,Copy,Debug,PartialEq,Eq)] + struct ZoltFlags: u32 { + const DRY = 0b000001; + const CONFIRM = 0b000010; + const NO_PRESERVE_ROOT = 0b000100; + const RECURSIVE = 0b001000; + const FORCE = 0b010000; + const VERBOSE = 0b100000; + } } /// Annihilate a file /// /// This command works similarly to 'rm', but behaves more destructively. -/// The file given as an argument is completely destroyed. The command works by shredding all of the data contained in the file, before truncating the length of the file to 0 to ensure that not even any metadata remains. +/// The file given as an argument is completely destroyed. The command works by +/// shredding all of the data contained in the file, before truncating the +/// length of the file to 0 to ensure that not even any metadata remains. pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let NdRule::Command { assignments: _, argv } = node.class else { - unreachable!() - }; - let mut flags = ZoltFlags::empty(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + let mut flags = ZoltFlags::empty(); - let (argv,opts) = get_opts_from_tokens(argv); + let (argv, opts) = get_opts_from_tokens(argv); - for opt in opts { - if !ZOLTRAAK_OPTS.contains(&opt) { - return Err( - ShErr::simple( - ShErrKind::SyntaxErr, - format!("zoltraak: unrecognized option '{opt}'") - ) - ) - } - match opt { - Opt::Long(flag) => { - match flag.as_str() { - "no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT, - "confirm" => flags |= ZoltFlags::CONFIRM, - "dry-run" => flags |= ZoltFlags::DRY, - _ => unreachable!() - } - } - Opt::Short(flag) => { - match flag { - 'r' => flags |= ZoltFlags::RECURSIVE, - 'f' => flags |= ZoltFlags::FORCE, - 'v' => flags |= ZoltFlags::VERBOSE, - _ => unreachable!() - } - } - } - } + for opt in opts { + if !ZOLTRAAK_OPTS.contains(&opt) { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{opt}'"), + )); + } + match opt { + Opt::Long(flag) => match flag.as_str() { + "no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT, + "confirm" => flags |= ZoltFlags::CONFIRM, + "dry-run" => flags |= ZoltFlags::DRY, + _ => unreachable!(), + }, + Opt::Short(flag) => match flag { + 'r' => flags |= ZoltFlags::RECURSIVE, + 'f' => flags |= ZoltFlags::FORCE, + 'v' => flags |= ZoltFlags::VERBOSE, + _ => unreachable!(), + }, + } + } - let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut io_frame = io_frame.unwrap(); - io_frame.redirect()?; + let mut io_frame = io_frame.unwrap(); + io_frame.redirect()?; - for (arg,span) in argv { - if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { - return Err( - ShErr::simple( - ShErrKind::ExecFail, - "zoltraak: Attempted to destroy root directory '/'" - ) - .with_note( - Note::new("If you really want to do this, you can use the --no-preserve-root flag") - .with_sub_notes(vec![ - "Example: 'zoltraak --no-preserve-root /'" - ]) - ) - ) - } - if let Err(e) = annihilate(&arg, flags).blame(span) { - io_frame.restore()?; - return Err(e); - } - } + for (arg, span) in argv { + if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { + return Err( + ShErr::simple( + ShErrKind::ExecFail, + "zoltraak: Attempted to destroy root directory '/'", + ) + .with_note( + Note::new("If you really want to do this, you can use the --no-preserve-root flag") + .with_sub_notes(vec!["Example: 'zoltraak --no-preserve-root /'"]), + ), + ); + } + if let Err(e) = annihilate(&arg, flags).blame(span) { + io_frame.restore()?; + return Err(e); + } + } - io_frame.restore()?; + io_frame.restore()?; - Ok(()) + Ok(()) } fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> { - let path_buf = PathBuf::from(path); - let is_recursive = flags.contains(ZoltFlags::RECURSIVE); - let is_verbose = flags.contains(ZoltFlags::VERBOSE); + let path_buf = PathBuf::from(path); + let is_recursive = flags.contains(ZoltFlags::RECURSIVE); + let is_verbose = flags.contains(ZoltFlags::VERBOSE); - const BLOCK_SIZE: u64 = 4096; + const BLOCK_SIZE: u64 = 4096; - if !path_buf.exists() { - return Err( - ShErr::simple( - ShErrKind::ExecFail, - format!("zoltraak: File '{path}' not found") - ) - ) - } + if !path_buf.exists() { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("zoltraak: File '{path}' not found"), + )); + } - if path_buf.is_file() { - let mut file = OpenOptions::new() - .write(true) - .custom_flags(libc::O_DIRECT) - .open(path_buf)?; + if path_buf.is_file() { + let mut file = OpenOptions::new() + .write(true) + .custom_flags(libc::O_DIRECT) + .open(path_buf)?; - let meta = file.metadata()?; - let file_size = meta.len(); - let full_blocks = file_size / BLOCK_SIZE; - let byte_remainder = file_size % BLOCK_SIZE; + let meta = file.metadata()?; + let file_size = meta.len(); + let full_blocks = file_size / BLOCK_SIZE; + let byte_remainder = file_size % BLOCK_SIZE; - let full_buf = vec![0; BLOCK_SIZE as usize]; - let remainder_buf = vec![0; byte_remainder as usize]; + let full_buf = vec![0; BLOCK_SIZE as usize]; + let remainder_buf = vec![0; byte_remainder as usize]; - for _ in 0..full_blocks { - file.write_all(&full_buf)?; - } + for _ in 0..full_blocks { + file.write_all(&full_buf)?; + } - if byte_remainder > 0 { - file.write_all(&remainder_buf)?; - } + if byte_remainder > 0 { + file.write_all(&remainder_buf)?; + } - file.set_len(0)?; - mem::drop(file); - fs::remove_file(path)?; - if is_verbose { - let stderr = borrow_fd(STDERR_FILENO); - write(stderr, format!("shredded file '{path}'\n").as_bytes())?; - } + file.set_len(0)?; + mem::drop(file); + fs::remove_file(path)?; + if is_verbose { + let stderr = borrow_fd(STDERR_FILENO); + write(stderr, format!("shredded file '{path}'\n").as_bytes())?; + } + } else if path_buf.is_dir() { + if is_recursive { + annihilate_recursive(path, flags)?; // scary + } else { + return Err( + ShErr::simple( + ShErrKind::ExecFail, + format!("zoltraak: '{path}' is a directory"), + ) + .with_note( + Note::new("Use the '-r' flag to recursively shred directories") + .with_sub_notes(vec!["Example: 'zoltraak -r directory'"]), + ), + ); + } + } - } else if path_buf.is_dir() { - if is_recursive { - annihilate_recursive(path, flags)?; // scary - } else { - return Err( - ShErr::simple( - ShErrKind::ExecFail, - format!("zoltraak: '{path}' is a directory") - ) - .with_note( - Note::new("Use the '-r' flag to recursively shred directories") - .with_sub_notes(vec![ - "Example: 'zoltraak -r directory'" - ]) - ) - ) - } - } - - Ok(()) + Ok(()) } fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> { - let dir_path = PathBuf::from(dir); - let is_verbose = flags.contains(ZoltFlags::VERBOSE); + let dir_path = PathBuf::from(dir); + let is_verbose = flags.contains(ZoltFlags::VERBOSE); - for dir_entry in fs::read_dir(&dir_path)? { - let entry = dir_entry?.path(); - let file = entry.to_str().unwrap(); + for dir_entry in fs::read_dir(&dir_path)? { + let entry = dir_entry?.path(); + let file = entry.to_str().unwrap(); - if entry.is_file() { - annihilate(file, flags)?; - } else if entry.is_dir() { - annihilate_recursive(file, flags)?; - } - } - fs::remove_dir(dir)?; - if is_verbose { - let stderr = borrow_fd(STDERR_FILENO); - write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?; - } - Ok(()) + if entry.is_file() { + annihilate(file, flags)?; + } else if entry.is_dir() { + annihilate_recursive(file, flags)?; + } + } + fs::remove_dir(dir)?; + if is_verbose { + let stderr = borrow_fd(STDERR_FILENO); + write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?; + } + Ok(()) } diff --git a/src/expand.rs b/src/expand.rs index efeca46..90964b8 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -5,23 +5,15 @@ use std::str::{Chars, FromStr}; use glob::Pattern; use regex::Regex; -use crate::state::{read_jobs, read_vars, write_jobs, write_meta, write_vars, LogTab}; -use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; -use crate::prelude::*; -use crate::parse::{Redir, RedirType}; +use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::parse::execute::exec_input; use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule}; -use crate::libsh::error::{ShErr, ShErrKind, ShResult}; +use crate::parse::{Redir, RedirType}; +use crate::prelude::*; +use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; +use crate::state::{read_jobs, read_vars, write_jobs, write_meta, write_vars, LogTab}; -const PARAMETERS: [char;7] = [ - '@', - '*', - '#', - '$', - '?', - '!', - '0' -]; +const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; /// Variable substitution marker pub const VAR_SUB: char = '\u{fdd0}'; @@ -39,1465 +31,1493 @@ pub const PROC_SUB_IN: char = '\u{fdd5}'; pub const PROC_SUB_OUT: char = '\u{fdd6}'; impl Tk { - /// Create a new expanded token - /// - /// params - /// tokens: A vector of raw tokens lexed from the expansion result - /// span: The span of the original token that is being expanded - /// flags: some TkFlags - pub fn expand(self) -> ShResult { - let flags = self.flags; - let span = self.span.clone(); - let exp = Expander::new(self).expand()?; - let class = TkRule::Expanded { exp }; - Ok(Self { class, span, flags, }) - } - /// Perform word splitting - pub fn get_words(&self) -> Vec { - match &self.class { - TkRule::Expanded { exp } => exp.clone(), - _ => vec![self.to_string()] - } - } + /// Create a new expanded token + /// + /// params + /// tokens: A vector of raw tokens lexed from the expansion result + /// span: The span of the original token that is being expanded + /// flags: some TkFlags + pub fn expand(self) -> ShResult { + let flags = self.flags; + let span = self.span.clone(); + let exp = Expander::new(self).expand()?; + let class = TkRule::Expanded { exp }; + Ok(Self { class, span, flags }) + } + /// Perform word splitting + pub fn get_words(&self) -> Vec { + match &self.class { + TkRule::Expanded { exp } => exp.clone(), + _ => vec![self.to_string()], + } + } } pub struct Expander { - raw: String, + raw: String, } impl Expander { - pub fn new(raw: Tk) -> Self { - let unescaped = unescape_str(raw.span.as_str()); - Self { raw: unescaped } - } - pub fn expand(&mut self) -> ShResult> { - let mut chars = self.raw.chars().peekable(); - self.raw = expand_raw(&mut chars)?; - if let Ok(glob_exp) = expand_glob(&self.raw) { - if !glob_exp.is_empty() { - self.raw = glob_exp; - } - } - Ok(self.split_words()) - } - pub fn split_words(&mut self) -> Vec { - let mut words = vec![]; - let mut chars = self.raw.chars(); - let mut cur_word = String::new(); + pub fn new(raw: Tk) -> Self { + let unescaped = unescape_str(raw.span.as_str()); + Self { raw: unescaped } + } + pub fn expand(&mut self) -> ShResult> { + let mut chars = self.raw.chars().peekable(); + self.raw = expand_raw(&mut chars)?; + if let Ok(glob_exp) = expand_glob(&self.raw) { + if !glob_exp.is_empty() { + self.raw = glob_exp; + } + } + Ok(self.split_words()) + } + pub fn split_words(&mut self) -> Vec { + let mut words = vec![]; + let mut chars = self.raw.chars(); + let mut cur_word = String::new(); - 'outer: while let Some(ch) = chars.next() { - match ch { - DUB_QUOTE | - SNG_QUOTE | - SUBSH => { - while let Some(q_ch) = chars.next() { - match q_ch { - _ if q_ch == ch => continue 'outer, // Isn't rust cool - _ => cur_word.push(q_ch) - } - } - } - _ if is_field_sep(ch) => { - words.push(mem::take(&mut cur_word)); - } - _ => cur_word.push(ch) - } - } - if !cur_word.is_empty() { - words.push(cur_word); - } - words - } + 'outer: while let Some(ch) = chars.next() { + match ch { + DUB_QUOTE | SNG_QUOTE | SUBSH => { + while let Some(q_ch) = chars.next() { + match q_ch { + _ if q_ch == ch => continue 'outer, // Isn't rust cool + _ => cur_word.push(q_ch), + } + } + } + _ if is_field_sep(ch) => { + words.push(mem::take(&mut cur_word)); + } + _ => cur_word.push(ch), + } + } + if !cur_word.is_empty() { + words.push(cur_word); + } + words + } } pub fn expand_raw(chars: &mut Peekable>) -> ShResult { - let mut result = String::new(); + let mut result = String::new(); - while let Some(ch) = chars.next() { - match ch { - TILDE_SUB => { - let home = env::var("HOME").unwrap_or_default(); - result.push_str(&home); - } - PROC_SUB_OUT =>{ - let mut inner = String::new(); - while let Some(ch) = chars.next() { - match ch { - PROC_SUB_OUT => break, - _ => inner.push(ch) - } - } - let fd_path = expand_proc_sub(&inner, false)?; - result.push_str(&fd_path); - } - PROC_SUB_IN =>{ - let mut inner = String::new(); - while let Some(ch) = chars.next() { - match ch { - PROC_SUB_IN => break, - _ => inner.push(ch) - } - } - let fd_path = expand_proc_sub(&inner, true)?; - result.push_str(&fd_path); - } - VAR_SUB => { - flog!(INFO, chars); - let expanded = expand_var(chars)?; - result.push_str(&expanded); - } - _ => result.push(ch) - } - } - Ok(result) + while let Some(ch) = chars.next() { + match ch { + TILDE_SUB => { + let home = env::var("HOME").unwrap_or_default(); + result.push_str(&home); + } + PROC_SUB_OUT => { + let mut inner = String::new(); + while let Some(ch) = chars.next() { + match ch { + PROC_SUB_OUT => break, + _ => inner.push(ch), + } + } + let fd_path = expand_proc_sub(&inner, false)?; + result.push_str(&fd_path); + } + PROC_SUB_IN => { + let mut inner = String::new(); + while let Some(ch) = chars.next() { + match ch { + PROC_SUB_IN => break, + _ => inner.push(ch), + } + } + let fd_path = expand_proc_sub(&inner, true)?; + result.push_str(&fd_path); + } + VAR_SUB => { + flog!(INFO, chars); + let expanded = expand_var(chars)?; + result.push_str(&expanded); + } + _ => result.push(ch), + } + } + Ok(result) } pub fn expand_var(chars: &mut Peekable>) -> ShResult { - let mut var_name = String::new(); - let mut in_brace = false; - while let Some(&ch) = chars.peek() { - match ch { - SUBSH if var_name.is_empty() => { - chars.next(); // now safe to consume - let mut subsh_body = String::new(); - while let Some(c) = chars.next() { - if c == SUBSH { break } - subsh_body.push(c); - } - let expanded = expand_cmd_sub(&subsh_body)?; - return Ok(expanded); - } - '{' if var_name.is_empty() => { - chars.next(); // consume the brace - in_brace = true; - } - '}' if in_brace => { - chars.next(); // consume the brace - let val = perform_param_expansion(&var_name)?; - return Ok(val); - } - ch if in_brace => { - chars.next(); // safe to consume - var_name.push(ch); - } - ch if var_name.is_empty() && PARAMETERS.contains(&ch) => { - chars.next(); - let parameter = format!("{ch}"); - let val = read_vars(|v| v.get_var(¶meter)); - flog!(DEBUG, val); - return Ok(val) - } - ch if is_hard_sep(ch) || !(ch.is_alphanumeric() || ch == '_' || ch == '-') => { - let val = read_vars(|v| v.get_var(&var_name)); - flog!(INFO,var_name); - flog!(INFO,val); - flog!(INFO,ch); - return Ok(val); - } - _ => { - chars.next(); - var_name.push(ch); - } - } - } - if !var_name.is_empty() { - let var_val = read_vars(|v| v.get_var(&var_name)); - flog!(INFO,var_val); - Ok(var_val) - } else { - Ok(String::new()) - } + let mut var_name = String::new(); + let mut in_brace = false; + while let Some(&ch) = chars.peek() { + match ch { + SUBSH if var_name.is_empty() => { + chars.next(); // now safe to consume + let mut subsh_body = String::new(); + while let Some(c) = chars.next() { + if c == SUBSH { + break; + } + subsh_body.push(c); + } + let expanded = expand_cmd_sub(&subsh_body)?; + return Ok(expanded); + } + '{' if var_name.is_empty() => { + chars.next(); // consume the brace + in_brace = true; + } + '}' if in_brace => { + chars.next(); // consume the brace + let val = perform_param_expansion(&var_name)?; + return Ok(val); + } + ch if in_brace => { + chars.next(); // safe to consume + var_name.push(ch); + } + ch if var_name.is_empty() && PARAMETERS.contains(&ch) => { + chars.next(); + let parameter = format!("{ch}"); + let val = read_vars(|v| v.get_var(¶meter)); + flog!(DEBUG, val); + return Ok(val); + } + ch if is_hard_sep(ch) || !(ch.is_alphanumeric() || ch == '_' || ch == '-') => { + let val = read_vars(|v| v.get_var(&var_name)); + flog!(INFO, var_name); + flog!(INFO, val); + flog!(INFO, ch); + return Ok(val); + } + _ => { + chars.next(); + var_name.push(ch); + } + } + } + if !var_name.is_empty() { + let var_val = read_vars(|v| v.get_var(&var_name)); + flog!(INFO, var_val); + Ok(var_val) + } else { + Ok(String::new()) + } } pub fn expand_glob(raw: &str) -> ShResult { - let mut words = vec![]; + let mut words = vec![]; - for entry in glob::glob(raw) - .map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? { - let entry = entry - .map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; + for entry in + glob::glob(raw).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? + { + let entry = + entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; - words.push(entry.to_str().unwrap().to_string()) - } - Ok(words.join(" ")) + words.push(entry.to_str().unwrap().to_string()) + } + Ok(words.join(" ")) } pub fn is_a_number(raw: &str) -> bool { - let trimmed = raw.trim(); - trimmed.parse::().is_ok() || trimmed.parse::().is_ok() + let trimmed = raw.trim(); + trimmed.parse::().is_ok() || trimmed.parse::().is_ok() } enum ArithTk { - Num(f64), - Op(ArithOp), - LParen, - RParen + Num(f64), + Op(ArithOp), + LParen, + RParen, } impl ArithTk { - pub fn tokenize(raw: &str) -> ShResult> { - let mut tokens = Vec::new(); - let mut chars = raw.chars().peekable(); + pub fn tokenize(raw: &str) -> ShResult> { + let mut tokens = Vec::new(); + let mut chars = raw.chars().peekable(); - while let Some(&ch) = chars.peek() { - match ch { - ' ' | '\t' => { chars.next(); } - '0'..='9' | '.' => { - let mut num = String::new(); - while let Some(&digit) = chars.peek() { - if digit.is_ascii_digit() || digit == '.' { - num.push(digit); - chars.next(); - } else { - break; - } - } - let Ok(num) = num.parse::() else { - panic!() - }; - tokens.push(Self::Num(num)); - } - '+' | '-' | '*' | '/' | '%' => { - let mut buf = String::new(); - buf.push(ch); - tokens.push(Self::Op(buf.parse::().unwrap())); - chars.next(); - } - '(' => { tokens.push(Self::LParen); chars.next(); } - ')' => { tokens.push(Self::RParen); chars.next(); } - _ => return Err(ShErr::Simple { kind: ShErrKind::ParseErr, msg: "Invalid character in arithmetic substitution".into(), notes: vec![] }) - } - } + while let Some(&ch) = chars.peek() { + match ch { + ' ' | '\t' => { + chars.next(); + } + '0'..='9' | '.' => { + let mut num = String::new(); + while let Some(&digit) = chars.peek() { + if digit.is_ascii_digit() || digit == '.' { + num.push(digit); + chars.next(); + } else { + break; + } + } + let Ok(num) = num.parse::() else { + panic!() + }; + tokens.push(Self::Num(num)); + } + '+' | '-' | '*' | '/' | '%' => { + let mut buf = String::new(); + buf.push(ch); + tokens.push(Self::Op(buf.parse::().unwrap())); + chars.next(); + } + '(' => { + tokens.push(Self::LParen); + chars.next(); + } + ')' => { + tokens.push(Self::RParen); + chars.next(); + } + _ => { + return Err(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Invalid character in arithmetic substitution".into(), + notes: vec![], + }) + } + } + } - Ok(tokens) - } + Ok(tokens) + } - fn to_rpn(tokens: Vec) -> ShResult> { - let mut output = Vec::new(); - let mut ops = Vec::new(); + fn to_rpn(tokens: Vec) -> ShResult> { + let mut output = Vec::new(); + let mut ops = Vec::new(); - fn precedence(op: &ArithOp) -> usize { - match op { - ArithOp::Add | - ArithOp::Sub => 1, - ArithOp::Mul | - ArithOp::Div | - ArithOp::Mod => 2, - } - } + fn precedence(op: &ArithOp) -> usize { + match op { + ArithOp::Add | ArithOp::Sub => 1, + ArithOp::Mul | ArithOp::Div | ArithOp::Mod => 2, + } + } - for token in tokens { - match token { - ArithTk::Num(_) => output.push(token), - ArithTk::Op(op1) => { - while let Some(ArithTk::Op(op2)) = ops.last() { - if precedence(op2) >= precedence(&op1) { - output.push(ops.pop().unwrap()); - } else { - break; - } - } - ops.push(ArithTk::Op(op1)); - } - ArithTk::LParen => ops.push(ArithTk::LParen), - ArithTk::RParen => { - while let Some(top) = ops.pop() { - if let ArithTk::LParen = top { - break; - } else { - output.push(top); - } - } - } - } - } + for token in tokens { + match token { + ArithTk::Num(_) => output.push(token), + ArithTk::Op(op1) => { + while let Some(ArithTk::Op(op2)) = ops.last() { + if precedence(op2) >= precedence(&op1) { + output.push(ops.pop().unwrap()); + } else { + break; + } + } + ops.push(ArithTk::Op(op1)); + } + ArithTk::LParen => ops.push(ArithTk::LParen), + ArithTk::RParen => { + while let Some(top) = ops.pop() { + if let ArithTk::LParen = top { + break; + } else { + output.push(top); + } + } + } + } + } - while let Some(op) = ops.pop() { - output.push(op); - } + while let Some(op) = ops.pop() { + output.push(op); + } - Ok(output) - } - pub fn eval_rpn(tokens: Vec) -> ShResult { - let mut stack = Vec::new(); + Ok(output) + } + pub fn eval_rpn(tokens: Vec) -> ShResult { + let mut stack = Vec::new(); - for token in tokens { - match token { - ArithTk::Num(n) => stack.push(n), - ArithTk::Op(op) => { - let rhs = stack.pop().ok_or(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Missing right-hand operand".into(), - notes: vec![], - })?; - let lhs = stack.pop().ok_or(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Missing left-hand operand".into(), - notes: vec![], - })?; - let result = match op { - ArithOp::Add => lhs + rhs, - ArithOp::Sub => lhs - rhs, - ArithOp::Mul => lhs * rhs, - ArithOp::Div => lhs / rhs, - ArithOp::Mod => lhs % rhs, - }; - stack.push(result); - } - _ => return Err(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Unexpected token during evaluation".into(), - notes: vec![], - }), - } - } + for token in tokens { + match token { + ArithTk::Num(n) => stack.push(n), + ArithTk::Op(op) => { + let rhs = stack.pop().ok_or(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Missing right-hand operand".into(), + notes: vec![], + })?; + let lhs = stack.pop().ok_or(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Missing left-hand operand".into(), + notes: vec![], + })?; + let result = match op { + ArithOp::Add => lhs + rhs, + ArithOp::Sub => lhs - rhs, + ArithOp::Mul => lhs * rhs, + ArithOp::Div => lhs / rhs, + ArithOp::Mod => lhs % rhs, + }; + stack.push(result); + } + _ => { + return Err(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Unexpected token during evaluation".into(), + notes: vec![], + }) + } + } + } - if stack.len() != 1 { - return Err(ShErr::Simple { - kind: ShErrKind::ParseErr, - msg: "Invalid arithmetic expression".into(), - notes: vec![], - }); - } + if stack.len() != 1 { + return Err(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Invalid arithmetic expression".into(), + notes: vec![], + }); + } - Ok(stack[0]) - } + Ok(stack[0]) + } } enum ArithOp { - Add, - Sub, - Mul, - Div, - Mod, + Add, + Sub, + Mul, + Div, + Mod, } impl FromStr for ArithOp { - type Err = ShErr; - fn from_str(s: &str) -> Result { - assert!(s.len() == 1); - match s.chars().next().unwrap() { - '+' => Ok(Self::Add), - '-' => Ok(Self::Sub), - '*' => Ok(Self::Mul), - '/' => Ok(Self::Div), - '%' => Ok(Self::Mod), - _ => Err(ShErr::Simple { kind: ShErrKind::ParseErr, msg: "Invalid arithmetic operator".into(), notes: vec![] }) - } - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + assert!(s.len() == 1); + match s.chars().next().unwrap() { + '+' => Ok(Self::Add), + '-' => Ok(Self::Sub), + '*' => Ok(Self::Mul), + '/' => Ok(Self::Div), + '%' => Ok(Self::Mod), + _ => Err(ShErr::Simple { + kind: ShErrKind::ParseErr, + msg: "Invalid arithmetic operator".into(), + notes: vec![], + }), + } + } } pub fn expand_arithmetic(raw: &str) -> ShResult { - let body = raw - .strip_prefix('(') - .unwrap() - .strip_suffix(')') - .unwrap(); // Unwraps are safe here, we already checked for the parens - let unescaped = unescape_math(body); - let expanded = expand_raw(&mut unescaped.chars().peekable())?; - let tokens = ArithTk::tokenize(&expanded)?; - let rpn = ArithTk::to_rpn(tokens)?; - let result = ArithTk::eval_rpn(rpn)?; - Ok(result.to_string()) + let body = raw.strip_prefix('(').unwrap().strip_suffix(')').unwrap(); // Unwraps are safe here, we already checked for the parens + let unescaped = unescape_math(body); + let expanded = expand_raw(&mut unescaped.chars().peekable())?; + let tokens = ArithTk::tokenize(&expanded)?; + let rpn = ArithTk::to_rpn(tokens)?; + let result = ArithTk::eval_rpn(rpn)?; + Ok(result.to_string()) } pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult { - // FIXME: Still a lot of issues here - // Seems like debugging will be a massive effort - let (rpipe, wpipe) = IoMode::get_pipes(); - let rpipe_raw = rpipe.src_fd(); - let wpipe_raw = wpipe.src_fd(); + // FIXME: Still a lot of issues here + // Seems like debugging will be a massive effort + let (rpipe, wpipe) = IoMode::get_pipes(); + let rpipe_raw = rpipe.src_fd(); + let wpipe_raw = wpipe.src_fd(); - let (proc_fd, register_fd, redir_type, path) = match is_input { - false => (wpipe, rpipe, RedirType::Output, format!("/proc/self/fd/{}", rpipe_raw)), - true => (rpipe, wpipe, RedirType::Input, format!("/proc/self/fd/{}", wpipe_raw)), - }; + let (proc_fd, register_fd, redir_type, path) = match is_input { + false => ( + wpipe, + rpipe, + RedirType::Output, + format!("/proc/self/fd/{}", rpipe_raw), + ), + true => ( + rpipe, + wpipe, + RedirType::Input, + format!("/proc/self/fd/{}", wpipe_raw), + ), + }; - match unsafe { fork()? } { - ForkResult::Child => { - let redir = Redir::new(proc_fd, redir_type); - let io_frame = IoFrame::from_redir(redir); - let mut io_stack = IoStack::new(); - io_stack.push_frame(io_frame); + match unsafe { fork()? } { + ForkResult::Child => { + let redir = Redir::new(proc_fd, redir_type); + let io_frame = IoFrame::from_redir(redir); + let mut io_stack = IoStack::new(); + io_stack.push_frame(io_frame); - if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) { - eprintln!("{e}"); - exit(1); - } - exit(0); - } - ForkResult::Parent { child } => { - write_jobs(|j| j.register_fd(child, register_fd)); - let registered = read_jobs(|j| j.registered_fds().to_vec()); - flog!(DEBUG,registered); - // Do not wait; process may run in background - Ok(path) - } - } -} + if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) { + eprintln!("{e}"); + exit(1); + } + exit(0); + } + ForkResult::Parent { child } => { + write_jobs(|j| j.register_fd(child, register_fd)); + let registered = read_jobs(|j| j.registered_fds().to_vec()); + flog!(DEBUG, registered); + // Do not wait; process may run in background + Ok(path) + } + } +} /// Get the command output of a given command input as a String pub fn expand_cmd_sub(raw: &str) -> ShResult { - flog!(DEBUG, "in expand_cmd_sub"); - flog!(DEBUG, raw); - if raw.starts_with('(') && raw.ends_with(')') { - if let Ok(output) = expand_arithmetic(raw) { - return Ok(output) // It's actually an arithmetic sub - } - } - let (rpipe,wpipe) = IoMode::get_pipes(); - let cmd_sub_redir = Redir::new(wpipe, RedirType::Output); - let cmd_sub_io_frame = IoFrame::from_redir(cmd_sub_redir); - let mut io_stack = IoStack::new(); - let mut io_buf = IoBuf::new(rpipe); + flog!(DEBUG, "in expand_cmd_sub"); + flog!(DEBUG, raw); + if raw.starts_with('(') && raw.ends_with(')') { + if let Ok(output) = expand_arithmetic(raw) { + return Ok(output); // It's actually an arithmetic sub + } + } + let (rpipe, wpipe) = IoMode::get_pipes(); + let cmd_sub_redir = Redir::new(wpipe, RedirType::Output); + let cmd_sub_io_frame = IoFrame::from_redir(cmd_sub_redir); + let mut io_stack = IoStack::new(); + let mut io_buf = IoBuf::new(rpipe); - match unsafe { fork()? } { - ForkResult::Child => { - io_stack.push_frame(cmd_sub_io_frame); - if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) { - eprintln!("{e}"); - exit(1); - } - exit(0); - } - ForkResult::Parent { child } => { - std::mem::drop(cmd_sub_io_frame); // Closes the write pipe - let status = waitpid(child, Some(WtFlag::WSTOPPED))?; - match status { - WtStat::Exited(_, _) => { - flog!(DEBUG, "filling buffer"); - io_buf.fill_buffer()?; - flog!(DEBUG, "done"); - Ok(io_buf.as_str()?.trim().to_string()) - } - _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")) - } - } - } + match unsafe { fork()? } { + ForkResult::Child => { + io_stack.push_frame(cmd_sub_io_frame); + if let Err(e) = exec_input(raw.to_string(), Some(io_stack)) { + eprintln!("{e}"); + exit(1); + } + exit(0); + } + ForkResult::Parent { child } => { + std::mem::drop(cmd_sub_io_frame); // Closes the write pipe + let status = waitpid(child, Some(WtFlag::WSTOPPED))?; + match status { + WtStat::Exited(_, _) => { + flog!(DEBUG, "filling buffer"); + io_buf.fill_buffer()?; + flog!(DEBUG, "done"); + Ok(io_buf.as_str()?.trim().to_string()) + } + _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")), + } + } + } } -/// Processes strings into intermediate representations that are more readable by the program +/// Processes strings into intermediate representations that are more readable +/// by the program /// -/// Clean up a single layer of escape characters, and then replace control characters like '$' with a non-character unicode representation that is unmistakable by the rest of the code +/// Clean up a single layer of escape characters, and then replace control +/// characters like '$' with a non-character unicode representation that is +/// unmistakable by the rest of the code pub fn unescape_str(raw: &str) -> String { - let mut chars = raw.chars().peekable(); - let mut result = String::new(); - let mut first_char = true; + let mut chars = raw.chars().peekable(); + let mut result = String::new(); + let mut first_char = true; - - while let Some(ch) = chars.next() { - match ch { - '~' if first_char => { - result.push(TILDE_SUB) - } - '\\' => { - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '(' => { - result.push(SUBSH); - let mut paren_count = 1; - while let Some(subsh_ch) = chars.next() { - match subsh_ch { - '\\' => { - result.push(subsh_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '$' if chars.peek() != Some(&'(') => result.push(VAR_SUB), - '(' => { - paren_count += 1; - result.push(subsh_ch) - } - ')' => { - paren_count -= 1; - if paren_count == 0 { - result.push(SUBSH); - break - } else { - result.push(subsh_ch) - } - } - _ => result.push(subsh_ch) - } - } - } - '"' => { - result.push(DUB_QUOTE); - while let Some(q_ch) = chars.next() { - match q_ch { - '\\' => { - result.push(q_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '$' => { - result.push(VAR_SUB); - if chars.peek() == Some(&'(') { - chars.next(); - let mut paren_count = 1; - result.push(SUBSH); - while let Some(subsh_ch) = chars.next() { - match subsh_ch { - '\\' => { - result.push(subsh_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '(' => { - result.push(subsh_ch); - paren_count += 1; - } - ')' => { - paren_count -= 1; - if paren_count <= 0 { - result.push(SUBSH); - break - } else { - result.push(subsh_ch); - } - } - _ => result.push(subsh_ch), - } - } - } - } - '"' => { - result.push(DUB_QUOTE); - break - } - _ => result.push(q_ch) - } - } - } - '\'' => { - result.push(SNG_QUOTE); - while let Some(q_ch) = chars.next() { - match q_ch { - '\'' => { - result.push(SNG_QUOTE); - break - } - _ => result.push(q_ch) - } - } - } - '<' if chars.peek() == Some(&'(') => { - chars.next(); - let mut paren_count = 1; - result.push(PROC_SUB_OUT); - while let Some(subsh_ch) = chars.next() { - match subsh_ch { - '\\' => { - result.push(subsh_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '(' => { - result.push(subsh_ch); - paren_count += 1; - } - ')' => { - paren_count -= 1; - if paren_count <= 0 { - result.push(PROC_SUB_OUT); - break - } else { - result.push(subsh_ch); - } - } - _ => result.push(subsh_ch), - } - } - } - '>' if chars.peek() == Some(&'(') => { - chars.next(); - let mut paren_count = 1; - result.push(PROC_SUB_IN); - while let Some(subsh_ch) = chars.next() { - match subsh_ch { - '\\' => { - result.push(subsh_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '(' => { - result.push(subsh_ch); - paren_count += 1; - } - ')' => { - paren_count -= 1; - if paren_count <= 0 { - result.push(PROC_SUB_IN); - break - } else { - result.push(subsh_ch); - } - } - _ => result.push(subsh_ch), - } - } - } - '$' => { - result.push(VAR_SUB); - if chars.peek() == Some(&'$') { - chars.next(); - result.push('$'); - } - } - _ => result.push(ch) - } - first_char = false; - } - result + while let Some(ch) = chars.next() { + match ch { + '~' if first_char => result.push(TILDE_SUB), + '\\' => { + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '(' => { + result.push(SUBSH); + let mut paren_count = 1; + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '$' if chars.peek() != Some(&'(') => result.push(VAR_SUB), + '(' => { + paren_count += 1; + result.push(subsh_ch) + } + ')' => { + paren_count -= 1; + if paren_count == 0 { + result.push(SUBSH); + break; + } else { + result.push(subsh_ch) + } + } + _ => result.push(subsh_ch), + } + } + } + '"' => { + result.push(DUB_QUOTE); + while let Some(q_ch) = chars.next() { + match q_ch { + '\\' => { + result.push(q_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '$' => { + result.push(VAR_SUB); + if chars.peek() == Some(&'(') { + chars.next(); + let mut paren_count = 1; + result.push(SUBSH); + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '(' => { + result.push(subsh_ch); + paren_count += 1; + } + ')' => { + paren_count -= 1; + if paren_count <= 0 { + result.push(SUBSH); + break; + } else { + result.push(subsh_ch); + } + } + _ => result.push(subsh_ch), + } + } + } + } + '"' => { + result.push(DUB_QUOTE); + break; + } + _ => result.push(q_ch), + } + } + } + '\'' => { + result.push(SNG_QUOTE); + while let Some(q_ch) = chars.next() { + match q_ch { + '\'' => { + result.push(SNG_QUOTE); + break; + } + _ => result.push(q_ch), + } + } + } + '<' if chars.peek() == Some(&'(') => { + chars.next(); + let mut paren_count = 1; + result.push(PROC_SUB_OUT); + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '(' => { + result.push(subsh_ch); + paren_count += 1; + } + ')' => { + paren_count -= 1; + if paren_count <= 0 { + result.push(PROC_SUB_OUT); + break; + } else { + result.push(subsh_ch); + } + } + _ => result.push(subsh_ch), + } + } + } + '>' if chars.peek() == Some(&'(') => { + chars.next(); + let mut paren_count = 1; + result.push(PROC_SUB_IN); + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '(' => { + result.push(subsh_ch); + paren_count += 1; + } + ')' => { + paren_count -= 1; + if paren_count <= 0 { + result.push(PROC_SUB_IN); + break; + } else { + result.push(subsh_ch); + } + } + _ => result.push(subsh_ch), + } + } + } + '$' => { + result.push(VAR_SUB); + if chars.peek() == Some(&'$') { + chars.next(); + result.push('$'); + } + } + _ => result.push(ch), + } + first_char = false; + } + result } pub fn unescape_math(raw: &str) -> String { - let mut chars = raw.chars().peekable(); - let mut result = String::new(); + let mut chars = raw.chars().peekable(); + let mut result = String::new(); - while let Some(ch) = chars.next() { - flog!(DEBUG,result); - match ch { - '\\' => { - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '$' => { - result.push(VAR_SUB); - if chars.peek() == Some(&'(') { - result.push(SUBSH); - chars.next(); - let mut paren_count = 1; - while let Some(subsh_ch) = chars.next() { - match subsh_ch { - '\\' => { - result.push(subsh_ch); - if let Some(next_ch) = chars.next() { - result.push(next_ch) - } - } - '$' if chars.peek() != Some(&'(') => result.push(VAR_SUB), - '(' => { - paren_count += 1; - result.push(subsh_ch) - } - ')' => { - paren_count -= 1; - if paren_count == 0 { - result.push(SUBSH); - break - } else { - result.push(subsh_ch) - } - } - _ => result.push(subsh_ch) - } - } - } - } - _ => result.push(ch) - } - } - flog!(INFO, result); - result + while let Some(ch) = chars.next() { + flog!(DEBUG, result); + match ch { + '\\' => { + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '$' => { + result.push(VAR_SUB); + if chars.peek() == Some(&'(') { + result.push(SUBSH); + chars.next(); + let mut paren_count = 1; + while let Some(subsh_ch) = chars.next() { + match subsh_ch { + '\\' => { + result.push(subsh_ch); + if let Some(next_ch) = chars.next() { + result.push(next_ch) + } + } + '$' if chars.peek() != Some(&'(') => result.push(VAR_SUB), + '(' => { + paren_count += 1; + result.push(subsh_ch) + } + ')' => { + paren_count -= 1; + if paren_count == 0 { + result.push(SUBSH); + break; + } else { + result.push(subsh_ch) + } + } + _ => result.push(subsh_ch), + } + } + } + } + _ => result.push(ch), + } + } + flog!(INFO, result); + result } #[derive(Debug)] pub enum ParamExp { - Len, // #var_name - DefaultUnsetOrNull(String), // :- - DefaultUnset(String), // - - SetDefaultUnsetOrNull(String), // := - SetDefaultUnset(String), // = - AltSetNotNull(String), // :+ - AltNotNull(String), // + - ErrUnsetOrNull(String), // :? - ErrUnset(String), // ? - Substr(usize), // :pos - SubstrLen(usize,usize), // :pos:len - RemShortestPrefix(String), // #pattern - RemLongestPrefix(String), // ##pattern - RemShortestSuffix(String), // %pattern - RemLongestSuffix(String), // %%pattern - ReplaceFirstMatch(String,String), // /search/replace - ReplaceAllMatches(String,String), // //search/replace - ReplacePrefix(String,String), // #search/replace - ReplaceSuffix(String,String), // %search/replace - VarNamesWithPrefix(String), // !prefix@ || !prefix* - ExpandInnerVar(String), // !var + Len, // #var_name + DefaultUnsetOrNull(String), // :- + DefaultUnset(String), // - + SetDefaultUnsetOrNull(String), // := + SetDefaultUnset(String), // = + AltSetNotNull(String), // :+ + AltNotNull(String), // + + ErrUnsetOrNull(String), // :? + ErrUnset(String), // ? + Substr(usize), // :pos + SubstrLen(usize, usize), // :pos:len + RemShortestPrefix(String), // #pattern + RemLongestPrefix(String), // ##pattern + RemShortestSuffix(String), // %pattern + RemLongestSuffix(String), // %%pattern + ReplaceFirstMatch(String, String), // /search/replace + ReplaceAllMatches(String, String), // //search/replace + ReplacePrefix(String, String), // #search/replace + ReplaceSuffix(String, String), // %search/replace + VarNamesWithPrefix(String), // !prefix@ || !prefix* + ExpandInnerVar(String), // !var } impl FromStr for ParamExp { - type Err = ShErr; + type Err = ShErr; - fn from_str(s: &str) -> Result { - use ParamExp::*; + fn from_str(s: &str) -> Result { + use ParamExp::*; - let parse_err = || Err(ShErr::Simple { - kind: ShErrKind::SyntaxErr, - msg: "Invalid parameter expansion".into(), - notes: vec![], - }); + let parse_err = || { + Err(ShErr::Simple { + kind: ShErrKind::SyntaxErr, + msg: "Invalid parameter expansion".into(), + notes: vec![], + }) + }; - // Handle indirect var expansion: ${!var} - if let Some(var) = s.strip_prefix('!') { - if var.ends_with('*') || var.ends_with('@') { - return Ok(VarNamesWithPrefix(var.to_string())); - } - return Ok(ExpandInnerVar(var.to_string())); - } + // Handle indirect var expansion: ${!var} + if let Some(var) = s.strip_prefix('!') { + if var.ends_with('*') || var.ends_with('@') { + return Ok(VarNamesWithPrefix(var.to_string())); + } + return Ok(ExpandInnerVar(var.to_string())); + } - // Pattern removals - if let Some(rest) = s.strip_prefix("##") { - return Ok(RemLongestPrefix(rest.to_string())); - } else if let Some(rest) = s.strip_prefix('#') { - return Ok(RemShortestPrefix(rest.to_string())); - } - if let Some(rest) = s.strip_prefix("%%") { - return Ok(RemLongestSuffix(rest.to_string())); - } else if let Some(rest) = s.strip_prefix('%') { - return Ok(RemShortestSuffix(rest.to_string())); - } + // Pattern removals + if let Some(rest) = s.strip_prefix("##") { + return Ok(RemLongestPrefix(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('#') { + return Ok(RemShortestPrefix(rest.to_string())); + } + if let Some(rest) = s.strip_prefix("%%") { + return Ok(RemLongestSuffix(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('%') { + return Ok(RemShortestSuffix(rest.to_string())); + } - // Replacements - if let Some(rest) = s.strip_prefix("//") { - let mut parts = rest.splitn(2, '/'); - let pattern = parts.next().unwrap_or(""); - let repl = parts.next().unwrap_or(""); - return Ok(ReplaceAllMatches(pattern.to_string(), repl.to_string())); - } - if let Some(rest) = s.strip_prefix('/') { - if let Some(rest) = rest.strip_prefix('%') { - let mut parts = rest.splitn(2, '/'); - let pattern = parts.next().unwrap_or(""); - let repl = parts.next().unwrap_or(""); - return Ok(ReplaceSuffix(pattern.to_string(), repl.to_string())); - } else if let Some(rest) = rest.strip_prefix('#') { - let mut parts = rest.splitn(2, '/'); - let pattern = parts.next().unwrap_or(""); - let repl = parts.next().unwrap_or(""); - return Ok(ReplacePrefix(pattern.to_string(), repl.to_string())); - } else { - let mut parts = rest.splitn(2, '/'); - let pattern = parts.next().unwrap_or(""); - let repl = parts.next().unwrap_or(""); - return Ok(ReplaceFirstMatch(pattern.to_string(), repl.to_string())); - } - } + // Replacements + if let Some(rest) = s.strip_prefix("//") { + let mut parts = rest.splitn(2, '/'); + let pattern = parts.next().unwrap_or(""); + let repl = parts.next().unwrap_or(""); + return Ok(ReplaceAllMatches(pattern.to_string(), repl.to_string())); + } + if let Some(rest) = s.strip_prefix('/') { + if let Some(rest) = rest.strip_prefix('%') { + let mut parts = rest.splitn(2, '/'); + let pattern = parts.next().unwrap_or(""); + let repl = parts.next().unwrap_or(""); + return Ok(ReplaceSuffix(pattern.to_string(), repl.to_string())); + } else if let Some(rest) = rest.strip_prefix('#') { + let mut parts = rest.splitn(2, '/'); + let pattern = parts.next().unwrap_or(""); + let repl = parts.next().unwrap_or(""); + return Ok(ReplacePrefix(pattern.to_string(), repl.to_string())); + } else { + let mut parts = rest.splitn(2, '/'); + let pattern = parts.next().unwrap_or(""); + let repl = parts.next().unwrap_or(""); + return Ok(ReplaceFirstMatch(pattern.to_string(), repl.to_string())); + } + } - // Fallback / assignment / alt - if let Some(rest) = s.strip_prefix(":-") { - return Ok(DefaultUnsetOrNull(rest.to_string())); - } else if let Some(rest) = s.strip_prefix('-') { - return Ok(DefaultUnset(rest.to_string())); - } else if let Some(rest) = s.strip_prefix(":+") { - return Ok(AltSetNotNull(rest.to_string())); - } else if let Some(rest) = s.strip_prefix('+') { - return Ok(AltNotNull(rest.to_string())); - } else if let Some(rest) = s.strip_prefix(":=") { - return Ok(SetDefaultUnsetOrNull(rest.to_string())); - } else if let Some(rest) = s.strip_prefix('=') { - return Ok(SetDefaultUnset(rest.to_string())); - } else if let Some(rest) = s.strip_prefix(":?") { - return Ok(ErrUnsetOrNull(rest.to_string())); - } else if let Some(rest) = s.strip_prefix('?') { - return Ok(ErrUnset(rest.to_string())); - } + // Fallback / assignment / alt + if let Some(rest) = s.strip_prefix(":-") { + return Ok(DefaultUnsetOrNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('-') { + return Ok(DefaultUnset(rest.to_string())); + } else if let Some(rest) = s.strip_prefix(":+") { + return Ok(AltSetNotNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('+') { + return Ok(AltNotNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix(":=") { + return Ok(SetDefaultUnsetOrNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('=') { + return Ok(SetDefaultUnset(rest.to_string())); + } else if let Some(rest) = s.strip_prefix(":?") { + return Ok(ErrUnsetOrNull(rest.to_string())); + } else if let Some(rest) = s.strip_prefix('?') { + return Ok(ErrUnset(rest.to_string())); + } - // Substring - if let Some((pos, len)) = parse_pos_len(s) { - return Ok(match len { - Some(l) => SubstrLen(pos, l), - None => Substr(pos), - }); - } + // Substring + if let Some((pos, len)) = parse_pos_len(s) { + return Ok(match len { + Some(l) => SubstrLen(pos, l), + None => Substr(pos), + }); + } - parse_err() - } + parse_err() + } } pub fn parse_pos_len(s: &str) -> Option<(usize, Option)> { - let raw = s.strip_prefix(':')?; - if let Some((start,len)) = raw.split_once(':') { - Some(( - start.parse::().ok()?, - len.parse::().ok(), - )) - } else { - Some(( - raw.parse::().ok()?, - None, - )) - } + let raw = s.strip_prefix(':')?; + if let Some((start, len)) = raw.split_once(':') { + Some((start.parse::().ok()?, len.parse::().ok())) + } else { + Some((raw.parse::().ok()?, None)) + } } pub fn perform_param_expansion(raw: &str) -> ShResult { - let vars = read_vars(|v| v.clone()); - let mut chars = raw.chars(); - let mut var_name = String::new(); - let mut rest = String::new(); - if raw.starts_with('#') { - return Ok(vars.get_var(raw.strip_prefix('#').unwrap()).len().to_string()) - } + let vars = read_vars(|v| v.clone()); + let mut chars = raw.chars(); + let mut var_name = String::new(); + let mut rest = String::new(); + if raw.starts_with('#') { + return Ok( + vars + .get_var(raw.strip_prefix('#').unwrap()) + .len() + .to_string(), + ); + } - while let Some(ch) = chars.next() { - match ch { - '!' | - '#' | - '%' | - ':' | - '-' | - '+' | - '=' | - '/' | - '?' => { - rest.push(ch); - rest.push_str(&chars.collect::()); - break - } - _ => var_name.push(ch) - } - } + while let Some(ch) = chars.next() { + match ch { + '!' | '#' | '%' | ':' | '-' | '+' | '=' | '/' | '?' => { + rest.push(ch); + rest.push_str(&chars.collect::()); + break; + } + _ => var_name.push(ch), + } + } - flog!(DEBUG,rest); - if let Ok(expansion) = rest.parse::() { - flog!(DEBUG,expansion); - match expansion { - ParamExp::Len => unreachable!(), - ParamExp::DefaultUnsetOrNull(default) => { - if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { - Ok(default) - } else { - Ok(vars.get_var(&var_name)) - } - } - ParamExp::DefaultUnset(default) => { - if !vars.var_exists(&var_name) { - Ok(default) - } else { - Ok(vars.get_var(&var_name)) - } - } - ParamExp::SetDefaultUnsetOrNull(default) => { - if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { - write_vars(|v| v.set_var(&var_name, &default, false)); - Ok(default) - } else { - Ok(vars.get_var(&var_name)) - } - } - ParamExp::SetDefaultUnset(default) => { - if !vars.var_exists(&var_name) { - write_vars(|v| v.set_var(&var_name, &default, false)); - Ok(default) - } else { - Ok(vars.get_var(&var_name)) - } - } - ParamExp::AltSetNotNull(alt) => { - if vars.var_exists(&var_name) && !vars.get_var(&var_name).is_empty() { - Ok(alt) - } else { - Ok("".into()) - } - } - ParamExp::AltNotNull(alt) => { - if vars.var_exists(&var_name) { - Ok(alt) - } else { - Ok("".into()) - } - } - ParamExp::ErrUnsetOrNull(err) => { - if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { - Err( - ShErr::Simple { kind: ShErrKind::ExecFail, msg: err, notes: vec![] } - ) - } else { - Ok(vars.get_var(&var_name)) - } - } - ParamExp::ErrUnset(err) => { - if !vars.var_exists(&var_name) { - Err( - ShErr::Simple { kind: ShErrKind::ExecFail, msg: err, notes: vec![] } - ) - } else { - Ok(vars.get_var(&var_name)) - } - } - ParamExp::Substr(pos) => { - let value = vars.get_var(&var_name); - if let Some(substr) = value.get(pos..) { - Ok(substr.to_string()) - } else { - Ok(value) - } - } - ParamExp::SubstrLen(pos, len) => { - let value = vars.get_var(&var_name); - let end = pos.saturating_add(len); - if let Some(substr) = value.get(pos..end) { - Ok(substr.to_string()) - } else { - Ok(value) - } - } - ParamExp::RemShortestPrefix(prefix) => { - let value = vars.get_var(&var_name); - let pattern = Pattern::new(&prefix).unwrap(); - for i in 0..=value.len() { - let sliced = &value[..i]; - if pattern.matches(sliced) { - return Ok(value[i..].to_string()) - } - } - Ok(value) - } - ParamExp::RemLongestPrefix(prefix) => { - let value = vars.get_var(&var_name); - let pattern = Pattern::new(&prefix).unwrap(); - for i in (0..=value.len()).rev() { - let sliced = &value[..i]; - if pattern.matches(sliced) { - return Ok(value[i..].to_string()); - } - } - Ok(value) // no match - } - ParamExp::RemShortestSuffix(suffix) => { - let value = vars.get_var(&var_name); - let pattern = Pattern::new(&suffix).unwrap(); - for i in (0..=value.len()).rev() { - let sliced = &value[i..]; - if pattern.matches(sliced) { - return Ok(value[..i].to_string()); - } - } - Ok(value) - } - ParamExp::RemLongestSuffix(suffix) => { - let value = vars.get_var(&var_name); - let pattern = Pattern::new(&suffix).unwrap(); - for i in 0..=value.len() { - let sliced = &value[i..]; - if pattern.matches(sliced) { - return Ok(value[..i].to_string()); - } - } - Ok(value) - } - ParamExp::ReplaceFirstMatch(search, replace) => { - let value = vars.get_var(&var_name); - let regex = glob_to_regex(&search, false); // unanchored pattern + flog!(DEBUG, rest); + if let Ok(expansion) = rest.parse::() { + flog!(DEBUG, expansion); + match expansion { + ParamExp::Len => unreachable!(), + ParamExp::DefaultUnsetOrNull(default) => { + if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::DefaultUnset(default) => { + if !vars.var_exists(&var_name) { + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::SetDefaultUnsetOrNull(default) => { + if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + write_vars(|v| v.set_var(&var_name, &default, false)); + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::SetDefaultUnset(default) => { + if !vars.var_exists(&var_name) { + write_vars(|v| v.set_var(&var_name, &default, false)); + Ok(default) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::AltSetNotNull(alt) => { + if vars.var_exists(&var_name) && !vars.get_var(&var_name).is_empty() { + Ok(alt) + } else { + Ok("".into()) + } + } + ParamExp::AltNotNull(alt) => { + if vars.var_exists(&var_name) { + Ok(alt) + } else { + Ok("".into()) + } + } + ParamExp::ErrUnsetOrNull(err) => { + if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { + Err(ShErr::Simple { + kind: ShErrKind::ExecFail, + msg: err, + notes: vec![], + }) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::ErrUnset(err) => { + if !vars.var_exists(&var_name) { + Err(ShErr::Simple { + kind: ShErrKind::ExecFail, + msg: err, + notes: vec![], + }) + } else { + Ok(vars.get_var(&var_name)) + } + } + ParamExp::Substr(pos) => { + let value = vars.get_var(&var_name); + if let Some(substr) = value.get(pos..) { + Ok(substr.to_string()) + } else { + Ok(value) + } + } + ParamExp::SubstrLen(pos, len) => { + let value = vars.get_var(&var_name); + let end = pos.saturating_add(len); + if let Some(substr) = value.get(pos..end) { + Ok(substr.to_string()) + } else { + Ok(value) + } + } + ParamExp::RemShortestPrefix(prefix) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&prefix).unwrap(); + for i in 0..=value.len() { + let sliced = &value[..i]; + if pattern.matches(sliced) { + return Ok(value[i..].to_string()); + } + } + Ok(value) + } + ParamExp::RemLongestPrefix(prefix) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&prefix).unwrap(); + for i in (0..=value.len()).rev() { + let sliced = &value[..i]; + if pattern.matches(sliced) { + return Ok(value[i..].to_string()); + } + } + Ok(value) // no match + } + ParamExp::RemShortestSuffix(suffix) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&suffix).unwrap(); + for i in (0..=value.len()).rev() { + let sliced = &value[i..]; + if pattern.matches(sliced) { + return Ok(value[..i].to_string()); + } + } + Ok(value) + } + ParamExp::RemLongestSuffix(suffix) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&suffix).unwrap(); + for i in 0..=value.len() { + let sliced = &value[i..]; + if pattern.matches(sliced) { + return Ok(value[..i].to_string()); + } + } + Ok(value) + } + ParamExp::ReplaceFirstMatch(search, replace) => { + let value = vars.get_var(&var_name); + let regex = glob_to_regex(&search, false); // unanchored pattern - if let Some(mat) = regex.find(&value) { - let before = &value[..mat.start()]; - let after = &value[mat.end()..]; - let result = format!("{}{}{}", before, replace, after); - Ok(result) - } else { - Ok(value) - } - } - ParamExp::ReplaceAllMatches(search, replace) => { - let value = vars.get_var(&var_name); - let regex = glob_to_regex(&search, false); - let mut result = String::new(); - let mut last_match_end = 0; + if let Some(mat) = regex.find(&value) { + let before = &value[..mat.start()]; + let after = &value[mat.end()..]; + let result = format!("{}{}{}", before, replace, after); + Ok(result) + } else { + Ok(value) + } + } + ParamExp::ReplaceAllMatches(search, replace) => { + let value = vars.get_var(&var_name); + let regex = glob_to_regex(&search, false); + let mut result = String::new(); + let mut last_match_end = 0; - for mat in regex.find_iter(&value) { - result.push_str(&value[last_match_end..mat.start()]); - result.push_str(&replace); - last_match_end = mat.end(); - } + for mat in regex.find_iter(&value) { + result.push_str(&value[last_match_end..mat.start()]); + result.push_str(&replace); + last_match_end = mat.end(); + } - // Append the rest of the string - result.push_str(&value[last_match_end..]); - Ok(result) - } - ParamExp::ReplacePrefix(search, replace) => { - let value = vars.get_var(&var_name); - let pattern = Pattern::new(&search).unwrap(); - for i in (0..=value.len()).rev() { - let sliced = &value[..i]; - if pattern.matches(sliced) { - return Ok(format!("{}{}",replace,&value[i..])) - } - } - Ok(value) - } - ParamExp::ReplaceSuffix(search, replace) => { - let value = vars.get_var(&var_name); - let pattern = Pattern::new(&search).unwrap(); - for i in (0..=value.len()).rev() { - let sliced = &value[i..]; - if pattern.matches(sliced) { - return Ok(format!("{}{}",&value[..i],replace)) - } - } - Ok(value) - } - ParamExp::VarNamesWithPrefix(prefix) => { - let mut match_vars = vec![]; - for var in vars.vars().keys() { - if var.starts_with(&prefix) { - match_vars.push(var.clone()) - } - } - Ok(match_vars.join(" ")) - } - ParamExp::ExpandInnerVar(var_name) => { - let value = vars.get_var(&var_name); - Ok(vars.get_var(&value)) - } - } - } else { - Ok(vars.get_var(&var_name)) - } + // Append the rest of the string + result.push_str(&value[last_match_end..]); + Ok(result) + } + ParamExp::ReplacePrefix(search, replace) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&search).unwrap(); + for i in (0..=value.len()).rev() { + let sliced = &value[..i]; + if pattern.matches(sliced) { + return Ok(format!("{}{}", replace, &value[i..])); + } + } + Ok(value) + } + ParamExp::ReplaceSuffix(search, replace) => { + let value = vars.get_var(&var_name); + let pattern = Pattern::new(&search).unwrap(); + for i in (0..=value.len()).rev() { + let sliced = &value[i..]; + if pattern.matches(sliced) { + return Ok(format!("{}{}", &value[..i], replace)); + } + } + Ok(value) + } + ParamExp::VarNamesWithPrefix(prefix) => { + let mut match_vars = vec![]; + for var in vars.vars().keys() { + if var.starts_with(&prefix) { + match_vars.push(var.clone()) + } + } + Ok(match_vars.join(" ")) + } + ParamExp::ExpandInnerVar(var_name) => { + let value = vars.get_var(&var_name); + Ok(vars.get_var(&value)) + } + } + } else { + Ok(vars.get_var(&var_name)) + } } fn glob_to_regex(glob: &str, anchored: bool) -> Regex { - let mut regex = String::new(); - if anchored { - regex.push('^'); - } - for ch in glob.chars() { - match ch { - '*' => regex.push_str(".*"), - '?' => regex.push('.'), - '.' | '+' | '(' | ')' | '|' | '^' | '$' | '[' | ']' | '{' | '}' | '\\' => { - regex.push('\\'); - regex.push(ch); - } - _ => regex.push(ch), - } - } - if anchored { - regex.push('$'); - } - flog!(DEBUG, regex); - Regex::new(®ex).unwrap() + let mut regex = String::new(); + if anchored { + regex.push('^'); + } + for ch in glob.chars() { + match ch { + '*' => regex.push_str(".*"), + '?' => regex.push('.'), + '.' | '+' | '(' | ')' | '|' | '^' | '$' | '[' | ']' | '{' | '}' | '\\' => { + regex.push('\\'); + regex.push(ch); + } + _ => regex.push(ch), + } + } + if anchored { + regex.push('$'); + } + flog!(DEBUG, regex); + Regex::new(®ex).unwrap() } #[derive(Debug)] pub enum PromptTk { - AsciiOct(i32), - Text(String), - AnsiSeq(String), - VisGrp, - UserSeq, - Runtime, - Weekday, - Dquote, - Squote, - Return, - Newline, - Pwd, - PwdShort, - Hostname, - HostnameShort, - ShellName, - Username, - PromptSymbol, - ExitCode, - SuccessSymbol, - FailureSymbol, - JobCount + AsciiOct(i32), + Text(String), + AnsiSeq(String), + VisGrp, + UserSeq, + Runtime, + Weekday, + Dquote, + Squote, + Return, + Newline, + Pwd, + PwdShort, + Hostname, + HostnameShort, + ShellName, + Username, + PromptSymbol, + ExitCode, + SuccessSymbol, + FailureSymbol, + JobCount, } pub fn format_cmd_runtime(dur: std::time::Duration) -> String { - const ETERNITY: u128 = f32::INFINITY as u128; - let mut micros = dur.as_micros(); - let mut millis = 0; - let mut seconds = 0; - let mut minutes = 0; - let mut hours = 0; - let mut days = 0; - let mut weeks = 0; - let mut months = 0; - let mut years = 0; - let mut decades = 0; - let mut centuries = 0; - let mut millennia = 0; - let mut epochs = 0; - let mut aeons = 0; - let mut eternities = 0; + const ETERNITY: u128 = f32::INFINITY as u128; + let mut micros = dur.as_micros(); + let mut millis = 0; + let mut seconds = 0; + let mut minutes = 0; + let mut hours = 0; + let mut days = 0; + let mut weeks = 0; + let mut months = 0; + let mut years = 0; + let mut decades = 0; + let mut centuries = 0; + let mut millennia = 0; + let mut epochs = 0; + let mut aeons = 0; + let mut eternities = 0; - if micros >= 1000 { - millis = micros / 1000; - micros %= 1000; - } - if millis >= 1000 { - seconds = millis / 1000; - millis %= 1000; - } - if seconds >= 60 { - minutes = seconds / 60; - seconds %= 60; - } - if minutes >= 60 { - hours = minutes / 60; - minutes %= 60; - } - if hours >= 24 { - days = hours / 24; - hours %= 24; - } - if days >= 7 { - weeks = days / 7; - days %= 7; - } - if weeks >= 4 { - months = weeks / 4; - weeks %= 4; - } - if months >= 12 { - years = months / 12; - weeks %= 12; - } - if years >= 10 { - decades = years / 10; - years %= 10; - } - if decades >= 10 { - centuries = decades / 10; - decades %= 10; - } - if centuries >= 10 { - millennia = centuries / 10; - centuries %= 10; - } - if millennia >= 1000 { - epochs = millennia / 1000; - millennia %= 1000; - } - if epochs >= 1000 { - aeons = epochs / 1000; - epochs %= aeons; - } - if aeons == ETERNITY { - eternities = aeons / ETERNITY; - aeons %= ETERNITY; - } + if micros >= 1000 { + millis = micros / 1000; + micros %= 1000; + } + if millis >= 1000 { + seconds = millis / 1000; + millis %= 1000; + } + if seconds >= 60 { + minutes = seconds / 60; + seconds %= 60; + } + if minutes >= 60 { + hours = minutes / 60; + minutes %= 60; + } + if hours >= 24 { + days = hours / 24; + hours %= 24; + } + if days >= 7 { + weeks = days / 7; + days %= 7; + } + if weeks >= 4 { + months = weeks / 4; + weeks %= 4; + } + if months >= 12 { + years = months / 12; + weeks %= 12; + } + if years >= 10 { + decades = years / 10; + years %= 10; + } + if decades >= 10 { + centuries = decades / 10; + decades %= 10; + } + if centuries >= 10 { + millennia = centuries / 10; + centuries %= 10; + } + if millennia >= 1000 { + epochs = millennia / 1000; + millennia %= 1000; + } + if epochs >= 1000 { + aeons = epochs / 1000; + epochs %= aeons; + } + if aeons == ETERNITY { + eternities = aeons / ETERNITY; + aeons %= ETERNITY; + } - // Format the result - let mut result = Vec::new(); - if eternities > 0 { - let mut string = format!("{} eternit", eternities); - if eternities > 1 { - string.push_str("ies"); - } else { - string.push('y'); - } - result.push(string) - } - if aeons > 0 { - let mut string = format!("{} aeon", aeons); - if aeons > 1 { - string.push('s') - } - result.push(string) - } - if epochs > 0 { - let mut string = format!("{} epoch", epochs); - if epochs > 1 { - string.push('s') - } - result.push(string) - } - if millennia > 0 { - let mut string = format!("{} millenni", millennia); - if millennia > 1 { - string.push_str("um") - } else { - string.push('a') - } - result.push(string) - } - if centuries > 0 { - let mut string = format!("{} centur", centuries); - if centuries > 1 { - string.push_str("ies") - } else { - string.push('y') - } - result.push(string) - } - if decades > 0 { - let mut string = format!("{} decade", decades); - if decades > 1 { - string.push('s') - } - result.push(string) - } - if years > 0 { - let mut string = format!("{} year", years); - if years > 1 { - string.push('s') - } - result.push(string) - } - if months > 0 { - let mut string = format!("{} month", months); - if months > 1 { - string.push('s') - } - result.push(string) - } - if weeks > 0 { - let mut string = format!("{} week", weeks); - if weeks > 1 { - string.push('s') - } - result.push(string) - } - if days > 0 { - let mut string = format!("{} day", days); - if days > 1 { - string.push('s') - } - result.push(string) - } - if hours > 0 { - let string = format!("{}h", hours); - result.push(string); - } - if minutes > 0 { - let string = format!("{}m", minutes); - result.push(string); - } - if seconds > 0 { - let string = format!("{}s", seconds); - result.push(string); - } - if millis > 0 { - let string = format!("{}ms",millis); - result.push(string); - } - if result.is_empty() && micros > 0 { - let string = format!("{}µs",micros); - result.push(string); - } + // Format the result + let mut result = Vec::new(); + if eternities > 0 { + let mut string = format!("{} eternit", eternities); + if eternities > 1 { + string.push_str("ies"); + } else { + string.push('y'); + } + result.push(string) + } + if aeons > 0 { + let mut string = format!("{} aeon", aeons); + if aeons > 1 { + string.push('s') + } + result.push(string) + } + if epochs > 0 { + let mut string = format!("{} epoch", epochs); + if epochs > 1 { + string.push('s') + } + result.push(string) + } + if millennia > 0 { + let mut string = format!("{} millenni", millennia); + if millennia > 1 { + string.push_str("um") + } else { + string.push('a') + } + result.push(string) + } + if centuries > 0 { + let mut string = format!("{} centur", centuries); + if centuries > 1 { + string.push_str("ies") + } else { + string.push('y') + } + result.push(string) + } + if decades > 0 { + let mut string = format!("{} decade", decades); + if decades > 1 { + string.push('s') + } + result.push(string) + } + if years > 0 { + let mut string = format!("{} year", years); + if years > 1 { + string.push('s') + } + result.push(string) + } + if months > 0 { + let mut string = format!("{} month", months); + if months > 1 { + string.push('s') + } + result.push(string) + } + if weeks > 0 { + let mut string = format!("{} week", weeks); + if weeks > 1 { + string.push('s') + } + result.push(string) + } + if days > 0 { + let mut string = format!("{} day", days); + if days > 1 { + string.push('s') + } + result.push(string) + } + if hours > 0 { + let string = format!("{}h", hours); + result.push(string); + } + if minutes > 0 { + let string = format!("{}m", minutes); + result.push(string); + } + if seconds > 0 { + let string = format!("{}s", seconds); + result.push(string); + } + if millis > 0 { + let string = format!("{}ms", millis); + result.push(string); + } + if result.is_empty() && micros > 0 { + let string = format!("{}µs", micros); + result.push(string); + } - result.join(" ") + result.join(" ") } fn tokenize_prompt(raw: &str) -> Vec { - let mut chars = raw.chars().peekable(); - let mut tk_text = String::new(); - let mut tokens = vec![]; + let mut chars = raw.chars().peekable(); + let mut tk_text = String::new(); + let mut tokens = vec![]; - while let Some(ch) = chars.next() { - match ch { - '\\' => { - // Push any accumulated text as a token - if !tk_text.is_empty() { - tokens.push(PromptTk::Text(std::mem::take(&mut tk_text))); - } + while let Some(ch) = chars.next() { + match ch { + '\\' => { + // Push any accumulated text as a token + if !tk_text.is_empty() { + tokens.push(PromptTk::Text(std::mem::take(&mut tk_text))); + } - // Handle the escape sequence - if let Some(ch) = chars.next() { - match ch { - 'w' => tokens.push(PromptTk::Pwd), - 'W' => tokens.push(PromptTk::PwdShort), - 'h' => tokens.push(PromptTk::Hostname), - 'H' => tokens.push(PromptTk::HostnameShort), - 's' => tokens.push(PromptTk::ShellName), - 'u' => tokens.push(PromptTk::Username), - '$' => tokens.push(PromptTk::PromptSymbol), - 'n' => tokens.push(PromptTk::Text("\n".into())), - 'r' => tokens.push(PromptTk::Text("\r".into())), - 'T' => tokens.push(PromptTk::Runtime), - '\\' => tokens.push(PromptTk::Text("\\".into())), - '"' => tokens.push(PromptTk::Text("\"".into())), - '\'' => tokens.push(PromptTk::Text("'".into())), - 'e' => { - if chars.next() == Some('[') { - let mut params = String::new(); + // Handle the escape sequence + if let Some(ch) = chars.next() { + match ch { + 'w' => tokens.push(PromptTk::Pwd), + 'W' => tokens.push(PromptTk::PwdShort), + 'h' => tokens.push(PromptTk::Hostname), + 'H' => tokens.push(PromptTk::HostnameShort), + 's' => tokens.push(PromptTk::ShellName), + 'u' => tokens.push(PromptTk::Username), + '$' => tokens.push(PromptTk::PromptSymbol), + 'n' => tokens.push(PromptTk::Text("\n".into())), + 'r' => tokens.push(PromptTk::Text("\r".into())), + 'T' => tokens.push(PromptTk::Runtime), + '\\' => tokens.push(PromptTk::Text("\\".into())), + '"' => tokens.push(PromptTk::Text("\"".into())), + '\'' => tokens.push(PromptTk::Text("'".into())), + 'e' => { + if chars.next() == Some('[') { + let mut params = String::new(); - // Collect parameters and final character - while let Some(ch) = chars.next() { - match ch { - '0'..='9' | ';' | '?' | ':' => params.push(ch), // Valid parameter characters - 'A'..='Z' | 'a'..='z' => { // Final character (letter) - params.push(ch); - break; - } - _ => { - // Invalid character in ANSI sequence - tokens.push(PromptTk::Text(format!("\x1b[{params}"))); - break; - } - } - } + // Collect parameters and final character + while let Some(ch) = chars.next() { + match ch { + '0'..='9' | ';' | '?' | ':' => params.push(ch), // Valid parameter characters + 'A'..='Z' | 'a'..='z' => { + // Final character (letter) + params.push(ch); + break; + } + _ => { + // Invalid character in ANSI sequence + tokens.push(PromptTk::Text(format!("\x1b[{params}"))); + break; + } + } + } - tokens.push(PromptTk::AnsiSeq(format!("\x1b[{params}"))); - } else { - // Handle case where 'e' is not followed by '[' - tokens.push(PromptTk::Text("\\e".into())); - } - } - '0'..='7' => { - // Handle octal escape - let mut octal_str = String::new(); - octal_str.push(ch); + tokens.push(PromptTk::AnsiSeq(format!("\x1b[{params}"))); + } else { + // Handle case where 'e' is not followed by '[' + tokens.push(PromptTk::Text("\\e".into())); + } + } + '0'..='7' => { + // Handle octal escape + let mut octal_str = String::new(); + octal_str.push(ch); - // Collect up to 2 more octal digits - for _ in 0..2 { - if let Some(&next_ch) = chars.peek() { - if ('0'..='7').contains(&next_ch) { - octal_str.push(chars.next().unwrap()); - } else { - break; - } - } else { - break; - } - } + // Collect up to 2 more octal digits + for _ in 0..2 { + if let Some(&next_ch) = chars.peek() { + if ('0'..='7').contains(&next_ch) { + octal_str.push(chars.next().unwrap()); + } else { + break; + } + } else { + break; + } + } - // Parse the octal string into an integer - if let Ok(octal) = i32::from_str_radix(&octal_str, 8) { - tokens.push(PromptTk::AsciiOct(octal)); - } else { - // Fallback: treat as raw text - tokens.push(PromptTk::Text(format!("\\{octal_str}"))); - } - } - _ => { - // Unknown escape sequence: treat as raw text - tokens.push(PromptTk::Text(format!("\\{ch}"))); - } - } - } else { - // Handle trailing backslash - tokens.push(PromptTk::Text("\\".into())); - } - } - _ => { - // Accumulate non-escape characters - tk_text.push(ch); - } - } - } + // Parse the octal string into an integer + if let Ok(octal) = i32::from_str_radix(&octal_str, 8) { + tokens.push(PromptTk::AsciiOct(octal)); + } else { + // Fallback: treat as raw text + tokens.push(PromptTk::Text(format!("\\{octal_str}"))); + } + } + _ => { + // Unknown escape sequence: treat as raw text + tokens.push(PromptTk::Text(format!("\\{ch}"))); + } + } + } else { + // Handle trailing backslash + tokens.push(PromptTk::Text("\\".into())); + } + } + _ => { + // Accumulate non-escape characters + tk_text.push(ch); + } + } + } - // Push any remaining text as a token - if !tk_text.is_empty() { - tokens.push(PromptTk::Text(tk_text)); - } + // Push any remaining text as a token + if !tk_text.is_empty() { + tokens.push(PromptTk::Text(tk_text)); + } - tokens + tokens } pub fn expand_prompt(raw: &str) -> ShResult { - let mut tokens = tokenize_prompt(raw).into_iter(); - let mut result = String::new(); + let mut tokens = tokenize_prompt(raw).into_iter(); + let mut result = String::new(); - while let Some(token) = tokens.next() { - match token { - PromptTk::AsciiOct(_) => todo!(), - PromptTk::Text(txt) => result.push_str(&txt), - PromptTk::AnsiSeq(params) => result.push_str(¶ms), - PromptTk::Runtime => { - if let Some(runtime) = write_meta(|m| m.stop_timer()) { - let runtime_fmt = format_cmd_runtime(runtime); - result.push_str(&runtime_fmt); - } - } - PromptTk::Pwd => { - let mut pwd = std::env::var("PWD").unwrap(); - let home = std::env::var("HOME").unwrap(); - if pwd.starts_with(&home) { - pwd = pwd.replacen(&home, "~", 1); - } - result.push_str(&pwd); - } - PromptTk::PwdShort => { - let mut path = std::env::var("PWD").unwrap(); - let home = std::env::var("HOME").unwrap(); - if path.starts_with(&home) { - path = path.replacen(&home, "~", 1); - } - let pathbuf = PathBuf::from(&path); - let mut segments = pathbuf.iter().count(); - let mut path_iter = pathbuf.iter(); - while segments > 4 { - path_iter.next(); - segments -= 1; - } - let path_rebuilt: PathBuf = path_iter.collect(); - let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string(); - if path_rebuilt.starts_with(&home) { - path_rebuilt = path_rebuilt.replacen(&home, "~", 1); - } - result.push_str(&path_rebuilt); - } - PromptTk::Hostname => { - let hostname = std::env::var("HOST").unwrap(); - result.push_str(&hostname); - } - PromptTk::HostnameShort => todo!(), - PromptTk::ShellName => result.push_str("fern"), - PromptTk::Username => { - let username = std::env::var("USER").unwrap(); - result.push_str(&username); - } - PromptTk::PromptSymbol => { - let uid = std::env::var("UID").unwrap(); - let symbol = if &uid == "0" { - '#' - } else { - '$' - }; - result.push(symbol); - } - PromptTk::ExitCode => todo!(), - PromptTk::SuccessSymbol => todo!(), - PromptTk::FailureSymbol => todo!(), - PromptTk::JobCount => todo!(), - _ => unimplemented!() - } - } + while let Some(token) = tokens.next() { + match token { + PromptTk::AsciiOct(_) => todo!(), + PromptTk::Text(txt) => result.push_str(&txt), + PromptTk::AnsiSeq(params) => result.push_str(¶ms), + PromptTk::Runtime => { + if let Some(runtime) = write_meta(|m| m.stop_timer()) { + let runtime_fmt = format_cmd_runtime(runtime); + result.push_str(&runtime_fmt); + } + } + PromptTk::Pwd => { + let mut pwd = std::env::var("PWD").unwrap(); + let home = std::env::var("HOME").unwrap(); + if pwd.starts_with(&home) { + pwd = pwd.replacen(&home, "~", 1); + } + result.push_str(&pwd); + } + PromptTk::PwdShort => { + let mut path = std::env::var("PWD").unwrap(); + let home = std::env::var("HOME").unwrap(); + if path.starts_with(&home) { + path = path.replacen(&home, "~", 1); + } + let pathbuf = PathBuf::from(&path); + let mut segments = pathbuf.iter().count(); + let mut path_iter = pathbuf.iter(); + while segments > 4 { + path_iter.next(); + segments -= 1; + } + let path_rebuilt: PathBuf = path_iter.collect(); + let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string(); + if path_rebuilt.starts_with(&home) { + path_rebuilt = path_rebuilt.replacen(&home, "~", 1); + } + result.push_str(&path_rebuilt); + } + PromptTk::Hostname => { + let hostname = std::env::var("HOST").unwrap(); + result.push_str(&hostname); + } + PromptTk::HostnameShort => todo!(), + PromptTk::ShellName => result.push_str("fern"), + PromptTk::Username => { + let username = std::env::var("USER").unwrap(); + result.push_str(&username); + } + PromptTk::PromptSymbol => { + let uid = std::env::var("UID").unwrap(); + let symbol = if &uid == "0" { '#' } else { '$' }; + result.push(symbol); + } + PromptTk::ExitCode => todo!(), + PromptTk::SuccessSymbol => todo!(), + PromptTk::FailureSymbol => todo!(), + PromptTk::JobCount => todo!(), + _ => unimplemented!(), + } + } - Ok(result) + Ok(result) } /// Expand aliases in the given input string /// /// Recursively calls itself until all aliases are expanded -pub fn expand_aliases(input: String, mut already_expanded: HashSet, log_tab: &LogTab) -> String { - let mut result = input.clone(); - let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect(); - let mut expanded_this_iter: Vec = vec![]; +pub fn expand_aliases( + input: String, + mut already_expanded: HashSet, + log_tab: &LogTab, +) -> String { + let mut result = input.clone(); + let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect(); + let mut expanded_this_iter: Vec = vec![]; - for token_result in tokens.into_iter().rev() { - let Ok(tk) = token_result else { continue }; + for token_result in tokens.into_iter().rev() { + let Ok(tk) = token_result else { continue }; - if !tk.flags.contains(TkFlags::IS_CMD) { continue } - if tk.flags.contains(TkFlags::KEYWORD) { continue } + if !tk.flags.contains(TkFlags::IS_CMD) { + continue; + } + if tk.flags.contains(TkFlags::KEYWORD) { + continue; + } - let raw_tk = tk.span.as_str().to_string(); + let raw_tk = tk.span.as_str().to_string(); - if already_expanded.contains(&raw_tk) { continue } + if already_expanded.contains(&raw_tk) { + continue; + } - if let Some(alias) = log_tab.get_alias(&raw_tk) { - result.replace_range(tk.span.range(), &alias); - expanded_this_iter.push(raw_tk); - } - } + if let Some(alias) = log_tab.get_alias(&raw_tk) { + result.replace_range(tk.span.range(), &alias); + expanded_this_iter.push(raw_tk); + } + } - if expanded_this_iter.is_empty() { - result - } else { - already_expanded.extend(expanded_this_iter); - expand_aliases(result, already_expanded, log_tab) - } + if expanded_this_iter.is_empty() { + result + } else { + already_expanded.extend(expanded_this_iter); + expand_aliases(result, already_expanded, log_tab) + } } diff --git a/src/fern.rs b/src/fern.rs index a405e4e..8c4fc88 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -1,127 +1,130 @@ #![allow( - clippy::derivable_impls, - clippy::tabs_in_doc_comments, - clippy::while_let_on_iterator + clippy::derivable_impls, + clippy::tabs_in_doc_comments, + clippy::while_let_on_iterator )] -pub mod prelude; -pub mod libsh; -pub mod prompt; -pub mod procio; -pub mod parse; -pub mod expand; -pub mod state; pub mod builtin; -pub mod jobs; -pub mod signal; +pub mod expand; pub mod getopt; +pub mod jobs; +pub mod libsh; +pub mod parse; +pub mod prelude; +pub mod procio; +pub mod prompt; pub mod shopt; +pub mod signal; +pub mod state; #[cfg(test)] pub mod tests; use crate::libsh::sys::{save_termios, set_termios}; use crate::parse::execute::exec_input; +use crate::prelude::*; use crate::signal::sig_setup; use crate::state::source_rc; -use crate::prelude::*; use clap::Parser; use shopt::FernEditMode; -use state::{read_shopts, read_vars, write_shopts, write_vars}; +use state::{read_vars, write_shopts, write_vars}; -#[derive(Parser,Debug)] +#[derive(Parser, Debug)] struct FernArgs { - script: Option, + script: Option, - #[arg(trailing_var_arg = true)] - script_args: Vec, + #[arg(trailing_var_arg = true)] + script_args: Vec, - #[arg(long)] - version: bool + #[arg(long)] + version: bool, } /// Force evaluation of lazily-initialized values early in shell startup. /// -/// In particular, this ensures that the variable table is initialized, which populates -/// environment variables from the system. If this initialization is deferred too long, -/// features like prompt expansion may fail due to missing environment variables. +/// In particular, this ensures that the variable table is initialized, which +/// populates environment variables from the system. If this initialization is +/// deferred too long, features like prompt expansion may fail due to missing +/// environment variables. /// -/// This function triggers initialization by calling `read_vars` with a no-op closure, -/// which forces access to the variable table and causes its `LazyLock` constructor to run. +/// This function triggers initialization by calling `read_vars` with a no-op +/// closure, which forces access to the variable table and causes its `LazyLock` +/// constructor to run. fn kickstart_lazy_evals() { - read_vars(|_| {}); + read_vars(|_| {}); } fn main() { - kickstart_lazy_evals(); - let args = FernArgs::parse(); - if args.version { - println!("fern {}", env!("CARGO_PKG_VERSION")); - return; - } + kickstart_lazy_evals(); + let args = FernArgs::parse(); + if args.version { + println!("fern {}", env!("CARGO_PKG_VERSION")); + return; + } - if let Some(path) = args.script { - run_script(path, args.script_args); - } else { - fern_interactive(); - } + if let Some(path) = args.script { + run_script(path, args.script_args); + } else { + fern_interactive(); + } } fn run_script>(path: P, args: Vec) { - let path = path.as_ref(); - if !path.is_file() { - eprintln!("fern: Failed to open input file: {}", path.display()); - exit(1); - } - let Ok(input) = fs::read_to_string(path) else { - eprintln!("fern: Failed to read input file: {}", path.display()); - exit(1); - }; + let path = path.as_ref(); + if !path.is_file() { + eprintln!("fern: Failed to open input file: {}", path.display()); + exit(1); + } + let Ok(input) = fs::read_to_string(path) else { + eprintln!("fern: Failed to read input file: {}", path.display()); + exit(1); + }; - write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string())); - for arg in args { - write_vars(|v| v.bpush_arg(arg)) - } + write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string())); + for arg in args { + write_vars(|v| v.bpush_arg(arg)) + } - if let Err(e) = exec_input(input,None) { - eprintln!("{e}"); - exit(1); - } + if let Err(e) = exec_input(input, None) { + eprintln!("{e}"); + exit(1); + } } fn fern_interactive() { - save_termios(); - set_termios(); - sig_setup(); + save_termios(); + set_termios(); + sig_setup(); - if let Err(e) = source_rc() { - eprintln!("{e}"); - } + if let Err(e) = source_rc() { + eprintln!("{e}"); + } - let mut readline_err_count: u32 = 0; + let mut readline_err_count: u32 = 0; - loop { // Main loop - let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) - .unwrap() - .map(|mode| mode.parse::().unwrap_or_default()) - .unwrap(); - let input = match prompt::readline(edit_mode) { - Ok(line) => { - readline_err_count = 0; - line - } - Err(e) => { - eprintln!("{e}"); - readline_err_count += 1; - if readline_err_count == 20 { - eprintln!("reached maximum readline error count, exiting"); - break - } else { - continue - } - } - }; + loop { + // Main loop + let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) + .unwrap() + .map(|mode| mode.parse::().unwrap_or_default()) + .unwrap(); + let input = match prompt::readline(edit_mode) { + Ok(line) => { + readline_err_count = 0; + line + } + Err(e) => { + eprintln!("{e}"); + readline_err_count += 1; + if readline_err_count == 20 { + eprintln!("reached maximum readline error count, exiting"); + break; + } else { + continue; + } + } + }; - if let Err(e) = exec_input(input,None) { - eprintln!("{e}"); - } - } + if let Err(e) = exec_input(input, None) { + eprintln!("{e}"); + } + } } diff --git a/src/getopt.rs b/src/getopt.rs index dc787a7..d2232ef 100644 --- a/src/getopt.rs +++ b/src/getopt.rs @@ -6,75 +6,74 @@ use crate::{parse::lex::Tk, prelude::*}; pub type OptSet = Arc<[Opt]>; - -#[derive(Clone,PartialEq,Eq,Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum Opt { - Long(String), - Short(char) + Long(String), + Short(char), } impl Opt { - pub fn parse(s: &str) -> Vec { - let mut opts = vec![]; + pub fn parse(s: &str) -> Vec { + let mut opts = vec![]; - if s.starts_with("--") { - opts.push(Opt::Long(s.trim_start_matches('-').to_string())) - } else if s.starts_with('-') { - let mut chars = s.trim_start_matches('-').chars(); - while let Some(ch) = chars.next() { - opts.push(Self::Short(ch)) - } - } + if s.starts_with("--") { + opts.push(Opt::Long(s.trim_start_matches('-').to_string())) + } else if s.starts_with('-') { + let mut chars = s.trim_start_matches('-').chars(); + while let Some(ch) = chars.next() { + opts.push(Self::Short(ch)) + } + } - opts - } + opts + } } impl Display for Opt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Long(opt) => write!(f,"--{}",opt), - Self::Short(opt) => write!(f,"-{}",opt), - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Long(opt) => write!(f, "--{}", opt), + Self::Short(opt) => write!(f, "-{}", opt), + } + } } -pub fn get_opts(words: Vec) -> (Vec,Vec) { - let mut words_iter = words.into_iter(); - let mut opts = vec![]; - let mut non_opts = vec![]; +pub fn get_opts(words: Vec) -> (Vec, Vec) { + let mut words_iter = words.into_iter(); + let mut opts = vec![]; + let mut non_opts = vec![]; - while let Some(word) = words_iter.next() { - if &word == "--" { - non_opts.extend(words_iter); - break - } - let parsed_opts = Opt::parse(&word); - if parsed_opts.is_empty() { - non_opts.push(word) - } else { - opts.extend(parsed_opts); - } - } - (non_opts,opts) + while let Some(word) = words_iter.next() { + if &word == "--" { + non_opts.extend(words_iter); + break; + } + let parsed_opts = Opt::parse(&word); + if parsed_opts.is_empty() { + non_opts.push(word) + } else { + opts.extend(parsed_opts); + } + } + (non_opts, opts) } pub fn get_opts_from_tokens(tokens: Vec) -> (Vec, Vec) { - let mut tokens_iter = tokens.into_iter(); - let mut opts = vec![]; - let mut non_opts = vec![]; + let mut tokens_iter = tokens.into_iter(); + let mut opts = vec![]; + let mut non_opts = vec![]; - while let Some(token) = tokens_iter.next() { - if &token.to_string() == "--" { - non_opts.extend(tokens_iter); - break - } - let parsed_opts = Opt::parse(&token.to_string()); - if parsed_opts.is_empty() { - non_opts.push(token) - } else { - opts.extend(parsed_opts); - } - } - (non_opts,opts) + while let Some(token) = tokens_iter.next() { + if &token.to_string() == "--" { + non_opts.extend(tokens_iter); + break; + } + let parsed_opts = Opt::parse(&token.to_string()); + if parsed_opts.is_empty() { + non_opts.push(token) + } else { + opts.extend(parsed_opts); + } + } + (non_opts, opts) } diff --git a/src/jobs.rs b/src/jobs.rs index 0739de8..8ab4ca4 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -1,713 +1,750 @@ -use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*, procio::{borrow_fd, IoMode}, state::{self, set_status, write_jobs}}; +use crate::{ + libsh::{ + error::ShResult, + term::{Style, Styled}, + }, + prelude::*, + procio::{borrow_fd, IoMode}, + state::{self, set_status, write_jobs}, +}; pub const SIG_EXIT_OFFSET: i32 = 128; bitflags! { - #[derive(Debug, Copy, Clone)] - pub struct JobCmdFlags: u8 { - const LONG = 0b0000_0001; // 0x01 - const PIDS = 0b0000_0010; // 0x02 - const NEW_ONLY = 0b0000_0100; // 0x04 - const RUNNING = 0b0000_1000; // 0x08 - const STOPPED = 0b0001_0000; // 0x10 - const INIT = 0b0010_0000; // 0x20 - } + #[derive(Debug, Copy, Clone)] + pub struct JobCmdFlags: u8 { + const LONG = 0b0000_0001; // 0x01 + const PIDS = 0b0000_0010; // 0x02 + const NEW_ONLY = 0b0000_0100; // 0x04 + const RUNNING = 0b0000_1000; // 0x08 + const STOPPED = 0b0001_0000; // 0x10 + const INIT = 0b0010_0000; // 0x20 + } } #[derive(Debug)] pub struct DisplayWaitStatus(pub WtStat); impl fmt::Display for DisplayWaitStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match &self.0 { - WtStat::Exited(_, code) => { - match code { - 0 => write!(f, "done"), - _ => write!(f, "failed: {}", code), - } - } - WtStat::Signaled(_, signal, _) => { - write!(f, "signaled: {:?}", signal) - } - WtStat::Stopped(_, signal) => { - write!(f, "stopped: {:?}", signal) - } - WtStat::PtraceEvent(_, signal, _) => { - write!(f, "ptrace event: {:?}", signal) - } - WtStat::PtraceSyscall(_) => { - write!(f, "ptrace syscall") - } - WtStat::Continued(_) => { - write!(f, "continued") - } - WtStat::StillAlive => { - write!(f, "running") - } - } - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.0 { + WtStat::Exited(_, code) => match code { + 0 => write!(f, "done"), + _ => write!(f, "failed: {}", code), + }, + WtStat::Signaled(_, signal, _) => { + write!(f, "signaled: {:?}", signal) + } + WtStat::Stopped(_, signal) => { + write!(f, "stopped: {:?}", signal) + } + WtStat::PtraceEvent(_, signal, _) => { + write!(f, "ptrace event: {:?}", signal) + } + WtStat::PtraceSyscall(_) => { + write!(f, "ptrace syscall") + } + WtStat::Continued(_) => { + write!(f, "continued") + } + WtStat::StillAlive => { + write!(f, "running") + } + } + } } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub enum JobID { - Pgid(Pid), - Pid(Pid), - TableID(usize), - Command(String) + Pgid(Pid), + Pid(Pid), + TableID(usize), + Command(String), } -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct ChildProc { - pgid: Pid, - pid: Pid, - command: Option, - stat: WtStat + pgid: Pid, + pid: Pid, + command: Option, + stat: WtStat, } impl ChildProc { - pub fn new(pid: Pid, command: Option<&str>, pgid: Option) -> ShResult { - let command = command.map(|str| str.to_string()); - let stat = if kill(pid,None).is_ok() { - WtStat::StillAlive - } else { - WtStat::Exited(pid, 0) - }; - let mut child = Self { pgid: pid, pid, command, stat }; - if let Some(pgid) = pgid { - child.set_pgid(pgid).ok(); - } - flog!(TRACE, "new child: {:?}", child); - Ok(child) - } - pub fn pid(&self) -> Pid { - self.pid - } - pub fn pgid(&self) -> Pid { - self.pgid - } - pub fn cmd(&self) -> Option<&str> { - self.command.as_deref() - } - pub fn stat(&self) -> WtStat { - self.stat - } - pub fn wait(&mut self, flags: Option) -> Result { - let result = waitpid(self.pid, flags); - if let Ok(stat) = result { - self.stat = stat - } - result - } - pub fn kill>>(&self, sig: T) -> ShResult<()> { - Ok(kill(self.pid, sig)?) - } - pub fn set_pgid(&mut self, pgid: Pid) -> ShResult<()> { - setpgid(self.pid, pgid)?; - self.pgid = pgid; - Ok(()) - } - pub fn set_stat(&mut self, stat: WtStat) { - self.stat = stat - } - pub fn is_alive(&self) -> bool { - self.stat == WtStat::StillAlive - } - pub fn is_stopped(&self) -> bool { - matches!(self.stat,WtStat::Stopped(..)) - } - pub fn exited(&self) -> bool { - matches!(self.stat,WtStat::Exited(..)) - } + pub fn new(pid: Pid, command: Option<&str>, pgid: Option) -> ShResult { + let command = command.map(|str| str.to_string()); + let stat = if kill(pid, None).is_ok() { + WtStat::StillAlive + } else { + WtStat::Exited(pid, 0) + }; + let mut child = Self { + pgid: pid, + pid, + command, + stat, + }; + if let Some(pgid) = pgid { + child.set_pgid(pgid).ok(); + } + flog!(TRACE, "new child: {:?}", child); + Ok(child) + } + pub fn pid(&self) -> Pid { + self.pid + } + pub fn pgid(&self) -> Pid { + self.pgid + } + pub fn cmd(&self) -> Option<&str> { + self.command.as_deref() + } + pub fn stat(&self) -> WtStat { + self.stat + } + pub fn wait(&mut self, flags: Option) -> Result { + let result = waitpid(self.pid, flags); + if let Ok(stat) = result { + self.stat = stat + } + result + } + pub fn kill>>(&self, sig: T) -> ShResult<()> { + Ok(kill(self.pid, sig)?) + } + pub fn set_pgid(&mut self, pgid: Pid) -> ShResult<()> { + setpgid(self.pid, pgid)?; + self.pgid = pgid; + Ok(()) + } + pub fn set_stat(&mut self, stat: WtStat) { + self.stat = stat + } + pub fn is_alive(&self) -> bool { + self.stat == WtStat::StillAlive + } + pub fn is_stopped(&self) -> bool { + matches!(self.stat, WtStat::Stopped(..)) + } + pub fn exited(&self) -> bool { + matches!(self.stat, WtStat::Exited(..)) + } } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct RegisteredFd { - pub fd: IoMode, - pub owner_pid: Pid, + pub fd: IoMode, + pub owner_pid: Pid, } -#[derive(Default,Debug)] +#[derive(Default, Debug)] pub struct JobTab { - fg: Option, - order: Vec, - new_updates: Vec, - jobs: Vec>, - fd_registry: Vec + fg: Option, + order: Vec, + new_updates: Vec, + jobs: Vec>, + fd_registry: Vec, } impl JobTab { - pub fn new() -> Self { - Self::default() - } - pub fn take_fg(&mut self) -> Option { - self.fg.take() - } - fn next_open_pos(&self) -> usize { - if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) { - position - } else { - self.jobs.len() - } - } - pub fn jobs(&self) -> &Vec> { - &self.jobs - } - pub fn jobs_mut(&mut self) -> &mut Vec> { - &mut self.jobs - } - pub fn curr_job(&self) -> Option { - self.order.last().copied() - } - pub fn prev_job(&self) -> Option { - self.order.last().copied() - } - pub fn close_job_fds(&mut self, pid: Pid) { - self.fd_registry.retain(|fd| fd.owner_pid != pid) - } - pub fn registered_fds(&self) -> &[RegisteredFd] { - &self.fd_registry - } - pub fn register_fd(&mut self, owner_pid: Pid, fd: IoMode) { - let registered_fd = RegisteredFd { - fd, - owner_pid - }; - self.fd_registry.push(registered_fd) - } - fn prune_jobs(&mut self) { - while let Some(job) = self.jobs.last() { - if job.is_none() { - self.jobs.pop(); - } else { - break - } - } - } - pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult { - self.prune_jobs(); - let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() }; - job.set_tabid(tab_pos); - self.order.push(tab_pos); - if !silent { - write(borrow_fd(1),job.display(&self.order, JobCmdFlags::INIT).as_bytes())?; - } - if tab_pos == self.jobs.len() { - self.jobs.push(Some(job)) - } else { - self.jobs[tab_pos] = Some(job); - } - Ok(tab_pos) - } - pub fn order(&self) -> &[usize] { - &self.order - } - pub fn query(&self, identifier: JobID) -> Option<&Job> { - match identifier { - // Match by process group ID - JobID::Pgid(pgid) => { - self.jobs.iter().find_map(|job| { - job.as_ref().filter(|j| j.pgid() == pgid) - }) - } - // Match by process ID - JobID::Pid(pid) => { - self.jobs.iter().find_map(|job| { - job.as_ref().filter(|j| j.children().iter().any(|child| child.pid() == pid)) - }) - } - // Match by table ID (index in the job table) - JobID::TableID(id) => { - self.jobs.get(id).and_then(|job| job.as_ref()) - } - // Match by command name (partial match) - JobID::Command(cmd) => { - self.jobs.iter().find_map(|job| { - job.as_ref().filter(|j| { - j.children().iter().any(|child| { - child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) - }) - }) - }) - } - } - } - pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { - match identifier { - // Match by process group ID - JobID::Pgid(pgid) => { - self.jobs.iter_mut().find_map(|job| { - job.as_mut().filter(|j| j.pgid() == pgid) - }) - } - // Match by process ID - JobID::Pid(pid) => { - self.jobs.iter_mut().find_map(|job| { - job.as_mut().filter(|j| j.children().iter().any(|child| child.pid() == pid)) - }) - } - // Match by table ID (index in the job table) - JobID::TableID(id) => { - self.jobs.get_mut(id).and_then(|job| job.as_mut()) - } - // Match by command name (partial match) - JobID::Command(cmd) => { - self.jobs.iter_mut().find_map(|job| { - job.as_mut().filter(|j| { - j.children().iter().any(|child| { - child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) - }) - }) - }) - } - } - } - pub fn get_fg(&self) -> Option<&Job> { - self.fg.as_ref() - } - pub fn get_fg_mut(&mut self) -> Option<&mut Job> { - self.fg.as_mut() - } - pub fn new_fg(&mut self, job: Job) -> ShResult> { - let pgid = job.pgid(); - self.fg = Some(job); - attach_tty(pgid)?; - let statuses = self.fg.as_mut().unwrap().wait_pgrp()?; - attach_tty(getpgrp())?; - Ok(statuses) - } - pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> { - if self.fg.is_none() { - return Ok(()) - } - take_term()?; - let fg = std::mem::take(&mut self.fg); - if let Some(mut job) = fg { - job.set_stats(stat); - self.insert_job(job, false)?; - } - Ok(()) - } - pub fn bg_to_fg(&mut self, id: JobID) -> ShResult<()> { - let job = self.remove_job(id); - if let Some(job) = job { - wait_fg(job)?; - } - Ok(()) - } - pub fn remove_job(&mut self, id: JobID) -> Option { - let tabid = self.query(id).map(|job| job.tabid().unwrap()); - if let Some(tabid) = tabid { - self.jobs.get_mut(tabid).and_then(Option::take) - } else { - None - } - } - pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> { - let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) { - &self.jobs - .iter() - .filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap()))) - .map(|job| job.as_ref()) - .collect::>>() - } else { - &self.jobs - .iter() - .map(|job| job.as_ref()) - .collect::>>() - }; - let mut jobs_to_remove = vec![]; - for job in jobs.iter().flatten() { - // Skip foreground job - let id = job.tabid().unwrap(); - // Filter jobs based on flags - if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) { - continue; - } - if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) { - continue; - } - // Print the job in the selected format - write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?; - if job.get_stats().iter().all(|stat| matches!(stat,WtStat::Exited(_, _))) { - jobs_to_remove.push(JobID::TableID(id)); - } - } - for id in jobs_to_remove { - self.remove_job(id); - } - Ok(()) - } + pub fn new() -> Self { + Self::default() + } + pub fn take_fg(&mut self) -> Option { + self.fg.take() + } + fn next_open_pos(&self) -> usize { + if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) { + position + } else { + self.jobs.len() + } + } + pub fn jobs(&self) -> &Vec> { + &self.jobs + } + pub fn jobs_mut(&mut self) -> &mut Vec> { + &mut self.jobs + } + pub fn curr_job(&self) -> Option { + self.order.last().copied() + } + pub fn prev_job(&self) -> Option { + self.order.last().copied() + } + pub fn close_job_fds(&mut self, pid: Pid) { + self.fd_registry.retain(|fd| fd.owner_pid != pid) + } + pub fn registered_fds(&self) -> &[RegisteredFd] { + &self.fd_registry + } + pub fn register_fd(&mut self, owner_pid: Pid, fd: IoMode) { + let registered_fd = RegisteredFd { fd, owner_pid }; + self.fd_registry.push(registered_fd) + } + fn prune_jobs(&mut self) { + while let Some(job) = self.jobs.last() { + if job.is_none() { + self.jobs.pop(); + } else { + break; + } + } + } + pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult { + self.prune_jobs(); + let tab_pos = if let Some(id) = job.tabid() { + id + } else { + self.next_open_pos() + }; + job.set_tabid(tab_pos); + self.order.push(tab_pos); + if !silent { + write( + borrow_fd(1), + job.display(&self.order, JobCmdFlags::INIT).as_bytes(), + )?; + } + if tab_pos == self.jobs.len() { + self.jobs.push(Some(job)) + } else { + self.jobs[tab_pos] = Some(job); + } + Ok(tab_pos) + } + pub fn order(&self) -> &[usize] { + &self.order + } + pub fn query(&self, identifier: JobID) -> Option<&Job> { + match identifier { + // Match by process group ID + JobID::Pgid(pgid) => self + .jobs + .iter() + .find_map(|job| job.as_ref().filter(|j| j.pgid() == pgid)), + // Match by process ID + JobID::Pid(pid) => self.jobs.iter().find_map(|job| { + job + .as_ref() + .filter(|j| j.children().iter().any(|child| child.pid() == pid)) + }), + // Match by table ID (index in the job table) + JobID::TableID(id) => self.jobs.get(id).and_then(|job| job.as_ref()), + // Match by command name (partial match) + JobID::Command(cmd) => self.jobs.iter().find_map(|job| { + job.as_ref().filter(|j| { + j.children() + .iter() + .any(|child| child.cmd().as_ref().is_some_and(|c| c.contains(&cmd))) + }) + }), + } + } + pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { + match identifier { + // Match by process group ID + JobID::Pgid(pgid) => self + .jobs + .iter_mut() + .find_map(|job| job.as_mut().filter(|j| j.pgid() == pgid)), + // Match by process ID + JobID::Pid(pid) => self.jobs.iter_mut().find_map(|job| { + job + .as_mut() + .filter(|j| j.children().iter().any(|child| child.pid() == pid)) + }), + // Match by table ID (index in the job table) + JobID::TableID(id) => self.jobs.get_mut(id).and_then(|job| job.as_mut()), + // Match by command name (partial match) + JobID::Command(cmd) => self.jobs.iter_mut().find_map(|job| { + job.as_mut().filter(|j| { + j.children() + .iter() + .any(|child| child.cmd().as_ref().is_some_and(|c| c.contains(&cmd))) + }) + }), + } + } + pub fn get_fg(&self) -> Option<&Job> { + self.fg.as_ref() + } + pub fn get_fg_mut(&mut self) -> Option<&mut Job> { + self.fg.as_mut() + } + pub fn new_fg(&mut self, job: Job) -> ShResult> { + let pgid = job.pgid(); + self.fg = Some(job); + attach_tty(pgid)?; + let statuses = self.fg.as_mut().unwrap().wait_pgrp()?; + attach_tty(getpgrp())?; + Ok(statuses) + } + pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> { + if self.fg.is_none() { + return Ok(()); + } + take_term()?; + let fg = std::mem::take(&mut self.fg); + if let Some(mut job) = fg { + job.set_stats(stat); + self.insert_job(job, false)?; + } + Ok(()) + } + pub fn bg_to_fg(&mut self, id: JobID) -> ShResult<()> { + let job = self.remove_job(id); + if let Some(job) = job { + wait_fg(job)?; + } + Ok(()) + } + pub fn remove_job(&mut self, id: JobID) -> Option { + let tabid = self.query(id).map(|job| job.tabid().unwrap()); + if let Some(tabid) = tabid { + self.jobs.get_mut(tabid).and_then(Option::take) + } else { + None + } + } + pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> { + let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) { + &self + .jobs + .iter() + .filter(|job| { + job + .as_ref() + .is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap())) + }) + .map(|job| job.as_ref()) + .collect::>>() + } else { + &self + .jobs + .iter() + .map(|job| job.as_ref()) + .collect::>>() + }; + let mut jobs_to_remove = vec![]; + for job in jobs.iter().flatten() { + // Skip foreground job + let id = job.tabid().unwrap(); + // Filter jobs based on flags + if flags.contains(JobCmdFlags::RUNNING) + && !matches!( + job.get_stats().get(id).unwrap(), + WtStat::StillAlive | WtStat::Continued(_) + ) + { + continue; + } + if flags.contains(JobCmdFlags::STOPPED) + && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_, _)) + { + continue; + } + // Print the job in the selected format + write( + borrow_fd(1), + format!("{}\n", job.display(&self.order, flags)).as_bytes(), + )?; + if job + .get_stats() + .iter() + .all(|stat| matches!(stat, WtStat::Exited(_, _))) + { + jobs_to_remove.push(JobID::TableID(id)); + } + } + for id in jobs_to_remove { + self.remove_job(id); + } + Ok(()) + } } #[derive(Debug)] pub struct JobBldr { - table_id: Option, - pgid: Option, - children: Vec + table_id: Option, + pgid: Option, + children: Vec, } impl Default for JobBldr { - fn default() -> Self { - Self::new() - } + fn default() -> Self { + Self::new() + } } impl JobBldr { - pub fn new() -> Self { - Self { table_id: None, pgid: None, children: vec![] } - } - pub fn with_id(self, id: usize) -> Self { - Self { - table_id: Some(id), - pgid: self.pgid, - children: self.children - } - } - pub fn with_pgid(self, pgid: Pid) -> Self { - Self { - table_id: self.table_id, - pgid: Some(pgid), - children: self.children - } - } - pub fn set_pgid(&mut self, pgid: Pid) { - self.pgid = Some(pgid); - } - pub fn pgid(&self) -> Option { - self.pgid - } - pub fn with_children(self, children: Vec) -> Self { - Self { - table_id: self.table_id, - pgid: self.pgid, - children - } - } - pub fn push_child(&mut self, child: ChildProc) { - self.children.push(child); - } - pub fn build(self) -> Job { - Job { - table_id: self.table_id, - pgid: self.pgid.unwrap_or(Pid::from_raw(0)), - children: self.children - } - } + pub fn new() -> Self { + Self { + table_id: None, + pgid: None, + children: vec![], + } + } + pub fn with_id(self, id: usize) -> Self { + Self { + table_id: Some(id), + pgid: self.pgid, + children: self.children, + } + } + pub fn with_pgid(self, pgid: Pid) -> Self { + Self { + table_id: self.table_id, + pgid: Some(pgid), + children: self.children, + } + } + pub fn set_pgid(&mut self, pgid: Pid) { + self.pgid = Some(pgid); + } + pub fn pgid(&self) -> Option { + self.pgid + } + pub fn with_children(self, children: Vec) -> Self { + Self { + table_id: self.table_id, + pgid: self.pgid, + children, + } + } + pub fn push_child(&mut self, child: ChildProc) { + self.children.push(child); + } + pub fn build(self) -> Job { + Job { + table_id: self.table_id, + pgid: self.pgid.unwrap_or(Pid::from_raw(0)), + children: self.children, + } + } } /// A wrapper around Vec with some job-specific methods -#[derive(Default,Debug)] +#[derive(Default, Debug)] pub struct JobStack(Vec); impl JobStack { - pub fn new() -> Self { - Self::default() - } - pub fn new_job(&mut self) { - self.0.push(JobBldr::new()) - } - pub fn curr_job_mut(&mut self) -> Option<&mut JobBldr> { - self.0.last_mut() - } - pub fn finalize_job(&mut self) -> Option { - self.0.pop().map(|bldr| bldr.build()) - } + pub fn new() -> Self { + Self::default() + } + pub fn new_job(&mut self) { + self.0.push(JobBldr::new()) + } + pub fn curr_job_mut(&mut self) -> Option<&mut JobBldr> { + self.0.last_mut() + } + pub fn finalize_job(&mut self) -> Option { + self.0.pop().map(|bldr| bldr.build()) + } } -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub struct Job { - table_id: Option, - pgid: Pid, - children: Vec + table_id: Option, + pgid: Pid, + children: Vec, } impl Job { - pub fn set_tabid(&mut self, id: usize) { - self.table_id = Some(id) - } - pub fn running(&self) -> bool { - !self.children.iter().all(|chld| chld.exited()) - } - pub fn tabid(&self) -> Option { - self.table_id - } - pub fn pgid(&self) -> Pid { - self.pgid - } - pub fn get_cmds(&self) -> Vec<&str> { - let mut cmds = vec![]; - for child in &self.children { - cmds.push(child.cmd().unwrap_or_default()) - } - cmds - } - pub fn set_stats(&mut self, stat: WtStat) { - for child in self.children.iter_mut() { - child.set_stat(stat); - } - } - pub fn get_stats(&self) -> Vec { - self.children - .iter() - .map(|chld| chld.stat()) - .collect::>() - } - pub fn get_pids(&self) -> Vec { - self.children - .iter() - .map(|chld| chld.pid()) - .collect::>() - } - pub fn children(&self) -> &[ChildProc] { - &self.children - } - pub fn children_mut(&mut self) -> &mut Vec { - &mut self.children - } - pub fn killpg(&mut self, sig: Signal) -> ShResult<()> { - let stat = match sig { - Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), - Signal::SIGCONT => WtStat::Continued(self.pgid), - Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false), - _ => unimplemented!("{}",sig) - }; - self.set_stats(stat); - Ok(killpg(self.pgid, sig)?) - } - pub fn wait_pgrp(&mut self) -> ShResult> { - let mut stats = vec![]; - flog!(TRACE, "waiting on children"); - flog!(TRACE, self.children); - for child in self.children.iter_mut() { - if child.pid == Pid::this() { - // TODO: figure out some way to get the exit code of builtins - let code = state::get_status(); - stats.push(WtStat::Exited(child.pid, code)); - continue - } - let result = child.wait(Some(WtFlag::WSTOPPED)); - match result { - Ok(stat) => { - stats.push(stat); - } - Err(Errno::ECHILD) => break, - Err(e) => return Err(e.into()) - } - } - Ok(stats) - } - pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { - match id { - JobID::Pid(pid) => { - let query_result = self.children.iter_mut().find(|chld| chld.pid == pid); - if let Some(child) = query_result { - child.set_stat(stat); - } - } - JobID::Command(cmd) => { - let query_result = self.children - .iter_mut() - .find(|chld| chld - .cmd() - .is_some_and(|chld_cmd| chld_cmd.contains(&cmd)) - ); - if let Some(child) = query_result { - child.set_stat(stat); - } - } - JobID::TableID(tid) => { - if self.table_id.is_some_and(|tblid| tblid == tid) { - for child in self.children.iter_mut() { - child.set_stat(stat); - } - } - } - JobID::Pgid(pgid) => { - if pgid == self.pgid { - for child in self.children.iter_mut() { - child.set_stat(stat); - } - } - } - } - Ok(()) - } - pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String { - let long = flags.contains(JobCmdFlags::LONG); - let init = flags.contains(JobCmdFlags::INIT); - let pids = flags.contains(JobCmdFlags::PIDS); + pub fn set_tabid(&mut self, id: usize) { + self.table_id = Some(id) + } + pub fn running(&self) -> bool { + !self.children.iter().all(|chld| chld.exited()) + } + pub fn tabid(&self) -> Option { + self.table_id + } + pub fn pgid(&self) -> Pid { + self.pgid + } + pub fn get_cmds(&self) -> Vec<&str> { + let mut cmds = vec![]; + for child in &self.children { + cmds.push(child.cmd().unwrap_or_default()) + } + cmds + } + pub fn set_stats(&mut self, stat: WtStat) { + for child in self.children.iter_mut() { + child.set_stat(stat); + } + } + pub fn get_stats(&self) -> Vec { + self + .children + .iter() + .map(|chld| chld.stat()) + .collect::>() + } + pub fn get_pids(&self) -> Vec { + self + .children + .iter() + .map(|chld| chld.pid()) + .collect::>() + } + pub fn children(&self) -> &[ChildProc] { + &self.children + } + pub fn children_mut(&mut self) -> &mut Vec { + &mut self.children + } + pub fn killpg(&mut self, sig: Signal) -> ShResult<()> { + let stat = match sig { + Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), + Signal::SIGCONT => WtStat::Continued(self.pgid), + Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false), + _ => unimplemented!("{}", sig), + }; + self.set_stats(stat); + Ok(killpg(self.pgid, sig)?) + } + pub fn wait_pgrp(&mut self) -> ShResult> { + let mut stats = vec![]; + flog!(TRACE, "waiting on children"); + flog!(TRACE, self.children); + for child in self.children.iter_mut() { + if child.pid == Pid::this() { + // TODO: figure out some way to get the exit code of builtins + let code = state::get_status(); + stats.push(WtStat::Exited(child.pid, code)); + continue; + } + let result = child.wait(Some(WtFlag::WSTOPPED)); + match result { + Ok(stat) => { + stats.push(stat); + } + Err(Errno::ECHILD) => break, + Err(e) => return Err(e.into()), + } + } + Ok(stats) + } + pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { + match id { + JobID::Pid(pid) => { + let query_result = self.children.iter_mut().find(|chld| chld.pid == pid); + if let Some(child) = query_result { + child.set_stat(stat); + } + } + JobID::Command(cmd) => { + let query_result = self + .children + .iter_mut() + .find(|chld| chld.cmd().is_some_and(|chld_cmd| chld_cmd.contains(&cmd))); + if let Some(child) = query_result { + child.set_stat(stat); + } + } + JobID::TableID(tid) => { + if self.table_id.is_some_and(|tblid| tblid == tid) { + for child in self.children.iter_mut() { + child.set_stat(stat); + } + } + } + JobID::Pgid(pgid) => { + if pgid == self.pgid { + for child in self.children.iter_mut() { + child.set_stat(stat); + } + } + } + } + Ok(()) + } + pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String { + let long = flags.contains(JobCmdFlags::LONG); + let init = flags.contains(JobCmdFlags::INIT); + let pids = flags.contains(JobCmdFlags::PIDS); - let current = job_order.last(); - let prev = if job_order.len() > 2 { - job_order.get(job_order.len() - 2) - } else { - None - }; + let current = job_order.last(); + let prev = if job_order.len() > 2 { + job_order.get(job_order.len() - 2) + } else { + None + }; - let id = self.table_id.unwrap(); - let symbol = if current == self.table_id.as_ref() { - "+" - } else if prev == self.table_id.as_ref() { - "-" - } else { - " " - }; - let padding_count = symbol.len() + id.to_string().len() + 3; - let padding = " ".repeat(padding_count); + let id = self.table_id.unwrap(); + let symbol = if current == self.table_id.as_ref() { + "+" + } else if prev == self.table_id.as_ref() { + "-" + } else { + " " + }; + let padding_count = symbol.len() + id.to_string().len() + 3; + let padding = " ".repeat(padding_count); - let mut output = format!("[{}]{}\t", id + 1, symbol); - for (i, cmd) in self.get_cmds().iter().enumerate() { - let pid = if pids || init { - let mut pid = self.get_pids().get(i).unwrap().to_string(); - pid.push(' '); - pid - } else { - "".to_string() - }; - let job_stat = *self.get_stats().get(i).unwrap(); - let fmt_stat = DisplayWaitStatus(job_stat).to_string(); + let mut output = format!("[{}]{}\t", id + 1, symbol); + for (i, cmd) in self.get_cmds().iter().enumerate() { + let pid = if pids || init { + let mut pid = self.get_pids().get(i).unwrap().to_string(); + pid.push(' '); + pid + } else { + "".to_string() + }; + let job_stat = *self.get_stats().get(i).unwrap(); + let fmt_stat = DisplayWaitStatus(job_stat).to_string(); - let mut stat_line = if init { - "".to_string() - } else { - fmt_stat.clone() - }; - stat_line = format!("{}{} ",pid,stat_line); - stat_line = format!("{} {}", stat_line, cmd); - stat_line = match job_stat { - WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta), - WtStat::Exited(_, code) => { - match code { - 0 => stat_line.styled(Style::Green), - _ => stat_line.styled(Style::Red), - } - } - _ => stat_line.styled(Style::Cyan) - }; - if i != self.get_cmds().len() - 1 { - stat_line = format!("{} |",stat_line); - } + let mut stat_line = if init { + "".to_string() + } else { + fmt_stat.clone() + }; + stat_line = format!("{}{} ", pid, stat_line); + stat_line = format!("{} {}", stat_line, cmd); + stat_line = match job_stat { + WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta), + WtStat::Exited(_, code) => match code { + 0 => stat_line.styled(Style::Green), + _ => stat_line.styled(Style::Red), + }, + _ => stat_line.styled(Style::Cyan), + }; + if i != self.get_cmds().len() - 1 { + stat_line = format!("{} |", stat_line); + } - let stat_final = if long { - format!( - "{}{} {}", - if i != 0 { &padding } else { "" }, - self.get_pids().get(i).unwrap(), - stat_line - ) - } else { - format!( - "{}{}", - if i != 0 { &padding } else { "" }, - stat_line - ) - }; - output.push_str(&stat_final); - output.push('\n'); - } - output - } + let stat_final = if long { + format!( + "{}{} {}", + if i != 0 { &padding } else { "" }, + self.get_pids().get(i).unwrap(), + stat_line + ) + } else { + format!("{}{}", if i != 0 { &padding } else { "" }, stat_line) + }; + output.push_str(&stat_final); + output.push('\n'); + } + output + } } pub fn term_ctlr() -> Pid { - tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp()) + tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp()) } -/// Calls attach_tty() on the shell's process group to retake control of the terminal +/// Calls attach_tty() on the shell's process group to retake control of the +/// terminal pub fn take_term() -> ShResult<()> { - attach_tty(getpgrp())?; - Ok(()) + attach_tty(getpgrp())?; + Ok(()) } pub fn disable_reaping() -> ShResult<()> { - flog!(TRACE, "Disabling reaping"); - unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::ignore_sigchld)) }?; - Ok(()) + flog!(TRACE, "Disabling reaping"); + unsafe { + signal( + Signal::SIGCHLD, + SigHandler::Handler(crate::signal::ignore_sigchld), + ) + }?; + Ok(()) } pub fn enable_reaping() -> ShResult<()> { - flog!(TRACE, "Enabling reaping"); - unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::handle_sigchld)) }.unwrap(); - Ok(()) + flog!(TRACE, "Enabling reaping"); + unsafe { + signal( + Signal::SIGCHLD, + SigHandler::Handler(crate::signal::handle_sigchld), + ) + } + .unwrap(); + Ok(()) } /// Waits on the current foreground job and updates the shell's last status code pub fn wait_fg(job: Job) -> ShResult<()> { - if job.children().is_empty() { - return Ok(()) // Nothing to do - } - flog!(TRACE, "Waiting on foreground job"); - let mut code = 0; - attach_tty(job.pgid())?; - disable_reaping()?; - let statuses = write_jobs(|j| j.new_fg(job))?; - for status in statuses { - match status { - WtStat::Exited(_, exit_code) => { - code = exit_code; - } - WtStat::Stopped(_, sig) => { - write_jobs(|j| j.fg_to_bg(status))?; - code = SIG_EXIT_OFFSET + sig as i32; - }, - WtStat::Signaled(_, sig, _) => { - if sig == Signal::SIGTSTP { - write_jobs(|j| j.fg_to_bg(status))?; - } - code = SIG_EXIT_OFFSET + sig as i32; - }, - _ => { /* Do nothing */ } - } - } - take_term()?; - set_status(code); - flog!(TRACE, "exit code: {}", code); - enable_reaping()?; - Ok(()) + if job.children().is_empty() { + return Ok(()); // Nothing to do + } + flog!(TRACE, "Waiting on foreground job"); + let mut code = 0; + attach_tty(job.pgid())?; + disable_reaping()?; + let statuses = write_jobs(|j| j.new_fg(job))?; + for status in statuses { + match status { + WtStat::Exited(_, exit_code) => { + code = exit_code; + } + WtStat::Stopped(_, sig) => { + write_jobs(|j| j.fg_to_bg(status))?; + code = SIG_EXIT_OFFSET + sig as i32; + } + WtStat::Signaled(_, sig, _) => { + if sig == Signal::SIGTSTP { + write_jobs(|j| j.fg_to_bg(status))?; + } + code = SIG_EXIT_OFFSET + sig as i32; + } + _ => { /* Do nothing */ } + } + } + take_term()?; + set_status(code); + flog!(TRACE, "exit code: {}", code); + enable_reaping()?; + Ok(()) } pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> { - if is_bg { - write_jobs(|j| { - j.insert_job(job, false) - })?; - } else { - wait_fg(job)?; - } - Ok(()) + if is_bg { + write_jobs(|j| j.insert_job(job, false))?; + } else { + wait_fg(job)?; + } + Ok(()) } pub fn attach_tty(pgid: Pid) -> ShResult<()> { - // If we aren't attached to a terminal, the pgid already controls it, or the process group does not exist - // Then return ok - if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() { - return Ok(()) - } - flog!(TRACE, "Attaching tty to pgid: {}",pgid); + // If we aren't attached to a terminal, the pgid already controls it, or the + // process group does not exist Then return ok + if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() { + return Ok(()); + } + flog!(TRACE, "Attaching tty to pgid: {}", pgid); - if pgid == getpgrp() && term_ctlr() != getpgrp() { - kill(term_ctlr(), Signal::SIGTTOU).ok(); - } + if pgid == getpgrp() && term_ctlr() != getpgrp() { + kill(term_ctlr(), Signal::SIGTTOU).ok(); + } - let mut new_mask = SigSet::empty(); - let mut mask_bkup = SigSet::empty(); + let mut new_mask = SigSet::empty(); + let mut mask_bkup = SigSet::empty(); - new_mask.add(Signal::SIGTSTP); - new_mask.add(Signal::SIGTTIN); - new_mask.add(Signal::SIGTTOU); + new_mask.add(Signal::SIGTSTP); + new_mask.add(Signal::SIGTTIN); + new_mask.add(Signal::SIGTTOU); - 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(0), pgid); - pthread_sigmask(SigmaskHow::SIG_SETMASK, Some(&mask_bkup), Some(&mut new_mask))?; + pthread_sigmask( + SigmaskHow::SIG_SETMASK, + Some(&mask_bkup), + Some(&mut new_mask), + )?; - match result { - Ok(_) => Ok(()), - Err(e) => { - flog!(ERROR, "error while switching term control: {}",e); - tcsetpgrp(borrow_fd(0), getpgrp())?; - Ok(()) - } - } + match result { + Ok(_) => Ok(()), + Err(e) => { + flog!(ERROR, "error while switching term control: {}", e); + tcsetpgrp(borrow_fd(0), getpgrp())?; + Ok(()) + } + } } diff --git a/src/libsh/error.rs b/src/libsh/error.rs index d508fdd..e130f03 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,356 +1,439 @@ use std::fmt::Display; use crate::{ - libsh::term::{Style, Styled}, - parse::lex::Span, - prelude::* + libsh::term::{Style, Styled}, + parse::lex::Span, + prelude::*, }; -pub type ShResult = Result; +pub type ShResult = Result; pub trait ShResultExt { - fn blame(self, span: Span) -> Self; - fn try_blame(self, span: Span) -> Self; + fn blame(self, span: Span) -> Self; + fn try_blame(self, span: Span) -> Self; } -impl ShResultExt for Result { - /// Blame a span for an error - fn blame(self, new_span: Span) -> Self { - let Err(e) = self else { - return self - }; - match e { - ShErr::Simple { kind, msg, notes } | - ShErr::Full { kind, msg, notes, span: _ } => Err(ShErr::Full { kind: kind.clone(), msg: msg.clone(), notes: notes.clone(), span: new_span }), - } - } - /// Blame a span if no blame has been assigned yet - fn try_blame(self, new_span: Span) -> Self { - let Err(e) = &self else { - return self - }; - match e { - ShErr::Simple { kind, msg, notes } => Err(ShErr::Full { kind: kind.clone(), msg: msg.clone(), notes: notes.clone(), span: new_span }), - ShErr::Full { kind: _, msg: _, span: _, notes: _ } => self - } - } +impl ShResultExt for Result { + /// Blame a span for an error + fn blame(self, new_span: Span) -> Self { + let Err(e) = self else { return self }; + match e { + ShErr::Simple { kind, msg, notes } + | ShErr::Full { + kind, + msg, + notes, + span: _, + } => Err(ShErr::Full { + kind: kind.clone(), + msg: msg.clone(), + notes: notes.clone(), + span: new_span, + }), + } + } + /// Blame a span if no blame has been assigned yet + fn try_blame(self, new_span: Span) -> Self { + let Err(e) = &self else { return self }; + match e { + ShErr::Simple { kind, msg, notes } => Err(ShErr::Full { + kind: kind.clone(), + msg: msg.clone(), + notes: notes.clone(), + span: new_span, + }), + ShErr::Full { + kind: _, + msg: _, + span: _, + notes: _, + } => self, + } + } } -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct Note { - main: String, - sub_notes: Vec, - depth: usize + main: String, + sub_notes: Vec, + depth: usize, } impl Note { - pub fn new(main: impl Into) -> Self { - Self { - main: main.into(), - sub_notes: vec![], - depth: 0 - } - } + pub fn new(main: impl Into) -> Self { + Self { + main: main.into(), + sub_notes: vec![], + depth: 0, + } + } - pub fn with_sub_notes(self, new_sub_notes: Vec>) -> Self { - let Self { main, mut sub_notes, depth } = self; - for raw_note in new_sub_notes { - let mut note = Note::new(raw_note); - note.depth = self.depth + 1; - sub_notes.push(note); - } - Self { main, sub_notes, depth } - } + pub fn with_sub_notes(self, new_sub_notes: Vec>) -> Self { + let Self { + main, + mut sub_notes, + depth, + } = self; + for raw_note in new_sub_notes { + let mut note = Note::new(raw_note); + note.depth = self.depth + 1; + sub_notes.push(note); + } + Self { + main, + sub_notes, + depth, + } + } } impl Display for Note { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let note = "note".styled(Style::Green); - let main = &self.main; - if self.depth == 0 { - writeln!(f, "{note}: {main}")?; - } else { - let bar_break = "-".styled(Style::Cyan | Style::Bold); - let indent = " ".repeat(self.depth); - writeln!(f, " {indent}{bar_break} {main}")?; - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let note = "note".styled(Style::Green); + let main = &self.main; + if self.depth == 0 { + writeln!(f, "{note}: {main}")?; + } else { + let bar_break = "-".styled(Style::Cyan | Style::Bold); + let indent = " ".repeat(self.depth); + writeln!(f, " {indent}{bar_break} {main}")?; + } - for sub_note in &self.sub_notes { - write!(f, "{sub_note}")?; - } - Ok(()) - } + for sub_note in &self.sub_notes { + write!(f, "{sub_note}")?; + } + Ok(()) + } } #[derive(Debug)] pub enum ShErr { - Simple { kind: ShErrKind, msg: String, notes: Vec }, - Full { kind: ShErrKind, msg: String, notes: Vec, span: Span } + Simple { + kind: ShErrKind, + msg: String, + notes: Vec, + }, + Full { + kind: ShErrKind, + msg: String, + notes: Vec, + span: Span, + }, } impl ShErr { - pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { - let msg = msg.into(); - Self::Simple { kind, msg, notes: vec![] } - } - pub fn full(kind: ShErrKind, msg: impl Into, span: Span) -> Self { - let msg = msg.into(); - Self::Full { kind, msg, span, notes: vec![] } - } - pub fn unpack(self) -> (ShErrKind,String,Vec,Option) { - match self { - ShErr::Simple { kind, msg, notes } => (kind,msg,notes,None), - ShErr::Full { kind, msg, notes, span } => (kind,msg,notes,Some(span)) - } - } - pub fn with_note(self, note: Note) -> Self { - let (kind,msg,mut notes,span) = self.unpack(); - notes.push(note); - if let Some(span) = span { - Self::Full { kind, msg, notes, span } - } else { - Self::Simple { kind, msg, notes } - } - } - pub fn with_span(sherr: ShErr, span: Span) -> Self { - let (kind,msg,notes,_) = sherr.unpack(); - Self::Full { kind, msg, notes, span } - } - pub fn kind(&self) -> &ShErrKind { - match self { - ShErr::Simple { kind, msg: _, notes: _ } | - ShErr::Full { kind, msg: _, notes: _, span: _ } => kind - } - } - pub fn get_window(&self) -> Vec<(usize,String)> { - let ShErr::Full { kind: _, msg: _, notes: _, span } = self else { - unreachable!() - }; - let mut total_len: usize = 0; - let mut total_lines: usize = 1; - let mut lines = vec![]; - let mut cur_line = String::new(); + pub fn simple(kind: ShErrKind, msg: impl Into) -> Self { + let msg = msg.into(); + Self::Simple { + kind, + msg, + notes: vec![], + } + } + pub fn full(kind: ShErrKind, msg: impl Into, span: Span) -> Self { + let msg = msg.into(); + Self::Full { + kind, + msg, + span, + notes: vec![], + } + } + pub fn unpack(self) -> (ShErrKind, String, Vec, Option) { + match self { + ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None), + ShErr::Full { + kind, + msg, + notes, + span, + } => (kind, msg, notes, Some(span)), + } + } + pub fn with_note(self, note: Note) -> Self { + let (kind, msg, mut notes, span) = self.unpack(); + notes.push(note); + if let Some(span) = span { + Self::Full { + kind, + msg, + notes, + span, + } + } else { + Self::Simple { kind, msg, notes } + } + } + pub fn with_span(sherr: ShErr, span: Span) -> Self { + let (kind, msg, notes, _) = sherr.unpack(); + Self::Full { + kind, + msg, + notes, + span, + } + } + pub fn kind(&self) -> &ShErrKind { + match self { + ShErr::Simple { + kind, + msg: _, + notes: _, + } + | ShErr::Full { + kind, + msg: _, + notes: _, + span: _, + } => kind, + } + } + pub fn get_window(&self) -> Vec<(usize, String)> { + let ShErr::Full { + kind: _, + msg: _, + notes: _, + span, + } = self + else { + unreachable!() + }; + let mut total_len: usize = 0; + let mut total_lines: usize = 1; + let mut lines = vec![]; + let mut cur_line = String::new(); - let src = span.get_source(); - let mut chars = src.chars(); + let src = span.get_source(); + let mut chars = src.chars(); - while let Some(ch) = chars.next() { - total_len += ch.len_utf8(); - cur_line.push(ch); - if ch == '\n' { - if total_len > span.start { - let line = ( - total_lines, - mem::take(&mut cur_line) - ); - lines.push(line); - } - if total_len >= span.end { - break - } - total_lines += 1; + while let Some(ch) = chars.next() { + total_len += ch.len_utf8(); + cur_line.push(ch); + if ch == '\n' { + if total_len > span.start { + let line = (total_lines, mem::take(&mut cur_line)); + lines.push(line); + } + if total_len >= span.end { + break; + } + total_lines += 1; - cur_line.clear(); - } - } + cur_line.clear(); + } + } - if !cur_line.is_empty() { - let line = ( - total_lines, - mem::take(&mut cur_line) - ); - lines.push(line); - } + if !cur_line.is_empty() { + let line = (total_lines, mem::take(&mut cur_line)); + lines.push(line); + } - lines - } - pub fn get_line_col(&self) -> (usize,usize) { - let ShErr::Full { kind: _, msg: _, notes: _, span } = self else { - unreachable!() - }; + lines + } + pub fn get_line_col(&self) -> (usize, usize) { + let ShErr::Full { + kind: _, + msg: _, + notes: _, + span, + } = self + else { + unreachable!() + }; - let mut lineno = 1; - let mut colno = 1; - let src = span.get_source(); - let mut chars = src.chars().enumerate(); - while let Some((pos,ch)) = chars.next() { - if pos >= span.start { - break - } - if ch == '\n' { - lineno += 1; - colno = 1; - } else { - colno += 1; - } - } - (lineno,colno) - } - pub fn get_indicator_lines(&self) -> Option> { - match self { - ShErr::Simple { kind: _, msg: _, notes: _ } => None, - ShErr::Full { kind: _, msg: _, notes: _, span } => { - let text = span.as_str(); - let lines = text.lines(); - let mut indicator_lines = vec![]; + let mut lineno = 1; + let mut colno = 1; + let src = span.get_source(); + let mut chars = src.chars().enumerate(); + while let Some((pos, ch)) = chars.next() { + if pos >= span.start { + break; + } + if ch == '\n' { + lineno += 1; + colno = 1; + } else { + colno += 1; + } + } + (lineno, colno) + } + pub fn get_indicator_lines(&self) -> Option> { + match self { + ShErr::Simple { + kind: _, + msg: _, + notes: _, + } => None, + ShErr::Full { + kind: _, + msg: _, + notes: _, + span, + } => { + let text = span.as_str(); + let lines = text.lines(); + let mut indicator_lines = vec![]; - for line in lines { - let indicator_line = "^".repeat(line.trim().len()).styled(Style::Red | Style::Bold); - indicator_lines.push(indicator_line); - } + for line in lines { + let indicator_line = "^" + .repeat(line.trim().len()) + .styled(Style::Red | Style::Bold); + indicator_lines.push(indicator_line); + } - Some(indicator_lines) - } - } - } + Some(indicator_lines) + } + } + } } impl Display for ShErr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Simple { msg, kind: _, notes } => { - let mut all_strings = vec![msg.to_string()]; - let mut notes_fmt = vec![]; - for note in notes { - let fmt = format!("{note}"); - notes_fmt.push(fmt); - } - all_strings.append(&mut notes_fmt); - let mut output = all_strings.join("\n"); - output.push('\n'); + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Simple { + msg, + kind: _, + notes, + } => { + let mut all_strings = vec![msg.to_string()]; + let mut notes_fmt = vec![]; + for note in notes { + let fmt = format!("{note}"); + notes_fmt.push(fmt); + } + all_strings.append(&mut notes_fmt); + let mut output = all_strings.join("\n"); + output.push('\n'); - writeln!(f, "{}", output) - } + writeln!(f, "{}", output) + } - Self::Full { msg, kind, notes, span: _ } => { - let window = self.get_window(); - let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter(); - let mut lineno_pad_count = 0; - for (lineno,_) in window.clone() { - if lineno.to_string().len() > lineno_pad_count { - lineno_pad_count = lineno.to_string().len() + 1 - } - } - let padding = " ".repeat(lineno_pad_count); - writeln!(f)?; + Self::Full { + msg, + kind, + notes, + span: _, + } => { + let window = self.get_window(); + let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter(); + let mut lineno_pad_count = 0; + for (lineno, _) in window.clone() { + if lineno.to_string().len() > lineno_pad_count { + lineno_pad_count = lineno.to_string().len() + 1 + } + } + let padding = " ".repeat(lineno_pad_count); + writeln!(f)?; + let (line, col) = self.get_line_col(); + let line_fmt = line.styled(Style::Cyan | Style::Bold); + let col_fmt = col.styled(Style::Cyan | Style::Bold); + let kind = kind.styled(Style::Red | Style::Bold); + let arrow = "->".styled(Style::Cyan | Style::Bold); + writeln!(f, "{kind} - {msg}",)?; + writeln!(f, "{padding}{arrow} [{line_fmt};{col_fmt}]",)?; - let (line,col) = self.get_line_col(); - let line_fmt = line.styled(Style::Cyan | Style::Bold); - let col_fmt = col.styled(Style::Cyan | Style::Bold); - let kind = kind.styled(Style::Red | Style::Bold); - let arrow = "->".styled(Style::Cyan | Style::Bold); - writeln!(f, - "{kind} - {msg}", - )?; - writeln!(f, - "{padding}{arrow} [{line_fmt};{col_fmt}]", - )?; + let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold); + writeln!(f, "{bar}")?; - let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold); - writeln!(f,"{bar}")?; + let mut first_ind_ln = true; + for (lineno, line) in window { + let lineno = lineno.to_string(); + let line = line.trim(); + let mut prefix = format!("{padding}|"); + prefix.replace_range(0..lineno.len(), &lineno); + prefix = prefix.styled(Style::Cyan | Style::Bold); + writeln!(f, "{prefix} {line}")?; - let mut first_ind_ln = true; - for (lineno,line) in window { - let lineno = lineno.to_string(); - let line = line.trim(); - let mut prefix = format!("{padding}|"); - prefix.replace_range(0..lineno.len(), &lineno); - prefix = prefix.styled(Style::Cyan | Style::Bold); - writeln!(f,"{prefix} {line}")?; + if let Some(ind_ln) = indicator_lines.next() { + if first_ind_ln { + let ind_ln_padding = " ".repeat(col); + let ind_ln = format!("{ind_ln_padding}{ind_ln}"); + writeln!(f, "{bar}{ind_ln}")?; + first_ind_ln = false; + } else { + writeln!(f, "{bar} {ind_ln}")?; + } + } + } - if let Some(ind_ln) = indicator_lines.next() { - if first_ind_ln { - let ind_ln_padding = " ".repeat(col); - let ind_ln = format!("{ind_ln_padding}{ind_ln}"); - writeln!(f, "{bar}{ind_ln}")?; - first_ind_ln = false; - } else { - writeln!(f, "{bar} {ind_ln}")?; - } - } - } + write!(f, "{bar}")?; - write!(f,"{bar}")?; - - - let bar_break = "-".styled(Style::Cyan | Style::Bold); - if !notes.is_empty() { - writeln!(f)?; - } - for note in notes { - - write!(f, - "{padding}{bar_break} {note}" - )?; - } - Ok(()) - } - } - } + let bar_break = "-".styled(Style::Cyan | Style::Bold); + if !notes.is_empty() { + writeln!(f)?; + } + for note in notes { + write!(f, "{padding}{bar_break} {note}")?; + } + Ok(()) + } + } + } } impl From for ShErr { - fn from(_: std::io::Error) -> Self { - let msg = std::io::Error::last_os_error(); - ShErr::simple(ShErrKind::IoErr, msg.to_string()) - } + fn from(_: std::io::Error) -> Self { + let msg = std::io::Error::last_os_error(); + ShErr::simple(ShErrKind::IoErr, msg.to_string()) + } } impl From for ShErr { - fn from(value: std::env::VarError) -> Self { - ShErr::simple(ShErrKind::InternalErr, value.to_string()) - } + fn from(value: std::env::VarError) -> Self { + ShErr::simple(ShErrKind::InternalErr, value.to_string()) + } } impl From for ShErr { - fn from(value: Errno) -> Self { - ShErr::simple(ShErrKind::Errno, value.to_string()) - } + fn from(value: Errno) -> Self { + ShErr::simple(ShErrKind::Errno, value.to_string()) + } } -#[derive(Debug,Clone)] +#[derive(Debug, Clone)] pub enum ShErrKind { - IoErr, - SyntaxErr, - ParseErr, - InternalErr, - ExecFail, - HistoryReadErr, - ResourceLimitExceeded, - BadPermission, - Errno, - FileNotFound(String), - CmdNotFound(String), - CleanExit(i32), - FuncReturn(i32), - LoopContinue(i32), - LoopBreak(i32), - ReadlineErr, - Null + IoErr, + SyntaxErr, + ParseErr, + InternalErr, + ExecFail, + HistoryReadErr, + ResourceLimitExceeded, + BadPermission, + Errno, + FileNotFound(String), + CmdNotFound(String), + CleanExit(i32), + FuncReturn(i32), + LoopContinue(i32), + LoopBreak(i32), + ReadlineErr, + Null, } impl Display for ShErrKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = match self { - Self::IoErr => "I/O Error", - Self::SyntaxErr => "Syntax Error", - Self::ParseErr => "Parse Error", - Self::InternalErr => "Internal Error", - Self::HistoryReadErr => "History Parse Error", - Self::ExecFail => "Execution Failed", - Self::ResourceLimitExceeded => "Resource Limit Exceeded", - Self::BadPermission => "Bad Permissions", - Self::Errno => "ERRNO", - Self::FileNotFound(file) => &format!("File not found: {file}"), - Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), - Self::CleanExit(_) => "", - Self::FuncReturn(_) => "", - Self::LoopContinue(_) => "", - Self::LoopBreak(_) => "", - Self::ReadlineErr => "Line Read Error", - Self::Null => "", - }; - write!(f,"{output}") - } + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = match self { + Self::IoErr => "I/O Error", + Self::SyntaxErr => "Syntax Error", + Self::ParseErr => "Parse Error", + Self::InternalErr => "Internal Error", + Self::HistoryReadErr => "History Parse Error", + Self::ExecFail => "Execution Failed", + Self::ResourceLimitExceeded => "Resource Limit Exceeded", + Self::BadPermission => "Bad Permissions", + Self::Errno => "ERRNO", + Self::FileNotFound(file) => &format!("File not found: {file}"), + Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), + Self::CleanExit(_) => "", + Self::FuncReturn(_) => "", + Self::LoopContinue(_) => "", + Self::LoopBreak(_) => "", + Self::ReadlineErr => "Line Read Error", + Self::Null => "", + }; + write!(f, "{output}") + } } diff --git a/src/libsh/flog.rs b/src/libsh/flog.rs index 1a3fce4..45438d4 100644 --- a/src/libsh/flog.rs +++ b/src/libsh/flog.rs @@ -2,55 +2,57 @@ use std::fmt::Display; use super::term::{Style, Styled}; -#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq , Debug)] +#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Debug)] #[repr(u8)] pub enum FernLogLevel { - NONE = 0, - ERROR = 1, - WARN = 2, - INFO = 3, - DEBUG = 4, - TRACE = 5 + NONE = 0, + ERROR = 1, + WARN = 2, + INFO = 3, + DEBUG = 4, + TRACE = 5, } impl Display for FernLogLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use FernLogLevel::*; - match self { - ERROR => write!(f,"{}","ERROR".styled(Style::Red | Style::Bold)), - WARN => write!(f,"{}","WARN".styled(Style::Yellow | Style::Bold)), - INFO => write!(f,"{}","INFO".styled(Style::Green | Style::Bold)), - DEBUG => write!(f,"{}","DEBUG".styled(Style::Magenta | Style::Bold)), - TRACE => write!(f,"{}","TRACE".styled(Style::Blue | Style::Bold)), - NONE => write!(f,"") - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use FernLogLevel::*; + match self { + ERROR => write!(f, "{}", "ERROR".styled(Style::Red | Style::Bold)), + WARN => write!(f, "{}", "WARN".styled(Style::Yellow | Style::Bold)), + INFO => write!(f, "{}", "INFO".styled(Style::Green | Style::Bold)), + DEBUG => write!(f, "{}", "DEBUG".styled(Style::Magenta | Style::Bold)), + TRACE => write!(f, "{}", "TRACE".styled(Style::Blue | Style::Bold)), + NONE => write!(f, ""), + } + } } pub fn log_level() -> FernLogLevel { - use FernLogLevel::*; - let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default(); - match level.to_lowercase().as_str() { - "error" => ERROR, - "warn" => WARN, - "info" => INFO, - "debug" => DEBUG, - "trace" => TRACE, - _ => NONE - } + use FernLogLevel::*; + let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default(); + match level.to_lowercase().as_str() { + "error" => ERROR, + "warn" => WARN, + "info" => INFO, + "debug" => DEBUG, + "trace" => TRACE, + _ => NONE, + } } /// A structured logging macro designed for `fern`. /// -/// `flog!` was implemented because `rustyline` uses `env_logger`, which clutters the debug output. -/// This macro prints log messages in a structured format, including the log level, filename, and line number. +/// `flog!` was implemented because `rustyline` uses `env_logger`, which +/// clutters the debug output. This macro prints log messages in a structured +/// format, including the log level, filename, and line number. /// /// # Usage /// /// The macro supports three types of arguments: /// /// ## 1. **Formatted Messages** -/// Similar to `println!` or `format!`, allows embedding values inside a formatted string. +/// Similar to `println!` or `format!`, allows embedding values inside a +/// formatted string. /// /// ```rust /// flog!(ERROR, "foo is {}", foo); @@ -73,7 +75,8 @@ pub fn log_level() -> FernLogLevel { /// ``` /// /// ## 3. **Expressions** -/// Logs the evaluated result of each given expression, displaying both the expression and its value. +/// Logs the evaluated result of each given expression, displaying both the +/// expression and its value. /// /// ```rust /// flog!(INFO, 1.min(2)); @@ -84,8 +87,10 @@ pub fn log_level() -> FernLogLevel { /// ``` /// /// # Considerations -/// - This macro uses `eprintln!()` internally, so its formatting rules must be followed. -/// - **Literals and formatted messages** require arguments that implement [`std::fmt::Display`]. +/// - This macro uses `eprintln!()` internally, so its formatting rules must be +/// followed. +/// - **Literals and formatted messages** require arguments that implement +/// [`std::fmt::Display`]. /// - **Expressions** require arguments that implement [`std::fmt::Debug`]. #[macro_export] macro_rules! flog { diff --git a/src/libsh/mod.rs b/src/libsh/mod.rs index a6e2e8c..820e4ab 100644 --- a/src/libsh/mod.rs +++ b/src/libsh/mod.rs @@ -1,5 +1,5 @@ pub mod error; -pub mod term; pub mod flog; pub mod sys; +pub mod term; pub mod utils; diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs index 75e1bb0..01b7b04 100644 --- a/src/libsh/sys.rs +++ b/src/libsh/sys.rs @@ -4,70 +4,91 @@ use crate::{prelude::*, state::write_jobs}; /// /// The previous state of the terminal options. /// -/// This variable stores the terminal settings at the start of the program and restores them when the program exits. -/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program. -/// It will not be mutated or accessed under any other circumstances. +/// This variable stores the terminal settings at the start of the program and +/// restores them when the program exits. It is initialized exactly once at the +/// start of the program and accessed exactly once at the end of the program. It +/// will not be mutated or accessed under any other circumstances. /// -/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky. +/// This ended up being necessary because wrapping Termios in a thread-safe way +/// was unreasonably tricky. /// /// The possible states of this variable are: -/// - `None`: The terminal options have not been set yet (before initialization). -/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected). -/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved. +/// - `None`: The terminal options have not been set yet (before +/// initialization). +/// - `Some(None)`: There were no terminal options to save (i.e., no terminal +/// input detected). +/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been +/// saved. /// -/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because: -/// - It is set once during program startup and accessed once during program exit. +/// **Important:** This static variable is mutable and accessed via unsafe code. +/// It is only safe to use because: +/// - It is set once during program startup and accessed once during program +/// exit. /// - It is not mutated or accessed after the initial setup and final read. /// -/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior. +/// **Caution:** Future changes to this code should respect these constraints to +/// ensure safety. Modifying or accessing this variable outside the defined +/// lifecycle could lead to undefined behavior. pub(crate) static mut SAVED_TERMIOS: Option> = None; pub fn save_termios() { - unsafe { - SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { - let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); - termios.local_flags &= !LocalFlags::ECHOCTL; - termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); - Some(termios) - } else { - None - }); - } + unsafe { + SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { + let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); + termios.local_flags &= !LocalFlags::ECHOCTL; + termios::tcsetattr( + std::io::stdin(), + nix::sys::termios::SetArg::TCSANOW, + &termios, + ) + .unwrap(); + Some(termios) + } else { + None + }); + } } #[allow(static_mut_refs)] ///Access the saved termios /// ///# Safety -///This function is unsafe because it accesses a public mutable static value. This function should only ever be called after save_termios() has already been called. +///This function is unsafe because it accesses a public mutable static value. +/// This function should only ever be called after save_termios() has already +/// been called. pub unsafe fn get_saved_termios() -> Option { - // SAVED_TERMIOS should *only ever* be set once and accessed once - // Set at the start of the program, and accessed during the exit of the program to reset the termios. - // Do not use this variable anywhere else - SAVED_TERMIOS.clone().flatten() + // SAVED_TERMIOS should *only ever* be set once and accessed once + // Set at the start of the program, and accessed during the exit of the program + // to reset the termios. Do not use this variable anywhere else + SAVED_TERMIOS.clone().flatten() } /// Set termios to not echo control characters, like ^Z for instance pub fn set_termios() { - if isatty(std::io::stdin().as_raw_fd()).unwrap() { - let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); - termios.local_flags &= !LocalFlags::ECHOCTL; - termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); - } + if isatty(std::io::stdin().as_raw_fd()).unwrap() { + let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); + termios.local_flags &= !LocalFlags::ECHOCTL; + termios::tcsetattr( + std::io::stdin(), + nix::sys::termios::SetArg::TCSANOW, + &termios, + ) + .unwrap(); + } } pub fn sh_quit(code: i32) -> ! { - write_jobs(|j| { - for job in j.jobs_mut().iter_mut().flatten() { - job.killpg(Signal::SIGTERM).ok(); - } - }); - if let Some(termios) = unsafe { get_saved_termios() } { - termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap(); - } - if code == 0 { - eprintln!("exit"); - } else { - eprintln!("exit {code}"); - } - exit(code); + write_jobs(|j| { + for job in j.jobs_mut().iter_mut().flatten() { + job.killpg(Signal::SIGTERM).ok(); + } + }); + if let Some(termios) = unsafe { get_saved_termios() } { + termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap(); + } + if code == 0 { + eprintln!("exit"); + } else { + eprintln!("exit {code}"); + } + exit(code); } diff --git a/src/libsh/term.rs b/src/libsh/term.rs index 2452015..e48cff7 100644 --- a/src/libsh/term.rs +++ b/src/libsh/term.rs @@ -1,11 +1,11 @@ use std::{fmt::Display, ops::BitOr}; pub trait Styled: Sized + Display { - fn styled>(self, style: S) -> String { - let styles: StyleSet = style.into(); - let reset = Style::Reset; - format!("{styles}{self}{reset}") - } + fn styled>(self, style: S) -> String { + let styles: StyleSet = style.into(); + let reset = Style::Reset; + format!("{styles}{self}{reset}") + } } impl Styled for T {} @@ -13,157 +13,157 @@ impl Styled for T {} /// Enum representing a single ANSI style #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Style { - // Undoes all styles - Reset, - // Foreground Colors - Black, - Red, - Green, - Yellow, - Blue, - Magenta, - Cyan, - White, - BrightBlack, - BrightRed, - BrightGreen, - BrightYellow, - BrightBlue, - BrightMagenta, - BrightCyan, - BrightWhite, - RGB(u8, u8, u8), // Custom foreground color + // Undoes all styles + Reset, + // Foreground Colors + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, + RGB(u8, u8, u8), // Custom foreground color - // Background Colors - BgBlack, - BgRed, - BgGreen, - BgYellow, - BgBlue, - BgMagenta, - BgCyan, - BgWhite, - BgBrightBlack, - BgBrightRed, - BgBrightGreen, - BgBrightYellow, - BgBrightBlue, - BgBrightMagenta, - BgBrightCyan, - BgBrightWhite, - BgRGB(u8, u8, u8), // Custom background color + // Background Colors + BgBlack, + BgRed, + BgGreen, + BgYellow, + BgBlue, + BgMagenta, + BgCyan, + BgWhite, + BgBrightBlack, + BgBrightRed, + BgBrightGreen, + BgBrightYellow, + BgBrightBlue, + BgBrightMagenta, + BgBrightCyan, + BgBrightWhite, + BgRGB(u8, u8, u8), // Custom background color - // Text Attributes - Bold, - Dim, - Italic, - Underline, - Strikethrough, - Reversed, + // Text Attributes + Bold, + Dim, + Italic, + Underline, + Strikethrough, + Reversed, } impl Display for Style { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Style::Reset => write!(f, "\x1b[0m"), + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Style::Reset => write!(f, "\x1b[0m"), - // Foreground colors - Style::Black => write!(f, "\x1b[30m"), - Style::Red => write!(f, "\x1b[31m"), - Style::Green => write!(f, "\x1b[32m"), - Style::Yellow => write!(f, "\x1b[33m"), - Style::Blue => write!(f, "\x1b[34m"), - Style::Magenta => write!(f, "\x1b[35m"), - Style::Cyan => write!(f, "\x1b[36m"), - Style::White => write!(f, "\x1b[37m"), - Style::BrightBlack => write!(f, "\x1b[90m"), - Style::BrightRed => write!(f, "\x1b[91m"), - Style::BrightGreen => write!(f, "\x1b[92m"), - Style::BrightYellow => write!(f, "\x1b[93m"), - Style::BrightBlue => write!(f, "\x1b[94m"), - Style::BrightMagenta => write!(f, "\x1b[95m"), - Style::BrightCyan => write!(f, "\x1b[96m"), - Style::BrightWhite => write!(f, "\x1b[97m"), - Style::RGB(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"), + // Foreground colors + Style::Black => write!(f, "\x1b[30m"), + Style::Red => write!(f, "\x1b[31m"), + Style::Green => write!(f, "\x1b[32m"), + Style::Yellow => write!(f, "\x1b[33m"), + Style::Blue => write!(f, "\x1b[34m"), + Style::Magenta => write!(f, "\x1b[35m"), + Style::Cyan => write!(f, "\x1b[36m"), + Style::White => write!(f, "\x1b[37m"), + Style::BrightBlack => write!(f, "\x1b[90m"), + Style::BrightRed => write!(f, "\x1b[91m"), + Style::BrightGreen => write!(f, "\x1b[92m"), + Style::BrightYellow => write!(f, "\x1b[93m"), + Style::BrightBlue => write!(f, "\x1b[94m"), + Style::BrightMagenta => write!(f, "\x1b[95m"), + Style::BrightCyan => write!(f, "\x1b[96m"), + Style::BrightWhite => write!(f, "\x1b[97m"), + Style::RGB(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"), - // Background colors - Style::BgBlack => write!(f, "\x1b[40m"), - Style::BgRed => write!(f, "\x1b[41m"), - Style::BgGreen => write!(f, "\x1b[42m"), - Style::BgYellow => write!(f, "\x1b[43m"), - Style::BgBlue => write!(f, "\x1b[44m"), - Style::BgMagenta => write!(f, "\x1b[45m"), - Style::BgCyan => write!(f, "\x1b[46m"), - Style::BgWhite => write!(f, "\x1b[47m"), - Style::BgBrightBlack => write!(f, "\x1b[100m"), - Style::BgBrightRed => write!(f, "\x1b[101m"), - Style::BgBrightGreen => write!(f, "\x1b[102m"), - Style::BgBrightYellow => write!(f, "\x1b[103m"), - Style::BgBrightBlue => write!(f, "\x1b[104m"), - Style::BgBrightMagenta => write!(f, "\x1b[105m"), - Style::BgBrightCyan => write!(f, "\x1b[106m"), - Style::BgBrightWhite => write!(f, "\x1b[107m"), - Style::BgRGB(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"), + // Background colors + Style::BgBlack => write!(f, "\x1b[40m"), + Style::BgRed => write!(f, "\x1b[41m"), + Style::BgGreen => write!(f, "\x1b[42m"), + Style::BgYellow => write!(f, "\x1b[43m"), + Style::BgBlue => write!(f, "\x1b[44m"), + Style::BgMagenta => write!(f, "\x1b[45m"), + Style::BgCyan => write!(f, "\x1b[46m"), + Style::BgWhite => write!(f, "\x1b[47m"), + Style::BgBrightBlack => write!(f, "\x1b[100m"), + Style::BgBrightRed => write!(f, "\x1b[101m"), + Style::BgBrightGreen => write!(f, "\x1b[102m"), + Style::BgBrightYellow => write!(f, "\x1b[103m"), + Style::BgBrightBlue => write!(f, "\x1b[104m"), + Style::BgBrightMagenta => write!(f, "\x1b[105m"), + Style::BgBrightCyan => write!(f, "\x1b[106m"), + Style::BgBrightWhite => write!(f, "\x1b[107m"), + Style::BgRGB(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"), - // Text attributes - Style::Bold => write!(f, "\x1b[1m"), - Style::Dim => write!(f, "\x1b[2m"), // New - Style::Italic => write!(f, "\x1b[3m"), - Style::Underline => write!(f, "\x1b[4m"), - Style::Strikethrough => write!(f, "\x1b[9m"), // New - Style::Reversed => write!(f, "\x1b[7m"), - } - } + // Text attributes + Style::Bold => write!(f, "\x1b[1m"), + Style::Dim => write!(f, "\x1b[2m"), // New + Style::Italic => write!(f, "\x1b[3m"), + Style::Underline => write!(f, "\x1b[4m"), + Style::Strikethrough => write!(f, "\x1b[9m"), // New + Style::Reversed => write!(f, "\x1b[7m"), + } + } } /// Struct representing a **set** of styles #[derive(Debug, Default, Clone)] pub struct StyleSet { - styles: Vec