Added rustfmt.toml, formatted codebase

This commit is contained in:
2025-08-12 13:58:25 -04:00
parent d2b3cd51e0
commit 5aead4fcdc
52 changed files with 15188 additions and 14451 deletions

7
rustfmt.toml Normal file
View File

@@ -0,0 +1,7 @@
max_width = 100
tab_spaces = 2
edition = "2021"
newline_style = "Unix"
wrap_comments = true

View File

@@ -1,9 +1,20 @@
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; use super::setup_builtin;
pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -26,23 +37,19 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
} else { } else {
for (arg, span) in argv { for (arg, span) in argv {
if arg == "command" || arg == "builtin" { if arg == "command" || arg == "builtin" {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("alias: Cannot assign alias to reserved name '{arg}'"), format!("alias: Cannot assign alias to reserved name '{arg}'"),
span span,
) ));
)
} }
let Some((name, body)) = arg.split_once('=') else { let Some((name, body)) = arg.split_once('=') else {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"alias: Expected an assignment in alias args", "alias: Expected an assignment in alias args",
span span,
) ));
)
}; };
write_logic(|l| l.insert_alias(name, body)); write_logic(|l| l.insert_alias(name, body));
} }
@@ -53,13 +60,16 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
} }
pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() 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() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
let mut alias_output = read_logic(|l| { let mut alias_output = read_logic(|l| {
@@ -78,13 +88,11 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul
for (arg, span) in argv { for (arg, span) in argv {
flog!(DEBUG, arg); flog!(DEBUG, arg);
if read_logic(|l| l.get_alias(&arg)).is_none() { if read_logic(|l| l.get_alias(&arg)).is_none() {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("unalias: alias '{arg}' not found"), format!("unalias: alias '{arg}' not found"),
span span,
) ));
)
}; };
write_logic(|l| l.remove_alias(&arg)) write_logic(|l| l.remove_alias(&arg))
} }

View File

@@ -1,10 +1,20 @@
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; use super::setup_builtin;
pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span(); let span = node.get_span();
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -17,23 +27,19 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
}; };
if !new_dir.exists() { if !new_dir.exists() {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("cd: No such file or directory '{}'", new_dir.display()), format!("cd: No such file or directory '{}'", new_dir.display()),
span, span,
) ));
)
} }
if !new_dir.is_dir() { if !new_dir.is_dir() {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("cd: Not a directory '{}'", new_dir.display()), format!("cd: Not a directory '{}'", new_dir.display()),
span, span,
) ));
)
} }
env::set_current_dir(new_dir).unwrap(); env::set_current_dir(new_dir).unwrap();

View File

@@ -1,13 +1,25 @@
use std::sync::LazyLock; 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<OptSet> = LazyLock::new(|| {[ pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
[
Opt::Short('n'), Opt::Short('n'),
Opt::Short('E'), Opt::Short('E'),
Opt::Short('e'), Opt::Short('e'),
Opt::Short('r'), Opt::Short('r'),
].into()}); ]
.into()
});
bitflags! { bitflags! {
pub struct EchoFlags: u32 { pub struct EchoFlags: u32 {
@@ -19,7 +31,11 @@ bitflags! {
pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
assert!(!argv.is_empty()); assert!(!argv.is_empty());
@@ -33,7 +49,8 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
borrow_fd(STDOUT_FILENO) borrow_fd(STDOUT_FILENO)
}; };
let mut echo_output = argv.into_iter() let mut echo_output = argv
.into_iter()
.map(|a| a.0) // Extract the String from the tuple of (String,Span) .map(|a| a.0) // Extract the String from the tuple of (String,Span)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "); .join(" ");
@@ -54,22 +71,18 @@ pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
while let Some(opt) = opts.pop() { while let Some(opt) = opts.pop() {
if !ECHO_OPTS.contains(&opt) { if !ECHO_OPTS.contains(&opt) {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("echo: Unexpected flag '{opt}'"), format!("echo: Unexpected flag '{opt}'"),
) ));
)
} }
let Opt::Short(opt) = opt else { let Opt::Short(opt) = opt else { unreachable!() };
unreachable!()
};
match opt { match opt {
'n' => flags |= EchoFlags::NO_NEWLINE, 'n' => flags |= EchoFlags::NO_NEWLINE,
'r' => flags |= EchoFlags::USE_STDERR, 'r' => flags |= EchoFlags::USE_STDERR,
'e' => flags |= EchoFlags::USE_ESCAPE, 'e' => flags |= EchoFlags::USE_ESCAPE,
_ => unreachable!() _ => unreachable!(),
} }
} }

View File

@@ -1,9 +1,20 @@
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; use super::setup_builtin;
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -23,9 +34,11 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
} else { } else {
for (arg, _) in argv { for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') { if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, true)); // Export an assignment like 'foo=bar' write_vars(|v| v.set_var(var, val, true)); // Export an assignment like
// 'foo=bar'
} else { } else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any
} }
} }
} }

View File

@@ -1,8 +1,16 @@
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<()> { pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
use ShErrKind::*; use ShErrKind::*;
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
let mut code = 0; let mut code = 0;
@@ -11,15 +19,14 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
let cmd = argv.remove(0).0; let cmd = argv.remove(0).0;
if !argv.is_empty() { if !argv.is_empty() {
let (arg,span) = argv let (arg, span) = argv.into_iter().next().unwrap();
.into_iter()
.next()
.unwrap();
let Ok(status) = arg.parse::<i32>() else { let Ok(status) = arg.parse::<i32>() else {
return Err( return Err(ShErr::full(
ShErr::full(ShErrKind::SyntaxErr, format!("{cmd}: Expected a number"), span) ShErrKind::SyntaxErr,
) format!("{cmd}: Expected a number"),
span,
));
}; };
code = status; code = status;
@@ -32,7 +39,7 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
LoopBreak(_) => LoopBreak(code), LoopBreak(_) => LoopBreak(code),
FuncReturn(_) => FuncReturn(code), FuncReturn(_) => FuncReturn(code),
CleanExit(_) => CleanExit(code), CleanExit(_) => CleanExit(code),
_ => unreachable!() _ => unreachable!(),
}; };
Err(ShErr::simple(kind, "")) Err(ShErr::simple(kind, ""))

View File

@@ -1,19 +1,30 @@
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; use super::setup_builtin;
pub enum JobBehavior { pub enum JobBehavior {
Foregound, Foregound,
Background Background,
} }
pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> { pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let cmd = match behavior { let cmd = match behavior {
JobBehavior::Foregound => "fg", JobBehavior::Foregound => "fg",
JobBehavior::Background => "bg" JobBehavior::Background => "bg",
}; };
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -21,30 +32,22 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) { if read_jobs(|j| j.get_fg().is_some()) {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("Somehow called '{}' with an existing foreground job", cmd), format!("Somehow called '{}' with an existing foreground job", cmd),
blame blame,
) ));
)
} }
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id id
} else { } else {
return Err( return Err(ShErr::full(ShErrKind::ExecFail, "No jobs found", blame));
ShErr::full(
ShErrKind::ExecFail,
"No jobs found",
blame
)
)
}; };
let tabid = match argv.next() { let tabid = match argv.next() {
Some((arg, blame)) => parse_job_id(&arg, blame)?, Some((arg, blame)) => parse_job_id(&arg, blame)?,
None => curr_job_id None => curr_job_id,
}; };
let mut job = write_jobs(|j| { let mut job = write_jobs(|j| {
@@ -53,13 +56,11 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR
if query_result.is_some() { if query_result.is_some() {
Ok(j.remove_job(id.clone()).unwrap()) Ok(j.remove_job(id.clone()).unwrap())
} else { } else {
Err( Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("Job id `{}' not found", tabid), format!("Job id `{}' not found", tabid),
blame blame,
) ))
)
} }
})?; })?;
@@ -71,7 +72,10 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR
} }
JobBehavior::Background => { JobBehavior::Background => {
let job_order = read_jobs(|j| j.order().to_vec()); let job_order = read_jobs(|j| j.order().to_vec());
write(borrow_fd(1), job.display(&job_order, JobCmdFlags::PIDS).as_bytes())?; write(
borrow_fd(1),
job.display(&job_order, JobCmdFlags::PIDS).as_bytes(),
)?;
write_jobs(|j| j.insert_job(job, true))?; write_jobs(|j| j.insert_job(job, true))?;
} }
} }
@@ -91,20 +95,18 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
}); });
match result { match result {
Some(id) => Ok(id), Some(id) => Ok(id),
None => Err( None => Err(ShErr::full(
ShErr::full(
ShErrKind::InternalErr, ShErrKind::InternalErr,
"Found a job but no table id in parse_job_id()", "Found a job but no table id in parse_job_id()",
blame blame,
) )),
)
} }
} }
} else if arg.chars().all(|ch| ch.is_ascii_digit()) { } else if arg.chars().all(|ch| ch.is_ascii_digit()) {
let result = write_jobs(|j| { let result = write_jobs(|j| {
let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap()))); let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap())));
if let Some(job) = pgid_query_result { if let Some(job) = pgid_query_result {
return Some(job.tabid().unwrap()) return Some(job.tabid().unwrap());
} }
if arg.parse::<i32>().unwrap() > 0 { if arg.parse::<i32>().unwrap() > 0 {
@@ -117,27 +119,27 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
match result { match result {
Some(id) => Ok(id), Some(id) => Ok(id),
None => Err( None => Err(ShErr::full(
ShErr::full(
ShErrKind::InternalErr, ShErrKind::InternalErr,
"Found a job but no table id in parse_job_id()", "Found a job but no table id in parse_job_id()",
blame blame,
) )),
)
} }
} else { } else {
Err( Err(ShErr::full(
ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("Invalid fd arg: {}", arg), format!("Invalid fd arg: {}", arg),
blame blame,
) ))
)
} }
} }
pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -147,13 +149,11 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
for (arg, span) in argv { for (arg, span) in argv {
let mut chars = arg.chars().peekable(); let mut chars = arg.chars().peekable();
if chars.peek().is_none_or(|ch| *ch != '-') { if chars.peek().is_none_or(|ch| *ch != '-') {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"Invalid flag in jobs call", "Invalid flag in jobs call",
span span,
) ));
)
} }
chars.next(); chars.next();
for ch in chars { for ch in chars {
@@ -163,14 +163,13 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
'n' => JobCmdFlags::NEW_ONLY, 'n' => JobCmdFlags::NEW_ONLY,
'r' => JobCmdFlags::RUNNING, 'r' => JobCmdFlags::RUNNING,
's' => JobCmdFlags::STOPPED, 's' => JobCmdFlags::STOPPED,
_ => return Err( _ => {
ShErr::full( return Err(ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"Invalid flag in jobs call", "Invalid flag in jobs call",
span span,
) ))
) }
}; };
flags |= flag flags |= flag
} }

View File

@@ -1,50 +1,45 @@
use nix::unistd::Pid; 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 alias;
pub mod cd;
pub mod echo;
pub mod export;
pub mod flowctl; pub mod flowctl;
pub mod zoltraak; pub mod jobctl;
pub mod pwd;
pub mod shift;
pub mod shopt; pub mod shopt;
pub mod test; // [[ ]] thing pub mod source;
pub mod test;
pub mod zoltraak; // [[ ]] thing
pub const BUILTINS: [&str; 19] = [ pub const BUILTINS: [&str; 19] = [
"echo", "echo", "cd", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
"cd", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command",
"export",
"pwd",
"source",
"shift",
"jobs",
"fg",
"bg",
"alias",
"unalias",
"return",
"break",
"continue",
"exit",
"zoltraak",
"shopt",
"builtin",
"command",
]; ];
/// Sets up a 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 /// # Parameters
/// * argv - The vector of raw argument tokens /// * argv - The vector of raw argument tokens
/// * job - A mutable reference to a `JobBldr` /// * 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 /// # Behavior
/// * Cleans, expands, and word splits the arg vector /// * Cleans, expands, and word splits the arg vector
@@ -56,8 +51,10 @@ pub const BUILTINS: [&str;19] = [
/// * The popped `IoFrame`, if any /// * The popped `IoFrame`, if any
/// ///
/// # Notes /// # Notes
/// * If redirections are given to this function, the caller must call `IoFrame.restore()` on the returned `IoFrame` /// * If redirections are given to this function, the caller must call
/// * If redirections are given, the second field of the resulting tuple will *always* be `Some()` /// `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` /// * If no redirections are given, the second field will *always* be `None`
type SetupReturns = ShResult<(Vec<(String, Span)>, Option<IoFrame>)>; type SetupReturns = ShResult<(Vec<(String, Span)>, Option<IoFrame>)>;
pub fn setup_builtin( pub fn setup_builtin(
@@ -86,6 +83,7 @@ pub fn setup_builtin(
None None
}; };
// We return the io_frame because the caller needs to also call io_frame.restore() // We return the io_frame because the caller needs to also call
// io_frame.restore()
Ok((argv, io_frame)) Ok((argv, io_frame))
} }

View File

@@ -1,9 +1,20 @@
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; use super::setup_builtin;
pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };

View File

@@ -1,9 +1,18 @@
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; use super::setup_builtin;
pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -12,13 +21,11 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
if let Some((arg, span)) = argv.next() { if let Some((arg, span)) = argv.next() {
let Ok(count) = arg.parse::<usize>() else { let Ok(count) = arg.parse::<usize>() else {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"Expected a number in shift args", "Expected a number in shift args",
span span,
) ));
)
}; };
for _ in 0..count { for _ in 0..count {
write_vars(|v| v.fpop_arg()); write_vars(|v| v.fpop_arg());

View File

@@ -1,9 +1,20 @@
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; use super::setup_builtin;
pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -13,7 +24,7 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
io_frame.redirect()?; io_frame.redirect()?;
for (arg, span) in argv { for (arg, span) in argv {
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
continue continue;
}; };
let output_channel = borrow_fd(STDOUT_FILENO); let output_channel = borrow_fd(STDOUT_FILENO);
@@ -21,7 +32,7 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
if let Err(e) = write(output_channel, output.as_bytes()) { if let Err(e) = write(output_channel, output.as_bytes()) {
io_frame.restore()?; io_frame.restore()?;
return Err(e.into()) return Err(e.into());
} }
} }
io_frame.restore()?; io_frame.restore()?;

View File

@@ -1,9 +1,19 @@
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; use super::setup_builtin;
pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
@@ -12,22 +22,18 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
for (arg, span) in argv { for (arg, span) in argv {
let path = PathBuf::from(arg); let path = PathBuf::from(arg);
if !path.exists() { if !path.exists() {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("source: File '{}' not found", path.display()), format!("source: File '{}' not found", path.display()),
span span,
) ));
);
} }
if !path.is_file() { if !path.is_file() {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("source: Given path '{}' is not a file", path.display()), format!("source: Given path '{}' is not a file", path.display()),
span span,
) ));
);
} }
source_file(path)?; source_file(path)?;
} }

View File

@@ -1,9 +1,16 @@
use std::{fs::metadata, path::PathBuf, str::FromStr}; 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 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)] #[derive(Debug, Clone)]
pub enum UnaryOp { pub enum UnaryOp {
@@ -55,7 +62,11 @@ impl FromStr for UnaryOp {
"-t" => Ok(Self::Terminal), "-t" => Ok(Self::Terminal),
"-n" => Ok(Self::NonNull), "-n" => Ok(Self::NonNull),
"-z" => Ok(Self::Null), "-z" => Ok(Self::Null),
_ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] }) _ => Err(ShErr::Simple {
kind: ShErrKind::SyntaxErr,
msg: "Invalid test operator".into(),
notes: vec![],
}),
} }
} }
} }
@@ -87,16 +98,19 @@ impl FromStr for TestOp {
"-lt" => Ok(Self::IntLt), "-lt" => Ok(Self::IntLt),
"-ge" => Ok(Self::IntGe), "-ge" => Ok(Self::IntGe),
"-le" => Ok(Self::IntLe), "-le" => Ok(Self::IntLe),
_ if TEST_UNARY_OPS.contains(&s) => { _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
Ok(Self::Unary(s.parse::<UnaryOp>()?)) _ => Err(ShErr::Simple {
} kind: ShErrKind::SyntaxErr,
_ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] }) msg: "Invalid test operator".into(),
notes: vec![],
}),
} }
} }
} }
fn replace_posix_classes(pat: &str) -> String { fn replace_posix_classes(pat: &str) -> String {
pat.replace("[[:alnum:]]", r"[A-Za-z0-9]") pat
.replace("[[:alnum:]]", r"[A-Za-z0-9]")
.replace("[[:alpha:]]", r"[A-Za-z]") .replace("[[:alpha:]]", r"[A-Za-z]")
.replace("[[:blank:]]", r"[ \t]") .replace("[[:blank:]]", r"[ \t]")
.replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]") .replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]")
@@ -119,13 +133,20 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
for case in cases { for case in cases {
let result = match case { let result = match case {
TestCase::Unary { operator, operand, conjunct } => { TestCase::Unary {
operator,
operand,
conjunct,
} => {
let operand = operand.expand()?.get_words().join(" "); let operand = operand.expand()?.get_words().join(" ");
conjunct_op = conjunct; conjunct_op = conjunct;
let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else {
return Err( return Err(ShErr::Full {
ShErr::Full { kind: ShErrKind::SyntaxErr, msg: "Invalid unary operator".into(), notes: vec![], span: err_span } kind: ShErrKind::SyntaxErr,
) msg: "Invalid unary operator".into(),
notes: vec![],
span: err_span,
});
}; };
match op { match op {
UnaryOp::Exists => { UnaryOp::Exists => {
@@ -135,9 +156,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
UnaryOp::Directory => { UnaryOp::Directory => {
let path = PathBuf::from(operand.as_str()); let path = PathBuf::from(operand.as_str());
if path.exists() { if path.exists() {
path.metadata() path.metadata().unwrap().is_dir()
.unwrap()
.is_dir()
} else { } else {
false false
} }
@@ -145,9 +164,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
UnaryOp::File => { UnaryOp::File => {
let path = PathBuf::from(operand.as_str()); let path = PathBuf::from(operand.as_str());
if path.exists() { if path.exists() {
path.metadata() path.metadata().unwrap().is_file()
.unwrap()
.is_file()
} else { } else {
false false
} }
@@ -155,10 +172,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
UnaryOp::Symlink => { UnaryOp::Symlink => {
let path = PathBuf::from(operand.as_str()); let path = PathBuf::from(operand.as_str());
if path.exists() { if path.exists() {
path.metadata() path.metadata().unwrap().file_type().is_symlink()
.unwrap()
.file_type()
.is_symlink()
} else { } else {
false false
} }
@@ -166,88 +180,69 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(), 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::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::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(),
UnaryOp::NonEmpty => { UnaryOp::NonEmpty => match metadata(operand.as_str()) {
match metadata(operand.as_str()) {
Ok(meta) => meta.len() > 0, Ok(meta) => meta.len() > 0,
Err(_) => false Err(_) => false,
} },
} UnaryOp::NamedPipe => match stat::stat(operand.as_str()) {
UnaryOp::NamedPipe => {
match stat::stat(operand.as_str()) {
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO), Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO),
Err(_) => false, Err(_) => false,
} },
} UnaryOp::Socket => match stat::stat(operand.as_str()) {
UnaryOp::Socket => {
match stat::stat(operand.as_str()) {
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK), Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK),
Err(_) => false, Err(_) => false,
} },
} UnaryOp::BlockSpecial => match stat::stat(operand.as_str()) {
UnaryOp::BlockSpecial => {
match stat::stat(operand.as_str()) {
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK), Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK),
Err(_) => false, Err(_) => false,
} },
} UnaryOp::CharSpecial => match stat::stat(operand.as_str()) {
UnaryOp::CharSpecial => {
match stat::stat(operand.as_str()) {
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR), Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR),
Err(_) => false, Err(_) => false,
} },
} UnaryOp::Sticky => match stat::stat(operand.as_str()) {
UnaryOp::Sticky => {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0, Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0,
Err(_) => false, Err(_) => false,
} },
} UnaryOp::UIDOwner => match stat::stat(operand.as_str()) {
UnaryOp::UIDOwner => {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(), Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(),
Err(_) => false, Err(_) => false,
} },
}
UnaryOp::GIDOwner => { UnaryOp::GIDOwner => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(), Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(),
Err(_) => false, Err(_) => false,
} },
}
UnaryOp::ModifiedSinceStatusChange => { UnaryOp::ModifiedSinceStatusChange => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_mtime > stat.st_ctime, Ok(stat) => stat.st_mtime > stat.st_ctime,
Err(_) => false, Err(_) => false,
} },
}
UnaryOp::SetUID => { UnaryOp::SetUID => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0, Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0,
Err(_) => false, Err(_) => false,
} },
}
UnaryOp::SetGID => { UnaryOp::SetGID => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0, Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0,
Err(_) => false, Err(_) => false,
} },
}
UnaryOp::Terminal => { UnaryOp::Terminal => match operand.as_str().parse::<nix::libc::c_int>() {
match operand.as_str().parse::<nix::libc::c_int>() {
Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 }, Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 },
Err(_) => false, Err(_) => false,
} },
}
UnaryOp::NonNull => !operand.is_empty(), UnaryOp::NonNull => !operand.is_empty(),
UnaryOp::Null => operand.is_empty(), UnaryOp::Null => operand.is_empty(),
} }
} }
TestCase::Binary { lhs, operator, rhs, conjunct } => { TestCase::Binary {
lhs,
operator,
rhs,
conjunct,
} => {
let lhs = lhs.expand()?.get_words().join(" "); let lhs = lhs.expand()?.get_words().join(" ");
let rhs = rhs.expand()?.get_words().join(" "); let rhs = rhs.expand()?.get_words().join(" ");
conjunct_op = conjunct; conjunct_op = conjunct;
@@ -257,34 +252,32 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
flog!(DEBUG, test_op); flog!(DEBUG, test_op);
match test_op { match test_op {
TestOp::Unary(_) => { TestOp::Unary(_) => {
return Err( return Err(ShErr::Full {
ShErr::Full {
kind: ShErrKind::SyntaxErr, kind: ShErrKind::SyntaxErr,
msg: "Expected a binary operator in this test call; found a unary operator".into(), msg: "Expected a binary operator in this test call; found a unary operator".into(),
notes: vec![], notes: vec![],
span: err_span span: err_span,
} })
)
} }
TestOp::StringEq => rhs.trim() == lhs.trim(), TestOp::StringEq => rhs.trim() == lhs.trim(),
TestOp::StringNeq => rhs.trim() != lhs.trim(), TestOp::StringNeq => rhs.trim() != lhs.trim(),
TestOp::IntNeq | TestOp::IntNeq
TestOp::IntGt | | TestOp::IntGt
TestOp::IntLt | | TestOp::IntLt
TestOp::IntGe | | TestOp::IntGe
TestOp::IntLe | | TestOp::IntLe
TestOp::IntEq => { | TestOp::IntEq => {
let err = ShErr::Full { let err = ShErr::Full {
kind: ShErrKind::SyntaxErr, kind: ShErrKind::SyntaxErr,
msg: format!("Expected an integer with '{}' operator", operator.as_str()), msg: format!("Expected an integer with '{}' operator", operator.as_str()),
notes: vec![], notes: vec![],
span: err_span.clone() span: err_span.clone(),
}; };
let Ok(lhs) = lhs.trim().parse::<i32>() else { let Ok(lhs) = lhs.trim().parse::<i32>() else {
return Err(err) return Err(err);
}; };
let Ok(rhs) = rhs.trim().parse::<i32>() else { let Ok(rhs) = rhs.trim().parse::<i32>() else {
return Err(err) return Err(err);
}; };
match test_op { match test_op {
TestOp::IntNeq => lhs != rhs, TestOp::IntNeq => lhs != rhs,
@@ -293,7 +286,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
TestOp::IntGe => lhs >= rhs, TestOp::IntGe => lhs >= rhs,
TestOp::IntLe => lhs <= rhs, TestOp::IntLe => lhs <= rhs,
TestOp::IntEq => lhs == rhs, TestOp::IntEq => lhs == rhs,
_ => unreachable!() _ => unreachable!(),
} }
} }
TestOp::RegexMatch => { TestOp::RegexMatch => {
@@ -311,11 +304,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
match op { match op {
ConjunctOp::And if !last_result => { ConjunctOp::And if !last_result => {
last_result = result; last_result = result;
break break;
} }
ConjunctOp::Or if last_result => { ConjunctOp::Or if last_result => {
last_result = result; last_result = result;
break break;
} }
_ => {} _ => {}
} }

View File

@@ -1,6 +1,13 @@
use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock}; 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; use super::setup_builtin;
@@ -11,8 +18,9 @@ pub static ZOLTRAAK_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
Opt::Long("no-preserve-root".into()), Opt::Long("no-preserve-root".into()),
Opt::Short('r'), Opt::Short('r'),
Opt::Short('f'), Opt::Short('f'),
Opt::Short('v') Opt::Short('v'),
].into() ]
.into()
}); });
bitflags! { bitflags! {
@@ -30,9 +38,15 @@ bitflags! {
/// Annihilate a file /// Annihilate a file
/// ///
/// This command works similarly to 'rm', but behaves more destructively. /// 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<()> { pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!() unreachable!()
}; };
let mut flags = ZoltFlags::empty(); let mut flags = ZoltFlags::empty();
@@ -41,30 +55,24 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
for opt in opts { for opt in opts {
if !ZOLTRAAK_OPTS.contains(&opt) { if !ZOLTRAAK_OPTS.contains(&opt) {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{opt}'") format!("zoltraak: unrecognized option '{opt}'"),
) ));
)
} }
match opt { match opt {
Opt::Long(flag) => { Opt::Long(flag) => match flag.as_str() {
match flag.as_str() {
"no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT, "no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT,
"confirm" => flags |= ZoltFlags::CONFIRM, "confirm" => flags |= ZoltFlags::CONFIRM,
"dry-run" => flags |= ZoltFlags::DRY, "dry-run" => flags |= ZoltFlags::DRY,
_ => unreachable!() _ => unreachable!(),
} },
} Opt::Short(flag) => match flag {
Opt::Short(flag) => {
match flag {
'r' => flags |= ZoltFlags::RECURSIVE, 'r' => flags |= ZoltFlags::RECURSIVE,
'f' => flags |= ZoltFlags::FORCE, 'f' => flags |= ZoltFlags::FORCE,
'v' => flags |= ZoltFlags::VERBOSE, 'v' => flags |= ZoltFlags::VERBOSE,
_ => unreachable!() _ => unreachable!(),
} },
}
} }
} }
@@ -78,15 +86,13 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"zoltraak: Attempted to destroy root directory '/'" "zoltraak: Attempted to destroy root directory '/'",
) )
.with_note( .with_note(
Note::new("If you really want to do this, you can use the --no-preserve-root flag") Note::new("If you really want to do this, you can use the --no-preserve-root flag")
.with_sub_notes(vec![ .with_sub_notes(vec!["Example: 'zoltraak --no-preserve-root /'"]),
"Example: 'zoltraak --no-preserve-root /'" ),
]) );
)
)
} }
if let Err(e) = annihilate(&arg, flags).blame(span) { if let Err(e) = annihilate(&arg, flags).blame(span) {
io_frame.restore()?; io_frame.restore()?;
@@ -107,12 +113,10 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
const BLOCK_SIZE: u64 = 4096; const BLOCK_SIZE: u64 = 4096;
if !path_buf.exists() { if !path_buf.exists() {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("zoltraak: File '{path}' not found") format!("zoltraak: File '{path}' not found"),
) ));
)
} }
if path_buf.is_file() { if path_buf.is_file() {
@@ -144,7 +148,6 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
let stderr = borrow_fd(STDERR_FILENO); let stderr = borrow_fd(STDERR_FILENO);
write(stderr, format!("shredded file '{path}'\n").as_bytes())?; write(stderr, format!("shredded file '{path}'\n").as_bytes())?;
} }
} else if path_buf.is_dir() { } else if path_buf.is_dir() {
if is_recursive { if is_recursive {
annihilate_recursive(path, flags)?; // scary annihilate_recursive(path, flags)?; // scary
@@ -152,15 +155,13 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("zoltraak: '{path}' is a directory") format!("zoltraak: '{path}' is a directory"),
) )
.with_note( .with_note(
Note::new("Use the '-r' flag to recursively shred directories") Note::new("Use the '-r' flag to recursively shred directories")
.with_sub_notes(vec![ .with_sub_notes(vec!["Example: 'zoltraak -r directory'"]),
"Example: 'zoltraak -r directory'" ),
]) );
)
)
} }
} }

View File

@@ -5,23 +5,15 @@ use std::str::{Chars, FromStr};
use glob::Pattern; use glob::Pattern;
use regex::Regex; use regex::Regex;
use crate::state::{read_jobs, read_vars, write_jobs, write_meta, write_vars, LogTab}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::prelude::*;
use crate::parse::{Redir, RedirType};
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule}; 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] = [ const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
'@',
'*',
'#',
'$',
'?',
'!',
'0'
];
/// Variable substitution marker /// Variable substitution marker
pub const VAR_SUB: char = '\u{fdd0}'; pub const VAR_SUB: char = '\u{fdd0}';
@@ -50,13 +42,13 @@ impl Tk {
let span = self.span.clone(); let span = self.span.clone();
let exp = Expander::new(self).expand()?; let exp = Expander::new(self).expand()?;
let class = TkRule::Expanded { exp }; let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags, }) Ok(Self { class, span, flags })
} }
/// Perform word splitting /// Perform word splitting
pub fn get_words(&self) -> Vec<String> { pub fn get_words(&self) -> Vec<String> {
match &self.class { match &self.class {
TkRule::Expanded { exp } => exp.clone(), TkRule::Expanded { exp } => exp.clone(),
_ => vec![self.to_string()] _ => vec![self.to_string()],
} }
} }
} }
@@ -87,20 +79,18 @@ impl Expander {
'outer: while let Some(ch) = chars.next() { 'outer: while let Some(ch) = chars.next() {
match ch { match ch {
DUB_QUOTE | DUB_QUOTE | SNG_QUOTE | SUBSH => {
SNG_QUOTE |
SUBSH => {
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
_ if q_ch == ch => continue 'outer, // Isn't rust cool _ if q_ch == ch => continue 'outer, // Isn't rust cool
_ => cur_word.push(q_ch) _ => cur_word.push(q_ch),
} }
} }
} }
_ if is_field_sep(ch) => { _ if is_field_sep(ch) => {
words.push(mem::take(&mut cur_word)); words.push(mem::take(&mut cur_word));
} }
_ => cur_word.push(ch) _ => cur_word.push(ch),
} }
} }
if !cur_word.is_empty() { if !cur_word.is_empty() {
@@ -124,7 +114,7 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
PROC_SUB_OUT => break, PROC_SUB_OUT => break,
_ => inner.push(ch) _ => inner.push(ch),
} }
} }
let fd_path = expand_proc_sub(&inner, false)?; let fd_path = expand_proc_sub(&inner, false)?;
@@ -135,7 +125,7 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
PROC_SUB_IN => break, PROC_SUB_IN => break,
_ => inner.push(ch) _ => inner.push(ch),
} }
} }
let fd_path = expand_proc_sub(&inner, true)?; let fd_path = expand_proc_sub(&inner, true)?;
@@ -146,7 +136,7 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let expanded = expand_var(chars)?; let expanded = expand_var(chars)?;
result.push_str(&expanded); result.push_str(&expanded);
} }
_ => result.push(ch) _ => result.push(ch),
} }
} }
Ok(result) Ok(result)
@@ -161,7 +151,9 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
chars.next(); // now safe to consume chars.next(); // now safe to consume
let mut subsh_body = String::new(); let mut subsh_body = String::new();
while let Some(c) = chars.next() { while let Some(c) = chars.next() {
if c == SUBSH { break } if c == SUBSH {
break;
}
subsh_body.push(c); subsh_body.push(c);
} }
let expanded = expand_cmd_sub(&subsh_body)?; let expanded = expand_cmd_sub(&subsh_body)?;
@@ -185,7 +177,7 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let parameter = format!("{ch}"); let parameter = format!("{ch}");
let val = read_vars(|v| v.get_var(&parameter)); let val = read_vars(|v| v.get_var(&parameter));
flog!(DEBUG, val); flog!(DEBUG, val);
return Ok(val) return Ok(val);
} }
ch if is_hard_sep(ch) || !(ch.is_alphanumeric() || ch == '_' || ch == '-') => { ch if is_hard_sep(ch) || !(ch.is_alphanumeric() || ch == '_' || ch == '-') => {
let val = read_vars(|v| v.get_var(&var_name)); let val = read_vars(|v| v.get_var(&var_name));
@@ -212,10 +204,11 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
pub fn expand_glob(raw: &str) -> ShResult<String> { pub fn expand_glob(raw: &str) -> ShResult<String> {
let mut words = vec![]; let mut words = vec![];
for entry in glob::glob(raw) for entry in
.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))? { 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"))?; let entry =
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
words.push(entry.to_str().unwrap().to_string()) words.push(entry.to_str().unwrap().to_string())
} }
@@ -231,7 +224,7 @@ enum ArithTk {
Num(f64), Num(f64),
Op(ArithOp), Op(ArithOp),
LParen, LParen,
RParen RParen,
} }
impl ArithTk { impl ArithTk {
@@ -241,7 +234,9 @@ impl ArithTk {
while let Some(&ch) = chars.peek() { while let Some(&ch) = chars.peek() {
match ch { match ch {
' ' | '\t' => { chars.next(); } ' ' | '\t' => {
chars.next();
}
'0'..='9' | '.' => { '0'..='9' | '.' => {
let mut num = String::new(); let mut num = String::new();
while let Some(&digit) = chars.peek() { while let Some(&digit) = chars.peek() {
@@ -263,9 +258,21 @@ impl ArithTk {
tokens.push(Self::Op(buf.parse::<ArithOp>().unwrap())); tokens.push(Self::Op(buf.parse::<ArithOp>().unwrap()));
chars.next(); chars.next();
} }
'(' => { tokens.push(Self::LParen); chars.next(); } '(' => {
')' => { tokens.push(Self::RParen); chars.next(); } tokens.push(Self::LParen);
_ => return Err(ShErr::Simple { kind: ShErrKind::ParseErr, msg: "Invalid character in arithmetic substitution".into(), notes: vec![] }) chars.next();
}
')' => {
tokens.push(Self::RParen);
chars.next();
}
_ => {
return Err(ShErr::Simple {
kind: ShErrKind::ParseErr,
msg: "Invalid character in arithmetic substitution".into(),
notes: vec![],
})
}
} }
} }
@@ -278,11 +285,8 @@ impl ArithTk {
fn precedence(op: &ArithOp) -> usize { fn precedence(op: &ArithOp) -> usize {
match op { match op {
ArithOp::Add | ArithOp::Add | ArithOp::Sub => 1,
ArithOp::Sub => 1, ArithOp::Mul | ArithOp::Div | ArithOp::Mod => 2,
ArithOp::Mul |
ArithOp::Div |
ArithOp::Mod => 2,
} }
} }
@@ -344,11 +348,13 @@ impl ArithTk {
}; };
stack.push(result); stack.push(result);
} }
_ => return Err(ShErr::Simple { _ => {
return Err(ShErr::Simple {
kind: ShErrKind::ParseErr, kind: ShErrKind::ParseErr,
msg: "Unexpected token during evaluation".into(), msg: "Unexpected token during evaluation".into(),
notes: vec![], notes: vec![],
}), })
}
} }
} }
@@ -382,17 +388,17 @@ impl FromStr for ArithOp {
'*' => Ok(Self::Mul), '*' => Ok(Self::Mul),
'/' => Ok(Self::Div), '/' => Ok(Self::Div),
'%' => Ok(Self::Mod), '%' => Ok(Self::Mod),
_ => Err(ShErr::Simple { kind: ShErrKind::ParseErr, msg: "Invalid arithmetic operator".into(), notes: vec![] }) _ => Err(ShErr::Simple {
kind: ShErrKind::ParseErr,
msg: "Invalid arithmetic operator".into(),
notes: vec![],
}),
} }
} }
} }
pub fn expand_arithmetic(raw: &str) -> ShResult<String> { pub fn expand_arithmetic(raw: &str) -> ShResult<String> {
let body = raw let body = raw.strip_prefix('(').unwrap().strip_suffix(')').unwrap(); // Unwraps are safe here, we already checked for the parens
.strip_prefix('(')
.unwrap()
.strip_suffix(')')
.unwrap(); // Unwraps are safe here, we already checked for the parens
let unescaped = unescape_math(body); let unescaped = unescape_math(body);
let expanded = expand_raw(&mut unescaped.chars().peekable())?; let expanded = expand_raw(&mut unescaped.chars().peekable())?;
let tokens = ArithTk::tokenize(&expanded)?; let tokens = ArithTk::tokenize(&expanded)?;
@@ -409,8 +415,18 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
let wpipe_raw = wpipe.src_fd(); let wpipe_raw = wpipe.src_fd();
let (proc_fd, register_fd, redir_type, path) = match is_input { let (proc_fd, register_fd, redir_type, path) = match is_input {
false => (wpipe, rpipe, RedirType::Output, format!("/proc/self/fd/{}", rpipe_raw)), false => (
true => (rpipe, wpipe, RedirType::Input, format!("/proc/self/fd/{}", wpipe_raw)), wpipe,
rpipe,
RedirType::Output,
format!("/proc/self/fd/{}", rpipe_raw),
),
true => (
rpipe,
wpipe,
RedirType::Input,
format!("/proc/self/fd/{}", wpipe_raw),
),
}; };
match unsafe { fork()? } { match unsafe { fork()? } {
@@ -442,7 +458,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
flog!(DEBUG, raw); flog!(DEBUG, raw);
if raw.starts_with('(') && raw.ends_with(')') { if raw.starts_with('(') && raw.ends_with(')') {
if let Ok(output) = expand_arithmetic(raw) { if let Ok(output) = expand_arithmetic(raw) {
return Ok(output) // It's actually an arithmetic sub return Ok(output); // It's actually an arithmetic sub
} }
} }
let (rpipe, wpipe) = IoMode::get_pipes(); let (rpipe, wpipe) = IoMode::get_pipes();
@@ -470,26 +486,26 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
flog!(DEBUG, "done"); flog!(DEBUG, "done");
Ok(io_buf.as_str()?.trim().to_string()) Ok(io_buf.as_str()?.trim().to_string())
} }
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")) _ => 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 { pub fn unescape_str(raw: &str) -> String {
let mut chars = raw.chars().peekable(); let mut chars = raw.chars().peekable();
let mut result = String::new(); let mut result = String::new();
let mut first_char = true; let mut first_char = true;
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'~' if first_char => { '~' if first_char => result.push(TILDE_SUB),
result.push(TILDE_SUB)
}
'\\' => { '\\' => {
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(next_ch) result.push(next_ch)
@@ -515,12 +531,12 @@ pub fn unescape_str(raw: &str) -> String {
paren_count -= 1; paren_count -= 1;
if paren_count == 0 { if paren_count == 0 {
result.push(SUBSH); result.push(SUBSH);
break break;
} else { } else {
result.push(subsh_ch) result.push(subsh_ch)
} }
} }
_ => result.push(subsh_ch) _ => result.push(subsh_ch),
} }
} }
} }
@@ -556,7 +572,7 @@ pub fn unescape_str(raw: &str) -> String {
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
result.push(SUBSH); result.push(SUBSH);
break break;
} else { } else {
result.push(subsh_ch); result.push(subsh_ch);
} }
@@ -568,9 +584,9 @@ pub fn unescape_str(raw: &str) -> String {
} }
'"' => { '"' => {
result.push(DUB_QUOTE); result.push(DUB_QUOTE);
break break;
} }
_ => result.push(q_ch) _ => result.push(q_ch),
} }
} }
} }
@@ -580,9 +596,9 @@ pub fn unescape_str(raw: &str) -> String {
match q_ch { match q_ch {
'\'' => { '\'' => {
result.push(SNG_QUOTE); result.push(SNG_QUOTE);
break break;
} }
_ => result.push(q_ch) _ => result.push(q_ch),
} }
} }
} }
@@ -606,7 +622,7 @@ pub fn unescape_str(raw: &str) -> String {
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
result.push(PROC_SUB_OUT); result.push(PROC_SUB_OUT);
break break;
} else { } else {
result.push(subsh_ch); result.push(subsh_ch);
} }
@@ -635,7 +651,7 @@ pub fn unescape_str(raw: &str) -> String {
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
result.push(PROC_SUB_IN); result.push(PROC_SUB_IN);
break break;
} else { } else {
result.push(subsh_ch); result.push(subsh_ch);
} }
@@ -651,7 +667,7 @@ pub fn unescape_str(raw: &str) -> String {
result.push('$'); result.push('$');
} }
} }
_ => result.push(ch) _ => result.push(ch),
} }
first_char = false; first_char = false;
} }
@@ -693,17 +709,17 @@ pub fn unescape_math(raw: &str) -> String {
paren_count -= 1; paren_count -= 1;
if paren_count == 0 { if paren_count == 0 {
result.push(SUBSH); result.push(SUBSH);
break break;
} else { } else {
result.push(subsh_ch) result.push(subsh_ch)
} }
} }
_ => result.push(subsh_ch) _ => result.push(subsh_ch),
} }
} }
} }
} }
_ => result.push(ch) _ => result.push(ch),
} }
} }
flog!(INFO, result); flog!(INFO, result);
@@ -741,11 +757,13 @@ impl FromStr for ParamExp {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
use ParamExp::*; use ParamExp::*;
let parse_err = || Err(ShErr::Simple { let parse_err = || {
Err(ShErr::Simple {
kind: ShErrKind::SyntaxErr, kind: ShErrKind::SyntaxErr,
msg: "Invalid parameter expansion".into(), msg: "Invalid parameter expansion".into(),
notes: vec![], notes: vec![],
}); })
};
// Handle indirect var expansion: ${!var} // Handle indirect var expansion: ${!var}
if let Some(var) = s.strip_prefix('!') { if let Some(var) = s.strip_prefix('!') {
@@ -827,15 +845,9 @@ impl FromStr for ParamExp {
pub fn parse_pos_len(s: &str) -> Option<(usize, Option<usize>)> { pub fn parse_pos_len(s: &str) -> Option<(usize, Option<usize>)> {
let raw = s.strip_prefix(':')?; let raw = s.strip_prefix(':')?;
if let Some((start, len)) = raw.split_once(':') { if let Some((start, len)) = raw.split_once(':') {
Some(( Some((start.parse::<usize>().ok()?, len.parse::<usize>().ok()))
start.parse::<usize>().ok()?,
len.parse::<usize>().ok(),
))
} else { } else {
Some(( Some((raw.parse::<usize>().ok()?, None))
raw.parse::<usize>().ok()?,
None,
))
} }
} }
@@ -845,25 +857,22 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
let mut var_name = String::new(); let mut var_name = String::new();
let mut rest = String::new(); let mut rest = String::new();
if raw.starts_with('#') { if raw.starts_with('#') {
return Ok(vars.get_var(raw.strip_prefix('#').unwrap()).len().to_string()) return Ok(
vars
.get_var(raw.strip_prefix('#').unwrap())
.len()
.to_string(),
);
} }
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'!' | '!' | '#' | '%' | ':' | '-' | '+' | '=' | '/' | '?' => {
'#' |
'%' |
':' |
'-' |
'+' |
'=' |
'/' |
'?' => {
rest.push(ch); rest.push(ch);
rest.push_str(&chars.collect::<String>()); rest.push_str(&chars.collect::<String>());
break break;
} }
_ => var_name.push(ch) _ => var_name.push(ch),
} }
} }
@@ -918,18 +927,22 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
ParamExp::ErrUnsetOrNull(err) => { ParamExp::ErrUnsetOrNull(err) => {
if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() { if !vars.var_exists(&var_name) || vars.get_var(&var_name).is_empty() {
Err( Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::ExecFail, msg: err, notes: vec![] } kind: ShErrKind::ExecFail,
) msg: err,
notes: vec![],
})
} else { } else {
Ok(vars.get_var(&var_name)) Ok(vars.get_var(&var_name))
} }
} }
ParamExp::ErrUnset(err) => { ParamExp::ErrUnset(err) => {
if !vars.var_exists(&var_name) { if !vars.var_exists(&var_name) {
Err( Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::ExecFail, msg: err, notes: vec![] } kind: ShErrKind::ExecFail,
) msg: err,
notes: vec![],
})
} else { } else {
Ok(vars.get_var(&var_name)) Ok(vars.get_var(&var_name))
} }
@@ -957,7 +970,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
for i in 0..=value.len() { for i in 0..=value.len() {
let sliced = &value[..i]; let sliced = &value[..i];
if pattern.matches(sliced) { if pattern.matches(sliced) {
return Ok(value[i..].to_string()) return Ok(value[i..].to_string());
} }
} }
Ok(value) Ok(value)
@@ -1030,7 +1043,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[..i]; let sliced = &value[..i];
if pattern.matches(sliced) { if pattern.matches(sliced) {
return Ok(format!("{}{}",replace,&value[i..])) return Ok(format!("{}{}", replace, &value[i..]));
} }
} }
Ok(value) Ok(value)
@@ -1041,7 +1054,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
for i in (0..=value.len()).rev() { for i in (0..=value.len()).rev() {
let sliced = &value[i..]; let sliced = &value[i..];
if pattern.matches(sliced) { if pattern.matches(sliced) {
return Ok(format!("{}{}",&value[..i],replace)) return Ok(format!("{}{}", &value[..i], replace));
} }
} }
Ok(value) Ok(value)
@@ -1111,7 +1124,7 @@ pub enum PromptTk {
ExitCode, ExitCode,
SuccessSymbol, SuccessSymbol,
FailureSymbol, FailureSymbol,
JobCount JobCount,
} }
pub fn format_cmd_runtime(dur: std::time::Duration) -> String { pub fn format_cmd_runtime(dur: std::time::Duration) -> String {
@@ -1328,7 +1341,8 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'0'..='9' | ';' | '?' | ':' => params.push(ch), // Valid parameter characters '0'..='9' | ';' | '?' | ':' => params.push(ch), // Valid parameter characters
'A'..='Z' | 'a'..='z' => { // Final character (letter) 'A'..='Z' | 'a'..='z' => {
// Final character (letter)
params.push(ch); params.push(ch);
break; break;
} }
@@ -1452,18 +1466,14 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
} }
PromptTk::PromptSymbol => { PromptTk::PromptSymbol => {
let uid = std::env::var("UID").unwrap(); let uid = std::env::var("UID").unwrap();
let symbol = if &uid == "0" { let symbol = if &uid == "0" { '#' } else { '$' };
'#'
} else {
'$'
};
result.push(symbol); result.push(symbol);
} }
PromptTk::ExitCode => todo!(), PromptTk::ExitCode => todo!(),
PromptTk::SuccessSymbol => todo!(), PromptTk::SuccessSymbol => todo!(),
PromptTk::FailureSymbol => todo!(), PromptTk::FailureSymbol => todo!(),
PromptTk::JobCount => todo!(), PromptTk::JobCount => todo!(),
_ => unimplemented!() _ => unimplemented!(),
} }
} }
@@ -1473,7 +1483,11 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
/// Expand aliases in the given input string /// Expand aliases in the given input string
/// ///
/// Recursively calls itself until all aliases are expanded /// Recursively calls itself until all aliases are expanded
pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>, log_tab: &LogTab) -> String { pub fn expand_aliases(
input: String,
mut already_expanded: HashSet<String>,
log_tab: &LogTab,
) -> String {
let mut result = input.clone(); let mut result = input.clone();
let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input), LexFlags::empty()).collect();
let mut expanded_this_iter: Vec<String> = vec![]; let mut expanded_this_iter: Vec<String> = vec![];
@@ -1481,12 +1495,18 @@ pub fn expand_aliases(input: String, mut already_expanded: HashSet<String>, log_
for token_result in tokens.into_iter().rev() { for token_result in tokens.into_iter().rev() {
let Ok(tk) = token_result else { continue }; let Ok(tk) = token_result else { continue };
if !tk.flags.contains(TkFlags::IS_CMD) { continue } if !tk.flags.contains(TkFlags::IS_CMD) {
if tk.flags.contains(TkFlags::KEYWORD) { continue } 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) { if let Some(alias) = log_tab.get_alias(&raw_tk) {
result.replace_range(tk.span.range(), &alias); result.replace_range(tk.span.range(), &alias);

View File

@@ -3,29 +3,29 @@
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator 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 builtin;
pub mod jobs; pub mod expand;
pub mod signal;
pub mod getopt; 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 shopt;
pub mod signal;
pub mod state;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
use crate::libsh::sys::{save_termios, set_termios}; use crate::libsh::sys::{save_termios, set_termios};
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*;
use crate::signal::sig_setup; use crate::signal::sig_setup;
use crate::state::source_rc; use crate::state::source_rc;
use crate::prelude::*;
use clap::Parser; use clap::Parser;
use shopt::FernEditMode; 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 { struct FernArgs {
@@ -35,17 +35,19 @@ struct FernArgs {
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool version: bool,
} }
/// Force evaluation of lazily-initialized values early in shell startup. /// Force evaluation of lazily-initialized values early in shell startup.
/// ///
/// In particular, this ensures that the variable table is initialized, which populates /// In particular, this ensures that the variable table is initialized, which
/// environment variables from the system. If this initialization is deferred too long, /// populates environment variables from the system. If this initialization is
/// features like prompt expansion may fail due to missing environment variables. /// 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, /// This function triggers initialization by calling `read_vars` with a no-op
/// which forces access to the variable table and causes its `LazyLock` constructor to run. /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
@@ -98,7 +100,8 @@ fn fern_interactive() {
let mut readline_err_count: u32 = 0; let mut readline_err_count: u32 = 0;
loop { // Main loop loop {
// Main loop
let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
.unwrap() .unwrap()
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default()) .map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
@@ -113,9 +116,9 @@ fn fern_interactive() {
readline_err_count += 1; readline_err_count += 1;
if readline_err_count == 20 { if readline_err_count == 20 {
eprintln!("reached maximum readline error count, exiting"); eprintln!("reached maximum readline error count, exiting");
break break;
} else { } else {
continue continue;
} }
} }
}; };

View File

@@ -6,11 +6,10 @@ use crate::{parse::lex::Tk, prelude::*};
pub type OptSet = Arc<[Opt]>; pub type OptSet = Arc<[Opt]>;
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Opt { pub enum Opt {
Long(String), Long(String),
Short(char) Short(char),
} }
impl Opt { impl Opt {
@@ -47,7 +46,7 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>,Vec<Opt>) {
while let Some(word) = words_iter.next() { while let Some(word) = words_iter.next() {
if &word == "--" { if &word == "--" {
non_opts.extend(words_iter); non_opts.extend(words_iter);
break break;
} }
let parsed_opts = Opt::parse(&word); let parsed_opts = Opt::parse(&word);
if parsed_opts.is_empty() { if parsed_opts.is_empty() {
@@ -67,7 +66,7 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>) -> (Vec<Tk>, Vec<Opt>) {
while let Some(token) = tokens_iter.next() { while let Some(token) = tokens_iter.next() {
if &token.to_string() == "--" { if &token.to_string() == "--" {
non_opts.extend(tokens_iter); non_opts.extend(tokens_iter);
break break;
} }
let parsed_opts = Opt::parse(&token.to_string()); let parsed_opts = Opt::parse(&token.to_string());
if parsed_opts.is_empty() { if parsed_opts.is_empty() {

View File

@@ -1,4 +1,12 @@
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; pub const SIG_EXIT_OFFSET: i32 = 128;
@@ -20,12 +28,10 @@ pub struct DisplayWaitStatus(pub WtStat);
impl fmt::Display for DisplayWaitStatus { impl fmt::Display for DisplayWaitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 { match &self.0 {
WtStat::Exited(_, code) => { WtStat::Exited(_, code) => match code {
match code {
0 => write!(f, "done"), 0 => write!(f, "done"),
_ => write!(f, "failed: {}", code), _ => write!(f, "failed: {}", code),
} },
}
WtStat::Signaled(_, signal, _) => { WtStat::Signaled(_, signal, _) => {
write!(f, "signaled: {:?}", signal) write!(f, "signaled: {:?}", signal)
} }
@@ -53,7 +59,7 @@ pub enum JobID {
Pgid(Pid), Pgid(Pid),
Pid(Pid), Pid(Pid),
TableID(usize), TableID(usize),
Command(String) Command(String),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -61,7 +67,7 @@ pub struct ChildProc {
pgid: Pid, pgid: Pid,
pid: Pid, pid: Pid,
command: Option<String>, command: Option<String>,
stat: WtStat stat: WtStat,
} }
impl ChildProc { impl ChildProc {
@@ -72,7 +78,12 @@ impl ChildProc {
} else { } else {
WtStat::Exited(pid, 0) WtStat::Exited(pid, 0)
}; };
let mut child = Self { pgid: pid, pid, command, stat }; let mut child = Self {
pgid: pid,
pid,
command,
stat,
};
if let Some(pgid) = pgid { if let Some(pgid) = pgid {
child.set_pgid(pgid).ok(); child.set_pgid(pgid).ok();
} }
@@ -132,7 +143,7 @@ pub struct JobTab {
order: Vec<usize>, order: Vec<usize>,
new_updates: Vec<usize>, new_updates: Vec<usize>,
jobs: Vec<Option<Job>>, jobs: Vec<Option<Job>>,
fd_registry: Vec<RegisteredFd> fd_registry: Vec<RegisteredFd>,
} }
impl JobTab { impl JobTab {
@@ -168,10 +179,7 @@ impl JobTab {
&self.fd_registry &self.fd_registry
} }
pub fn register_fd(&mut self, owner_pid: Pid, fd: IoMode) { pub fn register_fd(&mut self, owner_pid: Pid, fd: IoMode) {
let registered_fd = RegisteredFd { let registered_fd = RegisteredFd { fd, owner_pid };
fd,
owner_pid
};
self.fd_registry.push(registered_fd) self.fd_registry.push(registered_fd)
} }
fn prune_jobs(&mut self) { fn prune_jobs(&mut self) {
@@ -179,17 +187,24 @@ impl JobTab {
if job.is_none() { if job.is_none() {
self.jobs.pop(); self.jobs.pop();
} else { } else {
break break;
} }
} }
} }
pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult<usize> { pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult<usize> {
self.prune_jobs(); self.prune_jobs();
let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() }; let tab_pos = if let Some(id) = job.tabid() {
id
} else {
self.next_open_pos()
};
job.set_tabid(tab_pos); job.set_tabid(tab_pos);
self.order.push(tab_pos); self.order.push(tab_pos);
if !silent { if !silent {
write(borrow_fd(1),job.display(&self.order, JobCmdFlags::INIT).as_bytes())?; write(
borrow_fd(1),
job.display(&self.order, JobCmdFlags::INIT).as_bytes(),
)?;
} }
if tab_pos == self.jobs.len() { if tab_pos == self.jobs.len() {
self.jobs.push(Some(job)) self.jobs.push(Some(job))
@@ -204,61 +219,51 @@ impl JobTab {
pub fn query(&self, identifier: JobID) -> Option<&Job> { pub fn query(&self, identifier: JobID) -> Option<&Job> {
match identifier { match identifier {
// Match by process group ID // Match by process group ID
JobID::Pgid(pgid) => { JobID::Pgid(pgid) => self
self.jobs.iter().find_map(|job| { .jobs
job.as_ref().filter(|j| j.pgid() == pgid) .iter()
}) .find_map(|job| job.as_ref().filter(|j| j.pgid() == pgid)),
}
// Match by process ID // Match by process ID
JobID::Pid(pid) => { JobID::Pid(pid) => self.jobs.iter().find_map(|job| {
self.jobs.iter().find_map(|job| { job
job.as_ref().filter(|j| j.children().iter().any(|child| child.pid() == pid)) .as_ref()
}) .filter(|j| j.children().iter().any(|child| child.pid() == pid))
} }),
// Match by table ID (index in the job table) // Match by table ID (index in the job table)
JobID::TableID(id) => { JobID::TableID(id) => self.jobs.get(id).and_then(|job| job.as_ref()),
self.jobs.get(id).and_then(|job| job.as_ref())
}
// Match by command name (partial match) // Match by command name (partial match)
JobID::Command(cmd) => { JobID::Command(cmd) => self.jobs.iter().find_map(|job| {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| { job.as_ref().filter(|j| {
j.children().iter().any(|child| { j.children()
child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) .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> { pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> {
match identifier { match identifier {
// Match by process group ID // Match by process group ID
JobID::Pgid(pgid) => { JobID::Pgid(pgid) => self
self.jobs.iter_mut().find_map(|job| { .jobs
job.as_mut().filter(|j| j.pgid() == pgid) .iter_mut()
}) .find_map(|job| job.as_mut().filter(|j| j.pgid() == pgid)),
}
// Match by process ID // Match by process ID
JobID::Pid(pid) => { JobID::Pid(pid) => self.jobs.iter_mut().find_map(|job| {
self.jobs.iter_mut().find_map(|job| { job
job.as_mut().filter(|j| j.children().iter().any(|child| child.pid() == pid)) .as_mut()
}) .filter(|j| j.children().iter().any(|child| child.pid() == pid))
} }),
// Match by table ID (index in the job table) // Match by table ID (index in the job table)
JobID::TableID(id) => { JobID::TableID(id) => self.jobs.get_mut(id).and_then(|job| job.as_mut()),
self.jobs.get_mut(id).and_then(|job| job.as_mut())
}
// Match by command name (partial match) // Match by command name (partial match)
JobID::Command(cmd) => { JobID::Command(cmd) => self.jobs.iter_mut().find_map(|job| {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| { job.as_mut().filter(|j| {
j.children().iter().any(|child| { j.children()
child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)) .iter()
.any(|child| child.cmd().as_ref().is_some_and(|c| c.contains(&cmd)))
}) })
}) }),
})
}
} }
} }
pub fn get_fg(&self) -> Option<&Job> { pub fn get_fg(&self) -> Option<&Job> {
@@ -277,7 +282,7 @@ impl JobTab {
} }
pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> { pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> {
if self.fg.is_none() { if self.fg.is_none() {
return Ok(()) return Ok(());
} }
take_term()?; take_term()?;
let fg = std::mem::take(&mut self.fg); let fg = std::mem::take(&mut self.fg);
@@ -304,13 +309,19 @@ impl JobTab {
} }
pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> { pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> {
let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) { let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) {
&self.jobs &self
.jobs
.iter() .iter()
.filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap()))) .filter(|job| {
job
.as_ref()
.is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap()))
})
.map(|job| job.as_ref()) .map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>() .collect::<Vec<Option<&Job>>>()
} else { } else {
&self.jobs &self
.jobs
.iter() .iter()
.map(|job| job.as_ref()) .map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>() .collect::<Vec<Option<&Job>>>()
@@ -320,15 +331,29 @@ impl JobTab {
// Skip foreground job // Skip foreground job
let id = job.tabid().unwrap(); let id = job.tabid().unwrap();
// Filter jobs based on flags // Filter jobs based on flags
if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) { if flags.contains(JobCmdFlags::RUNNING)
&& !matches!(
job.get_stats().get(id).unwrap(),
WtStat::StillAlive | WtStat::Continued(_)
)
{
continue; continue;
} }
if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) { if flags.contains(JobCmdFlags::STOPPED)
&& !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_, _))
{
continue; continue;
} }
// Print the job in the selected format // Print the job in the selected format
write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?; write(
if job.get_stats().iter().all(|stat| matches!(stat,WtStat::Exited(_, _))) { 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)); jobs_to_remove.push(JobID::TableID(id));
} }
} }
@@ -343,7 +368,7 @@ impl JobTab {
pub struct JobBldr { pub struct JobBldr {
table_id: Option<usize>, table_id: Option<usize>,
pgid: Option<Pid>, pgid: Option<Pid>,
children: Vec<ChildProc> children: Vec<ChildProc>,
} }
impl Default for JobBldr { impl Default for JobBldr {
@@ -354,20 +379,24 @@ impl Default for JobBldr {
impl JobBldr { impl JobBldr {
pub fn new() -> Self { pub fn new() -> Self {
Self { table_id: None, pgid: None, children: vec![] } Self {
table_id: None,
pgid: None,
children: vec![],
}
} }
pub fn with_id(self, id: usize) -> Self { pub fn with_id(self, id: usize) -> Self {
Self { Self {
table_id: Some(id), table_id: Some(id),
pgid: self.pgid, pgid: self.pgid,
children: self.children children: self.children,
} }
} }
pub fn with_pgid(self, pgid: Pid) -> Self { pub fn with_pgid(self, pgid: Pid) -> Self {
Self { Self {
table_id: self.table_id, table_id: self.table_id,
pgid: Some(pgid), pgid: Some(pgid),
children: self.children children: self.children,
} }
} }
pub fn set_pgid(&mut self, pgid: Pid) { pub fn set_pgid(&mut self, pgid: Pid) {
@@ -380,7 +409,7 @@ impl JobBldr {
Self { Self {
table_id: self.table_id, table_id: self.table_id,
pgid: self.pgid, pgid: self.pgid,
children children,
} }
} }
pub fn push_child(&mut self, child: ChildProc) { pub fn push_child(&mut self, child: ChildProc) {
@@ -390,7 +419,7 @@ impl JobBldr {
Job { Job {
table_id: self.table_id, table_id: self.table_id,
pgid: self.pgid.unwrap_or(Pid::from_raw(0)), pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
children: self.children children: self.children,
} }
} }
} }
@@ -418,7 +447,7 @@ impl JobStack {
pub struct Job { pub struct Job {
table_id: Option<usize>, table_id: Option<usize>,
pgid: Pid, pgid: Pid,
children: Vec<ChildProc> children: Vec<ChildProc>,
} }
impl Job { impl Job {
@@ -447,13 +476,15 @@ impl Job {
} }
} }
pub fn get_stats(&self) -> Vec<WtStat> { pub fn get_stats(&self) -> Vec<WtStat> {
self.children self
.children
.iter() .iter()
.map(|chld| chld.stat()) .map(|chld| chld.stat())
.collect::<Vec<WtStat>>() .collect::<Vec<WtStat>>()
} }
pub fn get_pids(&self) -> Vec<Pid> { pub fn get_pids(&self) -> Vec<Pid> {
self.children self
.children
.iter() .iter()
.map(|chld| chld.pid()) .map(|chld| chld.pid())
.collect::<Vec<Pid>>() .collect::<Vec<Pid>>()
@@ -469,7 +500,7 @@ impl Job {
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
Signal::SIGCONT => WtStat::Continued(self.pgid), Signal::SIGCONT => WtStat::Continued(self.pgid),
Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false), Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false),
_ => unimplemented!("{}",sig) _ => unimplemented!("{}", sig),
}; };
self.set_stats(stat); self.set_stats(stat);
Ok(killpg(self.pgid, sig)?) Ok(killpg(self.pgid, sig)?)
@@ -483,7 +514,7 @@ impl Job {
// TODO: figure out some way to get the exit code of builtins // TODO: figure out some way to get the exit code of builtins
let code = state::get_status(); let code = state::get_status();
stats.push(WtStat::Exited(child.pid, code)); stats.push(WtStat::Exited(child.pid, code));
continue continue;
} }
let result = child.wait(Some(WtFlag::WSTOPPED)); let result = child.wait(Some(WtFlag::WSTOPPED));
match result { match result {
@@ -491,7 +522,7 @@ impl Job {
stats.push(stat); stats.push(stat);
} }
Err(Errno::ECHILD) => break, Err(Errno::ECHILD) => break,
Err(e) => return Err(e.into()) Err(e) => return Err(e.into()),
} }
} }
Ok(stats) Ok(stats)
@@ -505,12 +536,10 @@ impl Job {
} }
} }
JobID::Command(cmd) => { JobID::Command(cmd) => {
let query_result = self.children let query_result = self
.children
.iter_mut() .iter_mut()
.find(|chld| chld .find(|chld| chld.cmd().is_some_and(|chld_cmd| chld_cmd.contains(&cmd)));
.cmd()
.is_some_and(|chld_cmd| chld_cmd.contains(&cmd))
);
if let Some(child) = query_result { if let Some(child) = query_result {
child.set_stat(stat); child.set_stat(stat);
} }
@@ -576,13 +605,11 @@ impl Job {
stat_line = format!("{} {}", stat_line, cmd); stat_line = format!("{} {}", stat_line, cmd);
stat_line = match job_stat { stat_line = match job_stat {
WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta), WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta),
WtStat::Exited(_, code) => { WtStat::Exited(_, code) => match code {
match code {
0 => stat_line.styled(Style::Green), 0 => stat_line.styled(Style::Green),
_ => stat_line.styled(Style::Red), _ => stat_line.styled(Style::Red),
} },
} _ => stat_line.styled(Style::Cyan),
_ => stat_line.styled(Style::Cyan)
}; };
if i != self.get_cmds().len() - 1 { if i != self.get_cmds().len() - 1 {
stat_line = format!("{} |", stat_line); stat_line = format!("{} |", stat_line);
@@ -596,11 +623,7 @@ impl Job {
stat_line stat_line
) )
} else { } else {
format!( format!("{}{}", if i != 0 { &padding } else { "" }, stat_line)
"{}{}",
if i != 0 { &padding } else { "" },
stat_line
)
}; };
output.push_str(&stat_final); output.push_str(&stat_final);
output.push('\n'); output.push('\n');
@@ -613,7 +636,8 @@ 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<()> { pub fn take_term() -> ShResult<()> {
attach_tty(getpgrp())?; attach_tty(getpgrp())?;
Ok(()) Ok(())
@@ -621,20 +645,31 @@ pub fn take_term() -> ShResult<()> {
pub fn disable_reaping() -> ShResult<()> { pub fn disable_reaping() -> ShResult<()> {
flog!(TRACE, "Disabling reaping"); flog!(TRACE, "Disabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::ignore_sigchld)) }?; unsafe {
signal(
Signal::SIGCHLD,
SigHandler::Handler(crate::signal::ignore_sigchld),
)
}?;
Ok(()) Ok(())
} }
pub fn enable_reaping() -> ShResult<()> { pub fn enable_reaping() -> ShResult<()> {
flog!(TRACE, "Enabling reaping"); flog!(TRACE, "Enabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::handle_sigchld)) }.unwrap(); unsafe {
signal(
Signal::SIGCHLD,
SigHandler::Handler(crate::signal::handle_sigchld),
)
}
.unwrap();
Ok(()) Ok(())
} }
/// Waits on the current foreground job and updates the shell's last status code /// Waits on the current foreground job and updates the shell's last status code
pub fn wait_fg(job: Job) -> ShResult<()> { pub fn wait_fg(job: Job) -> ShResult<()> {
if job.children().is_empty() { if job.children().is_empty() {
return Ok(()) // Nothing to do return Ok(()); // Nothing to do
} }
flog!(TRACE, "Waiting on foreground job"); flog!(TRACE, "Waiting on foreground job");
let mut code = 0; let mut code = 0;
@@ -649,13 +684,13 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
WtStat::Stopped(_, sig) => { WtStat::Stopped(_, sig) => {
write_jobs(|j| j.fg_to_bg(status))?; write_jobs(|j| j.fg_to_bg(status))?;
code = SIG_EXIT_OFFSET + sig as i32; code = SIG_EXIT_OFFSET + sig as i32;
}, }
WtStat::Signaled(_, sig, _) => { WtStat::Signaled(_, sig, _) => {
if sig == Signal::SIGTSTP { if sig == Signal::SIGTSTP {
write_jobs(|j| j.fg_to_bg(status))?; write_jobs(|j| j.fg_to_bg(status))?;
} }
code = SIG_EXIT_OFFSET + sig as i32; code = SIG_EXIT_OFFSET + sig as i32;
}, }
_ => { /* Do nothing */ } _ => { /* Do nothing */ }
} }
} }
@@ -668,9 +703,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> { pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> {
if is_bg { if is_bg {
write_jobs(|j| { write_jobs(|j| j.insert_job(job, false))?;
j.insert_job(job, false)
})?;
} else { } else {
wait_fg(job)?; wait_fg(job)?;
} }
@@ -678,10 +711,10 @@ pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> {
} }
pub fn attach_tty(pgid: Pid) -> ShResult<()> { pub fn attach_tty(pgid: Pid) -> ShResult<()> {
// If we aren't attached to a terminal, the pgid already controls it, or the process group does not exist // If we aren't attached to a terminal, the pgid already controls it, or the
// Then return ok // process group does not exist Then return ok
if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() { if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() {
return Ok(()) return Ok(());
} }
flog!(TRACE, "Attaching tty to pgid: {}", pgid); flog!(TRACE, "Attaching tty to pgid: {}", pgid);
@@ -700,7 +733,11 @@ pub fn attach_tty(pgid: Pid) -> ShResult<()> {
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 { match result {
Ok(_) => Ok(()), Ok(_) => Ok(()),

View File

@@ -3,7 +3,7 @@ use std::fmt::Display;
use crate::{ use crate::{
libsh::term::{Style, Styled}, libsh::term::{Style, Styled},
parse::lex::Span, parse::lex::Span,
prelude::* prelude::*,
}; };
pub type ShResult<T> = Result<T, ShErr>; pub type ShResult<T> = Result<T, ShErr>;
@@ -16,22 +16,38 @@ pub trait ShResultExt {
impl<T> ShResultExt for Result<T, ShErr> { impl<T> ShResultExt for Result<T, ShErr> {
/// Blame a span for an error /// Blame a span for an error
fn blame(self, new_span: Span) -> Self { fn blame(self, new_span: Span) -> Self {
let Err(e) = self else { let Err(e) = self else { return self };
return self
};
match e { match e {
ShErr::Simple { kind, msg, notes } | 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 }), | 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 /// Blame a span if no blame has been assigned yet
fn try_blame(self, new_span: Span) -> Self { fn try_blame(self, new_span: Span) -> Self {
let Err(e) = &self else { let Err(e) = &self else { return self };
return self
};
match e { match e {
ShErr::Simple { kind, msg, notes } => Err(ShErr::Full { kind: kind.clone(), msg: msg.clone(), notes: notes.clone(), span: new_span }), ShErr::Simple { kind, msg, notes } => Err(ShErr::Full {
ShErr::Full { kind: _, msg: _, span: _, notes: _ } => self kind: kind.clone(),
msg: msg.clone(),
notes: notes.clone(),
span: new_span,
}),
ShErr::Full {
kind: _,
msg: _,
span: _,
notes: _,
} => self,
} }
} }
} }
@@ -40,7 +56,7 @@ impl<T> ShResultExt for Result<T,ShErr> {
pub struct Note { pub struct Note {
main: String, main: String,
sub_notes: Vec<Note>, sub_notes: Vec<Note>,
depth: usize depth: usize,
} }
impl Note { impl Note {
@@ -48,18 +64,26 @@ impl Note {
Self { Self {
main: main.into(), main: main.into(),
sub_notes: vec![], sub_notes: vec![],
depth: 0 depth: 0,
} }
} }
pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self { pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self {
let Self { main, mut sub_notes, depth } = self; let Self {
main,
mut sub_notes,
depth,
} = self;
for raw_note in new_sub_notes { for raw_note in new_sub_notes {
let mut note = Note::new(raw_note); let mut note = Note::new(raw_note);
note.depth = self.depth + 1; note.depth = self.depth + 1;
sub_notes.push(note); sub_notes.push(note);
} }
Self { main, sub_notes, depth } Self {
main,
sub_notes,
depth,
}
} }
} }
@@ -84,46 +108,94 @@ impl Display for Note {
#[derive(Debug)] #[derive(Debug)]
pub enum ShErr { pub enum ShErr {
Simple { kind: ShErrKind, msg: String, notes: Vec<Note> }, Simple {
Full { kind: ShErrKind, msg: String, notes: Vec<Note>, span: Span } kind: ShErrKind,
msg: String,
notes: Vec<Note>,
},
Full {
kind: ShErrKind,
msg: String,
notes: Vec<Note>,
span: Span,
},
} }
impl ShErr { impl ShErr {
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self { pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
let msg = msg.into(); let msg = msg.into();
Self::Simple { kind, msg, notes: vec![] } Self::Simple {
kind,
msg,
notes: vec![],
}
} }
pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self { pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self {
let msg = msg.into(); let msg = msg.into();
Self::Full { kind, msg, span, notes: vec![] } Self::Full {
kind,
msg,
span,
notes: vec![],
}
} }
pub fn unpack(self) -> (ShErrKind, String, Vec<Note>, Option<Span>) { pub fn unpack(self) -> (ShErrKind, String, Vec<Note>, Option<Span>) {
match self { match self {
ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None), ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None),
ShErr::Full { kind, msg, notes, span } => (kind,msg,notes,Some(span)) ShErr::Full {
kind,
msg,
notes,
span,
} => (kind, msg, notes, Some(span)),
} }
} }
pub fn with_note(self, note: Note) -> Self { pub fn with_note(self, note: Note) -> Self {
let (kind, msg, mut notes, span) = self.unpack(); let (kind, msg, mut notes, span) = self.unpack();
notes.push(note); notes.push(note);
if let Some(span) = span { if let Some(span) = span {
Self::Full { kind, msg, notes, span } Self::Full {
kind,
msg,
notes,
span,
}
} else { } else {
Self::Simple { kind, msg, notes } Self::Simple { kind, msg, notes }
} }
} }
pub fn with_span(sherr: ShErr, span: Span) -> Self { pub fn with_span(sherr: ShErr, span: Span) -> Self {
let (kind, msg, notes, _) = sherr.unpack(); let (kind, msg, notes, _) = sherr.unpack();
Self::Full { kind, msg, notes, span } Self::Full {
kind,
msg,
notes,
span,
}
} }
pub fn kind(&self) -> &ShErrKind { pub fn kind(&self) -> &ShErrKind {
match self { match self {
ShErr::Simple { kind, msg: _, notes: _ } | ShErr::Simple {
ShErr::Full { kind, msg: _, notes: _, span: _ } => kind kind,
msg: _,
notes: _,
}
| ShErr::Full {
kind,
msg: _,
notes: _,
span: _,
} => kind,
} }
} }
pub fn get_window(&self) -> Vec<(usize, String)> { pub fn get_window(&self) -> Vec<(usize, String)> {
let ShErr::Full { kind: _, msg: _, notes: _, span } = self else { let ShErr::Full {
kind: _,
msg: _,
notes: _,
span,
} = self
else {
unreachable!() unreachable!()
}; };
let mut total_len: usize = 0; let mut total_len: usize = 0;
@@ -139,14 +211,11 @@ impl ShErr {
cur_line.push(ch); cur_line.push(ch);
if ch == '\n' { if ch == '\n' {
if total_len > span.start { if total_len > span.start {
let line = ( let line = (total_lines, mem::take(&mut cur_line));
total_lines,
mem::take(&mut cur_line)
);
lines.push(line); lines.push(line);
} }
if total_len >= span.end { if total_len >= span.end {
break break;
} }
total_lines += 1; total_lines += 1;
@@ -155,17 +224,20 @@ impl ShErr {
} }
if !cur_line.is_empty() { if !cur_line.is_empty() {
let line = ( let line = (total_lines, mem::take(&mut cur_line));
total_lines,
mem::take(&mut cur_line)
);
lines.push(line); lines.push(line);
} }
lines lines
} }
pub fn get_line_col(&self) -> (usize, usize) { pub fn get_line_col(&self) -> (usize, usize) {
let ShErr::Full { kind: _, msg: _, notes: _, span } = self else { let ShErr::Full {
kind: _,
msg: _,
notes: _,
span,
} = self
else {
unreachable!() unreachable!()
}; };
@@ -175,7 +247,7 @@ impl ShErr {
let mut chars = src.chars().enumerate(); let mut chars = src.chars().enumerate();
while let Some((pos, ch)) = chars.next() { while let Some((pos, ch)) = chars.next() {
if pos >= span.start { if pos >= span.start {
break break;
} }
if ch == '\n' { if ch == '\n' {
lineno += 1; lineno += 1;
@@ -188,14 +260,25 @@ impl ShErr {
} }
pub fn get_indicator_lines(&self) -> Option<Vec<String>> { pub fn get_indicator_lines(&self) -> Option<Vec<String>> {
match self { match self {
ShErr::Simple { kind: _, msg: _, notes: _ } => None, ShErr::Simple {
ShErr::Full { kind: _, msg: _, notes: _, span } => { kind: _,
msg: _,
notes: _,
} => None,
ShErr::Full {
kind: _,
msg: _,
notes: _,
span,
} => {
let text = span.as_str(); let text = span.as_str();
let lines = text.lines(); let lines = text.lines();
let mut indicator_lines = vec![]; let mut indicator_lines = vec![];
for line in lines { for line in lines {
let indicator_line = "^".repeat(line.trim().len()).styled(Style::Red | Style::Bold); let indicator_line = "^"
.repeat(line.trim().len())
.styled(Style::Red | Style::Bold);
indicator_lines.push(indicator_line); indicator_lines.push(indicator_line);
} }
@@ -208,7 +291,11 @@ impl ShErr {
impl Display for ShErr { impl Display for ShErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Simple { msg, kind: _, notes } => { Self::Simple {
msg,
kind: _,
notes,
} => {
let mut all_strings = vec![msg.to_string()]; let mut all_strings = vec![msg.to_string()];
let mut notes_fmt = vec![]; let mut notes_fmt = vec![];
for note in notes { for note in notes {
@@ -222,7 +309,12 @@ impl Display for ShErr {
writeln!(f, "{}", output) writeln!(f, "{}", output)
} }
Self::Full { msg, kind, notes, span: _ } => { Self::Full {
msg,
kind,
notes,
span: _,
} => {
let window = self.get_window(); let window = self.get_window();
let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter(); let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter();
let mut lineno_pad_count = 0; let mut lineno_pad_count = 0;
@@ -234,18 +326,13 @@ impl Display for ShErr {
let padding = " ".repeat(lineno_pad_count); let padding = " ".repeat(lineno_pad_count);
writeln!(f)?; writeln!(f)?;
let (line, col) = self.get_line_col(); let (line, col) = self.get_line_col();
let line_fmt = line.styled(Style::Cyan | Style::Bold); let line_fmt = line.styled(Style::Cyan | Style::Bold);
let col_fmt = col.styled(Style::Cyan | Style::Bold); let col_fmt = col.styled(Style::Cyan | Style::Bold);
let kind = kind.styled(Style::Red | Style::Bold); let kind = kind.styled(Style::Red | Style::Bold);
let arrow = "->".styled(Style::Cyan | Style::Bold); let arrow = "->".styled(Style::Cyan | Style::Bold);
writeln!(f, writeln!(f, "{kind} - {msg}",)?;
"{kind} - {msg}", writeln!(f, "{padding}{arrow} [{line_fmt};{col_fmt}]",)?;
)?;
writeln!(f,
"{padding}{arrow} [{line_fmt};{col_fmt}]",
)?;
let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold); let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold);
writeln!(f, "{bar}")?; writeln!(f, "{bar}")?;
@@ -273,16 +360,12 @@ impl Display for ShErr {
write!(f, "{bar}")?; write!(f, "{bar}")?;
let bar_break = "-".styled(Style::Cyan | Style::Bold); let bar_break = "-".styled(Style::Cyan | Style::Bold);
if !notes.is_empty() { if !notes.is_empty() {
writeln!(f)?; writeln!(f)?;
} }
for note in notes { for note in notes {
write!(f, "{padding}{bar_break} {note}")?;
write!(f,
"{padding}{bar_break} {note}"
)?;
} }
Ok(()) Ok(())
} }
@@ -327,7 +410,7 @@ pub enum ShErrKind {
LoopContinue(i32), LoopContinue(i32),
LoopBreak(i32), LoopBreak(i32),
ReadlineErr, ReadlineErr,
Null Null,
} }
impl Display for ShErrKind { impl Display for ShErrKind {

View File

@@ -10,7 +10,7 @@ pub enum FernLogLevel {
WARN = 2, WARN = 2,
INFO = 3, INFO = 3,
DEBUG = 4, DEBUG = 4,
TRACE = 5 TRACE = 5,
} }
impl Display for FernLogLevel { impl Display for FernLogLevel {
@@ -22,7 +22,7 @@ impl Display for FernLogLevel {
INFO => write!(f, "{}", "INFO".styled(Style::Green | Style::Bold)), INFO => write!(f, "{}", "INFO".styled(Style::Green | Style::Bold)),
DEBUG => write!(f, "{}", "DEBUG".styled(Style::Magenta | Style::Bold)), DEBUG => write!(f, "{}", "DEBUG".styled(Style::Magenta | Style::Bold)),
TRACE => write!(f, "{}", "TRACE".styled(Style::Blue | Style::Bold)), TRACE => write!(f, "{}", "TRACE".styled(Style::Blue | Style::Bold)),
NONE => write!(f,"") NONE => write!(f, ""),
} }
} }
} }
@@ -36,21 +36,23 @@ pub fn log_level() -> FernLogLevel {
"info" => INFO, "info" => INFO,
"debug" => DEBUG, "debug" => DEBUG,
"trace" => TRACE, "trace" => TRACE,
_ => NONE _ => NONE,
} }
} }
/// A structured logging macro designed for `fern`. /// A structured logging macro designed for `fern`.
/// ///
/// `flog!` was implemented because `rustyline` uses `env_logger`, which clutters the debug output. /// `flog!` was implemented because `rustyline` uses `env_logger`, which
/// This macro prints log messages in a structured format, including the log level, filename, and line number. /// clutters the debug output. This macro prints log messages in a structured
/// format, including the log level, filename, and line number.
/// ///
/// # Usage /// # Usage
/// ///
/// The macro supports three types of arguments: /// The macro supports three types of arguments:
/// ///
/// ## 1. **Formatted Messages** /// ## 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 /// ```rust
/// flog!(ERROR, "foo is {}", foo); /// flog!(ERROR, "foo is {}", foo);
@@ -73,7 +75,8 @@ pub fn log_level() -> FernLogLevel {
/// ``` /// ```
/// ///
/// ## 3. **Expressions** /// ## 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 /// ```rust
/// flog!(INFO, 1.min(2)); /// flog!(INFO, 1.min(2));
@@ -84,8 +87,10 @@ pub fn log_level() -> FernLogLevel {
/// ``` /// ```
/// ///
/// # Considerations /// # Considerations
/// - This macro uses `eprintln!()` internally, so its formatting rules must be followed. /// - This macro uses `eprintln!()` internally, so its formatting rules must be
/// - **Literals and formatted messages** require arguments that implement [`std::fmt::Display`]. /// followed.
/// - **Literals and formatted messages** require arguments that implement
/// [`std::fmt::Display`].
/// - **Expressions** require arguments that implement [`std::fmt::Debug`]. /// - **Expressions** require arguments that implement [`std::fmt::Debug`].
#[macro_export] #[macro_export]
macro_rules! flog { macro_rules! flog {

View File

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

View File

@@ -4,22 +4,31 @@ use crate::{prelude::*, state::write_jobs};
/// ///
/// The previous state of the terminal options. /// 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. /// This variable stores the terminal settings at the start of the program and
/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program. /// restores them when the program exits. It is initialized exactly once at the
/// It will not be mutated or accessed under any other circumstances. /// 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: /// The possible states of this variable are:
/// - `None`: The terminal options have not been set yet (before initialization). /// - `None`: The terminal options have not been set yet (before
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected). /// initialization).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved. /// - `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: /// **Important:** This static variable is mutable and accessed via unsafe code.
/// - It is set once during program startup and accessed once during program exit. /// 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. /// - 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<Option<Termios>> = None; pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() { pub fn save_termios() {
@@ -27,7 +36,12 @@ pub fn save_termios() {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL; termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
Some(termios) Some(termios)
} else { } else {
None None
@@ -38,11 +52,13 @@ pub fn save_termios() {
///Access the saved termios ///Access the saved termios
/// ///
///# Safety ///# 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<Termios> { pub unsafe fn get_saved_termios() -> Option<Termios> {
// SAVED_TERMIOS should *only ever* be set once and accessed once // 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. // Set at the start of the program, and accessed during the exit of the program
// Do not use this variable anywhere else // to reset the termios. Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten() SAVED_TERMIOS.clone().flatten()
} }
@@ -51,7 +67,12 @@ pub fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() { if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL; termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
} }
} }

View File

@@ -51,7 +51,11 @@ impl CharDequeUtils for VecDeque<char> {
} }
// Compare from the back // Compare from the back
self.iter().rev().zip(pat_chars.rev()).all(|(c1, c2)| c1 == &c2) self
.iter()
.rev()
.zip(pat_chars.rev())
.all(|(c1, c2)| c1 == &c2)
} }
fn starts_with(&self, pat: &str) -> bool { fn starts_with(&self, pat: &str) -> bool {
@@ -71,12 +75,9 @@ impl CharDequeUtils for VecDeque<char> {
impl TkVecUtils<Tk> for Vec<Tk> { impl TkVecUtils<Tk> for Vec<Tk> {
fn get_span(&self) -> Option<Span> { fn get_span(&self) -> Option<Span> {
if let Some(first_tk) = self.first() { if let Some(first_tk) = self.first() {
self.last().map(|last_tk| { self
Span::new( .last()
first_tk.span.start..last_tk.span.end, .map(|last_tk| Span::new(first_tk.span.start..last_tk.span.end, first_tk.source()))
first_tk.source()
)
})
} else { } else {
None None
} }
@@ -95,15 +96,12 @@ impl RedirVecUtils<Redir> for Vec<Redir> {
for redir in self { for redir in self {
match redir.class { match redir.class {
RedirType::Input => input.push(redir), RedirType::Input => input.push(redir),
RedirType::Pipe => { RedirType::Pipe => match redir.io_mode.tgt_fd() {
match redir.io_mode.tgt_fd() {
STDIN_FILENO => input.push(redir), STDIN_FILENO => input.push(redir),
STDOUT_FILENO | STDOUT_FILENO | STDERR_FILENO => output.push(redir),
STDERR_FILENO => output.push(redir), _ => unreachable!(),
_ => unreachable!() },
} _ => output.push(redir),
}
_ => output.push(redir)
} }
} }
(input, output) (input, output)

View File

@@ -1,20 +1,50 @@
use std::collections::{HashSet, VecDeque}; use std::collections::{HashSet, VecDeque};
use crate::{
builtin::{
alias::{alias, unalias},
cd::cd,
echo::echo,
export::export,
flowctl::flowctl,
jobctl::{continue_job, jobs, JobBehavior},
pwd::pwd,
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
zoltraak::zoltraak,
},
expand::expand_aliases,
jobs::{dispatch_job, ChildProc, JobBldr, JobStack},
libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt},
utils::RedirVecUtils,
},
prelude::*,
procio::{IoFrame, IoMode, IoStack},
state::{
self, get_snapshots, read_logic, restore_snapshot, write_logic, write_meta, write_vars, ShFunc,
VarTab, LOGIC_TABLE,
},
};
use crate::{builtin::{alias::{alias, unalias}, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{continue_job, jobs, JobBehavior}, pwd::pwd, shift::shift, shopt::shopt, source::source, test::double_bracket_test, zoltraak::zoltraak}, expand::expand_aliases, jobs::{dispatch_job, ChildProc, JobBldr, JobStack}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, prelude::*, procio::{IoFrame, IoMode, IoStack}, state::{self, get_snapshots, read_logic, restore_snapshot, write_logic, write_meta, write_vars, ShFunc, VarTab, LOGIC_TABLE}}; use super::{
lex::{Span, Tk, TkFlags, KEYWORDS},
use super::{lex::{Span, Tk, TkFlags, KEYWORDS}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType}; AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node,
ParsedSrc, Redir, RedirType,
};
pub enum AssignBehavior { pub enum AssignBehavior {
Export, Export,
Set Set,
} }
/// Arguments to the execvpe function /// Arguments to the execvpe function
pub struct ExecArgs { pub struct ExecArgs {
pub cmd: (CString, Span), pub cmd: (CString, Span),
pub argv: Vec<CString>, pub argv: Vec<CString>,
pub envp: Vec<CString> pub envp: Vec<CString>,
} }
impl ExecArgs { impl ExecArgs {
@@ -32,10 +62,15 @@ impl ExecArgs {
(CString::new(cmd).unwrap(), span) (CString::new(cmd).unwrap(), span)
} }
pub fn get_argv(argv: Vec<(String, Span)>) -> Vec<CString> { pub fn get_argv(argv: Vec<(String, Span)>) -> Vec<CString> {
argv.into_iter().map(|s| CString::new(s.0).unwrap()).collect() argv
.into_iter()
.map(|s| CString::new(s.0).unwrap())
.collect()
} }
pub fn get_envp() -> Vec<CString> { pub fn get_envp() -> Vec<CString> {
std::env::vars().map(|v| CString::new(format!("{}={}",v.0,v.1)).unwrap()).collect() std::env::vars()
.map(|v| CString::new(format!("{}={}", v.0, v.1)).unwrap())
.collect()
} }
} }
@@ -49,7 +84,7 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>) -> ShResult<()> {
for error in errors { for error in errors {
eprintln!("{error}"); eprintln!("{error}");
} }
return Ok(()) return Ok(());
} }
let mut dispatcher = Dispatcher::new(parser.extract_nodes()); let mut dispatcher = Dispatcher::new(parser.extract_nodes());
@@ -62,13 +97,17 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>) -> ShResult<()> {
pub struct Dispatcher { pub struct Dispatcher {
nodes: VecDeque<Node>, nodes: VecDeque<Node>,
pub io_stack: IoStack, pub io_stack: IoStack,
pub job_stack: JobStack pub job_stack: JobStack,
} }
impl Dispatcher { impl Dispatcher {
pub fn new(nodes: Vec<Node>) -> Self { pub fn new(nodes: Vec<Node>) -> Self {
let nodes = VecDeque::from(nodes); let nodes = VecDeque::from(nodes);
Self { nodes, io_stack: IoStack::new(), job_stack: JobStack::new() } Self {
nodes,
io_stack: IoStack::new(),
job_stack: JobStack::new(),
}
} }
pub fn begin_dispatch(&mut self) -> ShResult<()> { pub fn begin_dispatch(&mut self) -> ShResult<()> {
flog!(TRACE, "beginning dispatch"); flog!(TRACE, "beginning dispatch");
@@ -90,13 +129,13 @@ impl Dispatcher {
NdRule::FuncDef { .. } => self.exec_func_def(node)?, NdRule::FuncDef { .. } => self.exec_func_def(node)?,
NdRule::Command { .. } => self.dispatch_cmd(node)?, NdRule::Command { .. } => self.dispatch_cmd(node)?,
NdRule::Test { .. } => self.exec_test(node)?, NdRule::Test { .. } => self.exec_test(node)?,
_ => unreachable!() _ => unreachable!(),
} }
Ok(()) Ok(())
} }
pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> { pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> {
let Some(cmd) = node.get_command() else { let Some(cmd) = node.get_command() else {
return self.exec_cmd(node) // Argv is empty, probably an assignment return self.exec_cmd(node); // Argv is empty, probably an assignment
}; };
if cmd.flags.contains(TkFlags::BUILTIN) { if cmd.flags.contains(TkFlags::BUILTIN) {
self.exec_builtin(node) self.exec_builtin(node)
@@ -120,9 +159,17 @@ impl Dispatcher {
let status = state::get_status(); let status = state::get_status();
match operator { match operator {
ConjunctOp::And => if status != 0 { break }, ConjunctOp::And => {
ConjunctOp::Or => if status == 0 { break }, if status != 0 {
ConjunctOp::Null => break break;
}
}
ConjunctOp::Or => {
if status == 0 {
break;
}
}
ConjunctOp::Null => break,
} }
} }
Ok(()) Ok(())
@@ -145,13 +192,11 @@ impl Dispatcher {
let name = name.span.as_str().strip_suffix("()").unwrap(); let name = name.span.as_str().strip_suffix("()").unwrap();
if KEYWORDS.contains(&name) { if KEYWORDS.contains(&name) {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("function: Forbidden function name `{name}`"), format!("function: Forbidden function name `{name}`"),
blame blame,
) ));
)
} }
let mut func_parser = ParsedSrc::new(Arc::new(body)); let mut func_parser = ParsedSrc::new(Arc::new(body));
@@ -159,7 +204,7 @@ impl Dispatcher {
for error in errors { for error in errors {
eprintln!("{error}"); eprintln!("{error}");
} }
return Ok(()) return Ok(());
} }
let func = ShFunc::new(func_parser); let func = ShFunc::new(func_parser);
@@ -181,7 +226,7 @@ impl Dispatcher {
if let Err(e) = exec_input(subsh_body, None) { if let Err(e) = exec_input(subsh_body, None) {
restore_snapshot(snapshot); restore_snapshot(snapshot);
return Err(e) return Err(e);
} }
restore_snapshot(snapshot); restore_snapshot(snapshot);
@@ -189,7 +234,11 @@ impl Dispatcher {
} }
fn exec_func(&mut self, func: Node) -> ShResult<()> { fn exec_func(&mut self, func: Node) -> ShResult<()> {
let blame = func.get_span().clone(); let blame = func.get_span().clone();
let NdRule::Command { assignments, mut argv } = func.class else { let NdRule::Command {
assignments,
mut argv,
} = func.class
else {
unreachable!() unreachable!()
}; };
@@ -215,26 +264,21 @@ impl Dispatcher {
match e.kind() { match e.kind() {
ShErrKind::FuncReturn(code) => { ShErrKind::FuncReturn(code) => {
state::set_status(*code); state::set_status(*code);
return Ok(()) return Ok(());
} }
_ => return { _ => return { Err(e) },
Err(e)
} }
} }
}
// Return to the outer scope // Return to the outer scope
restore_snapshot(snapshot); restore_snapshot(snapshot);
Ok(()) Ok(())
} else { } else {
Err( Err(ShErr::full(
ShErr::full(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("Failed to find function '{}'", func_name), format!("Failed to find function '{}'", func_name),
blame blame,
) ))
)
} }
} }
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> { fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
@@ -253,7 +297,11 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> {
let NdRule::CaseNode { pattern, case_blocks } = case_stmt.class else { let NdRule::CaseNode {
pattern,
case_blocks,
} = case_stmt.class
else {
unreachable!() unreachable!()
}; };
@@ -277,7 +325,7 @@ impl Dispatcher {
for node in &body { for node in &body {
self.dispatch_node(node.clone())?; self.dispatch_node(node.clone())?;
} }
break 'outer break 'outer;
} }
} }
} }
@@ -291,7 +339,7 @@ impl Dispatcher {
let keep_going = |kind: LoopKind, status: i32| -> bool { let keep_going = |kind: LoopKind, status: i32| -> bool {
match kind { match kind {
LoopKind::While => status == 0, LoopKind::While => status == 0,
LoopKind::Until => status != 0 LoopKind::Until => status != 0,
} }
}; };
@@ -318,18 +366,18 @@ impl Dispatcher {
match e.kind() { match e.kind() {
ShErrKind::LoopBreak(code) => { ShErrKind::LoopBreak(code) => {
state::set_status(*code); state::set_status(*code);
break 'outer break 'outer;
} }
ShErrKind::LoopContinue(code) => { ShErrKind::LoopContinue(code) => {
state::set_status(*code); state::set_status(*code);
continue 'outer continue 'outer;
} }
_ => return Err(e) _ => return Err(e),
} }
} }
} }
} else { } else {
break break;
} }
} }
@@ -348,37 +396,39 @@ impl Dispatcher {
'outer: for chunk in arr.chunks(vars.len()) { 'outer: for chunk in arr.chunks(vars.len()) {
let empty = Tk::default(); let empty = Tk::default();
let chunk_iter = vars.iter().zip( let chunk_iter = vars.iter().zip(
chunk.iter().chain(std::iter::repeat(&empty)) // Or however you define an empty token chunk.iter().chain(std::iter::repeat(&empty)), // Or however you define an empty token
); );
for (var, val) in chunk_iter { for (var, val) in chunk_iter {
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), false)); write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), false));
} }
for node in body.clone() { for node in body.clone() {
self.io_stack.push(body_frame.clone()); self.io_stack.push(body_frame.clone());
if let Err(e) = self.dispatch_node(node) { if let Err(e) = self.dispatch_node(node) {
match e.kind() { match e.kind() {
ShErrKind::LoopBreak(code) => { ShErrKind::LoopBreak(code) => {
state::set_status(*code); state::set_status(*code);
break 'outer break 'outer;
} }
ShErrKind::LoopContinue(code) => { ShErrKind::LoopContinue(code) => {
state::set_status(*code); state::set_status(*code);
continue 'outer continue 'outer;
} }
_ => return Err(e) _ => return Err(e),
} }
} }
} }
} }
Ok(()) Ok(())
} }
fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> {
let NdRule::IfNode { cond_nodes, else_block } = if_stmt.class else { let NdRule::IfNode {
cond_nodes,
else_block,
} = if_stmt.class
else {
unreachable!(); unreachable!();
}; };
// Pop the current frame and split it // Pop the current frame and split it
@@ -404,7 +454,7 @@ impl Dispatcher {
self.dispatch_node(body_node)?; self.dispatch_node(body_node)?;
} }
} }
_ => continue _ => continue,
} }
} }
@@ -423,10 +473,7 @@ impl Dispatcher {
}; };
self.job_stack.new_job(); self.job_stack.new_job();
// Zip the commands and their respective pipes into an iterator // Zip the commands and their respective pipes into an iterator
let pipes_and_cmds = get_pipe_stack(cmds.len()) let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds);
.into_iter()
.zip(cmds);
for ((rpipe, wpipe), cmd) in pipes_and_cmds { for ((rpipe, wpipe), cmd) in pipes_and_cmds {
if let Some(pipe) = rpipe { if let Some(pipe) = rpipe {
@@ -443,7 +490,11 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> { fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
let NdRule::Command { ref mut assignments, ref mut argv } = &mut cmd.class else { let NdRule::Command {
ref mut assignments,
ref mut argv,
} = &mut cmd.class
else {
unreachable!() unreachable!()
}; };
let env_vars_to_unset = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?; let env_vars_to_unset = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?;
@@ -452,17 +503,19 @@ impl Dispatcher {
let io_stack_mut = &mut self.io_stack; let io_stack_mut = &mut self.io_stack;
if cmd_raw.as_str() == "builtin" { if cmd_raw.as_str() == "builtin" {
*argv = argv.iter_mut() *argv = argv
.iter_mut()
.skip(1) .skip(1)
.map(|tk| tk.clone()) .map(|tk| tk.clone())
.collect::<Vec<Tk>>(); .collect::<Vec<Tk>>();
return self.exec_builtin(cmd) return self.exec_builtin(cmd);
} else if cmd_raw.as_str() == "command" { } else if cmd_raw.as_str() == "command" {
*argv = argv.iter_mut() *argv = argv
.iter_mut()
.skip(1) .skip(1)
.map(|tk| tk.clone()) .map(|tk| tk.clone())
.collect::<Vec<Tk>>(); .collect::<Vec<Tk>>();
return self.dispatch_cmd(cmd) return self.dispatch_cmd(cmd);
} }
flog!(TRACE, "doing builtin"); flog!(TRACE, "doing builtin");
@@ -484,7 +537,10 @@ impl Dispatcher {
"exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)),
"zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut),
"shopt" => shopt(cmd, io_stack_mut, curr_job_mut), "shopt" => shopt(cmd, io_stack_mut, curr_job_mut),
_ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw.span.as_str()) _ => unimplemented!(
"Have not yet added support for builtin '{}'",
cmd_raw.span.as_str()
),
}; };
for var in env_vars_to_unset { for var in env_vars_to_unset {
@@ -493,7 +549,7 @@ impl Dispatcher {
if let Err(e) = result { if let Err(e) = result {
state::set_status(1); state::set_status(1);
return Err(e) return Err(e);
} }
Ok(()) Ok(())
} }
@@ -512,7 +568,7 @@ impl Dispatcher {
} }
if argv.is_empty() { if argv.is_empty() {
return Ok(()) return Ok(());
} }
self.io_stack.append_to_frame(cmd.redirs); self.io_stack.append_to_frame(cmd.redirs);
@@ -524,7 +580,7 @@ impl Dispatcher {
Some(exec_args), Some(exec_args),
self.job_stack.curr_job_mut().unwrap(), self.job_stack.curr_job_mut().unwrap(),
def_child_action, def_child_action,
def_parent_action def_parent_action,
)?; )?;
for var in env_vars_to_unset { for var in env_vars_to_unset {
@@ -596,7 +652,7 @@ pub fn run_fork<C,P>(
) -> ShResult<()> ) -> ShResult<()>
where where
C: Fn(IoFrame, Option<ExecArgs>), C: Fn(IoFrame, Option<ExecArgs>),
P: Fn(&mut JobBldr,Option<&str>,Pid) -> ShResult<()> P: Fn(&mut JobBldr, Option<&str>, Pid) -> ShResult<()>,
{ {
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
@@ -628,19 +684,11 @@ pub fn def_child_action(mut io_frame: IoFrame, exec_args: Option<ExecArgs>) {
let cmd = cmd.to_str().unwrap().to_string(); let cmd = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => { Errno::ENOENT => {
let err = ShErr::full( let err = ShErr::full(ShErrKind::CmdNotFound(cmd), "", span);
ShErrKind::CmdNotFound(cmd),
"",
span
);
eprintln!("{err}"); eprintln!("{err}");
} }
_ => { _ => {
let err = ShErr::full( let err = ShErr::full(ShErrKind::Errno, format!("{e}"), span);
ShErrKind::Errno,
format!("{e}"),
span
);
eprintln!("{err}"); eprintln!("{err}");
} }
} }
@@ -648,11 +696,7 @@ pub fn def_child_action(mut io_frame: IoFrame, exec_args: Option<ExecArgs>) {
} }
/// The default behavior for the parent process after forking /// The default behavior for the parent process after forking
pub fn def_parent_action( pub fn def_parent_action(job: &mut JobBldr, cmd: Option<&str>, child_pid: Pid) -> ShResult<()> {
job: &mut JobBldr,
cmd: Option<&str>,
child_pid: Pid
) -> ShResult<()> {
let child_pgid = if let Some(pgid) = job.pgid() { let child_pgid = if let Some(pgid) = job.pgid() {
pgid pgid
} else { } else {
@@ -664,7 +708,6 @@ pub fn def_parent_action(
Ok(()) Ok(())
} }
/// Initialize the pipes for a pipeline /// Initialize the pipes for a pipeline
/// The first command gets `(None, WPipe)` /// The first command gets `(None, WPipe)`
/// The last command gets `(RPipe, None)` /// The last command gets `(RPipe, None)`
@@ -691,9 +734,7 @@ pub fn get_pipe_stack(num_cmds: usize) -> Vec<(Option<Redir>,Option<Redir>)> {
} }
pub fn is_func(tk: Option<Tk>) -> bool { pub fn is_func(tk: Option<Tk>) -> bool {
let Some(tk) = tk else { let Some(tk) = tk else { return false };
return false
};
read_logic(|l| l.get_func(&tk.to_string())).is_some() read_logic(|l| l.get_func(&tk.to_string())).is_some()
} }

View File

@@ -1,51 +1,41 @@
use std::{collections::VecDeque, fmt::Display, iter::Peekable, ops::{Bound, Deref, Range, RangeBounds}, str::Chars, sync::Arc}; use std::{
collections::VecDeque,
fmt::Display,
iter::Peekable,
ops::{Bound, Deref, Range, RangeBounds},
str::Chars,
sync::Arc,
};
use bitflags::bitflags; use bitflags::bitflags;
use crate::{builtin::BUILTINS, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::CharDequeUtils}, prelude::*}; use crate::{
builtin::BUILTINS,
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::CharDequeUtils,
},
prelude::*,
};
pub const KEYWORDS: [&str; 16] = [ pub const KEYWORDS: [&str; 16] = [
"if", "if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
"then", "case", "esac", "[[", "]]",
"elif",
"else",
"fi",
"while",
"until",
"select",
"for",
"in",
"do",
"done",
"case",
"esac",
"[[",
"]]"
]; ];
pub const OPENERS: [&str;6] = [ pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
"if",
"while",
"until",
"for",
"select",
"case"
];
/// Span::new(10..20) /// Span::new(10..20)
#[derive(Clone, PartialEq, Default, Debug)] #[derive(Clone, PartialEq, Default, Debug)]
pub struct Span { pub struct Span {
range: Range<usize>, range: Range<usize>,
source: Arc<String> source: Arc<String>,
} }
impl Span { impl Span {
/// New `Span`. Wraps a range and a string slice that it refers to. /// New `Span`. Wraps a range and a string slice that it refers to.
pub fn new(range: Range<usize>, source: Arc<String>) -> Self { pub fn new(range: Range<usize>, source: Arc<String>) -> Self {
Span { Span { range, source }
range,
source,
}
} }
/// Slice the source string at the wrapped range /// Slice the source string at the wrapped range
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
@@ -97,13 +87,18 @@ impl Default for TkRule {
pub struct Tk { pub struct Tk {
pub class: TkRule, pub class: TkRule,
pub span: Span, pub span: Span,
pub flags: TkFlags pub flags: TkFlags,
} }
// There's one impl here and then another in expand.rs which has the expansion logic // There's one impl here and then another in expand.rs which has the expansion
// logic
impl Tk { impl Tk {
pub fn new(class: TkRule, span: Span) -> Self { pub fn new(class: TkRule, span: Span) -> Self {
Self { class, span, flags: TkFlags::empty() } Self {
class,
span,
flags: TkFlags::empty(),
}
} }
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
self.span.as_str() self.span.as_str()
@@ -127,12 +122,11 @@ impl Display for Tk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.class { match &self.class {
TkRule::Expanded { exp } => write!(f, "{}", exp.join(" ")), TkRule::Expanded { exp } => write!(f, "{}", exp.join(" ")),
_ => write!(f,"{}",self.span.as_str()) _ => write!(f, "{}", self.span.as_str()),
} }
} }
} }
bitflags! { bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Default)] #[derive(Debug,Clone,Copy,PartialEq,Default)]
pub struct TkFlags: u32 { pub struct TkFlags: u32 {
@@ -184,7 +178,12 @@ impl LexStream {
pub fn new(source: Arc<String>, flags: LexFlags) -> Self { pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
flog!(TRACE, "new lex stream"); flog!(TRACE, "new lex stream");
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
Self { source, cursor: 0, in_quote: false, flags } Self {
source,
cursor: 0,
in_quote: false,
flags,
}
} }
/// Returns a slice of the source input using the given range /// Returns a slice of the source input using the given range
/// Returns None if the range is out of the bounds of the string slice /// Returns None if the range is out of the bounds of the string slice
@@ -195,17 +194,16 @@ impl LexStream {
/// `LexStream.slice(1..=10)` /// `LexStream.slice(1..=10)`
/// `LexStream.slice(..10)` /// `LexStream.slice(..10)`
/// `LexStream.slice(1..)` /// `LexStream.slice(1..)`
///
pub fn slice<R: RangeBounds<usize>>(&self, range: R) -> Option<&str> { pub fn slice<R: RangeBounds<usize>>(&self, range: R) -> Option<&str> {
let start = match range.start_bound() { let start = match range.start_bound() {
Bound::Included(&start) => start, Bound::Included(&start) => start,
Bound::Excluded(&start) => start + 1, Bound::Excluded(&start) => start + 1,
Bound::Unbounded => 0 Bound::Unbounded => 0,
}; };
let end = match range.end_bound() { let end = match range.end_bound() {
Bound::Included(&end) => end, Bound::Included(&end) => end,
Bound::Excluded(&end) => end + 1, Bound::Excluded(&end) => end + 1,
Bound::Unbounded => self.source.len() Bound::Unbounded => self.source.len(),
}; };
self.source.get(start..end) self.source.get(start..end)
} }
@@ -244,7 +242,7 @@ impl LexStream {
match ch { match ch {
'>' => { '>' => {
if chars.peek() == Some(&'(') { if chars.peek() == Some(&'(') {
return None // It's a process sub return None; // It's a process sub
} }
pos += 1; pos += 1;
if let Some('>') = chars.peek() { if let Some('>') = chars.peek() {
@@ -262,27 +260,24 @@ impl LexStream {
pos += 1; pos += 1;
} }
if !found_fd { if !found_fd {
return Some(Err( return Some(Err(ShErr::full(
ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Invalid redirection", "Invalid redirection",
Span::new(self.cursor..pos, self.source.clone()) Span::new(self.cursor..pos, self.source.clone()),
) )));
));
} else { } else {
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break break;
} }
} else { } else {
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break break;
} }
} }
'<' => { '<' => {
if chars.peek() == Some(&'(') { if chars.peek() == Some(&'(') {
return None // It's a process sub return None; // It's a process sub
} }
pos += 1; pos += 1;
@@ -291,11 +286,11 @@ impl LexStream {
chars.next(); chars.next();
pos += 1; pos += 1;
} else { } else {
break break;
} }
} }
tk = self.get_token(self.cursor..pos, TkRule::Redir); tk = self.get_token(self.cursor..pos, TkRule::Redir);
break break;
} }
'0'..='9' => { '0'..='9' => {
pos += 1; pos += 1;
@@ -311,7 +306,7 @@ impl LexStream {
} }
if tk == Tk::default() { if tk == Tk::default() {
return None return None;
} }
self.cursor = pos; self.cursor = pos;
@@ -330,7 +325,7 @@ impl LexStream {
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern); let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
self.cursor = pos; self.cursor = pos;
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
return Ok(casepat_tk) return Ok(casepat_tk);
} }
} }
@@ -369,10 +364,10 @@ impl LexStream {
pos += 1; pos += 1;
brace_count -= 1; brace_count -= 1;
if brace_count == 0 { if brace_count == 0 {
break break;
} }
} }
_ => pos += ch.len_utf8() _ => pos += ch.len_utf8(),
} }
} }
} }
@@ -397,26 +392,24 @@ impl LexStream {
pos += 1; pos += 1;
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
break break;
} }
} }
_ => pos += ch.len_utf8() _ => pos += ch.len_utf8(),
} }
} }
if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos; self.cursor = pos;
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unclosed subshell", "Unclosed subshell",
Span::new(paren_pos..paren_pos + 1, self.source.clone()) Span::new(paren_pos..paren_pos + 1, self.source.clone()),
) ));
)
} }
let mut proc_sub_tk = self.get_token(self.cursor..pos, TkRule::Str); let mut proc_sub_tk = self.get_token(self.cursor..pos, TkRule::Str);
proc_sub_tk.flags |= TkFlags::IS_PROCSUB; proc_sub_tk.flags |= TkFlags::IS_PROCSUB;
self.cursor = pos; self.cursor = pos;
return Ok(proc_sub_tk) return Ok(proc_sub_tk);
} }
'>' if chars.peek() == Some(&'(') => { '>' if chars.peek() == Some(&'(') => {
pos += 2; pos += 2;
@@ -439,26 +432,24 @@ impl LexStream {
pos += 1; pos += 1;
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
break break;
} }
} }
_ => pos += ch.len_utf8() _ => pos += ch.len_utf8(),
} }
} }
if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos; self.cursor = pos;
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unclosed subshell", "Unclosed subshell",
Span::new(paren_pos..paren_pos + 1, self.source.clone()) Span::new(paren_pos..paren_pos + 1, self.source.clone()),
) ));
)
} }
let mut proc_sub_tk = self.get_token(self.cursor..pos, TkRule::Str); let mut proc_sub_tk = self.get_token(self.cursor..pos, TkRule::Str);
proc_sub_tk.flags |= TkFlags::IS_PROCSUB; proc_sub_tk.flags |= TkFlags::IS_PROCSUB;
self.cursor = pos; self.cursor = pos;
return Ok(proc_sub_tk) return Ok(proc_sub_tk);
} }
'$' if chars.peek() == Some(&'(') => { '$' if chars.peek() == Some(&'(') => {
pos += 2; pos += 2;
@@ -481,26 +472,24 @@ impl LexStream {
pos += 1; pos += 1;
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
break break;
} }
} }
_ => pos += ch.len_utf8() _ => pos += ch.len_utf8(),
} }
} }
if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos; self.cursor = pos;
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unclosed subshell", "Unclosed subshell",
Span::new(paren_pos..paren_pos + 1, self.source.clone()) Span::new(paren_pos..paren_pos + 1, self.source.clone()),
) ));
)
} }
let mut cmdsub_tk = self.get_token(self.cursor..pos, TkRule::Str); let mut cmdsub_tk = self.get_token(self.cursor..pos, TkRule::Str);
cmdsub_tk.flags |= TkFlags::IS_CMDSUB; cmdsub_tk.flags |= TkFlags::IS_CMDSUB;
self.cursor = pos; self.cursor = pos;
return Ok(cmdsub_tk) return Ok(cmdsub_tk);
} }
'(' if self.next_is_cmd() && can_be_subshell => { '(' if self.next_is_cmd() && can_be_subshell => {
pos += 1; pos += 1;
@@ -522,28 +511,26 @@ impl LexStream {
pos += 1; pos += 1;
paren_count -= 1; paren_count -= 1;
if paren_count <= 0 { if paren_count <= 0 {
break break;
} }
} }
_ => pos += ch.len_utf8() _ => pos += ch.len_utf8(),
} }
} }
if paren_count != 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if paren_count != 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos; self.cursor = pos;
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unclosed subshell", "Unclosed subshell",
Span::new(paren_pos..paren_pos + 1, self.source.clone()) Span::new(paren_pos..paren_pos + 1, self.source.clone()),
) ));
)
} }
let mut subsh_tk = self.get_token(self.cursor..pos, TkRule::Str); let mut subsh_tk = self.get_token(self.cursor..pos, TkRule::Str);
subsh_tk.flags |= TkFlags::IS_CMD; subsh_tk.flags |= TkFlags::IS_CMD;
subsh_tk.flags |= TkFlags::IS_SUBSH; subsh_tk.flags |= TkFlags::IS_SUBSH;
self.cursor = pos; self.cursor = pos;
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
return Ok(subsh_tk) return Ok(subsh_tk);
} }
'{' if pos == self.cursor && self.next_is_cmd() => { '{' if pos == self.cursor && self.next_is_cmd() => {
pos += 1; pos += 1;
@@ -553,7 +540,7 @@ impl LexStream {
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
self.cursor = pos; self.cursor = pos;
return Ok(tk) return Ok(tk);
} }
'}' if pos == self.cursor && self.in_brc_grp() => { '}' if pos == self.cursor && self.in_brc_grp() => {
pos += 1; pos += 1;
@@ -561,7 +548,7 @@ impl LexStream {
self.set_in_brc_grp(false); self.set_in_brc_grp(false);
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
self.cursor = pos; self.cursor = pos;
return Ok(tk) return Ok(tk);
} }
'\'' => { '\'' => {
self.in_quote = true; self.in_quote = true;
@@ -577,13 +564,13 @@ impl LexStream {
_ if q_ch == '\'' => { _ if q_ch == '\'' => {
pos += 1; pos += 1;
self.in_quote = false; self.in_quote = false;
break break;
} }
// Any time an ambiguous character is found // Any time an ambiguous character is found
// we must push the cursor by the length of the character // we must push the cursor by the length of the character
// instead of just assuming a length of 1. // instead of just assuming a length of 1.
// Allows spans to work for wide characters // Allows spans to work for wide characters
_ => pos += q_ch.len_utf8() _ => pos += q_ch.len_utf8(),
} }
} }
} }
@@ -619,7 +606,7 @@ impl LexStream {
cmdsub_count -= 1; cmdsub_count -= 1;
pos += 1; pos += 1;
if cmdsub_count <= 0 { if cmdsub_count <= 0 {
break break;
} }
} }
_ => pos += cmdsub_ch.len_utf8(), _ => pos += cmdsub_ch.len_utf8(),
@@ -629,7 +616,7 @@ impl LexStream {
_ if q_ch == '"' => { _ if q_ch == '"' => {
pos += 1; pos += 1;
self.in_quote = false; self.in_quote = false;
break break;
} }
// Any time an ambiguous character is found // Any time an ambiguous character is found
// we must push the cursor by the length of the character // we must push the cursor by the length of the character
@@ -641,18 +628,16 @@ impl LexStream {
} }
_ if !self.in_quote && is_op(ch) => break, _ if !self.in_quote && is_op(ch) => break,
_ if is_hard_sep(ch) => break, _ if is_hard_sep(ch) => break,
_ => pos += ch.len_utf8() _ => pos += ch.len_utf8(),
} }
} }
let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str);
if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
return Err( return Err(ShErr::full(
ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unterminated quote", "Unterminated quote",
new_tk.span, new_tk.span,
) ));
);
} }
let text = new_tk.span.as_str(); let text = new_tk.span.as_str();
@@ -712,25 +697,25 @@ impl Iterator for LexStream {
if self.cursor == self.source.len() { if self.cursor == self.source.len() {
if self.flags.contains(LexFlags::STALE) { if self.flags.contains(LexFlags::STALE) {
// We've already returned an EOI token, nothing left to do // We've already returned an EOI token, nothing left to do
return None return None;
} else { } else {
// Return the EOI token // Return the EOI token
let token = self.get_token(self.cursor..self.cursor, TkRule::EOI); let token = self.get_token(self.cursor..self.cursor, TkRule::EOI);
self.flags |= LexFlags::STALE; self.flags |= LexFlags::STALE;
return Some(Ok(token)) return Some(Ok(token));
} }
} }
// Return the SOI token // Return the SOI token
if self.flags.contains(LexFlags::FRESH) { if self.flags.contains(LexFlags::FRESH) {
self.flags &= !LexFlags::FRESH; self.flags &= !LexFlags::FRESH;
let token = self.get_token(self.cursor..self.cursor, TkRule::SOI); let token = self.get_token(self.cursor..self.cursor, TkRule::SOI);
return Some(Ok(token)) return Some(Ok(token));
} }
// If we are just reading raw words, short circuit here // If we are just reading raw words, short circuit here
// Used for word splitting variable values // Used for word splitting variable values
if self.flags.contains(LexFlags::RAW) { if self.flags.contains(LexFlags::RAW) {
return Some(self.read_string()) return Some(self.read_string());
} }
loop { loop {
@@ -740,12 +725,12 @@ impl Iterator for LexStream {
} else if pos < self.source.len() && is_field_sep(get_char(&self.source, pos).unwrap()) { } else if pos < self.source.len() && is_field_sep(get_char(&self.source, pos).unwrap()) {
self.cursor += 1; self.cursor += 1;
} else { } else {
break break;
} }
} }
if self.cursor == self.source.len() { if self.cursor == self.source.len() {
return None return None;
} }
let token = match get_char(&self.source, self.cursor).unwrap() { let token = match get_char(&self.source, self.cursor).unwrap() {
@@ -755,10 +740,11 @@ impl Iterator for LexStream {
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
while let Some(ch) = get_char(&self.source, self.cursor) { while let Some(ch) = get_char(&self.source, self.cursor) {
if is_hard_sep(ch) { // Combine consecutive separators into one, including whitespace if is_hard_sep(ch) {
// Combine consecutive separators into one, including whitespace
self.cursor += 1; self.cursor += 1;
} else { } else {
break break;
} }
} }
self.get_token(ch_idx..self.cursor, TkRule::Sep) self.get_token(ch_idx..self.cursor, TkRule::Sep)
@@ -770,7 +756,7 @@ impl Iterator for LexStream {
while let Some(ch) = get_char(&self.source, self.cursor) { while let Some(ch) = get_char(&self.source, self.cursor) {
self.cursor += 1; self.cursor += 1;
if ch == '\n' { if ch == '\n' {
break break;
} }
} }
@@ -811,14 +797,14 @@ impl Iterator for LexStream {
self.set_next_is_cmd(false); self.set_next_is_cmd(false);
match tk { match tk {
Ok(tk) => tk, Ok(tk) => tk,
Err(e) => return Some(Err(e)) Err(e) => return Some(Err(e)),
} }
} else { } else {
match self.read_string() { match self.read_string() {
Ok(tk) => tk, Ok(tk) => tk,
Err(e) => { Err(e) => {
flog!(ERROR, e); flog!(ERROR, e);
return Some(Err(e)) return Some(Err(e));
} }
} }
} }
@@ -828,7 +814,6 @@ impl Iterator for LexStream {
} }
} }
pub fn get_char(src: &str, idx: usize) -> Option<char> { pub fn get_char(src: &str, idx: usize) -> Option<char> {
src.get(idx..)?.chars().next() src.get(idx..)?.chars().next()
} }
@@ -838,9 +823,11 @@ pub fn is_assignment(text: &str) -> bool {
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' => { chars.next(); } '\\' => {
chars.next();
}
'=' => return true, '=' => return true,
_ => continue _ => continue,
} }
} }
false false
@@ -862,8 +849,7 @@ pub fn is_field_sep(ch: char) -> bool {
} }
pub fn is_keyword(slice: &str) -> bool { pub fn is_keyword(slice: &str) -> bool {
KEYWORDS.contains(&slice) || KEYWORDS.contains(&slice) || (slice.ends_with("()") && !slice.ends_with("\\()"))
(slice.ends_with("()") && !slice.ends_with("\\()"))
} }
pub fn is_cmd_sub(slice: &str) -> bool { pub fn is_cmd_sub(slice: &str) -> bool {
@@ -879,7 +865,7 @@ pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> {
char_deque.pop_front(); char_deque.pop_front();
} }
if char_deque.starts_with(pat) { if char_deque.starts_with(pat) {
return Some(pos) return Some(pos);
} }
pos += 1; pos += 1;
} }
@@ -892,7 +878,9 @@ pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
pos += 1; pos += 1;
match ch { match ch {
_ if is_hard_sep(ch) => return None, _ if is_hard_sep(ch) => return None,
'\\' => { chars.next(); } '\\' => {
chars.next();
}
')' => return Some(pos), ')' => return Some(pos),
'(' => return None, '(' => return None,
_ => { /* continue */ } _ => { /* continue */ }

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,37 @@
// Standard Library Common IO and FS Abstractions // Standard Library Common IO and FS Abstractions
pub use std::io::{
self,
BufRead,
BufReader,
BufWriter,
Error,
ErrorKind,
Read,
Seek,
SeekFrom,
Write,
};
pub use std::fs::{ self, File, OpenOptions };
pub use std::path::{ Path, PathBuf };
pub use std::ffi::{ CStr, CString };
pub use std::process::exit;
pub use std::time::Instant;
pub use std::sync::Arc;
pub use std::mem;
pub use std::env; pub use std::env;
pub use std::ffi::{CStr, CString};
pub use std::fmt; pub use std::fmt;
pub use std::fs::{self, File, OpenOptions};
pub use std::io::{
self, BufRead, BufReader, BufWriter, Error, ErrorKind, Read, Seek, SeekFrom, Write,
};
pub use std::mem;
pub use std::path::{Path, PathBuf};
pub use std::process::exit;
pub use std::sync::Arc;
pub use std::time::Instant;
// Unix-specific IO abstractions // Unix-specific IO abstractions
pub use std::os::unix::io::{ AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd, }; pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
// Nix crate for POSIX APIs // Nix crate for POSIX APIs
pub use bitflags::bitflags;
pub use nix::{ pub use nix::{
errno::Errno, errno::Errno,
fcntl::{open, OFlag}, fcntl::{open, OFlag},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{ sys::{
termios::{ self }, signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
signal::{ self, signal, kill, killpg, pthread_sigmask, SigSet, SigmaskHow, SigHandler, Signal },
stat::Mode, stat::Mode,
termios::{self},
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat}, wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
}, },
libc::{ self, STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO },
unistd::{ unistd::{
dup, read, isatty, write, close, setpgid, dup2, getpgrp, getpgid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
execvpe, tcgetpgrp, tcsetpgrp, fork, pipe, Pid, ForkResult tcsetpgrp, write, ForkResult, Pid,
}, },
}; };
pub use bitflags::bitflags;
pub use crate::flog; pub use crate::flog;
pub use crate::libsh::flog::FernLogLevel::*; pub use crate::libsh::flog::FernLogLevel::*;

View File

@@ -1,16 +1,39 @@
use std::{fmt::Debug, ops::{Deref, DerefMut}}; use std::{
fmt::Debug,
ops::{Deref, DerefMut},
};
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, utils::RedirVecUtils}, parse::{get_redir_file, Redir, RedirType}, prelude::*}; use crate::{
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::RedirVecUtils,
},
parse::{get_redir_file, Redir, RedirType},
prelude::*,
};
// Credit to fish-shell for many of the implementation ideas present in this module // Credit to fish-shell for many of the implementation ideas present in this
// https://fishshell.com/ // module https://fishshell.com/
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum IoMode { pub enum IoMode {
Fd { tgt_fd: RawFd, src_fd: Arc<OwnedFd> }, Fd {
File { tgt_fd: RawFd, path: PathBuf, mode: RedirType }, tgt_fd: RawFd,
Pipe { tgt_fd: RawFd, pipe: Arc<OwnedFd> }, src_fd: Arc<OwnedFd>,
Buffer { buf: String, pipe: Arc<OwnedFd> } },
File {
tgt_fd: RawFd,
path: PathBuf,
mode: RedirType,
},
Pipe {
tgt_fd: RawFd,
pipe: Arc<OwnedFd>,
},
Buffer {
buf: String,
pipe: Arc<OwnedFd>,
},
} }
impl IoMode { impl IoMode {
@@ -27,10 +50,10 @@ impl IoMode {
} }
pub fn tgt_fd(&self) -> RawFd { pub fn tgt_fd(&self) -> RawFd {
match self { match self {
IoMode::Fd { tgt_fd, .. } | IoMode::Fd { tgt_fd, .. } | IoMode::File { tgt_fd, .. } | IoMode::Pipe { tgt_fd, .. } => {
IoMode::File { tgt_fd, .. } | *tgt_fd
IoMode::Pipe { tgt_fd, .. } => *tgt_fd, }
_ => panic!() _ => panic!(),
} }
} }
pub fn src_fd(&self) -> RawFd { pub fn src_fd(&self) -> RawFd {
@@ -38,21 +61,30 @@ impl IoMode {
IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(), IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(),
IoMode::File { .. } => panic!("Attempted to obtain src_fd from file before opening"), IoMode::File { .. } => panic!("Attempted to obtain src_fd from file before opening"),
IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(), IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(),
_ => panic!() _ => panic!(),
} }
} }
pub fn open_file(mut self) -> ShResult<Self> { pub fn open_file(mut self) -> ShResult<Self> {
if let IoMode::File { tgt_fd, path, mode } = self { if let IoMode::File { tgt_fd, path, mode } = self {
let file = get_redir_file(mode, path)?; let file = get_redir_file(mode, path)?;
self = IoMode::Fd { tgt_fd, src_fd: Arc::new(OwnedFd::from(file)) } self = IoMode::Fd {
tgt_fd,
src_fd: Arc::new(OwnedFd::from(file)),
}
} }
Ok(self) Ok(self)
} }
pub fn get_pipes() -> (Self, Self) { pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = pipe().unwrap(); let (rpipe, wpipe) = pipe().unwrap();
( (
Self::Pipe { tgt_fd: STDIN_FILENO, pipe: rpipe.into() }, Self::Pipe {
Self::Pipe { tgt_fd: STDOUT_FILENO, pipe: wpipe.into() } tgt_fd: STDIN_FILENO,
pipe: rpipe.into(),
},
Self::Pipe {
tgt_fd: STDOUT_FILENO,
pipe: wpipe.into(),
},
) )
} }
} }
@@ -102,13 +134,13 @@ impl<R: Read> IoBuf<R> {
/// Get current buffer contents as a string (if valid UTF-8) /// Get current buffer contents as a string (if valid UTF-8)
pub fn as_str(&self) -> ShResult<&str> { pub fn as_str(&self) -> ShResult<&str> {
std::str::from_utf8(&self.buf).map_err(|_| { std::str::from_utf8(&self.buf)
ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf") .map_err(|_| ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf"))
})
} }
} }
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and `stderr` respectively /// A struct wrapping three fildescs representing `stdin`, `stdout`, and
/// `stderr` respectively
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct IoGroup(RawFd, RawFd, RawFd); pub struct IoGroup(RawFd, RawFd, RawFd);
@@ -125,24 +157,34 @@ impl<'e> IoFrame {
Default::default() Default::default()
} }
pub fn from_redirs(redirs: Vec<Redir>) -> Self { pub fn from_redirs(redirs: Vec<Redir>) -> Self {
Self { redirs, saved_io: None } Self {
redirs,
saved_io: None,
}
} }
pub fn from_redir(redir: Redir) -> Self { pub fn from_redir(redir: Redir) -> Self {
Self { redirs: vec![redir], saved_io: None } Self {
redirs: vec![redir],
saved_io: None,
}
} }
/// Splits the frame into two frames /// Splits the frame into two frames
/// ///
/// One frame contains input redirections, the other contains output redirections /// One frame contains input redirections, the other contains output
/// This is used in shell structures to route redirections either *to* the condition, or *from* the body /// redirections This is used in shell structures to route redirections
/// The first field of the tuple contains input redirections (used for the condition) /// either *to* the condition, or *from* the body The first field of the
/// The second field contains output redirections (used for the body) /// tuple contains input redirections (used for the condition) The second
/// field contains output redirections (used for the body)
pub fn split_frame(self) -> (Self, Self) { pub fn split_frame(self) -> (Self, Self) {
let Self { redirs, saved_io: _ } = self; let Self {
redirs,
saved_io: _,
} = self;
let (input_redirs, output_redirs) = redirs.split_by_channel(); let (input_redirs, output_redirs) = redirs.split_by_channel();
( (
Self::from_redirs(input_redirs), Self::from_redirs(input_redirs),
Self::from_redirs(output_redirs) Self::from_redirs(output_redirs),
) )
} }
pub fn save(&'e mut self) { pub fn save(&'e mut self) {
@@ -195,9 +237,11 @@ impl DerefMut for IoFrame {
/// A stack that maintains the current state of I/O for commands /// A stack that maintains the current state of I/O for commands
/// ///
/// This struct maintains the current state of I/O for the `Dispatcher` struct /// This struct maintains the current state of I/O for the `Dispatcher` struct
/// Each executed command requires an `IoFrame` in order to perform redirections. /// Each executed command requires an `IoFrame` in order to perform
/// As nodes are walked through by the `Dispatcher`, it pushes new frames in certain contexts, and pops frames in others. /// redirections. As nodes are walked through by the `Dispatcher`, it pushes new
/// Each command calls pop_frame() in order to get the current IoFrame in order to perform redirection /// frames in certain contexts, and pops frames in others. Each command calls
/// pop_frame() in order to get the current IoFrame in order to perform
/// redirection
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct IoStack { pub struct IoStack {
stack: Vec<IoFrame>, stack: Vec<IoFrame>,
@@ -223,8 +267,9 @@ impl IoStack {
} }
/// Pop the current stack frame /// Pop the current stack frame
/// This differs from using `pop()` because it always returns a stack frame /// This differs from using `pop()` because it always returns a stack frame
/// If `self.pop()` would empty the `IoStack`, it instead uses `std::mem::take()` to take the last frame /// If `self.pop()` would empty the `IoStack`, it instead uses
/// There will always be at least one frame in the `IoStack`. /// `std::mem::take()` to take the last frame There will always be at least
/// one frame in the `IoStack`.
pub fn pop_frame(&mut self) -> IoFrame { pub fn pop_frame(&mut self) -> IoFrame {
if self.stack.len() > 1 { if self.stack.len() > 1 {
self.pop().unwrap() self.pop().unwrap()
@@ -238,7 +283,8 @@ impl IoStack {
} }
/// Flatten the `IoStack` /// Flatten the `IoStack`
/// All of the current stack frames will be flattened into a single one /// All of the current stack frames will be flattened into a single one
/// Not sure what use this will serve, but my gut said this was worthy of writing /// Not sure what use this will serve, but my gut said this was worthy of
/// writing
pub fn flatten(&mut self) { pub fn flatten(&mut self) {
let mut flat_frame = IoFrame::new(); let mut flat_frame = IoFrame::new();
while let Some(mut frame) = self.pop() { while let Some(mut frame) = self.pop() {

View File

@@ -0,0 +1 @@

View File

@@ -1,11 +1,14 @@
pub mod readline;
pub mod highlight; pub mod highlight;
pub mod readline;
use std::path::Path; use std::path::Path;
use readline::{FernVi, Readline}; use readline::{FernVi, Readline};
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode, state::read_shopts}; use crate::{
expand::expand_prompt, libsh::error::ShResult, prelude::*, shopt::FernEditMode,
state::read_shopts,
};
/// Initialize the line editor /// Initialize the line editor
fn get_prompt() -> ShResult<String> { fn get_prompt() -> ShResult<String> {
@@ -15,8 +18,9 @@ fn get_prompt() -> ShResult<String> {
// username@hostname // username@hostname
// short/path/to/pwd/ // short/path/to/pwd/
// $ _ // $ _
let default = "\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; let default =
return expand_prompt(default) "\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
return expand_prompt(default);
}; };
expand_prompt(&prompt) expand_prompt(&prompt)
@@ -26,7 +30,7 @@ pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
let prompt = get_prompt()?; let prompt = get_prompt()?;
let mut reader: Box<dyn Readline> = match edit_mode { let mut reader: Box<dyn Readline> = match edit_mode {
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?), FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
FernEditMode::Emacs => todo!() FernEditMode::Emacs => todo!(),
}; };
reader.readline() reader.readline()
} }

View File

@@ -1,4 +1,12 @@
use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}}; use std::{
env,
fmt::{Display, Write},
fs::{self, OpenOptions},
io::Write as IoWrite,
path::{Path, PathBuf},
str::FromStr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::prelude::*; use crate::prelude::*;
@@ -9,7 +17,7 @@ use super::vicmd::Direction; // surprisingly useful
pub enum SearchKind { pub enum SearchKind {
Fuzzy, Fuzzy,
#[default] #[default]
Prefix Prefix,
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
@@ -29,7 +37,7 @@ pub struct HistEntry {
id: u32, id: u32,
timestamp: SystemTime, timestamp: SystemTime,
command: String, command: String,
new: bool new: bool,
} }
impl HistEntry { impl HistEntry {
@@ -69,22 +77,39 @@ impl HistEntry {
impl FromStr for HistEntry { impl FromStr for HistEntry {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = Err( let err = Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] } kind: ShErrKind::HistoryReadErr,
); msg: format!("Bad formatting on history entry '{s}'"),
notes: vec![],
});
//: 248972349;148;echo foo; echo bar //: 248972349;148;echo foo; echo bar
let Some(cleaned) = s.strip_prefix(": ") else { return err }; let Some(cleaned) = s.strip_prefix(": ") else {
return err;
};
//248972349;148;echo foo; echo bar //248972349;148;echo foo; echo bar
let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err }; let Some((timestamp, id_and_command)) = cleaned.split_once(';') else {
return err;
};
//("248972349","148;echo foo; echo bar") //("248972349","148;echo foo; echo bar")
let Some((id,command)) = id_and_command.split_once(';') else { return err }; let Some((id, command)) = id_and_command.split_once(';') else {
return err;
};
//("148","echo foo; echo bar") //("148","echo foo; echo bar")
let Ok(ts_seconds) = timestamp.parse::<u64>() else { return err }; let Ok(ts_seconds) = timestamp.parse::<u64>() else {
let Ok(id) = id.parse::<u32>() else { return err }; return err;
};
let Ok(id) = id.parse::<u32>() else {
return err;
};
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds); let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
let command = command.to_string(); let command = command.to_string();
Ok(Self { id, timestamp, command, new: false }) Ok(Self {
id,
timestamp,
command,
new: false,
})
} }
} }
@@ -92,7 +117,12 @@ impl Display for HistEntry {
/// Similar to zsh's history format, but not entirely /// Similar to zsh's history format, but not entirely
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let command = self.with_escaped_newlines(); let command = self.with_escaped_newlines();
let HistEntry { id, timestamp, command: _, new: _ } = self; let HistEntry {
id,
timestamp,
command: _,
new: _,
} = self;
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs(); let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
writeln!(f, ": {timestamp};{id};{command}") writeln!(f, ": {timestamp};{id};{command}")
} }
@@ -100,7 +130,6 @@ impl Display for HistEntry {
pub struct HistEntries(Vec<HistEntry>); pub struct HistEntries(Vec<HistEntry>);
impl FromStr for HistEntries { impl FromStr for HistEntries {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -111,9 +140,11 @@ impl FromStr for HistEntries {
while let Some((i, line)) = lines.next() { while let Some((i, line)) = lines.next() {
if !line.starts_with(": ") { if !line.starts_with(": ") {
return Err( return Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } kind: ShErrKind::HistoryReadErr,
) msg: format!("Bad formatting on line {i}"),
notes: vec![],
});
} }
let mut chars = line.chars().peekable(); let mut chars = line.chars().peekable();
let mut feeding_lines = true; let mut feeding_lines = true;
@@ -129,9 +160,7 @@ impl FromStr for HistEntries {
feeding_lines = true; feeding_lines = true;
} }
} }
'\n' => { '\n' => break,
break
}
_ => { _ => {
cur_line.push(ch); cur_line.push(ch);
} }
@@ -139,9 +168,11 @@ impl FromStr for HistEntries {
} }
if feeding_lines { if feeding_lines {
let Some((_, line)) = lines.next() else { let Some((_, line)) = lines.next() else {
return Err( return Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } kind: ShErrKind::HistoryReadErr,
) msg: format!("Bad formatting on line {i}"),
notes: vec![],
});
}; };
chars = line.chars().peekable(); chars = line.chars().peekable();
} }
@@ -151,7 +182,6 @@ impl FromStr for HistEntries {
cur_line.clear(); cur_line.clear();
} }
Ok(Self(entries)) Ok(Self(entries))
} }
} }
@@ -185,7 +215,12 @@ impl History {
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0); let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
let timestamp = SystemTime::now(); let timestamp = SystemTime::now();
let command = "".into(); let command = "".into();
entries.push(HistEntry { id, timestamp, command, new: true }) entries.push(HistEntry {
id,
timestamp,
command,
new: true,
})
} }
let search_mask = entries.clone(); let search_mask = entries.clone();
let cursor = entries.len() - 1; let cursor = entries.len() - 1;
@@ -210,20 +245,19 @@ impl History {
&self.search_mask &self.search_mask
} }
pub fn push_empty_entry(&mut self) { pub fn push_empty_entry(&mut self) {}
}
pub fn cursor_entry(&self) -> Option<&HistEntry> { pub fn cursor_entry(&self) -> Option<&HistEntry> {
self.search_mask.get(self.cursor) self.search_mask.get(self.cursor)
} }
pub fn update_pending_cmd(&mut self, command: &str) { pub fn update_pending_cmd(&mut self, command: &str) {
let Some(ent) = self.last_mut() else { let Some(ent) = self.last_mut() else { return };
return
};
let cmd = command.to_string(); let cmd = command.to_string();
let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() }; let constraint = SearchConstraint {
kind: SearchKind::Prefix,
term: cmd.clone(),
};
ent.command = cmd; ent.command = cmd;
self.constrain_entries(constraint); self.constrain_entries(constraint);
@@ -235,7 +269,7 @@ impl History {
pub fn get_new_id(&self) -> u32 { pub fn get_new_id(&self) -> u32 {
let Some(ent) = self.entries.last() else { let Some(ent) = self.entries.last() else {
return 0 return 0;
}; };
ent.id + 1 ent.id + 1
} }
@@ -256,7 +290,8 @@ impl History {
if term.is_empty() { if term.is_empty() {
self.search_mask = self.entries.clone(); self.search_mask = self.entries.clone();
} else { } else {
let filtered = self.entries let filtered = self
.entries
.clone() .clone()
.into_iter() .into_iter()
.filter(|ent| ent.command().starts_with(&term)); .filter(|ent| ent.command().starts_with(&term));
@@ -275,7 +310,10 @@ impl History {
} }
pub fn get_hint(&self) -> Option<String> { pub fn get_hint(&self) -> Option<String> {
if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) { if self
.cursor_entry()
.is_some_and(|ent| ent.is_new() && !ent.command().is_empty())
{
let entry = self.hint_entry()?; let entry = self.hint_entry()?;
let prefix = self.cursor_entry()?.command(); let prefix = self.cursor_entry()?.command();
Some(entry.command().to_string()) Some(entry.command().to_string())
@@ -285,7 +323,8 @@ impl History {
} }
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
let new_idx = self.cursor let new_idx = self
.cursor
.saturating_add_signed(offset) .saturating_add_signed(offset)
.clamp(0, self.search_mask.len().saturating_sub(1)); .clamp(0, self.search_mask.len().saturating_sub(1));
let ent = self.search_mask.get(new_idx)?; let ent = self.search_mask.get(new_idx)?;
@@ -299,14 +338,19 @@ impl History {
let timestamp = SystemTime::now(); let timestamp = SystemTime::now();
let id = self.get_new_id(); let id = self.get_new_id();
if self.ignore_dups && self.is_dup(&command) { if self.ignore_dups && self.is_dup(&command) {
return return;
} }
self.entries.push(HistEntry { id, timestamp, command, new: true }); self.entries.push(HistEntry {
id,
timestamp,
command,
new: true,
});
} }
pub fn is_dup(&self, other: &str) -> bool { pub fn is_dup(&self, other: &str) -> bool {
let Some(ent) = self.entries.last() else { let Some(ent) = self.entries.last() else {
return false return false;
}; };
let ent_cmd = &ent.command; let ent_cmd = &ent.command;
ent_cmd == other ent_cmd == other
@@ -318,19 +362,18 @@ impl History {
.append(true) .append(true)
.open(&self.path)?; .open(&self.path)?;
let last_file_entry = self.entries let last_file_entry = self
.entries
.iter() .iter()
.filter(|ent| !ent.new) .filter(|ent| !ent.new)
.next_back() .next_back()
.map(|ent| ent.command.clone()) .map(|ent| ent.command.clone())
.unwrap_or_default(); .unwrap_or_default();
let entries = self.entries let entries = self.entries.iter_mut().filter(|ent| {
.iter_mut() ent.new
.filter(|ent| { && !ent.command.is_empty()
ent.new && && if self.ignore_dups {
!ent.command.is_empty() &&
if self.ignore_dups {
ent.command() != last_file_entry ent.command() != last_file_entry
} else { } else {
true true

View File

@@ -6,7 +6,6 @@ use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct KeyEvent(pub KeyCode, pub ModKeys); pub struct KeyEvent(pub KeyCode, pub ModKeys);
impl KeyEvent { impl KeyEvent {
pub fn new(ch: &str, mut mods: ModKeys) -> Self { pub fn new(ch: &str, mut mods: ModKeys) -> Self {
use {KeyCode as K, KeyEvent as E, ModKeys as M}; use {KeyCode as K, KeyEvent as E, ModKeys as M};
@@ -29,8 +28,7 @@ impl KeyEvent {
let is_single_char = chars.next().is_none(); let is_single_char = chars.next().is_none();
match single_char { match single_char {
Some(c) if is_single_char && c.is_control() => { Some(c) if is_single_char && c.is_control() => match c {
match c {
'\x00' => E(K::Char('@'), mods | M::CTRL), '\x00' => E(K::Char('@'), mods | M::CTRL),
'\x01' => E(K::Char('A'), mods | M::CTRL), '\x01' => E(K::Char('A'), mods | M::CTRL),
'\x02' => E(K::Char('B'), mods | M::CTRL), '\x02' => E(K::Char('B'), mods | M::CTRL),
@@ -73,8 +71,7 @@ impl KeyEvent {
'\x7f' => E(K::Backspace, mods), '\x7f' => E(K::Backspace, mods),
'\u{9b}' => E(K::Esc, mods | M::SHIFT), '\u{9b}' => E(K::Esc, mods | M::SHIFT),
_ => E(K::Null, mods), _ => E(K::Null, mods),
} },
}
Some(c) if is_single_char => { Some(c) if is_single_char => {
if !mods.is_empty() { if !mods.is_empty() {
mods.remove(M::SHIFT); mods.remove(M::SHIFT);

View File

@@ -0,0 +1 @@

File diff suppressed because it is too large Load Diff

View File

@@ -6,17 +6,21 @@ use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, Te
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}}; use crate::libsh::{
error::{ShErr, ShErrKind, ShResult},
sys::sh_quit,
term::{Style, Styled},
};
use crate::prelude::*; use crate::prelude::*;
pub mod term;
pub mod linebuf;
pub mod layout;
pub mod keys;
pub mod vicmd;
pub mod register;
pub mod vimode;
pub mod history; pub mod history;
pub mod keys;
pub mod layout;
pub mod linebuf;
pub mod register;
pub mod term;
pub mod vicmd;
pub mod vimode;
// Very useful for testing // Very useful for testing
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."; const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
@@ -34,7 +38,7 @@ pub struct FernVi {
pub repeat_action: Option<CmdReplay>, pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>, pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf, pub editor: LineBuf,
pub history: History pub history: History,
} }
impl Readline for FernVi { impl Readline for FernVi {
@@ -47,7 +51,7 @@ impl Readline for FernVi {
let Some(key) = self.reader.read_key() else { let Some(key) = self.reader.read_key() else {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")) return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
}; };
flog!(DEBUG, key); flog!(DEBUG, key);
@@ -55,12 +59,12 @@ impl Readline for FernVi {
self.editor.accept_hint(); self.editor.accept_hint();
self.history.update_pending_cmd(self.editor.as_str()); self.history.update_pending_cmd(self.editor.as_str());
self.print_line()?; self.print_line()?;
continue continue;
} }
let Some(mut cmd) = self.mode.handle_key(key) else { let Some(mut cmd) = self.mode.handle_key(key) else {
flog!(DEBUG, "got none??"); flog!(DEBUG, "got none??");
continue continue;
}; };
flog!(DEBUG, cmd); flog!(DEBUG, cmd);
cmd.alter_line_motion_if_no_verb(); cmd.alter_line_motion_if_no_verb();
@@ -68,13 +72,13 @@ impl Readline for FernVi {
if self.should_grab_history(&cmd) { if self.should_grab_history(&cmd) {
self.scroll_history(cmd); self.scroll_history(cmd);
self.print_line()?; self.print_line()?;
continue continue;
} }
if cmd.should_submit() { if cmd.should_submit() {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
return Ok(self.editor.take_buf()) return Ok(self.editor.take_buf());
} }
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
@@ -83,7 +87,7 @@ impl Readline for FernVi {
sh_quit(0); sh_quit(0);
} else { } else {
self.editor.buffer.clear(); self.editor.buffer.clear();
continue continue;
} }
} }
flog!(DEBUG, cmd); flog!(DEBUG, cmd);
@@ -113,7 +117,7 @@ impl FernVi {
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0), editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
history: History::new()? history: History::new()?,
}) })
} }
@@ -122,13 +126,7 @@ impl FernVi {
flog!(DEBUG, line); flog!(DEBUG, line);
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(STDIN_FILENO); let (cols, _) = get_win_size(STDIN_FILENO);
Layout::from_parts( Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, &line)
/*tab_stop:*/ 8,
cols,
&self.prompt,
to_cursor,
&line
)
} }
pub fn scroll_history(&mut self, cmd: ViCmd) { pub fn scroll_history(&mut self, cmd: ViCmd) {
flog!(DEBUG, "scrolling"); flog!(DEBUG, "scrolling");
@@ -145,7 +143,7 @@ impl FernVi {
let entry = match motion { let entry = match motion {
Motion::LineUpCharwise => { Motion::LineUpCharwise => {
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
return return;
}; };
flog!(DEBUG, "found entry"); flog!(DEBUG, "found entry");
flog!(DEBUG, hist_entry.command()); flog!(DEBUG, hist_entry.command());
@@ -153,22 +151,20 @@ impl FernVi {
} }
Motion::LineDownCharwise => { Motion::LineDownCharwise => {
let Some(hist_entry) = self.history.scroll(*count as isize) else { let Some(hist_entry) = self.history.scroll(*count as isize) else {
return return;
}; };
flog!(DEBUG, "found entry"); flog!(DEBUG, "found entry");
flog!(DEBUG, hist_entry.command()); flog!(DEBUG, hist_entry.command());
hist_entry hist_entry
} }
_ => unreachable!() _ => unreachable!(),
}; };
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col()); let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
let mut buf = LineBuf::new().with_initial(entry.command(), 0); let mut buf = LineBuf::new().with_initial(entry.command(), 0);
let line_end = buf.end_of_line(); let line_end = buf.end_of_line();
if let Some(dest) = self.mode.hist_scroll_start_pos() { if let Some(dest) = self.mode.hist_scroll_start_pos() {
match dest { match dest {
To::Start => { To::Start => { /* Already at 0 */ }
/* Already at 0 */
}
To::End => { To::End => {
// History entries cannot be empty // History entries cannot be empty
// So this subtraction is safe (maybe) // So this subtraction is safe (maybe)
@@ -187,28 +183,15 @@ impl FernVi {
flog!(DEBUG, self.editor.cursor); flog!(DEBUG, self.editor.cursor);
if self.editor.cursor_at_max() && self.editor.has_hint() { if self.editor.cursor_at_max() && self.editor.has_hint() {
match self.mode.report_mode() { match self.mode.report_mode() {
ModeReport::Replace | ModeReport::Replace | ModeReport::Insert => {
ModeReport::Insert => { matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
matches!(
event,
KeyEvent(KeyCode::Right, ModKeys::NONE)
)
} }
ModeReport::Visual | ModeReport::Visual | ModeReport::Normal => {
ModeReport::Normal => { matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
matches!( || (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
event, && matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
KeyEvent(KeyCode::Right, ModKeys::NONE)
) ||
(
self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() &&
matches!(
event,
KeyEvent(KeyCode::Char('l'), ModKeys::NONE)
)
)
} }
_ => unimplemented!() _ => unimplemented!(),
} }
} else { } else {
false false
@@ -216,16 +199,16 @@ impl FernVi {
} }
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool { pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
cmd.verb().is_none() && cmd.verb().is_none()
( && (cmd
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) && .motion()
self.editor.start_of_line() == 0 .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
) || && self.editor.start_of_line() == 0)
( || (cmd
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) && .motion()
self.editor.end_of_line() == self.editor.cursor_max() && .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
!self.history.cursor_entry().is_some_and(|ent| ent.is_new()) && self.editor.end_of_line() == self.editor.cursor_max()
) && !self.history.cursor_entry().is_some_and(|ent| ent.is_new()))
} }
pub fn print_line(&mut self) -> ShResult<()> { pub fn print_line(&mut self) -> ShResult<()> {
@@ -234,11 +217,9 @@ impl FernVi {
self.writer.clear_rows(layout)?; self.writer.clear_rows(layout)?;
} }
self.writer.redraw( self
&self.prompt, .writer
&self.editor, .redraw(&self.prompt, &self.editor, &new_layout)?;
&new_layout
)?;
self.writer.flush_write(&self.mode.cursor_style())?; self.writer.flush_write(&self.mode.cursor_style())?;
@@ -252,35 +233,33 @@ impl FernVi {
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
let count = cmd.verb_count(); let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 { let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
Verb::Change | Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
Verb::InsertModeLineBreak(_) |
Verb::InsertMode => {
is_insert_mode = true; is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16)) Box::new(ViInsert::new().with_count(count as u16))
} }
Verb::NormalMode => { Verb::NormalMode => Box::new(ViNormal::new()),
Box::new(ViNormal::new())
}
Verb::ReplaceMode => Box::new(ViReplace::new()), Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::VisualModeSelectLast => { Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual { if self.mode.report_mode() != ModeReport::Visual {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
} }
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new()); let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
std::mem::swap(&mut mode, &mut self.mode); std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
return self.editor.exec_cmd(cmd) return self.editor.exec_cmd(cmd);
} }
Verb::VisualMode => { Verb::VisualMode => {
selecting = true; selecting = true;
Box::new(ViVisual::new()) Box::new(ViVisual::new())
} }
_ => unreachable!() _ => unreachable!(),
}; };
std::mem::swap(&mut mode, &mut self.mode); std::mem::swap(&mut mode, &mut self.mode);
@@ -293,7 +272,9 @@ impl FernVi {
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if selecting { if selecting {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
} else { } else {
self.editor.stop_selecting(); self.editor.stop_selecting();
} }
@@ -302,10 +283,10 @@ impl FernVi {
} else { } else {
self.editor.clear_insert_mode_start_pos(); self.editor.clear_insert_mode_start_pos();
} }
return Ok(()) return Ok(());
} else if cmd.is_cmd_repeat() { } else if cmd.is_cmd_repeat() {
let Some(replay) = self.repeat_action.clone() else { let Some(replay) = self.repeat_action.clone() else {
return Ok(()) return Ok(());
}; };
let ViCmd { verb, .. } = cmd; let ViCmd { verb, .. } = cmd;
let VerbCmd(count, _) = verb.unwrap(); let VerbCmd(count, _) = verb.unwrap();
@@ -332,32 +313,33 @@ impl FernVi {
m_mut.0 = 1 m_mut.0 = 1
} }
} else { } else {
return Ok(()) // it has to have a verb to be repeatable, something weird happened return Ok(()); // it has to have a verb to be repeatable,
// something weird happened
} }
} }
self.editor.exec_cmd(cmd)?; self.editor.exec_cmd(cmd)?;
} }
_ => unreachable!("motions should be handled in the other branch") _ => unreachable!("motions should be handled in the other branch"),
} }
return Ok(()) return Ok(());
} else if cmd.is_motion_repeat() { } else if cmd.is_motion_repeat() {
match cmd.motion.as_ref().unwrap() { match cmd.motion.as_ref().unwrap() {
MotionCmd(count, Motion::RepeatMotion) => { MotionCmd(count, Motion::RepeatMotion) => {
let Some(motion) = self.repeat_motion.clone() else { let Some(motion) = self.repeat_motion.clone() else {
return Ok(()) return Ok(());
}; };
let repeat_cmd = ViCmd { let repeat_cmd = ViCmd {
register: RegisterName::default(), register: RegisterName::default(),
verb: None, verb: None,
motion: Some(motion), motion: Some(motion),
raw_seq: format!("{count};"), raw_seq: format!("{count};"),
flags: CmdFlags::empty() flags: CmdFlags::empty(),
}; };
return self.editor.exec_cmd(repeat_cmd); return self.editor.exec_cmd(repeat_cmd);
} }
MotionCmd(count, Motion::RepeatMotionRev) => { MotionCmd(count, Motion::RepeatMotionRev) => {
let Some(motion) = self.repeat_motion.clone() else { let Some(motion) = self.repeat_motion.clone() else {
return Ok(()) return Ok(());
}; };
let mut new_motion = motion.invert_char_motion(); let mut new_motion = motion.invert_char_motion();
new_motion.0 = *count; new_motion.0 = *count;
@@ -366,18 +348,18 @@ impl FernVi {
verb: None, verb: None,
motion: Some(new_motion), motion: Some(new_motion),
raw_seq: format!("{count},"), raw_seq: format!("{count},"),
flags: CmdFlags::empty() flags: CmdFlags::empty(),
}; };
return self.editor.exec_cmd(repeat_cmd); return self.editor.exec_cmd(repeat_cmd);
} }
_ => unreachable!() _ => unreachable!(),
} }
} }
if cmd.is_repeatable() { if cmd.is_repeatable() {
if self.mode.report_mode() == ModeReport::Visual { if self.mode.report_mode() == ModeReport::Visual {
// The motion is assigned in the line buffer execution, so we also have to assign it here // The motion is assigned in the line buffer execution, so we also have to
// in order to be able to repeat it // assign it here in order to be able to repeat it
let range = self.editor.select_range().unwrap(); let range = self.editor.select_range().unwrap();
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
} }
@@ -398,4 +380,3 @@ impl FernVi {
Ok(()) Ok(())
} }
} }

View File

@@ -9,12 +9,16 @@ pub fn read_register(ch: Option<char>) -> Option<String> {
pub fn write_register(ch: Option<char>, buf: String) { pub fn write_register(ch: Option<char>, buf: String) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } if let Some(r) = lock.get_reg_mut(ch) {
r.write(buf)
}
} }
pub fn append_register(ch: Option<char>, buf: String) { pub fn append_register(ch: Option<char>, buf: String) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } if let Some(r) = lock.get_reg_mut(ch) {
r.append(buf)
}
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
@@ -82,7 +86,7 @@ impl Registers {
} }
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> { pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
let Some(ch) = ch else { let Some(ch) = ch else {
return Some(&self.default) return Some(&self.default);
}; };
match ch { match ch {
'a' => Some(&self.a), 'a' => Some(&self.a),
@@ -111,12 +115,12 @@ impl Registers {
'x' => Some(&self.x), 'x' => Some(&self.x),
'y' => Some(&self.y), 'y' => Some(&self.y),
'z' => Some(&self.z), 'z' => Some(&self.z),
_ => None _ => None,
} }
} }
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> { pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
let Some(ch) = ch else { let Some(ch) = ch else {
return Some(&mut self.default) return Some(&mut self.default);
}; };
match ch { match ch {
'a' => Some(&mut self.a), 'a' => Some(&mut self.a),
@@ -145,7 +149,7 @@ impl Registers {
'x' => Some(&mut self.x), 'x' => Some(&mut self.x),
'y' => Some(&mut self.y), 'y' => Some(&mut self.y),
'z' => Some(&mut self.z), 'z' => Some(&mut self.z),
_ => None _ => None,
} }
} }
} }

View File

@@ -1,21 +1,45 @@
use std::{env, fmt::{Debug, Write}, io::{BufRead, BufReader, Read}, iter::Peekable, os::fd::{AsFd, BorrowedFd, RawFd}, str::Chars}; use std::{
env,
fmt::{Debug, Write},
io::{BufRead, BufReader, Read},
iter::Peekable,
os::fd::{AsFd, BorrowedFd, RawFd},
str::Chars,
};
use nix::{errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, sys::termios, unistd::isatty}; use nix::{
errno::Errno,
libc::{self, STDIN_FILENO},
poll::{self, PollFlags, PollTimeout},
sys::termios,
unistd::isatty,
};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}};
use crate::prelude::*; use crate::prelude::*;
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
prompt::readline::keys::{KeyCode, ModKeys},
};
use super::{keys::KeyEvent, linebuf::LineBuf}; use super::{keys::KeyEvent, linebuf::LineBuf};
pub fn raw_mode() -> RawModeGuard { pub fn raw_mode() -> RawModeGuard {
let orig = termios::tcgetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}).expect("Failed to get terminal attributes"); let orig = termios::tcgetattr(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) })
.expect("Failed to get terminal attributes");
let mut raw = orig.clone(); let mut raw = orig.clone();
termios::cfmakeraw(&mut raw); termios::cfmakeraw(&mut raw);
termios::tcsetattr(unsafe{BorrowedFd::borrow_raw(STDIN_FILENO)}, termios::SetArg::TCSANOW, &raw) termios::tcsetattr(
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
termios::SetArg::TCSANOW,
&raw,
)
.expect("Failed to set terminal to raw mode"); .expect("Failed to set terminal to raw mode");
RawModeGuard { orig, fd: STDIN_FILENO } RawModeGuard {
orig,
fd: STDIN_FILENO,
}
} }
pub type Row = u16; pub type Row = u16;
@@ -24,7 +48,7 @@ pub type Col = u16;
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] #[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Pos { pub struct Pos {
col: Col, col: Col,
row: Row row: Row,
} }
// I'd like to thank rustyline for this idea // I'd like to thank rustyline for this idea
@@ -34,7 +58,7 @@ pub fn get_win_size(fd: RawFd) -> (Col,Row) {
use std::mem::zeroed; use std::mem::zeroed;
if cfg!(test) { if cfg!(test) {
return (80,24) return (80, 24);
} }
unsafe { unsafe {
@@ -55,7 +79,7 @@ pub fn get_win_size(fd: RawFd) -> (Col,Row) {
}; };
(cols, rows) (cols, rows)
} }
_ => (80,24) _ => (80, 24),
} }
} }
} }
@@ -112,9 +136,9 @@ pub fn width_calculator() -> Box<dyn WidthCalculator> {
Ok("WezTerm") => Box::new(UnicodeWidth), Ok("WezTerm") => Box::new(UnicodeWidth),
Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() { Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() {
Ok("xterm-kitty") => Box::new(NoZwj), Ok("xterm-kitty") => Box::new(NoZwj),
_ => Box::new(WcWidth) _ => Box::new(WcWidth),
}, },
_ => Box::new(WcWidth) _ => Box::new(WcWidth),
} }
} }
@@ -135,11 +159,9 @@ fn read_digits_until(rdr: &mut TermReader, sep: char) -> ShResult<Option<u32>> {
} }
pub fn append_digit(left: u32, right: u32) -> u32 { pub fn append_digit(left: u32, right: u32) -> u32 {
left.saturating_mul(10) left.saturating_mul(10).saturating_add(right)
.saturating_add(right)
} }
pub trait WidthCalculator { pub trait WidthCalculator {
fn width(&self, text: &str) -> usize; fn width(&self, text: &str) -> usize;
} }
@@ -150,12 +172,7 @@ pub trait KeyReader {
pub trait LineWriter { pub trait LineWriter {
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>; fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>;
fn redraw( fn redraw(&mut self, prompt: &str, line: &LineBuf, new_layout: &Layout) -> ShResult<()>;
&mut self,
prompt: &str,
line: &LineBuf,
new_layout: &Layout,
) -> ShResult<()>;
fn flush_write(&mut self, buf: &str) -> ShResult<()>; fn flush_write(&mut self, buf: &str) -> ShResult<()>;
} }
@@ -202,15 +219,13 @@ impl WidthCalculator for NoZwj {
} }
pub struct TermBuffer { pub struct TermBuffer {
tty: RawFd tty: RawFd,
} }
impl TermBuffer { impl TermBuffer {
pub fn new(tty: RawFd) -> Self { pub fn new(tty: RawFd) -> Self {
assert!(isatty(tty).is_ok_and(|r| r)); assert!(isatty(tty).is_ok_and(|r| r));
Self { Self { tty }
tty
}
} }
} }
@@ -221,7 +236,7 @@ impl Read for TermBuffer {
match nix::unistd::read(self.tty, buf) { match nix::unistd::read(self.tty, buf) {
Ok(n) => return Ok(n), Ok(n) => return Ok(n),
Err(Errno::EINTR) => {} Err(Errno::EINTR) => {}
Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)) Err(e) => return Err(std::io::Error::from_raw_os_error(e as i32)),
} }
} }
} }
@@ -247,8 +262,7 @@ impl RawModeGuard {
// Re-enable raw mode // Re-enable raw mode
let mut raw = self.orig.clone(); let mut raw = self.orig.clone();
termios::cfmakeraw(&mut raw); termios::cfmakeraw(&mut raw);
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw) termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode");
.expect("Failed to re-enable raw mode");
result result
} }
@@ -258,13 +272,17 @@ impl RawModeGuard {
impl Drop for RawModeGuard { impl Drop for RawModeGuard {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {
let _ = termios::tcsetattr(BorrowedFd::borrow_raw(self.fd), termios::SetArg::TCSANOW, &self.orig); let _ = termios::tcsetattr(
BorrowedFd::borrow_raw(self.fd),
termios::SetArg::TCSANOW,
&self.orig,
);
} }
} }
} }
pub struct TermReader { pub struct TermReader {
buffer: BufReader<TermBuffer> buffer: BufReader<TermBuffer>,
} }
impl Default for TermReader { impl Default for TermReader {
@@ -276,18 +294,18 @@ impl Default for TermReader {
impl TermReader { impl TermReader {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
buffer: BufReader::new(TermBuffer::new(1)) buffer: BufReader::new(TermBuffer::new(1)),
} }
} }
/// Execute some logic in raw mode /// Execute some logic in raw mode
/// ///
/// Saves the termios before running the given function. /// Saves the termios before running the given function.
/// If the given function panics, the panic will halt momentarily to restore the termios /// If the given function panics, the panic will halt momentarily to restore
/// the termios
pub fn poll(&mut self, timeout: PollTimeout) -> ShResult<bool> { pub fn poll(&mut self, timeout: PollTimeout) -> ShResult<bool> {
if !self.buffer.buffer().is_empty() { if !self.buffer.buffer().is_empty() {
return Ok(true) return Ok(true);
} }
let mut fds = [poll::PollFd::new(self.as_fd(), PollFlags::POLLIN)]; let mut fds = [poll::PollFd::new(self.as_fd(), PollFlags::POLLIN)];
@@ -295,7 +313,7 @@ impl TermReader {
match r { match r {
Ok(n) => Ok(n != 0), Ok(n) => Ok(n != 0),
Err(Errno::EINTR) => Ok(false), Err(Errno::EINTR) => Ok(false),
Err(e) => Err(e.into()) Err(e) => Err(e.into()),
} }
} }
@@ -308,7 +326,10 @@ impl TermReader {
pub fn peek_byte(&mut self) -> std::io::Result<u8> { pub fn peek_byte(&mut self) -> std::io::Result<u8> {
let buf = self.buffer.fill_buf()?; let buf = self.buffer.fill_buf()?;
if buf.is_empty() { if buf.is_empty() {
Err(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "EOF")) Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"EOF",
))
} else { } else {
Ok(buf[0]) Ok(buf[0])
} }
@@ -318,8 +339,6 @@ impl TermReader {
self.buffer.consume(1); self.buffer.consume(1);
} }
pub fn parse_esc_seq(&mut self) -> ShResult<KeyEvent> { pub fn parse_esc_seq(&mut self) -> ShResult<KeyEvent> {
let mut seq = vec![0x1b]; let mut seq = vec![0x1b];
@@ -398,7 +417,6 @@ impl TermReader {
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())), _ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
} }
} }
} }
impl KeyReader for TermReader { impl KeyReader for TermReader {
@@ -443,7 +461,7 @@ pub struct Layout {
pub w_calc: Box<dyn WidthCalculator>, pub w_calc: Box<dyn WidthCalculator>,
pub prompt_end: Pos, pub prompt_end: Pos,
pub cursor: Pos, pub cursor: Pos,
pub end: Pos pub end: Pos,
} }
impl Debug for Layout { impl Debug for Layout {
@@ -476,7 +494,12 @@ impl Layout {
let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 }); let prompt_end = Self::calc_pos(tab_stop, term_width, prompt, Pos { col: 0, row: 0 });
let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end); let cursor = Self::calc_pos(tab_stop, term_width, to_cursor, prompt_end);
let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end); let end = Self::calc_pos(tab_stop, term_width, to_end, prompt_end);
Layout { w_calc: width_calculator(), prompt_end, cursor, end } Layout {
w_calc: width_calculator(),
prompt_end,
cursor,
end,
}
} }
pub fn calc_pos(tab_stop: u16, term_width: u16, s: &str, orig: Pos) -> Pos { pub fn calc_pos(tab_stop: u16, term_width: u16, s: &str, orig: Pos) -> Pos {
@@ -530,26 +553,31 @@ impl TermWriter {
t_cols, t_cols,
buffer: String::new(), buffer: String::new(),
w_calc, w_calc,
tab_stop: 8 // TODO: add a way to configure this tab_stop: 8, // TODO: add a way to configure this
} }
} }
pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult<String> { pub fn get_cursor_movement(&self, old: Pos, new: Pos) -> ShResult<String> {
let mut buffer = String::new(); let mut buffer = String::new();
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to cursor movement buffer"); let err = |_| {
ShErr::simple(
ShErrKind::InternalErr,
"Failed to write to cursor movement buffer",
)
};
match new.row.cmp(&old.row) { match new.row.cmp(&old.row) {
std::cmp::Ordering::Greater => { std::cmp::Ordering::Greater => {
let shift = new.row - old.row; let shift = new.row - old.row;
match shift { match shift {
1 => buffer.push_str("\x1b[B"), 1 => buffer.push_str("\x1b[B"),
_ => write!(buffer, "\x1b[{shift}B").map_err(err)? _ => write!(buffer, "\x1b[{shift}B").map_err(err)?,
} }
} }
std::cmp::Ordering::Less => { std::cmp::Ordering::Less => {
let shift = old.row - new.row; let shift = old.row - new.row;
match shift { match shift {
1 => buffer.push_str("\x1b[A"), 1 => buffer.push_str("\x1b[A"),
_ => write!(buffer, "\x1b[{shift}A").map_err(err)? _ => write!(buffer, "\x1b[{shift}A").map_err(err)?,
} }
} }
std::cmp::Ordering::Equal => { /* Do nothing */ } std::cmp::Ordering::Equal => { /* Do nothing */ }
@@ -560,14 +588,14 @@ impl TermWriter {
let shift = new.col - old.col; let shift = new.col - old.col;
match shift { match shift {
1 => buffer.push_str("\x1b[C"), 1 => buffer.push_str("\x1b[C"),
_ => write!(buffer, "\x1b[{shift}C").map_err(err)? _ => write!(buffer, "\x1b[{shift}C").map_err(err)?,
} }
} }
std::cmp::Ordering::Less => { std::cmp::Ordering::Less => {
let shift = old.col - new.col; let shift = old.col - new.col;
match shift { match shift {
1 => buffer.push_str("\x1b[D"), 1 => buffer.push_str("\x1b[D"),
_ => write!(buffer, "\x1b[{shift}D").map_err(err)? _ => write!(buffer, "\x1b[{shift}D").map_err(err)?,
} }
} }
std::cmp::Ordering::Equal => { /* Do nothing */ } std::cmp::Ordering::Equal => { /* Do nothing */ }
@@ -582,38 +610,40 @@ impl TermWriter {
Ok(()) Ok(())
} }
pub fn update_t_cols(&mut self) { pub fn update_t_cols(&mut self) {
let (t_cols, _) = get_win_size(self.out); let (t_cols, _) = get_win_size(self.out);
self.t_cols = t_cols; self.t_cols = t_cols;
} }
pub fn move_cursor_at_leftmost(&mut self, rdr: &mut TermReader, use_newline: bool) -> ShResult<()> { pub fn move_cursor_at_leftmost(
&mut self,
rdr: &mut TermReader,
use_newline: bool,
) -> ShResult<()> {
let result = rdr.poll(PollTimeout::ZERO)?; let result = rdr.poll(PollTimeout::ZERO)?;
if result { if result {
// The terminals reply is going to be stuck behind the currently buffered output // The terminals reply is going to be stuck behind the currently buffered output
// So let's get out of here // So let's get out of here
return Ok(()) return Ok(());
} }
// Ping the cursor's position // Ping the cursor's position
self.flush_write("\x1b[6n\n")?; self.flush_write("\x1b[6n\n")?;
if !rdr.poll(PollTimeout::from(255u8))? { if !rdr.poll(PollTimeout::from(255u8))? {
return Ok(()) return Ok(());
} }
if rdr.next_byte()? as char != '\x1b' { if rdr.next_byte()? as char != '\x1b' {
return Ok(()) return Ok(());
} }
if rdr.next_byte()? as char != '[' { if rdr.next_byte()? as char != '[' {
return Ok(()) return Ok(());
} }
if read_digits_until(rdr, ';')?.is_none() { if read_digits_until(rdr, ';')?.is_none() {
return Ok(()) return Ok(());
} }
// We just consumed everything up to the column number, so let's get that now // We just consumed everything up to the column number, so let's get that now
@@ -622,12 +652,13 @@ impl TermWriter {
// The cursor is not at the leftmost, so let's fix that // The cursor is not at the leftmost, so let's fix that
if col != Some(1) { if col != Some(1) {
if use_newline { if use_newline {
// We use '\n' instead of '\r' sometimes because if there's a bunch of garbage on this line, // We use '\n' instead of '\r' sometimes because if there's a bunch of garbage
// It might pollute the prompt/line buffer if those are shorter than said garbage // on this line, It might pollute the prompt/line buffer if those are
// shorter than said garbage
self.flush_write("\n")?; self.flush_write("\n")?;
} else { } else {
// Sometimes though, we know that there's nothing to the right of the cursor after moving // Sometimes though, we know that there's nothing to the right of the cursor
// So we just move to the left. // after moving So we just move to the left.
self.flush_write("\r")?; self.flush_write("\r")?;
} }
} }
@@ -656,13 +687,13 @@ impl LineWriter for TermWriter {
Ok(()) Ok(())
} }
fn redraw( fn redraw(&mut self, prompt: &str, line: &LineBuf, new_layout: &Layout) -> ShResult<()> {
&mut self, let err = |_| {
prompt: &str, ShErr::simple(
line: &LineBuf, ShErrKind::InternalErr,
new_layout: &Layout, "Failed to write to LineWriter internal buffer",
) -> ShResult<()> { )
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer"); };
self.buffer.clear(); self.buffer.clear();
let end = new_layout.end; let end = new_layout.end;

View File

@@ -2,19 +2,20 @@ use bitflags::bitflags;
use super::register::{append_register, read_register, write_register}; use super::register::{append_register, read_register, write_register};
//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor //TODO: write tests that take edit results and cursor positions from actual
// neovim edits and test them against the behavior of this editor
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct RegisterName { pub struct RegisterName {
name: Option<char>, name: Option<char>,
count: usize, count: usize,
append: bool append: bool,
} }
impl RegisterName { impl RegisterName {
pub fn new(name: Option<char>, count: Option<usize>) -> Self { pub fn new(name: Option<char>, count: Option<usize>) -> Self {
let Some(ch) = name else { let Some(ch) = name else {
return Self::default() return Self::default();
}; };
let append = ch.is_uppercase(); let append = ch.is_uppercase();
@@ -22,7 +23,7 @@ impl RegisterName {
Self { Self {
name: Some(name), name: Some(name),
count: count.unwrap_or(1), count: count.unwrap_or(1),
append append,
} }
} }
pub fn name(&self) -> Option<char> { pub fn name(&self) -> Option<char> {
@@ -51,7 +52,7 @@ impl Default for RegisterName {
Self { Self {
name: None, name: None,
count: 1, count: 1,
append: false append: false,
} }
} }
} }
@@ -97,8 +98,12 @@ impl ViCmd {
self.motion.as_ref().map(|m| m.0).unwrap_or(1) self.motion.as_ref().map(|m| m.0).unwrap_or(1)
} }
pub fn normalize_counts(&mut self) { pub fn normalize_counts(&mut self) {
let Some(verb) = self.verb.as_mut() else { return }; let Some(verb) = self.verb.as_mut() else {
let Some(motion) = self.motion.as_mut() else { return }; return;
};
let Some(motion) = self.motion.as_mut() else {
return;
};
let VerbCmd(v_count, _) = verb; let VerbCmd(v_count, _) = verb;
let MotionCmd(m_count, _) = motion; let MotionCmd(m_count, _) = motion;
let product = *v_count * *m_count; let product = *v_count * *m_count;
@@ -109,31 +114,48 @@ impl ViCmd {
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
} }
pub fn is_cmd_repeat(&self) -> bool { pub fn is_cmd_repeat(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast)) self
.verb
.as_ref()
.is_some_and(|v| matches!(v.1, Verb::RepeatLast))
} }
pub fn is_motion_repeat(&self) -> bool { pub fn is_motion_repeat(&self) -> bool {
self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev)) self
.motion
.as_ref()
.is_some_and(|m| matches!(m.1, Motion::RepeatMotion | Motion::RepeatMotionRev))
} }
pub fn is_char_search(&self) -> bool { pub fn is_char_search(&self) -> bool {
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) self
.motion
.as_ref()
.is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
} }
pub fn should_submit(&self) -> bool { pub fn should_submit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline)) self
.verb
.as_ref()
.is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
} }
pub fn is_undo_op(&self) -> bool { pub fn is_undo_op(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) self
.verb
.as_ref()
.is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
} }
pub fn is_inplace_edit(&self) -> bool { pub fn is_inplace_edit(&self) -> bool {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) && self.verb.as_ref().is_some_and(|v| {
self.motion.is_none() matches!(
v.1,
Verb::ReplaceCharInplace(_, _) | Verb::ToggleCaseInplace(_)
)
}) && self.motion.is_none()
} }
pub fn is_line_motion(&self) -> bool { pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| { self.motion.as_ref().is_some_and(|m| {
matches!(m.1, matches!(
Motion::LineUp | m.1,
Motion::LineDown | Motion::LineUp | Motion::LineDown | Motion::LineUpCharwise | Motion::LineDownCharwise
Motion::LineUpCharwise |
Motion::LineDownCharwise
) )
}) })
} }
@@ -144,21 +166,22 @@ impl ViCmd {
match motion.1 { match motion.1 {
Motion::LineUp => motion.1 = Motion::LineUpCharwise, Motion::LineUp => motion.1 = Motion::LineUpCharwise,
Motion::LineDown => motion.1 = Motion::LineDownCharwise, Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => unreachable!() _ => unreachable!(),
} }
} }
} }
} }
pub fn is_mode_transition(&self) -> bool { pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| { self.verb.as_ref().is_some_and(|v| {
matches!(v.1, matches!(
Verb::Change | v.1,
Verb::InsertMode | Verb::Change
Verb::InsertModeLineBreak(_) | | Verb::InsertMode
Verb::NormalMode | | Verb::InsertModeLineBreak(_)
Verb::VisualModeSelectLast | | Verb::NormalMode
Verb::VisualMode | | Verb::VisualModeSelectLast
Verb::ReplaceMode | Verb::VisualMode
| Verb::ReplaceMode
) )
}) })
} }
@@ -217,59 +240,58 @@ pub enum Verb {
Dedent, Dedent,
Equalize, Equalize,
AcceptLineOrNewline, AcceptLineOrNewline,
EndOfFile EndOfFile,
} }
impl Verb { impl Verb {
pub fn is_repeatable(&self) -> bool { pub fn is_repeatable(&self) -> bool {
matches!(self, matches!(
Self::Delete | self,
Self::Change | Self::Delete
Self::ReplaceChar(_) | | Self::Change
Self::ReplaceCharInplace(_,_) | | Self::ReplaceChar(_)
Self::ToLower | | Self::ReplaceCharInplace(_, _)
Self::ToUpper | | Self::ToLower
Self::ToggleCaseRange | | Self::ToUpper
Self::ToggleCaseInplace(_) | | Self::ToggleCaseRange
Self::Put(_) | | Self::ToggleCaseInplace(_)
Self::ReplaceMode | | Self::Put(_)
Self::InsertModeLineBreak(_) | | Self::ReplaceMode
Self::JoinLines | | Self::InsertModeLineBreak(_)
Self::InsertChar(_) | | Self::JoinLines
Self::Insert(_) | | Self::InsertChar(_)
Self::Indent | | Self::Insert(_)
Self::Dedent | | Self::Indent
Self::Equalize | Self::Dedent
| Self::Equalize
) )
} }
pub fn is_edit(&self) -> bool { pub fn is_edit(&self) -> bool {
matches!(self, matches!(
Self::Delete | self,
Self::Change | Self::Delete
Self::ReplaceChar(_) | | Self::Change
Self::ReplaceCharInplace(_,_) | | Self::ReplaceChar(_)
Self::ToggleCaseRange | | Self::ReplaceCharInplace(_, _)
Self::ToggleCaseInplace(_) | | Self::ToggleCaseRange
Self::ToLower | | Self::ToggleCaseInplace(_)
Self::ToUpper | | Self::ToLower
Self::RepeatLast | | Self::ToUpper
Self::Put(_) | | Self::RepeatLast
Self::ReplaceMode | | Self::Put(_)
Self::InsertModeLineBreak(_) | | Self::ReplaceMode
Self::JoinLines | | Self::InsertModeLineBreak(_)
Self::InsertChar(_) | | Self::JoinLines
Self::Insert(_) | | Self::InsertChar(_)
Self::Rot13 | | Self::Insert(_)
Self::EndOfFile | Self::Rot13
| Self::EndOfFile
) )
} }
pub fn is_char_insert(&self) -> bool { pub fn is_char_insert(&self) -> bool {
matches!(self, matches!(
Self::Change | self,
Self::InsertChar(_) | Self::Change | Self::InsertChar(_) | Self::ReplaceChar(_) | Self::ReplaceCharInplace(_, _)
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_)
) )
} }
} }
@@ -311,14 +333,14 @@ pub enum Motion {
Range(usize, usize), Range(usize, usize),
RepeatMotion, RepeatMotion,
RepeatMotionRev, RepeatMotionRev,
Null Null,
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MotionBehavior { pub enum MotionBehavior {
Exclusive, Exclusive,
Inclusive, Inclusive,
Linewise Linewise,
} }
impl Motion { impl Motion {
@@ -332,35 +354,33 @@ impl Motion {
} }
} }
pub fn is_exclusive(&self) -> bool { pub fn is_exclusive(&self) -> bool {
matches!(&self, matches!(
Self::BeginningOfLine | &self,
Self::BeginningOfFirstWord | Self::BeginningOfLine
Self::BeginningOfScreenLine | | Self::BeginningOfFirstWord
Self::FirstGraphicalOnScreenLine | | Self::BeginningOfScreenLine
Self::LineDownCharwise | | Self::FirstGraphicalOnScreenLine
Self::LineUpCharwise | | Self::LineDownCharwise
Self::ScreenLineUpCharwise | | Self::LineUpCharwise
Self::ScreenLineDownCharwise | | Self::ScreenLineUpCharwise
Self::ToColumn | | Self::ScreenLineDownCharwise
Self::TextObj(TextObj::Sentence(_)) | | Self::ToColumn
Self::TextObj(TextObj::Paragraph(_)) | | Self::TextObj(TextObj::Sentence(_))
Self::CharSearch(Direction::Backward, _, _) | | Self::TextObj(TextObj::Paragraph(_))
Self::WordMotion(To::Start,_,_) | | Self::CharSearch(Direction::Backward, _, _)
Self::ToBrace(_) | | Self::WordMotion(To::Start, _, _)
Self::ToBracket(_) | | Self::ToBrace(_)
Self::ToParen(_) | | Self::ToBracket(_)
Self::ScreenLineDown | | Self::ToParen(_)
Self::ScreenLineUp | | Self::ScreenLineDown
Self::Range(_, _) | Self::ScreenLineUp
| Self::Range(_, _)
) )
} }
pub fn is_linewise(&self) -> bool { pub fn is_linewise(&self) -> bool {
matches!(self, matches!(
Self::WholeLine | self,
Self::LineUp | Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
Self::LineDown |
Self::ScreenLineDown |
Self::ScreenLineUp
) )
} }
} }
@@ -368,7 +388,7 @@ impl Motion {
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum Anchor { pub enum Anchor {
After, After,
Before Before,
} }
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum TextObj { pub enum TextObj {
@@ -410,30 +430,30 @@ pub enum TextObj {
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Word { pub enum Word {
Big, Big,
Normal Normal,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Bound { pub enum Bound {
Inside, Inside,
Around Around,
} }
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] #[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction { pub enum Direction {
#[default] #[default]
Forward, Forward,
Backward Backward,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Dest { pub enum Dest {
On, On,
Before, Before,
After After,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum To { pub enum To {
Start, Start,
End End,
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,9 @@
use std::{collections::HashMap, fmt::Display, str::FromStr}; use std::{collections::HashMap, fmt::Display, str::FromStr};
use crate::{
use crate::{libsh::error::{Note, ShErr, ShErrKind, ShResult}, state::ShFunc}; libsh::error::{Note, ShErr, ShErrKind, ShResult},
state::ShFunc,
};
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum FernBellStyle { pub enum FernBellStyle {
@@ -10,7 +12,6 @@ pub enum FernBellStyle {
Disable, Disable,
} }
impl FromStr for FernBellStyle { impl FromStr for FernBellStyle {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -18,12 +19,10 @@ impl FromStr for FernBellStyle {
"audible" => Ok(Self::Audible), "audible" => Ok(Self::Audible),
"visible" => Ok(Self::Visible), "visible" => Ok(Self::Visible),
"disable" => Ok(Self::Disable), "disable" => Ok(Self::Disable),
_ => Err( _ => Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("Invalid bell style '{s}'") format!("Invalid bell style '{s}'"),
) )),
)
} }
} }
} }
@@ -42,7 +41,7 @@ impl Display for FernBellStyle {
pub enum FernEditMode { pub enum FernEditMode {
#[default] #[default]
Vi, Vi,
Emacs Emacs,
} }
impl FromStr for FernEditMode { impl FromStr for FernEditMode {
@@ -51,12 +50,10 @@ impl FromStr for FernEditMode {
match s.to_ascii_lowercase().as_str() { match s.to_ascii_lowercase().as_str() {
"vi" => Ok(Self::Vi), "vi" => Ok(Self::Vi),
"emacs" => Ok(Self::Emacs), "emacs" => Ok(Self::Emacs),
_ => Err( _ => Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("Invalid edit mode '{s}'") format!("Invalid edit mode '{s}'"),
) )),
)
} }
} }
} }
@@ -73,7 +70,7 @@ impl Display for FernEditMode {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ShOpts { pub struct ShOpts {
pub core: ShOptCore, pub core: ShOptCore,
pub prompt: ShOptPrompt pub prompt: ShOptPrompt,
} }
impl Default for ShOpts { impl Default for ShOpts {
@@ -99,12 +96,10 @@ impl ShOpts {
pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> { pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> {
let mut query = opt.split('.'); let mut query = opt.split('.');
let Some(key) = query.next() else { let Some(key) = query.next() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: No option given" "shopt: No option given",
) ));
)
}; };
let remainder = query.collect::<Vec<_>>().join("."); let remainder = query.collect::<Vec<_>>().join(".");
@@ -116,14 +111,12 @@ impl ShOpts {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key" "shopt: Expected 'core' or 'prompt' in shopt key",
) )
.with_note( .with_note(
Note::new("'shopt' takes arguments separated by periods to denote namespaces") Note::new("'shopt' takes arguments separated by periods to denote namespaces")
.with_sub_notes(vec![ .with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
"Example: 'shopt core.autocd=true'" ),
])
)
) )
} }
} }
@@ -134,32 +127,26 @@ impl ShOpts {
// TODO: handle escapes? // TODO: handle escapes?
let mut query = query.split('.'); let mut query = query.split('.');
let Some(key) = query.next() else { let Some(key) = query.next() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: No option given" "shopt: No option given",
) ));
)
}; };
let remainder = query.collect::<Vec<_>>().join("."); let remainder = query.collect::<Vec<_>>().join(".");
match key { match key {
"core" => self.core.get(&remainder), "core" => self.core.get(&remainder),
"prompt" => self.prompt.get(&remainder), "prompt" => self.prompt.get(&remainder),
_ => { _ => Err(
Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key" "shopt: Expected 'core' or 'prompt' in shopt key",
) )
.with_note( .with_note(
Note::new("'shopt' takes arguments separated by periods to denote namespaces") Note::new("'shopt' takes arguments separated by periods to denote namespaces")
.with_sub_notes(vec![ .with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
"Example: 'shopt core.autocd=true'" ),
]) ),
)
)
}
} }
} }
} }
@@ -181,67 +168,55 @@ impl ShOptCore {
match opt { match opt {
"dotglob" => { "dotglob" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for dotglob value" "shopt: expected 'true' or 'false' for dotglob value",
) ));
)
}; };
self.dotglob = val; self.dotglob = val;
} }
"autocd" => { "autocd" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for autocd value" "shopt: expected 'true' or 'false' for autocd value",
) ));
)
}; };
self.autocd = val; self.autocd = val;
} }
"hist_ignore_dupes" => { "hist_ignore_dupes" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for hist_ignore_dupes value" "shopt: expected 'true' or 'false' for hist_ignore_dupes value",
) ));
)
}; };
self.hist_ignore_dupes = val; self.hist_ignore_dupes = val;
} }
"max_hist" => { "max_hist" => {
let Ok(val) = val.parse::<usize>() else { let Ok(val) = val.parse::<usize>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a positive integer for hist_ignore_dupes value" "shopt: expected a positive integer for hist_ignore_dupes value",
) ));
)
}; };
self.max_hist = val; self.max_hist = val;
} }
"interactive_comments" => { "interactive_comments" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for interactive_comments value" "shopt: expected 'true' or 'false' for interactive_comments value",
) ));
)
}; };
self.interactive_comments = val; self.interactive_comments = val;
} }
"auto_hist" => { "auto_hist" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for auto_hist value" "shopt: expected 'true' or 'false' for auto_hist value",
) ));
)
}; };
self.auto_hist = val; self.auto_hist = val;
} }
@@ -250,28 +225,22 @@ impl ShOptCore {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a bell style for bell_style value" "shopt: expected a bell style for bell_style value",
) )
.with_note( .with_note(
Note::new("bell_style takes these options as values") Note::new("bell_style takes these options as values")
.with_sub_notes(vec![ .with_sub_notes(vec!["audible", "visible", "disable"]),
"audible", ),
"visible", );
"disable"
])
)
)
}; };
self.bell_style = val; self.bell_style = val;
} }
"max_recurse_depth" => { "max_recurse_depth" => {
let Ok(val) = val.parse::<usize>() else { let Ok(val) = val.parse::<usize>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a positive integer for max_recurse_depth value" "shopt: expected a positive integer for max_recurse_depth value",
) ));
)
}; };
self.max_recurse_depth = val; self.max_recurse_depth = val;
} }
@@ -279,12 +248,11 @@ impl ShOptCore {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{opt}'") format!("shopt: Unexpected 'core' option '{opt}'"),
) )
.with_note(Note::new("options can be accessed like 'core.option_name'")) .with_note(Note::new("options can be accessed like 'core.option_name'"))
.with_note( .with_note(
Note::new("'core' contains the following options") Note::new("'core' contains the following options").with_sub_notes(vec![
.with_sub_notes(vec![
"dotglob", "dotglob",
"autocd", "autocd",
"hist_ignore_dupes", "hist_ignore_dupes",
@@ -293,9 +261,8 @@ impl ShOptCore {
"auto_hist", "auto_hist",
"bell_style", "bell_style",
"max_recurse_depth", "max_recurse_depth",
] ]),
) ),
)
) )
} }
} }
@@ -303,7 +270,7 @@ impl ShOptCore {
} }
pub fn get(&self, query: &str) -> ShResult<Option<String>> { pub fn get(&self, query: &str) -> ShResult<Option<String>> {
if query.is_empty() { if query.is_empty() {
return Ok(Some(format!("{self}"))) return Ok(Some(format!("{self}")));
} }
match query { match query {
@@ -313,7 +280,9 @@ impl ShOptCore {
Ok(Some(output)) Ok(Some(output))
} }
"autocd" => { "autocd" => {
let mut output = String::from("Allow navigation to directories by passing the directory as a command directly\n"); let mut output = String::from(
"Allow navigation to directories by passing the directory as a command directly\n",
);
output.push_str(&format!("{}", self.autocd)); output.push_str(&format!("{}", self.autocd));
Ok(Some(output)) Ok(Some(output))
} }
@@ -323,7 +292,9 @@ impl ShOptCore {
Ok(Some(output)) Ok(Some(output))
} }
"max_hist" => { "max_hist" => {
let mut output = String::from("Maximum number of entries in the command history file (default '.fernhist')\n"); let mut output = String::from(
"Maximum number of entries in the command history file (default '.fernhist')\n",
);
output.push_str(&format!("{}", self.max_hist)); output.push_str(&format!("{}", self.max_hist));
Ok(Some(output)) Ok(Some(output))
} }
@@ -333,7 +304,9 @@ impl ShOptCore {
Ok(Some(output)) Ok(Some(output))
} }
"auto_hist" => { "auto_hist" => {
let mut output = String::from("Whether or not to automatically save commands to the command history file\n"); let mut output = String::from(
"Whether or not to automatically save commands to the command history file\n",
);
output.push_str(&format!("{}", self.auto_hist)); output.push_str(&format!("{}", self.auto_hist));
Ok(Some(output)) Ok(Some(output))
} }
@@ -347,16 +320,14 @@ impl ShOptCore {
output.push_str(&format!("{}", self.max_recurse_depth)); output.push_str(&format!("{}", self.max_recurse_depth));
Ok(Some(output)) Ok(Some(output))
} }
_ => { _ => Err(
Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{query}'") format!("shopt: Unexpected 'core' option '{query}'"),
) )
.with_note(Note::new("options can be accessed like 'core.option_name'")) .with_note(Note::new("options can be accessed like 'core.option_name'"))
.with_note( .with_note(
Note::new("'core' contains the following options") Note::new("'core' contains the following options").with_sub_notes(vec![
.with_sub_notes(vec![
"dotglob", "dotglob",
"autocd", "autocd",
"hist_ignore_dupes", "hist_ignore_dupes",
@@ -365,11 +336,9 @@ impl ShOptCore {
"auto_hist", "auto_hist",
"bell_style", "bell_style",
"max_recurse_depth", "max_recurse_depth",
] ]),
) ),
) ),
)
}
} }
} }
} }
@@ -381,7 +350,10 @@ impl Display for ShOptCore {
output.push(format!("autocd = {}", self.autocd)); output.push(format!("autocd = {}", self.autocd));
output.push(format!("hist_ignore_dupes = {}", self.hist_ignore_dupes)); output.push(format!("hist_ignore_dupes = {}", self.hist_ignore_dupes));
output.push(format!("max_hist = {}", self.max_hist)); output.push(format!("max_hist = {}", self.max_hist));
output.push(format!("interactive_comments = {}",self.interactive_comments)); output.push(format!(
"interactive_comments = {}",
self.interactive_comments
));
output.push(format!("auto_hist = {}", self.auto_hist)); output.push(format!("auto_hist = {}", self.auto_hist));
output.push(format!("bell_style = {}", self.bell_style)); output.push(format!("bell_style = {}", self.bell_style));
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth)); output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
@@ -414,7 +386,7 @@ pub struct ShOptPrompt {
pub comp_limit: usize, pub comp_limit: usize,
pub prompt_highlight: bool, pub prompt_highlight: bool,
pub tab_stop: usize, pub tab_stop: usize,
pub custom: HashMap<String,ShFunc> // Contains functions for prompt modules pub custom: HashMap<String, ShFunc>, // Contains functions for prompt modules
} }
impl ShOptPrompt { impl ShOptPrompt {
@@ -422,56 +394,46 @@ impl ShOptPrompt {
match opt { match opt {
"trunc_prompt_path" => { "trunc_prompt_path" => {
let Ok(val) = val.parse::<usize>() else { let Ok(val) = val.parse::<usize>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a positive integer for trunc_prompt_path value" "shopt: expected a positive integer for trunc_prompt_path value",
) ));
)
}; };
self.trunc_prompt_path = val; self.trunc_prompt_path = val;
} }
"edit_mode" => { "edit_mode" => {
let Ok(val) = val.parse::<FernEditMode>() else { let Ok(val) = val.parse::<FernEditMode>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'vi' or 'emacs' for edit_mode value" "shopt: expected 'vi' or 'emacs' for edit_mode value",
) ));
)
}; };
self.edit_mode = val; self.edit_mode = val;
} }
"comp_limit" => { "comp_limit" => {
let Ok(val) = val.parse::<usize>() else { let Ok(val) = val.parse::<usize>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a positive integer for comp_limit value" "shopt: expected a positive integer for comp_limit value",
) ));
)
}; };
self.comp_limit = val; self.comp_limit = val;
} }
"prompt_highlight" => { "prompt_highlight" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for prompt_highlight value" "shopt: expected 'true' or 'false' for prompt_highlight value",
) ));
)
}; };
self.prompt_highlight = val; self.prompt_highlight = val;
} }
"tab_stop" => { "tab_stop" => {
let Ok(val) = val.parse::<usize>() else { let Ok(val) = val.parse::<usize>() else {
return Err( return Err(ShErr::simple(
ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected a positive integer for tab_stop value" "shopt: expected a positive integer for tab_stop value",
) ));
)
}; };
self.tab_stop = val; self.tab_stop = val;
} }
@@ -482,12 +444,11 @@ impl ShOptPrompt {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{opt}'") format!("shopt: Unexpected 'core' option '{opt}'"),
) )
.with_note(Note::new("options can be accessed like 'core.option_name'")) .with_note(Note::new("options can be accessed like 'core.option_name'"))
.with_note( .with_note(
Note::new("'core' contains the following options") Note::new("'core' contains the following options").with_sub_notes(vec![
.with_sub_notes(vec![
"dotglob", "dotglob",
"autocd", "autocd",
"hist_ignore_dupes", "hist_ignore_dupes",
@@ -496,9 +457,8 @@ impl ShOptPrompt {
"auto_hist", "auto_hist",
"bell_style", "bell_style",
"max_recurse_depth", "max_recurse_depth",
] ]),
) ),
)
) )
} }
} }
@@ -506,27 +466,32 @@ impl ShOptPrompt {
} }
pub fn get(&self, query: &str) -> ShResult<Option<String>> { pub fn get(&self, query: &str) -> ShResult<Option<String>> {
if query.is_empty() { if query.is_empty() {
return Ok(Some(format!("{self}"))) return Ok(Some(format!("{self}")));
} }
match query { match query {
"trunc_prompt_path" => { "trunc_prompt_path" => {
let mut output = String::from("Maximum number of path segments used in the '\\W' prompt escape sequence\n"); let mut output = String::from(
"Maximum number of path segments used in the '\\W' prompt escape sequence\n",
);
output.push_str(&format!("{}", self.trunc_prompt_path)); output.push_str(&format!("{}", self.trunc_prompt_path));
Ok(Some(output)) Ok(Some(output))
} }
"edit_mode" => { "edit_mode" => {
let mut output = String::from("The style of editor shortcuts used in the line-editing of the prompt\n"); let mut output =
String::from("The style of editor shortcuts used in the line-editing of the prompt\n");
output.push_str(&format!("{}", self.edit_mode)); output.push_str(&format!("{}", self.edit_mode));
Ok(Some(output)) Ok(Some(output))
} }
"comp_limit" => { "comp_limit" => {
let mut output = String::from("Maximum number of completion candidates displayed upon pressing tab\n"); let mut output =
String::from("Maximum number of completion candidates displayed upon pressing tab\n");
output.push_str(&format!("{}", self.comp_limit)); output.push_str(&format!("{}", self.comp_limit));
Ok(Some(output)) Ok(Some(output))
} }
"prompt_highlight" => { "prompt_highlight" => {
let mut output = String::from("Whether to enable or disable syntax highlighting on the prompt\n"); let mut output =
String::from("Whether to enable or disable syntax highlighting on the prompt\n");
output.push_str(&format!("{}", self.prompt_highlight)); output.push_str(&format!("{}", self.prompt_highlight));
Ok(Some(output)) Ok(Some(output))
} }
@@ -536,23 +501,23 @@ impl ShOptPrompt {
Ok(Some(output)) Ok(Some(output))
} }
"custom" => { "custom" => {
let mut output = String::from("A table of custom 'modules' executed as shell functions for prompt scripting\n"); let mut output = String::from(
"A table of custom 'modules' executed as shell functions for prompt scripting\n",
);
output.push_str("Current modules: \n"); output.push_str("Current modules: \n");
for key in self.custom.keys() { for key in self.custom.keys() {
output.push_str(&format!(" - {key}\n")); output.push_str(&format!(" - {key}\n"));
} }
Ok(Some(output.trim().to_string())) Ok(Some(output.trim().to_string()))
} }
_ => { _ => Err(
Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{query}'") format!("shopt: Unexpected 'core' option '{query}'"),
) )
.with_note(Note::new("options can be accessed like 'core.option_name'")) .with_note(Note::new("options can be accessed like 'core.option_name'"))
.with_note( .with_note(
Note::new("'core' contains the following options") Note::new("'core' contains the following options").with_sub_notes(vec![
.with_sub_notes(vec![
"dotglob", "dotglob",
"autocd", "autocd",
"hist_ignore_dupes", "hist_ignore_dupes",
@@ -561,11 +526,9 @@ impl ShOptPrompt {
"auto_hist", "auto_hist",
"bell_style", "bell_style",
"max_recurse_depth", "max_recurse_depth",
] ]),
) ),
) ),
)
}
} }
} }
} }
@@ -598,7 +561,7 @@ impl Default for ShOptPrompt {
comp_limit: 100, comp_limit: 100,
prompt_highlight: true, prompt_highlight: true,
tab_stop: 4, tab_stop: 4,
custom: HashMap::new() custom: HashMap::new(),
} }
} }
} }

View File

@@ -1,4 +1,9 @@
use crate::{jobs::{take_term, JobCmdFlags, JobID}, libsh::{error::ShResult, sys::sh_quit}, prelude::*, state::{read_jobs, write_jobs}}; use crate::{
jobs::{take_term, JobCmdFlags, JobID},
libsh::{error::ShResult, sys::sh_quit},
prelude::*,
state::{read_jobs, write_jobs},
};
pub fn sig_setup() { pub fn sig_setup() {
unsafe { unsafe {
@@ -12,7 +17,6 @@ pub fn sig_setup() {
} }
} }
extern "C" fn handle_sighup(_: libc::c_int) { extern "C" fn handle_sighup(_: libc::c_int) {
write_jobs(|j| { write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() { for job in j.jobs_mut().iter_mut().flatten() {
@@ -65,7 +69,7 @@ pub extern "C" fn handle_sigchld(_: libc::c_int) {
WtStat::Stopped(pid, signal) => child_stopped(pid, signal), WtStat::Stopped(pid, signal) => child_stopped(pid, signal),
WtStat::Continued(pid) => child_continued(pid), WtStat::Continued(pid) => child_continued(pid),
WtStat::StillAlive => break, WtStat::StillAlive => break,
_ => unimplemented!() _ => unimplemented!(),
} { } {
eprintln!("{}", e) eprintln!("{}", e)
} }
@@ -76,7 +80,11 @@ pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid); let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) { if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap(); let child = job
.children_mut()
.iter_mut()
.find(|chld| pid == chld.pid())
.unwrap();
let stat = WtStat::Signaled(pid, sig, false); let stat = WtStat::Signaled(pid, sig, false);
child.set_stat(stat); child.set_stat(stat);
} }
@@ -91,7 +99,11 @@ pub fn child_stopped(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid); let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) { if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap(); let child = job
.children_mut()
.iter_mut()
.find(|chld| pid == chld.pid())
.unwrap();
let status = WtStat::Stopped(pid, sig); let status = WtStat::Stopped(pid, sig);
child.set_stat(status); child.set_stat(status);
} else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) { } else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) {
@@ -114,18 +126,15 @@ pub fn child_continued(pid: Pid) -> ShResult<()> {
pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
/* /*
* Here we are going to get metadata on the exited process by querying the job table with the pid. * Here we are going to get metadata on the exited process by querying the
* Then if the discovered job is the fg task, return terminal control to rsh * job table with the pid. Then if the discovered job is the fg task,
* If it is not the fg task, print the display info for the job in the job table * return terminal control to rsh If it is not the fg task, print the
* We can reasonably assume that if it is not a foreground job, then it exists in the job table * display info for the job in the job table We can reasonably assume that
* if it is not a foreground job, then it exists in the job table
* If this assumption is incorrect, the code has gone wrong somewhere. * If this assumption is incorrect, the code has gone wrong somewhere.
*/ */
write_jobs(|j| j.close_job_fds(pid)); write_jobs(|j| j.close_job_fds(pid));
if let Some(( if let Some((pgid, is_fg, is_finished)) = write_jobs(|j| {
pgid,
is_fg,
is_finished
)) = write_jobs(|j| {
let fg_pgid = j.get_fg().map(|job| job.pgid()); let fg_pgid = j.get_fg().map(|job| job.pgid());
if let Some(job) = j.query_mut(JobID::Pid(pid)) { if let Some(job) = j.query_mut(JobID::Pid(pid)) {
let pgid = job.pgid(); let pgid = job.pgid();
@@ -133,7 +142,6 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
job.update_by_id(JobID::Pid(pid), status).unwrap(); job.update_by_id(JobID::Pid(pid), status).unwrap();
let is_finished = !job.running(); let is_finished = !job.running();
if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) { if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) {
child.set_stat(status); child.set_stat(status);
} }
@@ -143,7 +151,6 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
None None
} }
}) { }) {
if is_finished { if is_finished {
if is_fg { if is_fg {
take_term()?; take_term()?;

View File

@@ -1,8 +1,23 @@
use std::{collections::{HashMap, VecDeque}, ops::Deref, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration}; use std::{
collections::{HashMap, VecDeque},
ops::Deref,
sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
time::Duration,
};
use nix::unistd::{gethostname, getppid, User}; use nix::unistd::{gethostname, getppid, User};
use crate::{exec_input, jobs::JobTab, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts}; use crate::{
exec_input,
jobs::JobTab,
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::VecDequeExt,
},
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
prelude::*,
shopt::ShOpts,
};
pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new())); pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new()));
@@ -16,9 +31,9 @@ pub static SHOPTS: LazyLock<RwLock<ShOpts>> = LazyLock::new(|| RwLock::new(ShOpt
/// A shell function /// A shell function
/// ///
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers to /// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers
/// The Node must be stored with the ParsedSrc because the tokens of the node contain an Arc<String> /// to The Node must be stored with the ParsedSrc because the tokens of the node
/// Which refers to the String held in ParsedSrc /// contain an Arc<String> Which refers to the String held in ParsedSrc
/// ///
/// Can be dereferenced to pull out the wrapped Node /// Can be dereferenced to pull out the wrapped Node
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -54,7 +69,7 @@ impl Deref for ShFunc {
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct LogTab { pub struct LogTab {
functions: HashMap<String, ShFunc>, functions: HashMap<String, ShFunc>,
aliases: HashMap<String,String> aliases: HashMap<String, String>,
} }
impl LogTab { impl LogTab {
@@ -96,12 +111,15 @@ impl LogTab {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Var { pub struct Var {
export: bool, export: bool,
value: String value: String,
} }
impl Var { impl Var {
pub fn new(value: String) -> Self { pub fn new(value: String) -> Self {
Self { export: false, value } Self {
export: false,
value,
}
} }
pub fn mark_for_export(&mut self) { pub fn mark_for_export(&mut self) {
self.export = true; self.export = true;
@@ -119,7 +137,8 @@ impl Deref for Var {
pub struct VarTab { pub struct VarTab {
vars: HashMap<String, Var>, vars: HashMap<String, Var>,
params: HashMap<String, String>, params: HashMap<String, String>,
sh_argv: VecDeque<String>, // Using a VecDeque makes the implementation of `shift` straightforward sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift`
* straightforward */
} }
impl VarTab { impl VarTab {
@@ -127,7 +146,11 @@ impl VarTab {
let vars = HashMap::new(); let vars = HashMap::new();
let params = Self::init_params(); let params = Self::init_params();
Self::init_env(); Self::init_env();
let mut var_tab = Self { vars, params, sh_argv: VecDeque::new() }; let mut var_tab = Self {
vars,
params,
sh_argv: VecDeque::new(),
};
var_tab.init_sh_argv(); var_tab.init_sh_argv();
var_tab var_tab
} }
@@ -135,13 +158,21 @@ impl VarTab {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("?".into(), "0".into()); // Last command exit status params.insert("?".into(), "0".into()); // Last command exit status
params.insert("#".into(), "0".into()); // Number of positional parameters params.insert("#".into(), "0".into()); // Number of positional parameters
params.insert("0".into(), std::env::current_exe().unwrap().to_str().unwrap().to_string()); // Name of the shell params.insert(
"0".into(),
std::env::current_exe()
.unwrap()
.to_str()
.unwrap()
.to_string(),
); // Name of the shell
params.insert("$".into(), Pid::this().to_string()); // PID of the shell params.insert("$".into(), Pid::this().to_string()); // PID of the shell
params.insert("!".into(), "".into()); // PID of the last background job (if any) params.insert("!".into(), "".into()); // PID of the last background job (if any)
params params
} }
fn init_env() { fn init_env() {
let pathbuf_to_string = |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string(); let pathbuf_to_string =
|pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
// First, inherit any env vars from the parent process // First, inherit any env vars from the parent process
let term = { let term = {
if isatty(1).unwrap() { if isatty(1).unwrap() {
@@ -167,7 +198,9 @@ impl VarTab {
uid = 0.into(); uid = 0.into();
} }
let home = pathbuf_to_string(Ok(home)); let home = pathbuf_to_string(Ok(home));
let hostname = gethostname().map(|hname| hname.to_string_lossy().to_string()).unwrap_or_default(); let hostname = gethostname()
.map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default();
env::set_var("IFS", " \t\n"); env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone()); env::set_var("HOST", hostname.clone());
@@ -258,11 +291,10 @@ impl VarTab {
} }
} }
pub fn get_var(&self, var: &str) -> String { pub fn get_var(&self, var: &str) -> String {
if var.chars().count() == 1 || if var.chars().count() == 1 || var.parse::<usize>().is_ok() {
var.parse::<usize>().is_ok() {
let param = self.get_param(var); let param = self.get_param(var);
if !param.is_empty() { if !param.is_empty() {
return param return param;
} }
} }
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) { if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
@@ -290,27 +322,32 @@ impl VarTab {
if var_name.parse::<usize>().is_ok() { if var_name.parse::<usize>().is_ok() {
return self.params.contains_key(var_name); return self.params.contains_key(var_name);
} }
self.vars.contains_key(var_name) || self.vars.contains_key(var_name) || (var_name.len() == 1 && self.params.contains_key(var_name))
(
var_name.len() == 1 &&
self.params.contains_key(var_name)
)
} }
pub fn set_param(&mut self, param: &str, val: &str) { pub fn set_param(&mut self, param: &str, val: &str) {
self.params.insert(param.to_string(), val.to_string()); self.params.insert(param.to_string(), val.to_string());
} }
pub fn get_param(&self, param: &str) -> String { pub fn get_param(&self, param: &str) -> String {
if param.parse::<usize>().is_ok() { if param.parse::<usize>().is_ok() {
let argv_idx = param let argv_idx = param.to_string().parse::<usize>().unwrap();
.to_string() let arg = self
.parse::<usize>() .sh_argv
.unwrap(); .get(argv_idx)
let arg = self.sh_argv.get(argv_idx).map(|s| s.to_string()).unwrap_or_default(); .map(|s| s.to_string())
.unwrap_or_default();
arg arg
} else if param == "?" { } else if param == "?" {
self.params.get(param).map(|s| s.to_string()).unwrap_or("0".into()) self
.params
.get(param)
.map(|s| s.to_string())
.unwrap_or("0".into())
} else { } else {
self.params.get(param).map(|s| s.to_string()).unwrap_or_default() self
.params
.get(param)
.map(|s| s.to_string())
.unwrap_or_default()
} }
} }
} }
@@ -318,7 +355,7 @@ impl VarTab {
/// A table of metadata for the shell /// A table of metadata for the shell
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct MetaTab { pub struct MetaTab {
runtime_start: Option<Instant> runtime_start: Option<Instant>,
} }
impl MetaTab { impl MetaTab {
@@ -329,7 +366,8 @@ impl MetaTab {
self.runtime_start = Some(Instant::now()); self.runtime_start = Some(Instant::now());
} }
pub fn stop_timer(&mut self) -> Option<Duration> { pub fn stop_timer(&mut self) -> Option<Duration> {
self.runtime_start self
.runtime_start
.take() // runtime_start returns to None .take() // runtime_start returns to None
.map(|start| start.elapsed()) // return the duration, if any .map(|start| start.elapsed()) // return the duration, if any
} }
@@ -407,12 +445,13 @@ pub fn set_status(code: i32) {
write_vars(|v| v.set_param("?", &code.to_string())) write_vars(|v| v.set_param("?", &code.to_string()))
} }
/// Save the current state of the logic and variable table, and the working directory path /// Save the current state of the logic and variable table, and the working
/// directory path
pub fn get_snapshots() -> (LogTab, VarTab, String) { pub fn get_snapshots() -> (LogTab, VarTab, String) {
( (
read_logic(|l| l.clone()), read_logic(|l| l.clone()),
read_vars(|v| v.clone()), read_vars(|v| v.clone()),
env::var("PWD").unwrap_or_default() env::var("PWD").unwrap_or_default(),
) )
} }
@@ -434,17 +473,13 @@ pub fn source_rc() -> ShResult<()> {
PathBuf::from(format!("{home}/.fernrc")) PathBuf::from(format!("{home}/.fernrc"))
}; };
if !path.exists() { if !path.exists() {
return Err( return Err(ShErr::simple(ShErrKind::InternalErr, ".fernrc not found"));
ShErr::simple(ShErrKind::InternalErr, ".fernrc not found")
)
} }
source_file(path) source_file(path)
} }
pub fn source_file(path: PathBuf) -> ShResult<()> { pub fn source_file(path: PathBuf) -> ShResult<()> {
let mut file = OpenOptions::new() let mut file = OpenOptions::new().read(true).open(path)?;
.read(true)
.open(path)?;
let mut buf = String::new(); let mut buf = String::new();
file.read_to_string(&mut buf)?; file.read_to_string(&mut buf)?;

View File

@@ -3,7 +3,10 @@ use super::*;
#[test] #[test]
fn cmd_not_found() { fn cmd_not_found() {
let input = "foo"; let input = "foo";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).next().unwrap().unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.next()
.unwrap()
.unwrap();
let err = ShErr::full(ShErrKind::CmdNotFound("foo".into()), "", token.span); let err = ShErr::full(ShErrKind::CmdNotFound("foo".into()), "", token.span);
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
@@ -13,7 +16,9 @@ fn cmd_not_found() {
#[test] #[test]
fn unclosed_subsh() { fn unclosed_subsh() {
let input = "(foo"; let input = "(foo";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.nth(1)
.unwrap();
let Err(err) = token else { let Err(err) = token else {
panic!("{:?}", token); panic!("{:?}", token);
}; };
@@ -25,7 +30,9 @@ fn unclosed_subsh() {
#[test] #[test]
fn unclosed_dquote() { fn unclosed_dquote() {
let input = "\"foo bar"; let input = "\"foo bar";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.nth(1)
.unwrap();
let Err(err) = token else { let Err(err) = token else {
panic!(); panic!();
}; };
@@ -37,7 +44,9 @@ fn unclosed_dquote() {
#[test] #[test]
fn unclosed_squote() { fn unclosed_squote() {
let input = "'foo bar"; let input = "'foo bar";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.nth(1)
.unwrap();
let Err(err) = token else { let Err(err) = token else {
panic!(); panic!();
}; };
@@ -160,14 +169,7 @@ fn error_with_notes() {
fn error_with_notes_and_sub_notes() { fn error_with_notes_and_sub_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed") let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason")) .with_note(Note::new("Execution failed for this reason"))
.with_note( .with_note(Note::new("Here is how to fix it:").with_sub_notes(vec!["blah", "blah", "blah"]));
Note::new("Here is how to fix it:")
.with_sub_notes(vec![
"blah",
"blah",
"blah"
])
);
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)

View File

@@ -57,18 +57,22 @@ fn expand_alias_multiline() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "echo bar"); l.insert_alias("bar", "echo bar");
let input = String::from(" let input = String::from(
"
foo foo
if true; then if true; then
bar bar
fi fi
"); ",
let expected = String::from(" );
let expected = String::from(
"
echo foo echo foo
if true; then if true; then
echo bar echo bar
fi fi
"); ",
);
let result = expand_aliases(input, HashSet::new(), l); let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result, expected) assert_eq!(result, expected)
@@ -122,7 +126,6 @@ fn test_infinite_recursive_alias() {
assert_eq!(result.as_str(), "foo bar"); assert_eq!(result.as_str(), "foo bar");
l.clear_aliases(); l.clear_aliases();
}); });
} }
#[test] #[test]

View File

@@ -6,10 +6,16 @@ use super::super::*;
#[test] #[test]
fn getopt_from_argv() { fn getopt_from_argv() {
let node = get_nodes("echo -n -e foo", |node| matches!(node.class, NdRule::Command {..})) let node = get_nodes("echo -n -e foo", |node| {
matches!(node.class, NdRule::Command { .. })
})
.pop() .pop()
.unwrap(); .unwrap();
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
panic!() panic!()
}; };
@@ -20,7 +26,10 @@ fn getopt_from_argv() {
#[test] #[test]
fn getopt_simple() { fn getopt_simple() {
let raw = "echo -n foo".split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>(); let raw = "echo -n foo"
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let (words, opts) = get_opts(raw); let (words, opts) = get_opts(raw);
insta::assert_debug_snapshot!(words); insta::assert_debug_snapshot!(words);
@@ -29,7 +38,10 @@ fn getopt_simple() {
#[test] #[test]
fn getopt_multiple_short() { fn getopt_multiple_short() {
let raw = "echo -nre foo".split_whitespace().map(|s| s.to_string()).collect::<Vec<_>>(); let raw = "echo -nre foo"
.split_whitespace()
.map(|s| s.to_string())
.collect::<Vec<_>>();
let (words, opts) = get_opts(raw); let (words, opts) = get_opts(raw);
insta::assert_debug_snapshot!(words); insta::assert_debug_snapshot!(words);

View File

@@ -1,37 +1,28 @@
use std::sync::Arc; use std::sync::Arc;
use super::*; use super::*;
use crate::libsh::error::{ use crate::expand::{expand_aliases, unescape_str};
Note, ShErr, ShErrKind use crate::libsh::error::{Note, ShErr, ShErrKind};
};
use crate::parse::{ use crate::parse::{
node_operation, Node, NdRule, ParseStream, lex::{LexFlags, LexStream, Tk, TkRule},
lex::{ node_operation, NdRule, Node, ParseStream,
Tk, TkRule, LexFlags, LexStream
}
};
use crate::expand::{
expand_aliases, unescape_str
};
use crate::state::{
write_logic, write_vars
}; };
use crate::state::{write_logic, write_vars};
pub mod error;
pub mod expand;
pub mod getopt;
pub mod highlight;
pub mod lexer; pub mod lexer;
pub mod parser; pub mod parser;
pub mod expand;
pub mod term;
pub mod error;
pub mod getopt;
pub mod script;
pub mod highlight;
pub mod readline; pub mod readline;
pub mod script;
pub mod term;
/// Unsafe to use outside of tests /// Unsafe to use outside of tests
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node> pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
where where
F1: Fn(&Node) -> bool F1: Fn(&Node) -> bool,
{ {
let mut nodes = vec![]; let mut nodes = vec![];
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
@@ -42,10 +33,9 @@ pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for node in parsed_nodes.iter_mut() { for node in parsed_nodes.iter_mut() {
node_operation(node, node_operation(node, &filter, &mut |node: &mut Node| {
&filter, nodes.push(node.clone())
&mut |node: &mut Node| nodes.push(node.clone()) });
);
} }
nodes nodes
} }

View File

@@ -221,11 +221,11 @@ fn test_node_operation() {
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes = ParseStream::new(tokens) let nodes = ParseStream::new(tokens).map(|nd| nd.unwrap());
.map(|nd| nd.unwrap());
for mut node in nodes { for mut node in nodes {
node_operation(&mut node, node_operation(
&mut node,
&|node: &Node| matches!(node.class, NdRule::Command { .. }), &|node: &Node| matches!(node.class, NdRule::Command { .. }),
&mut |node: &mut Node| check_nodes.push(node.clone()), &mut |node: &mut Node| check_nodes.push(node.clone()),
); );

View File

@@ -1,6 +1,16 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{libsh::term::{Style, Styled}, prompt::readline::{history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{raw_mode, KeyReader, LineWriter}, vimode::{ViInsert, ViMode, ViNormal}, FernVi, Readline}}; use crate::{
libsh::term::{Style, Styled},
prompt::readline::{
history::History,
keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf,
term::{raw_mode, KeyReader, LineWriter},
vimode::{ViInsert, ViMode, ViNormal},
FernVi, Readline,
},
};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -8,7 +18,7 @@ use super::super::*;
#[derive(Default, Debug)] #[derive(Default, Debug)]
struct TestReader { struct TestReader {
pub bytes: VecDeque<u8> pub bytes: VecDeque<u8>,
} }
impl TestReader { impl TestReader {
@@ -114,7 +124,7 @@ impl KeyReader for TestReader {
println!("found escape seq"); println!("found escape seq");
let seq = self.parse_esc_seq_from_bytes(); let seq = self.parse_esc_seq_from_bytes();
println!("{seq:?}"); println!("{seq:?}");
return seq return seq;
} }
} }
@@ -132,8 +142,7 @@ impl KeyReader for TestReader {
} }
} }
pub struct TestWriter { pub struct TestWriter {}
}
impl TestWriter { impl TestWriter {
pub fn new() -> Self { pub fn new() -> Self {
@@ -171,7 +180,7 @@ impl FernVi {
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
history: History::new().unwrap(), history: History::new().unwrap(),
editor: LineBuf::new().with_initial(initial, 0) editor: LineBuf::new().with_initial(initial, 0),
} }
} }
} }
@@ -185,10 +194,7 @@ fn fernvi_test(input: &str, initial: &str) -> String {
} }
fn normal_cmd(cmd: &str, buf: &str, cursor: usize) -> (String, usize) { fn normal_cmd(cmd: &str, buf: &str, cursor: usize) -> (String, usize) {
let cmd = ViNormal::new() let cmd = ViNormal::new().cmds_from_raw(cmd).pop().unwrap();
.cmds_from_raw(cmd)
.pop()
.unwrap();
let mut buf = LineBuf::new().with_initial(buf, cursor); let mut buf = LineBuf::new().with_initial(buf, cursor);
buf.exec_cmd(cmd).unwrap(); buf.exec_cmd(cmd).unwrap();
(buf.as_str().to_string(), buf.cursor.get()) (buf.as_str().to_string(), buf.cursor.get())
@@ -340,7 +346,8 @@ fn linebuf_prev_line_only_newlines() {
#[test] #[test]
fn linebuf_cursor_motion() { fn linebuf_cursor_motion() {
let mut buf = LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0); let mut buf =
LineBuf::new().with_initial("Thé quíck 🦊 bröwn fóx jumpś óver the 💤 lázy dóg 🐶", 0);
buf.update_graphemes_lazy(); buf.update_graphemes_lazy();
let total = buf.grapheme_indices.as_ref().unwrap().len(); let total = buf.grapheme_indices.as_ref().unwrap().len();
@@ -348,17 +355,33 @@ fn linebuf_cursor_motion() {
for i in 0..total { for i in 0..total {
buf.cursor.set(i); buf.cursor.set(i);
let expected_to = buf.buffer.get(..buf.grapheme_indices_owned()[i]).unwrap_or("").to_string(); let expected_to = buf
.buffer
.get(..buf.grapheme_indices_owned()[i])
.unwrap_or("")
.to_string();
let expected_from = if i + 1 < total { let expected_from = if i + 1 < total {
buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() buf
.buffer
.get(buf.grapheme_indices_owned()[i]..)
.unwrap_or("")
.to_string()
} else { } else {
// last grapheme, ends at buffer end // last grapheme, ends at buffer end
buf.buffer.get(buf.grapheme_indices_owned()[i]..).unwrap_or("").to_string() buf
.buffer
.get(buf.grapheme_indices_owned()[i]..)
.unwrap_or("")
.to_string()
}; };
let expected_at = { let expected_at = {
let start = buf.grapheme_indices_owned()[i]; let start = buf.grapheme_indices_owned()[i];
let end = buf.grapheme_indices_owned().get(i + 1).copied().unwrap_or(buf.buffer.len()); let end = buf
.grapheme_indices_owned()
.get(i + 1)
.copied()
.unwrap_or(buf.buffer.len());
buf.buffer.get(start..end).map(|slice| slice.to_string()) buf.buffer.get(start..end).map(|slice| slice.to_string())
}; };
@@ -382,216 +405,171 @@ fn linebuf_cursor_motion() {
#[test] #[test]
fn editor_delete_word() { fn editor_delete_word() {
assert_eq!(normal_cmd( assert_eq!(
"dw", normal_cmd("dw", "The quick brown fox jumps over the lazy dog", 16),
"The quick brown fox jumps over the lazy dog",
16),
("The quick brown jumps over the lazy dog".into(), 16) ("The quick brown jumps over the lazy dog".into(), 16)
); );
} }
#[test] #[test]
fn editor_delete_backwards() { fn editor_delete_backwards() {
assert_eq!(normal_cmd( assert_eq!(
"2db", normal_cmd("2db", "The quick brown fox jumps over the lazy dog", 16),
"The quick brown fox jumps over the lazy dog",
16),
("The fox jumps over the lazy dog".into(), 4) ("The fox jumps over the lazy dog".into(), 4)
); );
} }
#[test] #[test]
fn editor_rot13_five_words_backwards() { fn editor_rot13_five_words_backwards() {
assert_eq!(normal_cmd( assert_eq!(
"g?5b", normal_cmd("g?5b", "The quick brown fox jumps over the lazy dog", 31),
"The quick brown fox jumps over the lazy dog",
31),
("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4) ("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4)
); );
} }
#[test] #[test]
fn editor_delete_word_on_whitespace() { fn editor_delete_word_on_whitespace() {
assert_eq!(normal_cmd( assert_eq!(
"dw", normal_cmd("dw", "The quick brown fox", 10), //on the whitespace between "quick" and "brown"
"The quick brown fox",
10), //on the whitespace between "quick" and "brown"
("The quick brown fox".into(), 10) ("The quick brown fox".into(), 10)
); );
} }
#[test] #[test]
fn editor_delete_5_words() { fn editor_delete_5_words() {
assert_eq!(normal_cmd( assert_eq!(
"5dw", normal_cmd("5dw", "The quick brown fox jumps over the lazy dog", 16,),
"The quick brown fox jumps over the lazy dog",
16,),
("The quick brown dog".into(), 16) ("The quick brown dog".into(), 16)
); );
} }
#[test] #[test]
fn editor_delete_end_includes_last() { fn editor_delete_end_includes_last() {
assert_eq!(normal_cmd( assert_eq!(
"de", normal_cmd("de", "The quick brown fox::::jumps over the lazy dog", 16),
"The quick brown fox::::jumps over the lazy dog",
16),
("The quick brown ::::jumps over the lazy dog".into(), 16) ("The quick brown ::::jumps over the lazy dog".into(), 16)
); );
} }
#[test] #[test]
fn editor_delete_end_unicode_word() { fn editor_delete_end_unicode_word() {
assert_eq!(normal_cmd( assert_eq!(
"de", normal_cmd("de", "naïve café world", 0),
"naïve café world",
0),
(" café world".into(), 0) (" café world".into(), 0)
); );
} }
#[test] #[test]
fn editor_inplace_edit_cursor_position() { fn editor_inplace_edit_cursor_position() {
assert_eq!(normal_cmd( assert_eq!(normal_cmd("5~", "foobar", 0), ("FOOBAr".into(), 4));
"5~", assert_eq!(normal_cmd("5rg", "foobar", 0), ("gggggr".into(), 4));
"foobar",
0),
("FOOBAr".into(), 4)
);
assert_eq!(normal_cmd(
"5rg",
"foobar",
0),
("gggggr".into(), 4)
);
} }
#[test] #[test]
fn editor_insert_mode_not_clamped() { fn editor_insert_mode_not_clamped() {
assert_eq!(normal_cmd( assert_eq!(normal_cmd("a", "foobar", 5), ("foobar".into(), 6))
"a",
"foobar",
5),
("foobar".into(), 6)
)
} }
#[test] #[test]
fn editor_overshooting_motions() { fn editor_overshooting_motions() {
assert_eq!(normal_cmd( assert_eq!(normal_cmd("5dw", "foo bar", 0), ("".into(), 0));
"5dw", assert_eq!(normal_cmd("3db", "foo bar", 0), ("foo bar".into(), 0));
"foo bar", assert_eq!(normal_cmd("3dj", "foo bar", 0), ("foo bar".into(), 0));
0), assert_eq!(normal_cmd("3dk", "foo bar", 0), ("foo bar".into(), 0));
("".into(), 0)
);
assert_eq!(normal_cmd(
"3db",
"foo bar",
0),
("foo bar".into(), 0)
);
assert_eq!(normal_cmd(
"3dj",
"foo bar",
0),
("foo bar".into(), 0)
);
assert_eq!(normal_cmd(
"3dk",
"foo bar",
0),
("foo bar".into(), 0)
);
} }
#[test] #[test]
fn editor_textobj_quoted() { fn editor_textobj_quoted() {
assert_eq!(normal_cmd( assert_eq!(
"di\"", normal_cmd("di\"", "this buffer has \"some \\\"quoted\" text", 0),
"this buffer has \"some \\\"quoted\" text",
0),
("this buffer has \"\" text".into(), 17) ("this buffer has \"\" text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
"da\"", normal_cmd("da\"", "this buffer has \"some \\\"quoted\" text", 0),
"this buffer has \"some \\\"quoted\" text",
0),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
assert_eq!(normal_cmd( assert_eq!(
"di'", normal_cmd("di'", "this buffer has 'some \\'quoted' text", 0),
"this buffer has 'some \\'quoted' text",
0),
("this buffer has '' text".into(), 17) ("this buffer has '' text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
"da'", normal_cmd("da'", "this buffer has 'some \\'quoted' text", 0),
"this buffer has 'some \\'quoted' text",
0),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
assert_eq!(normal_cmd( assert_eq!(
"di`", normal_cmd("di`", "this buffer has `some \\`quoted` text", 0),
"this buffer has `some \\`quoted` text",
0),
("this buffer has `` text".into(), 17) ("this buffer has `` text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
"da`", normal_cmd("da`", "this buffer has `some \\`quoted` text", 0),
"this buffer has `some \\`quoted` text",
0),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
} }
#[test] #[test]
fn editor_textobj_delimited() { fn editor_textobj_delimited() {
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"di)", "di)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text", "this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0), 0
),
("this buffer has () text".into(), 17) ("this buffer has () text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"da)", "da)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text", "this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0), 0
),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"di]", "di]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text", "this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0), 0
),
("this buffer has [] text".into(), 17) ("this buffer has [] text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"da]", "da]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text", "this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0), 0
),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"di}", "di}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text", "this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0), 0
),
("this buffer has {} text".into(), 17) ("this buffer has {} text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"da}", "da}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text", "this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0), 0
),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"di>", "di>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text", "this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0), 0
),
("this buffer has <> text".into(), 17) ("this buffer has <> text".into(), 17)
); );
assert_eq!(normal_cmd( assert_eq!(
normal_cmd(
"da>", "da>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text", "this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0), 0
),
("this buffer has text".into(), 16) ("this buffer has text".into(), 16)
); );
} }
@@ -610,18 +588,13 @@ fn editor_delete_line_up() {
#[test] #[test]
fn fernvi_test_simple() { fn fernvi_test_simple() {
assert_eq!(fernvi_test( assert_eq!(fernvi_test("foo bar\x1bbdw\r", ""), "foo ")
"foo bar\x1bbdw\r",
""),
"foo "
)
} }
#[test] #[test]
fn fernvi_test_mode_change() { fn fernvi_test_mode_change() {
assert_eq!(fernvi_test( assert_eq!(
"foo bar biz buzz\x1bbbb2cwbiz buzz bar\r", fernvi_test("foo bar biz buzz\x1bbbb2cwbiz buzz bar\r", ""),
""),
"foo biz buzz bar buzz" "foo biz buzz bar buzz"
) )
} }
@@ -637,9 +610,11 @@ fn fernvi_test_lorem_ipsum_1() {
#[test] #[test]
fn fernvi_test_lorem_ipsum_undo() { fn fernvi_test_lorem_ipsum_undo() {
assert_eq!(fernvi_test( assert_eq!(
fernvi_test(
"\x1bwwwwwwwwainserting some characters now...\x1bu\r", "\x1bwwwwwwwwainserting some characters now...\x1bu\r",
LOREM_IPSUM), LOREM_IPSUM
),
LOREM_IPSUM LOREM_IPSUM
) )
} }

View File

@@ -6,8 +6,7 @@ use super::super::*;
fn get_script_output(name: &str, args: &[&str]) -> Output { fn get_script_output(name: &str, args: &[&str]) -> Output {
// Resolve the path to the fern binary. // Resolve the path to the fern binary.
// Do not question me. // Do not question me.
let mut fern_path = env::current_exe() let mut fern_path = env::current_exe().expect("Failed to get test executable"); // The path to the test executable
.expect("Failed to get test executable"); // The path to the test executable
fern_path.pop(); // Hocus pocus fern_path.pop(); // Hocus pocus
fern_path.pop(); fern_path.pop();
fern_path.push("fern"); // Abra Kadabra fern_path.push("fern"); // Abra Kadabra

View File

@@ -29,7 +29,9 @@ fn styled_background() {
#[test] #[test]
fn styled_set() { fn styled_set() {
let input = "multi-style text"; let input = "multi-style text";
let style_set = StyleSet::new().add_style(Style::Magenta).add_style(Style::Italic); let style_set = StyleSet::new()
.add_style(Style::Magenta)
.add_style(Style::Italic);
let styled = input.styled(style_set); let styled = input.styled(style_set);
insta::assert_snapshot!(styled); insta::assert_snapshot!(styled);
} }