Added rustfmt.toml, formatted codebase

This commit is contained in:
2025-08-12 13:58:25 -04:00
parent 23fb67aba8
commit 8ad53f09b3
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;
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!()
};
@@ -26,23 +37,19 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
} else {
for (arg, span) in argv {
if arg == "command" || arg == "builtin" {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("alias: Cannot assign alias to reserved name '{arg}'"),
span
)
)
span,
));
}
let Some((name, body)) = arg.split_once('=') else {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::SyntaxErr,
"alias: Expected an assignment in alias args",
span
)
)
span,
));
};
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<()> {
let NdRule::Command { assignments: _, argv } = node.class else {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the environment variables
let mut alias_output = read_logic(|l| {
@@ -78,13 +88,11 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul
for (arg, span) in argv {
flog!(DEBUG, arg);
if read_logic(|l| l.get_alias(&arg)).is_none() {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::SyntaxErr,
format!("unalias: alias '{arg}' not found"),
span
)
)
span,
));
};
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;
pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span();
let NdRule::Command { assignments: _, argv } = node.class else {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
@@ -17,23 +27,19 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
};
if !new_dir.exists() {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("cd: No such file or directory '{}'", new_dir.display()),
span,
)
)
));
}
if !new_dir.is_dir() {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("cd: Not a directory '{}'", new_dir.display()),
span,
)
)
));
}
env::set_current_dir(new_dir).unwrap();

View File

@@ -1,13 +1,25 @@
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('E'),
Opt::Short('e'),
Opt::Short('r'),
].into()});
]
.into()
});
bitflags! {
pub struct EchoFlags: u32 {
@@ -19,7 +31,11 @@ bitflags! {
pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone();
let NdRule::Command { assignments: _, argv } = node.class else {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
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)
};
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)
.collect::<Vec<_>>()
.join(" ");
@@ -54,22 +71,18 @@ pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
while let Some(opt) = opts.pop() {
if !ECHO_OPTS.contains(&opt) {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("echo: Unexpected flag '{opt}'"),
)
)
));
}
let Opt::Short(opt) = opt else {
unreachable!()
};
let Opt::Short(opt) = opt else { unreachable!() };
match opt {
'n' => flags |= EchoFlags::NO_NEWLINE,
'r' => flags |= EchoFlags::USE_STDERR,
'e' => flags |= EchoFlags::USE_ESCAPE,
_ => unreachable!()
_ => 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;
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!()
};
@@ -23,9 +34,11 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
} else {
for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, true)); // Export an assignment like 'foo=bar'
write_vars(|v| v.set_var(var, val, true)); // Export an assignment like
// 'foo=bar'
} else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any
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<()> {
use ShErrKind::*;
let NdRule::Command { assignments: _, argv } = node.class else {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let mut code = 0;
@@ -11,15 +19,14 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
let cmd = argv.remove(0).0;
if !argv.is_empty() {
let (arg,span) = argv
.into_iter()
.next()
.unwrap();
let (arg, span) = argv.into_iter().next().unwrap();
let Ok(status) = arg.parse::<i32>() else {
return Err(
ShErr::full(ShErrKind::SyntaxErr, format!("{cmd}: Expected a number"), span)
)
return Err(ShErr::full(
ShErrKind::SyntaxErr,
format!("{cmd}: Expected a number"),
span,
));
};
code = status;
@@ -32,7 +39,7 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
LoopBreak(_) => LoopBreak(code),
FuncReturn(_) => FuncReturn(code),
CleanExit(_) => CleanExit(code),
_ => unreachable!()
_ => unreachable!(),
};
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;
pub enum JobBehavior {
Foregound,
Background
Background,
}
pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> {
let blame = node.get_span().clone();
let cmd = match behavior {
JobBehavior::Foregound => "fg",
JobBehavior::Background => "bg"
JobBehavior::Background => "bg",
};
let NdRule::Command { assignments: _, argv } = node.class else {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
@@ -21,30 +32,22 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR
let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::InternalErr,
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()) {
id
} else {
return Err(
ShErr::full(
ShErrKind::ExecFail,
"No jobs found",
blame
)
)
return Err(ShErr::full(ShErrKind::ExecFail, "No jobs found", blame));
};
let tabid = match argv.next() {
Some((arg, blame)) => parse_job_id(&arg, blame)?,
None => curr_job_id
None => curr_job_id,
};
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() {
Ok(j.remove_job(id.clone()).unwrap())
} else {
Err(
ShErr::full(
Err(ShErr::full(
ShErrKind::ExecFail,
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 => {
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))?;
}
}
@@ -91,20 +95,18 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
});
match result {
Some(id) => Ok(id),
None => Err(
ShErr::full(
None => Err(ShErr::full(
ShErrKind::InternalErr,
"Found a job but no table id in parse_job_id()",
blame
)
)
blame,
)),
}
}
} else if arg.chars().all(|ch| ch.is_ascii_digit()) {
let result = write_jobs(|j| {
let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap())));
if let Some(job) = pgid_query_result {
return Some(job.tabid().unwrap())
return Some(job.tabid().unwrap());
}
if arg.parse::<i32>().unwrap() > 0 {
@@ -117,27 +119,27 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
match result {
Some(id) => Ok(id),
None => Err(
ShErr::full(
None => Err(ShErr::full(
ShErrKind::InternalErr,
"Found a job but no table id in parse_job_id()",
blame
)
)
blame,
)),
}
} else {
Err(
ShErr::full(
Err(ShErr::full(
ShErrKind::SyntaxErr,
format!("Invalid fd arg: {}", arg),
blame
)
)
blame,
))
}
}
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!()
};
@@ -147,13 +149,11 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
for (arg, span) in argv {
let mut chars = arg.chars().peekable();
if chars.peek().is_none_or(|ch| *ch != '-') {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::SyntaxErr,
"Invalid flag in jobs call",
span
)
)
span,
));
}
chars.next();
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,
'r' => JobCmdFlags::RUNNING,
's' => JobCmdFlags::STOPPED,
_ => return Err(
ShErr::full(
_ => {
return Err(ShErr::full(
ShErrKind::SyntaxErr,
"Invalid flag in jobs call",
span
)
)
span,
))
}
};
flags |= flag
}

View File

@@ -1,50 +1,45 @@
use nix::unistd::Pid;
use crate::{jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{execute::prepare_argv, lex::{Span, Tk}, Redir}, procio::{IoFrame, IoStack}};
use crate::{
jobs::{ChildProc, JobBldr},
libsh::error::ShResult,
parse::{
execute::prepare_argv,
lex::{Span, Tk},
Redir,
},
procio::{IoFrame, IoStack},
};
pub mod echo;
pub mod cd;
pub mod export;
pub mod pwd;
pub mod source;
pub mod shift;
pub mod jobctl;
pub mod alias;
pub mod cd;
pub mod echo;
pub mod export;
pub mod flowctl;
pub mod zoltraak;
pub mod jobctl;
pub mod pwd;
pub mod shift;
pub mod shopt;
pub mod test; // [[ ]] thing
pub mod source;
pub mod test;
pub mod zoltraak; // [[ ]] thing
pub const BUILTINS: [&str; 19] = [
"echo",
"cd",
"export",
"pwd",
"source",
"shift",
"jobs",
"fg",
"bg",
"alias",
"unalias",
"return",
"break",
"continue",
"exit",
"zoltraak",
"shopt",
"builtin",
"command",
"echo", "cd", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command",
];
/// Sets up a builtin command
///
/// Prepares a builtin for execution by processing arguments, setting up redirections, and registering the command as a child process in the given `JobBldr`
/// Prepares a builtin for execution by processing arguments, setting up
/// redirections, and registering the command as a child process in the given
/// `JobBldr`
///
/// # Parameters
/// * argv - The vector of raw argument tokens
/// * job - A mutable reference to a `JobBldr`
/// * io_mode - An optional 2-tuple consisting of a mutable reference to an `IoStack` and a vector of `Redirs`
/// * io_mode - An optional 2-tuple consisting of a mutable reference to an
/// `IoStack` and a vector of `Redirs`
///
/// # Behavior
/// * Cleans, expands, and word splits the arg vector
@@ -56,8 +51,10 @@ pub const BUILTINS: [&str;19] = [
/// * The popped `IoFrame`, if any
///
/// # Notes
/// * If redirections are given to this function, the caller must call `IoFrame.restore()` on the returned `IoFrame`
/// * If redirections are given, the second field of the resulting tuple will *always* be `Some()`
/// * If redirections are given to this function, the caller must call
/// `IoFrame.restore()` on the returned `IoFrame`
/// * If redirections are given, the second field of the resulting tuple will
/// *always* be `Some()`
/// * If no redirections are given, the second field will *always* be `None`
type SetupReturns = ShResult<(Vec<(String, Span)>, Option<IoFrame>)>;
pub fn setup_builtin(
@@ -86,6 +83,7 @@ pub fn setup_builtin(
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))
}

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;
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!()
};

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;
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!()
};
@@ -12,13 +21,11 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
if let Some((arg, span)) = argv.next() {
let Ok(count) = arg.parse::<usize>() else {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ExecFail,
"Expected a number in shift args",
span
)
)
span,
));
};
for _ in 0..count {
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;
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!()
};
@@ -13,7 +24,7 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
io_frame.redirect()?;
for (arg, span) in argv {
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
continue
continue;
};
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()) {
io_frame.restore()?;
return Err(e.into())
return Err(e.into());
}
}
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;
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!()
};
@@ -12,22 +22,18 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
for (arg, span) in argv {
let path = PathBuf::from(arg);
if !path.exists() {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("source: File '{}' not found", path.display()),
span
)
);
span,
));
}
if !path.is_file() {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("source: Given path '{}' is not a file", path.display()),
span
)
);
span,
));
}
source_file(path)?;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,22 +4,31 @@ use crate::{prelude::*, state::write_jobs};
///
/// The previous state of the terminal options.
///
/// This variable stores the terminal settings at the start of the program and restores them when the program exits.
/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program.
/// It will not be mutated or accessed under any other circumstances.
/// This variable stores the terminal settings at the start of the program and
/// restores them when the program exits. It is initialized exactly once at the
/// start of the program and accessed exactly once at the end of the program. It
/// will not be mutated or accessed under any other circumstances.
///
/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky.
/// This ended up being necessary because wrapping Termios in a thread-safe way
/// was unreasonably tricky.
///
/// The possible states of this variable are:
/// - `None`: The terminal options have not been set yet (before initialization).
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved.
/// - `None`: The terminal options have not been set yet (before
/// initialization).
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal
/// input detected).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been
/// saved.
///
/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because:
/// - It is set once during program startup and accessed once during program exit.
/// **Important:** This static variable is mutable and accessed via unsafe code.
/// It is only safe to use because:
/// - It is set once during program startup and accessed once during program
/// exit.
/// - It is not mutated or accessed after the initial setup and final read.
///
/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior.
/// **Caution:** Future changes to this code should respect these constraints to
/// ensure safety. Modifying or accessing this variable outside the defined
/// lifecycle could lead to undefined behavior.
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() {
@@ -27,7 +36,12 @@ pub fn save_termios() {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
termios::tcsetattr(
std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
Some(termios)
} else {
None
@@ -38,11 +52,13 @@ pub fn save_termios() {
///Access the saved termios
///
///# Safety
///This function is unsafe because it accesses a public mutable static value. This function should only ever be called after save_termios() has already been called.
///This function is unsafe because it accesses a public mutable static value.
/// This function should only ever be called after save_termios() has already
/// been called.
pub unsafe fn get_saved_termios() -> Option<Termios> {
// SAVED_TERMIOS should *only ever* be set once and accessed once
// Set at the start of the program, and accessed during the exit of the program to reset the termios.
// Do not use this variable anywhere else
// Set at the start of the program, and accessed during the exit of the program
// to reset the termios. Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten()
}
@@ -51,7 +67,12 @@ pub fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
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
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 {
@@ -71,12 +75,9 @@ impl CharDequeUtils for VecDeque<char> {
impl TkVecUtils<Tk> for Vec<Tk> {
fn get_span(&self) -> Option<Span> {
if let Some(first_tk) = self.first() {
self.last().map(|last_tk| {
Span::new(
first_tk.span.start..last_tk.span.end,
first_tk.source()
)
})
self
.last()
.map(|last_tk| Span::new(first_tk.span.start..last_tk.span.end, first_tk.source()))
} else {
None
}
@@ -95,15 +96,12 @@ impl RedirVecUtils<Redir> for Vec<Redir> {
for redir in self {
match redir.class {
RedirType::Input => input.push(redir),
RedirType::Pipe => {
match redir.io_mode.tgt_fd() {
RedirType::Pipe => match redir.io_mode.tgt_fd() {
STDIN_FILENO => input.push(redir),
STDOUT_FILENO |
STDERR_FILENO => output.push(redir),
_ => unreachable!()
}
}
_ => output.push(redir)
STDOUT_FILENO | STDERR_FILENO => output.push(redir),
_ => unreachable!(),
},
_ => output.push(redir),
}
}
(input, output)

View File

@@ -1,20 +1,50 @@
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}, AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node, ParsedSrc, Redir, RedirType};
use super::{
lex::{Span, Tk, TkFlags, KEYWORDS},
AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node,
ParsedSrc, Redir, RedirType,
};
pub enum AssignBehavior {
Export,
Set
Set,
}
/// Arguments to the execvpe function
pub struct ExecArgs {
pub cmd: (CString, Span),
pub argv: Vec<CString>,
pub envp: Vec<CString>
pub envp: Vec<CString>,
}
impl ExecArgs {
@@ -32,10 +62,15 @@ impl ExecArgs {
(CString::new(cmd).unwrap(), span)
}
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> {
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 {
eprintln!("{error}");
}
return Ok(())
return Ok(());
}
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 {
nodes: VecDeque<Node>,
pub io_stack: IoStack,
pub job_stack: JobStack
pub job_stack: JobStack,
}
impl Dispatcher {
pub fn new(nodes: Vec<Node>) -> Self {
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<()> {
flog!(TRACE, "beginning dispatch");
@@ -90,13 +129,13 @@ impl Dispatcher {
NdRule::FuncDef { .. } => self.exec_func_def(node)?,
NdRule::Command { .. } => self.dispatch_cmd(node)?,
NdRule::Test { .. } => self.exec_test(node)?,
_ => unreachable!()
_ => unreachable!(),
}
Ok(())
}
pub fn dispatch_cmd(&mut self, node: Node) -> ShResult<()> {
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) {
self.exec_builtin(node)
@@ -120,9 +159,17 @@ impl Dispatcher {
let status = state::get_status();
match operator {
ConjunctOp::And => if status != 0 { break },
ConjunctOp::Or => if status == 0 { break },
ConjunctOp::Null => break
ConjunctOp::And => {
if status != 0 {
break;
}
}
ConjunctOp::Or => {
if status == 0 {
break;
}
}
ConjunctOp::Null => break,
}
}
Ok(())
@@ -145,13 +192,11 @@ impl Dispatcher {
let name = name.span.as_str().strip_suffix("()").unwrap();
if KEYWORDS.contains(&name) {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::SyntaxErr,
format!("function: Forbidden function name `{name}`"),
blame
)
)
blame,
));
}
let mut func_parser = ParsedSrc::new(Arc::new(body));
@@ -159,7 +204,7 @@ impl Dispatcher {
for error in errors {
eprintln!("{error}");
}
return Ok(())
return Ok(());
}
let func = ShFunc::new(func_parser);
@@ -181,7 +226,7 @@ impl Dispatcher {
if let Err(e) = exec_input(subsh_body, None) {
restore_snapshot(snapshot);
return Err(e)
return Err(e);
}
restore_snapshot(snapshot);
@@ -189,7 +234,11 @@ impl Dispatcher {
}
fn exec_func(&mut self, func: Node) -> ShResult<()> {
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!()
};
@@ -215,26 +264,21 @@ impl Dispatcher {
match e.kind() {
ShErrKind::FuncReturn(code) => {
state::set_status(*code);
return Ok(())
return Ok(());
}
_ => return {
Err(e)
_ => return { Err(e) },
}
}
}
// Return to the outer scope
restore_snapshot(snapshot);
Ok(())
} else {
Err(
ShErr::full(
Err(ShErr::full(
ShErrKind::InternalErr,
format!("Failed to find function '{}'", func_name),
blame
)
)
blame,
))
}
}
fn exec_brc_grp(&mut self, brc_grp: Node) -> ShResult<()> {
@@ -253,7 +297,11 @@ impl Dispatcher {
Ok(())
}
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!()
};
@@ -277,7 +325,7 @@ impl Dispatcher {
for node in &body {
self.dispatch_node(node.clone())?;
}
break 'outer
break 'outer;
}
}
}
@@ -291,7 +339,7 @@ impl Dispatcher {
let keep_going = |kind: LoopKind, status: i32| -> bool {
match kind {
LoopKind::While => status == 0,
LoopKind::Until => status != 0
LoopKind::Until => status != 0,
}
};
@@ -318,18 +366,18 @@ impl Dispatcher {
match e.kind() {
ShErrKind::LoopBreak(code) => {
state::set_status(*code);
break 'outer
break 'outer;
}
ShErrKind::LoopContinue(code) => {
state::set_status(*code);
continue 'outer
continue 'outer;
}
_ => return Err(e)
_ => return Err(e),
}
}
}
} else {
break
break;
}
}
@@ -348,37 +396,39 @@ impl Dispatcher {
'outer: for chunk in arr.chunks(vars.len()) {
let empty = Tk::default();
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 {
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), false));
}
for node in body.clone() {
self.io_stack.push(body_frame.clone());
if let Err(e) = self.dispatch_node(node) {
match e.kind() {
ShErrKind::LoopBreak(code) => {
state::set_status(*code);
break 'outer
break 'outer;
}
ShErrKind::LoopContinue(code) => {
state::set_status(*code);
continue 'outer
continue 'outer;
}
_ => return Err(e)
_ => return Err(e),
}
}
}
}
Ok(())
}
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!();
};
// Pop the current frame and split it
@@ -404,7 +454,7 @@ impl Dispatcher {
self.dispatch_node(body_node)?;
}
}
_ => continue
_ => continue,
}
}
@@ -423,10 +473,7 @@ impl Dispatcher {
};
self.job_stack.new_job();
// Zip the commands and their respective pipes into an iterator
let pipes_and_cmds = get_pipe_stack(cmds.len())
.into_iter()
.zip(cmds);
let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds);
for ((rpipe, wpipe), cmd) in pipes_and_cmds {
if let Some(pipe) = rpipe {
@@ -443,7 +490,11 @@ impl Dispatcher {
Ok(())
}
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!()
};
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;
if cmd_raw.as_str() == "builtin" {
*argv = argv.iter_mut()
*argv = argv
.iter_mut()
.skip(1)
.map(|tk| tk.clone())
.collect::<Vec<Tk>>();
return self.exec_builtin(cmd)
return self.exec_builtin(cmd);
} else if cmd_raw.as_str() == "command" {
*argv = argv.iter_mut()
*argv = argv
.iter_mut()
.skip(1)
.map(|tk| tk.clone())
.collect::<Vec<Tk>>();
return self.dispatch_cmd(cmd)
return self.dispatch_cmd(cmd);
}
flog!(TRACE, "doing builtin");
@@ -484,7 +537,10 @@ impl Dispatcher {
"exit" => flowctl(cmd, ShErrKind::CleanExit(0)),
"zoltraak" => zoltraak(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 {
@@ -493,7 +549,7 @@ impl Dispatcher {
if let Err(e) = result {
state::set_status(1);
return Err(e)
return Err(e);
}
Ok(())
}
@@ -512,7 +568,7 @@ impl Dispatcher {
}
if argv.is_empty() {
return Ok(())
return Ok(());
}
self.io_stack.append_to_frame(cmd.redirs);
@@ -524,7 +580,7 @@ impl Dispatcher {
Some(exec_args),
self.job_stack.curr_job_mut().unwrap(),
def_child_action,
def_parent_action
def_parent_action,
)?;
for var in env_vars_to_unset {
@@ -596,7 +652,7 @@ pub fn run_fork<C,P>(
) -> ShResult<()>
where
C: Fn(IoFrame, Option<ExecArgs>),
P: Fn(&mut JobBldr,Option<&str>,Pid) -> ShResult<()>
P: Fn(&mut JobBldr, Option<&str>, Pid) -> ShResult<()>,
{
match unsafe { fork()? } {
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();
match e {
Errno::ENOENT => {
let err = ShErr::full(
ShErrKind::CmdNotFound(cmd),
"",
span
);
let err = ShErr::full(ShErrKind::CmdNotFound(cmd), "", span);
eprintln!("{err}");
}
_ => {
let err = ShErr::full(
ShErrKind::Errno,
format!("{e}"),
span
);
let err = ShErr::full(ShErrKind::Errno, format!("{e}"), span);
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
pub fn def_parent_action(
job: &mut JobBldr,
cmd: Option<&str>,
child_pid: Pid
) -> ShResult<()> {
pub fn def_parent_action(job: &mut JobBldr, cmd: Option<&str>, child_pid: Pid) -> ShResult<()> {
let child_pgid = if let Some(pgid) = job.pgid() {
pgid
} else {
@@ -664,7 +708,6 @@ pub fn def_parent_action(
Ok(())
}
/// Initialize the pipes for a pipeline
/// The first command gets `(None, WPipe)`
/// 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 {
let Some(tk) = tk else {
return false
};
let Some(tk) = tk else { return false };
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 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] = [
"if",
"then",
"elif",
"else",
"fi",
"while",
"until",
"select",
"for",
"in",
"do",
"done",
"case",
"esac",
"[[",
"]]"
"if", "then", "elif", "else", "fi", "while", "until", "select", "for", "in", "do", "done",
"case", "esac", "[[", "]]",
];
pub const OPENERS: [&str;6] = [
"if",
"while",
"until",
"for",
"select",
"case"
];
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
/// Span::new(10..20)
#[derive(Clone, PartialEq, Default, Debug)]
pub struct Span {
range: Range<usize>,
source: Arc<String>
source: Arc<String>,
}
impl Span {
/// New `Span`. Wraps a range and a string slice that it refers to.
pub fn new(range: Range<usize>, source: Arc<String>) -> Self {
Span {
range,
source,
}
Span { range, source }
}
/// Slice the source string at the wrapped range
pub fn as_str(&self) -> &str {
@@ -97,13 +87,18 @@ impl Default for TkRule {
pub struct Tk {
pub class: TkRule,
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 {
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 {
self.span.as_str()
@@ -127,12 +122,11 @@ impl Display for Tk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.class {
TkRule::Expanded { exp } => write!(f, "{}", exp.join(" ")),
_ => write!(f,"{}",self.span.as_str())
_ => write!(f, "{}", self.span.as_str()),
}
}
}
bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Default)]
pub struct TkFlags: u32 {
@@ -184,7 +178,12 @@ impl LexStream {
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
flog!(TRACE, "new lex stream");
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 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(..10)`
/// `LexStream.slice(1..)`
///
pub fn slice<R: RangeBounds<usize>>(&self, range: R) -> Option<&str> {
let start = match range.start_bound() {
Bound::Included(&start) => start,
Bound::Excluded(&start) => start + 1,
Bound::Unbounded => 0
Bound::Unbounded => 0,
};
let end = match range.end_bound() {
Bound::Included(&end) => end,
Bound::Excluded(&end) => end + 1,
Bound::Unbounded => self.source.len()
Bound::Unbounded => self.source.len(),
};
self.source.get(start..end)
}
@@ -244,7 +242,7 @@ impl LexStream {
match ch {
'>' => {
if chars.peek() == Some(&'(') {
return None // It's a process sub
return None; // It's a process sub
}
pos += 1;
if let Some('>') = chars.peek() {
@@ -262,27 +260,24 @@ impl LexStream {
pos += 1;
}
if !found_fd {
return Some(Err(
ShErr::full(
return Some(Err(ShErr::full(
ShErrKind::ParseErr,
"Invalid redirection",
Span::new(self.cursor..pos, self.source.clone())
)
));
Span::new(self.cursor..pos, self.source.clone()),
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break
break;
}
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break
break;
}
}
'<' => {
if chars.peek() == Some(&'(') {
return None // It's a process sub
return None; // It's a process sub
}
pos += 1;
@@ -291,11 +286,11 @@ impl LexStream {
chars.next();
pos += 1;
} else {
break
break;
}
}
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break
break;
}
'0'..='9' => {
pos += 1;
@@ -311,7 +306,7 @@ impl LexStream {
}
if tk == Tk::default() {
return None
return None;
}
self.cursor = pos;
@@ -330,7 +325,7 @@ impl LexStream {
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
self.cursor = pos;
self.set_next_is_cmd(true);
return Ok(casepat_tk)
return Ok(casepat_tk);
}
}
@@ -369,10 +364,10 @@ impl LexStream {
pos += 1;
brace_count -= 1;
if brace_count == 0 {
break
break;
}
}
_ => pos += ch.len_utf8()
_ => pos += ch.len_utf8(),
}
}
}
@@ -397,26 +392,24 @@ impl LexStream {
pos += 1;
paren_count -= 1;
if paren_count <= 0 {
break
break;
}
}
_ => pos += ch.len_utf8()
_ => pos += ch.len_utf8(),
}
}
if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos;
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ParseErr,
"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);
proc_sub_tk.flags |= TkFlags::IS_PROCSUB;
self.cursor = pos;
return Ok(proc_sub_tk)
return Ok(proc_sub_tk);
}
'>' if chars.peek() == Some(&'(') => {
pos += 2;
@@ -439,26 +432,24 @@ impl LexStream {
pos += 1;
paren_count -= 1;
if paren_count <= 0 {
break
break;
}
}
_ => pos += ch.len_utf8()
_ => pos += ch.len_utf8(),
}
}
if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos;
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ParseErr,
"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);
proc_sub_tk.flags |= TkFlags::IS_PROCSUB;
self.cursor = pos;
return Ok(proc_sub_tk)
return Ok(proc_sub_tk);
}
'$' if chars.peek() == Some(&'(') => {
pos += 2;
@@ -481,26 +472,24 @@ impl LexStream {
pos += 1;
paren_count -= 1;
if paren_count <= 0 {
break
break;
}
}
_ => pos += ch.len_utf8()
_ => pos += ch.len_utf8(),
}
}
if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos;
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ParseErr,
"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);
cmdsub_tk.flags |= TkFlags::IS_CMDSUB;
self.cursor = pos;
return Ok(cmdsub_tk)
return Ok(cmdsub_tk);
}
'(' if self.next_is_cmd() && can_be_subshell => {
pos += 1;
@@ -522,28 +511,26 @@ impl LexStream {
pos += 1;
paren_count -= 1;
if paren_count <= 0 {
break
break;
}
}
_ => pos += ch.len_utf8()
_ => pos += ch.len_utf8(),
}
}
if paren_count != 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos;
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ParseErr,
"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);
subsh_tk.flags |= TkFlags::IS_CMD;
subsh_tk.flags |= TkFlags::IS_SUBSH;
self.cursor = pos;
self.set_next_is_cmd(true);
return Ok(subsh_tk)
return Ok(subsh_tk);
}
'{' if pos == self.cursor && self.next_is_cmd() => {
pos += 1;
@@ -553,7 +540,7 @@ impl LexStream {
self.set_next_is_cmd(true);
self.cursor = pos;
return Ok(tk)
return Ok(tk);
}
'}' if pos == self.cursor && self.in_brc_grp() => {
pos += 1;
@@ -561,7 +548,7 @@ impl LexStream {
self.set_in_brc_grp(false);
self.set_next_is_cmd(true);
self.cursor = pos;
return Ok(tk)
return Ok(tk);
}
'\'' => {
self.in_quote = true;
@@ -577,13 +564,13 @@ impl LexStream {
_ if q_ch == '\'' => {
pos += 1;
self.in_quote = false;
break
break;
}
// Any time an ambiguous character is found
// we must push the cursor by the length of the character
// instead of just assuming a length of 1.
// 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;
pos += 1;
if cmdsub_count <= 0 {
break
break;
}
}
_ => pos += cmdsub_ch.len_utf8(),
@@ -629,7 +616,7 @@ impl LexStream {
_ if q_ch == '"' => {
pos += 1;
self.in_quote = false;
break
break;
}
// Any time an ambiguous character is found
// 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 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);
if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
return Err(
ShErr::full(
return Err(ShErr::full(
ShErrKind::ParseErr,
"Unterminated quote",
new_tk.span,
)
);
));
}
let text = new_tk.span.as_str();
@@ -712,25 +697,25 @@ impl Iterator for LexStream {
if self.cursor == self.source.len() {
if self.flags.contains(LexFlags::STALE) {
// We've already returned an EOI token, nothing left to do
return None
return None;
} else {
// Return the EOI token
let token = self.get_token(self.cursor..self.cursor, TkRule::EOI);
self.flags |= LexFlags::STALE;
return Some(Ok(token))
return Some(Ok(token));
}
}
// Return the SOI token
if self.flags.contains(LexFlags::FRESH) {
self.flags &= !LexFlags::FRESH;
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
// Used for word splitting variable values
if self.flags.contains(LexFlags::RAW) {
return Some(self.read_string())
return Some(self.read_string());
}
loop {
@@ -740,12 +725,12 @@ impl Iterator for LexStream {
} else if pos < self.source.len() && is_field_sep(get_char(&self.source, pos).unwrap()) {
self.cursor += 1;
} else {
break
break;
}
}
if self.cursor == self.source.len() {
return None
return None;
}
let token = match get_char(&self.source, self.cursor).unwrap() {
@@ -755,10 +740,11 @@ impl Iterator for LexStream {
self.set_next_is_cmd(true);
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;
} else {
break
break;
}
}
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) {
self.cursor += 1;
if ch == '\n' {
break
break;
}
}
@@ -811,14 +797,14 @@ impl Iterator for LexStream {
self.set_next_is_cmd(false);
match tk {
Ok(tk) => tk,
Err(e) => return Some(Err(e))
Err(e) => return Some(Err(e)),
}
} else {
match self.read_string() {
Ok(tk) => tk,
Err(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> {
src.get(idx..)?.chars().next()
}
@@ -838,9 +823,11 @@ pub fn is_assignment(text: &str) -> bool {
while let Some(ch) = chars.next() {
match ch {
'\\' => { chars.next(); }
'\\' => {
chars.next();
}
'=' => return true,
_ => continue
_ => continue,
}
}
false
@@ -862,8 +849,7 @@ pub fn is_field_sep(ch: char) -> bool {
}
pub fn is_keyword(slice: &str) -> bool {
KEYWORDS.contains(&slice) ||
(slice.ends_with("()") && !slice.ends_with("\\()"))
KEYWORDS.contains(&slice) || (slice.ends_with("()") && !slice.ends_with("\\()"))
}
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();
}
if char_deque.starts_with(pat) {
return Some(pos)
return Some(pos);
}
pos += 1;
}
@@ -892,7 +878,9 @@ pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
pos += 1;
match ch {
_ if is_hard_sep(ch) => return None,
'\\' => { chars.next(); }
'\\' => {
chars.next();
}
')' => return Some(pos),
'(' => return None,
_ => { /* continue */ }

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,37 @@
// 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::ffi::{CStr, CString};
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
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
pub use bitflags::bitflags;
pub use nix::{
errno::Errno,
fcntl::{open, OFlag},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{
termios::{ self },
signal::{ self, signal, kill, killpg, pthread_sigmask, SigSet, SigmaskHow, SigHandler, Signal },
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
stat::Mode,
termios::{self},
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
},
libc::{ self, STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO },
unistd::{
dup, read, isatty, write, close, setpgid, dup2, getpgrp, getpgid,
execvpe, tcgetpgrp, tcsetpgrp, fork, pipe, Pid, ForkResult
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
tcsetpgrp, write, ForkResult, Pid,
},
};
pub use bitflags::bitflags;
pub use crate::flog;
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
// https://fishshell.com/
// Credit to fish-shell for many of the implementation ideas present in this
// module https://fishshell.com/
#[derive(Clone, Debug)]
pub enum IoMode {
Fd { tgt_fd: RawFd, src_fd: Arc<OwnedFd> },
File { tgt_fd: RawFd, path: PathBuf, mode: RedirType },
Pipe { tgt_fd: RawFd, pipe: Arc<OwnedFd> },
Buffer { buf: String, pipe: Arc<OwnedFd> }
Fd {
tgt_fd: RawFd,
src_fd: 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 {
@@ -27,10 +50,10 @@ impl IoMode {
}
pub fn tgt_fd(&self) -> RawFd {
match self {
IoMode::Fd { tgt_fd, .. } |
IoMode::File { tgt_fd, .. } |
IoMode::Pipe { tgt_fd, .. } => *tgt_fd,
_ => panic!()
IoMode::Fd { tgt_fd, .. } | IoMode::File { tgt_fd, .. } | IoMode::Pipe { tgt_fd, .. } => {
*tgt_fd
}
_ => panic!(),
}
}
pub fn src_fd(&self) -> RawFd {
@@ -38,21 +61,30 @@ impl IoMode {
IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(),
IoMode::File { .. } => panic!("Attempted to obtain src_fd from file before opening"),
IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(),
_ => panic!()
_ => panic!(),
}
}
pub fn open_file(mut self) -> ShResult<Self> {
if let IoMode::File { tgt_fd, path, mode } = self {
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)
}
pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = pipe().unwrap();
(
Self::Pipe { tgt_fd: STDIN_FILENO, pipe: rpipe.into() },
Self::Pipe { tgt_fd: STDOUT_FILENO, pipe: wpipe.into() }
Self::Pipe {
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)
pub fn as_str(&self) -> ShResult<&str> {
std::str::from_utf8(&self.buf).map_err(|_| {
ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf")
})
std::str::from_utf8(&self.buf)
.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)]
pub struct IoGroup(RawFd, RawFd, RawFd);
@@ -125,24 +157,34 @@ impl<'e> IoFrame {
Default::default()
}
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 {
Self { redirs: vec![redir], saved_io: None }
Self {
redirs: vec![redir],
saved_io: None,
}
}
/// Splits the frame into two frames
///
/// One frame contains input redirections, the other contains output redirections
/// This is used in shell structures to route redirections either *to* the condition, or *from* the body
/// The first field of the tuple contains input redirections (used for the condition)
/// The second field contains output redirections (used for the body)
/// One frame contains input redirections, the other contains output
/// redirections This is used in shell structures to route redirections
/// either *to* the condition, or *from* the body The first field of the
/// 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) {
let Self { redirs, saved_io: _ } = self;
let Self {
redirs,
saved_io: _,
} = self;
let (input_redirs, output_redirs) = redirs.split_by_channel();
(
Self::from_redirs(input_redirs),
Self::from_redirs(output_redirs)
Self::from_redirs(output_redirs),
)
}
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
///
/// This struct maintains the current state of I/O for the `Dispatcher` struct
/// Each executed command requires an `IoFrame` in order to perform redirections.
/// As nodes are walked through by the `Dispatcher`, it pushes new 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
/// Each executed command requires an `IoFrame` in order to perform
/// redirections. As nodes are walked through by the `Dispatcher`, it pushes new
/// 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)]
pub struct IoStack {
stack: Vec<IoFrame>,
@@ -223,8 +267,9 @@ impl IoStack {
}
/// Pop the current 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
/// There will always be at least one frame in the `IoStack`.
/// If `self.pop()` would empty the `IoStack`, it instead uses
/// `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 {
if self.stack.len() > 1 {
self.pop().unwrap()
@@ -238,7 +283,8 @@ impl IoStack {
}
/// Flatten the `IoStack`
/// 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) {
let mut flat_frame = IoFrame::new();
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 readline;
use std::path::Path;
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
fn get_prompt() -> ShResult<String> {
@@ -15,8 +18,9 @@ fn get_prompt() -> ShResult<String> {
// username@hostname
// 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 ";
return expand_prompt(default)
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 ";
return expand_prompt(default);
};
expand_prompt(&prompt)
@@ -26,7 +30,7 @@ pub fn readline(edit_mode: FernEditMode) -> ShResult<String> {
let prompt = get_prompt()?;
let mut reader: Box<dyn Readline> = match edit_mode {
FernEditMode::Vi => Box::new(FernVi::new(Some(prompt))?),
FernEditMode::Emacs => todo!()
FernEditMode::Emacs => todo!(),
};
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::prelude::*;
@@ -9,7 +17,7 @@ use super::vicmd::Direction; // surprisingly useful
pub enum SearchKind {
Fuzzy,
#[default]
Prefix
Prefix,
}
#[derive(Default, Clone, Debug)]
@@ -29,7 +37,7 @@ pub struct HistEntry {
id: u32,
timestamp: SystemTime,
command: String,
new: bool
new: bool,
}
impl HistEntry {
@@ -69,22 +77,39 @@ impl HistEntry {
impl FromStr for HistEntry {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = Err(
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] }
);
let err = Err(ShErr::Simple {
kind: ShErrKind::HistoryReadErr,
msg: format!("Bad formatting on history entry '{s}'"),
notes: vec![],
});
//: 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
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")
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")
let Ok(ts_seconds) = timestamp.parse::<u64>() else { return err };
let Ok(id) = id.parse::<u32>() else { return err };
let Ok(ts_seconds) = timestamp.parse::<u64>() else {
return err;
};
let Ok(id) = id.parse::<u32>() else {
return err;
};
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
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
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
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();
writeln!(f, ": {timestamp};{id};{command}")
}
@@ -100,7 +130,6 @@ impl Display for HistEntry {
pub struct HistEntries(Vec<HistEntry>);
impl FromStr for HistEntries {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -111,9 +140,11 @@ impl FromStr for HistEntries {
while let Some((i, line)) = lines.next() {
if !line.starts_with(": ") {
return Err(
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
)
return Err(ShErr::Simple {
kind: ShErrKind::HistoryReadErr,
msg: format!("Bad formatting on line {i}"),
notes: vec![],
});
}
let mut chars = line.chars().peekable();
let mut feeding_lines = true;
@@ -129,9 +160,7 @@ impl FromStr for HistEntries {
feeding_lines = true;
}
}
'\n' => {
break
}
'\n' => break,
_ => {
cur_line.push(ch);
}
@@ -139,9 +168,11 @@ impl FromStr for HistEntries {
}
if feeding_lines {
let Some((_, line)) = lines.next() else {
return Err(
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] }
)
return Err(ShErr::Simple {
kind: ShErrKind::HistoryReadErr,
msg: format!("Bad formatting on line {i}"),
notes: vec![],
});
};
chars = line.chars().peekable();
}
@@ -151,7 +182,6 @@ impl FromStr for HistEntries {
cur_line.clear();
}
Ok(Self(entries))
}
}
@@ -185,7 +215,12 @@ impl History {
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
let timestamp = SystemTime::now();
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 cursor = entries.len() - 1;
@@ -210,20 +245,19 @@ impl History {
&self.search_mask
}
pub fn push_empty_entry(&mut self) {
}
pub fn push_empty_entry(&mut self) {}
pub fn cursor_entry(&self) -> Option<&HistEntry> {
self.search_mask.get(self.cursor)
}
pub fn update_pending_cmd(&mut self, command: &str) {
let Some(ent) = self.last_mut() else {
return
};
let Some(ent) = self.last_mut() else { return };
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;
self.constrain_entries(constraint);
@@ -235,7 +269,7 @@ impl History {
pub fn get_new_id(&self) -> u32 {
let Some(ent) = self.entries.last() else {
return 0
return 0;
};
ent.id + 1
}
@@ -256,7 +290,8 @@ impl History {
if term.is_empty() {
self.search_mask = self.entries.clone();
} else {
let filtered = self.entries
let filtered = self
.entries
.clone()
.into_iter()
.filter(|ent| ent.command().starts_with(&term));
@@ -275,7 +310,10 @@ impl History {
}
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 prefix = self.cursor_entry()?.command();
Some(entry.command().to_string())
@@ -285,7 +323,8 @@ impl History {
}
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
let new_idx = self.cursor
let new_idx = self
.cursor
.saturating_add_signed(offset)
.clamp(0, self.search_mask.len().saturating_sub(1));
let ent = self.search_mask.get(new_idx)?;
@@ -299,14 +338,19 @@ impl History {
let timestamp = SystemTime::now();
let id = self.get_new_id();
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 {
let Some(ent) = self.entries.last() else {
return false
return false;
};
let ent_cmd = &ent.command;
ent_cmd == other
@@ -318,19 +362,18 @@ impl History {
.append(true)
.open(&self.path)?;
let last_file_entry = self.entries
let last_file_entry = self
.entries
.iter()
.filter(|ent| !ent.new)
.next_back()
.map(|ent| ent.command.clone())
.unwrap_or_default();
let entries = self.entries
.iter_mut()
.filter(|ent| {
ent.new &&
!ent.command.is_empty() &&
if self.ignore_dups {
let entries = self.entries.iter_mut().filter(|ent| {
ent.new
&& !ent.command.is_empty()
&& if self.ignore_dups {
ent.command() != last_file_entry
} else {
true

View File

@@ -6,7 +6,6 @@ use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct KeyEvent(pub KeyCode, pub ModKeys);
impl KeyEvent {
pub fn new(ch: &str, mut mods: ModKeys) -> Self {
use {KeyCode as K, KeyEvent as E, ModKeys as M};
@@ -29,8 +28,7 @@ impl KeyEvent {
let is_single_char = chars.next().is_none();
match single_char {
Some(c) if is_single_char && c.is_control() => {
match c {
Some(c) if is_single_char && c.is_control() => match c {
'\x00' => E(K::Char('@'), mods | M::CTRL),
'\x01' => E(K::Char('A'), mods | M::CTRL),
'\x02' => E(K::Char('B'), mods | M::CTRL),
@@ -73,8 +71,7 @@ impl KeyEvent {
'\x7f' => E(K::Backspace, mods),
'\u{9b}' => E(K::Esc, mods | M::SHIFT),
_ => E(K::Null, mods),
}
}
},
Some(c) if is_single_char => {
if !mods.is_empty() {
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 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::*;
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 keys;
pub mod layout;
pub mod linebuf;
pub mod register;
pub mod term;
pub mod vicmd;
pub mod vimode;
// 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.";
@@ -34,7 +38,7 @@ pub struct FernVi {
pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf,
pub history: History
pub history: History,
}
impl Readline for FernVi {
@@ -47,7 +51,7 @@ impl Readline for FernVi {
let Some(key) = self.reader.read_key() else {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard);
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"))
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
};
flog!(DEBUG, key);
@@ -55,12 +59,12 @@ impl Readline for FernVi {
self.editor.accept_hint();
self.history.update_pending_cmd(self.editor.as_str());
self.print_line()?;
continue
continue;
}
let Some(mut cmd) = self.mode.handle_key(key) else {
flog!(DEBUG, "got none??");
continue
continue;
};
flog!(DEBUG, cmd);
cmd.alter_line_motion_if_no_verb();
@@ -68,13 +72,13 @@ impl Readline for FernVi {
if self.should_grab_history(&cmd) {
self.scroll_history(cmd);
self.print_line()?;
continue
continue;
}
if cmd.should_submit() {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
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) {
@@ -83,7 +87,7 @@ impl Readline for FernVi {
sh_quit(0);
} else {
self.editor.buffer.clear();
continue
continue;
}
}
flog!(DEBUG, cmd);
@@ -113,7 +117,7 @@ impl FernVi {
repeat_action: None,
repeat_motion: None,
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
history: History::new()?
history: History::new()?,
})
}
@@ -122,13 +126,7 @@ impl FernVi {
flog!(DEBUG, line);
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols, _) = get_win_size(STDIN_FILENO);
Layout::from_parts(
/*tab_stop:*/ 8,
cols,
&self.prompt,
to_cursor,
&line
)
Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, &line)
}
pub fn scroll_history(&mut self, cmd: ViCmd) {
flog!(DEBUG, "scrolling");
@@ -145,7 +143,7 @@ impl FernVi {
let entry = match motion {
Motion::LineUpCharwise => {
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
return
return;
};
flog!(DEBUG, "found entry");
flog!(DEBUG, hist_entry.command());
@@ -153,22 +151,20 @@ impl FernVi {
}
Motion::LineDownCharwise => {
let Some(hist_entry) = self.history.scroll(*count as isize) else {
return
return;
};
flog!(DEBUG, "found entry");
flog!(DEBUG, hist_entry.command());
hist_entry
}
_ => unreachable!()
_ => unreachable!(),
};
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
let mut buf = LineBuf::new().with_initial(entry.command(), 0);
let line_end = buf.end_of_line();
if let Some(dest) = self.mode.hist_scroll_start_pos() {
match dest {
To::Start => {
/* Already at 0 */
}
To::Start => { /* Already at 0 */ }
To::End => {
// History entries cannot be empty
// So this subtraction is safe (maybe)
@@ -187,28 +183,15 @@ impl FernVi {
flog!(DEBUG, self.editor.cursor);
if self.editor.cursor_at_max() && self.editor.has_hint() {
match self.mode.report_mode() {
ModeReport::Replace |
ModeReport::Insert => {
matches!(
event,
KeyEvent(KeyCode::Right, ModKeys::NONE)
)
ModeReport::Replace | ModeReport::Insert => {
matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
}
ModeReport::Visual |
ModeReport::Normal => {
matches!(
event,
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)
)
)
ModeReport::Visual | ModeReport::Normal => {
matches!(event, 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 {
false
@@ -216,16 +199,16 @@ impl FernVi {
}
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
cmd.verb().is_none() &&
(
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) &&
self.editor.start_of_line() == 0
) ||
(
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) &&
self.editor.end_of_line() == self.editor.cursor_max() &&
!self.history.cursor_entry().is_some_and(|ent| ent.is_new())
)
cmd.verb().is_none()
&& (cmd
.motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
&& self.editor.start_of_line() == 0)
|| (cmd
.motion()
.is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
&& 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<()> {
@@ -234,11 +217,9 @@ impl FernVi {
self.writer.clear_rows(layout)?;
}
self.writer.redraw(
&self.prompt,
&self.editor,
&new_layout
)?;
self
.writer
.redraw(&self.prompt, &self.editor, &new_layout)?;
self.writer.flush_write(&self.mode.cursor_style())?;
@@ -252,35 +233,33 @@ impl FernVi {
if cmd.is_mode_transition() {
let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
Verb::Change |
Verb::InsertModeLineBreak(_) |
Verb::InsertMode => {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::NormalMode => {
Box::new(ViNormal::new())
}
Verb::NormalMode => Box::new(ViNormal::new()),
Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::VisualModeSelectLast => {
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());
std::mem::swap(&mut mode, &mut self.mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
return self.editor.exec_cmd(cmd)
return self.editor.exec_cmd(cmd);
}
Verb::VisualMode => {
selecting = true;
Box::new(ViVisual::new())
}
_ => unreachable!()
_ => unreachable!(),
};
std::mem::swap(&mut mode, &mut self.mode);
@@ -293,7 +272,9 @@ impl FernVi {
self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if selecting {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End));
self
.editor
.start_selecting(SelectMode::Char(SelectAnchor::End));
} else {
self.editor.stop_selecting();
}
@@ -302,10 +283,10 @@ impl FernVi {
} else {
self.editor.clear_insert_mode_start_pos();
}
return Ok(())
return Ok(());
} else if cmd.is_cmd_repeat() {
let Some(replay) = self.repeat_action.clone() else {
return Ok(())
return Ok(());
};
let ViCmd { verb, .. } = cmd;
let VerbCmd(count, _) = verb.unwrap();
@@ -332,32 +313,33 @@ impl FernVi {
m_mut.0 = 1
}
} 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)?;
}
_ => 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() {
match cmd.motion.as_ref().unwrap() {
MotionCmd(count, Motion::RepeatMotion) => {
let Some(motion) = self.repeat_motion.clone() else {
return Ok(())
return Ok(());
};
let repeat_cmd = ViCmd {
register: RegisterName::default(),
verb: None,
motion: Some(motion),
raw_seq: format!("{count};"),
flags: CmdFlags::empty()
flags: CmdFlags::empty(),
};
return self.editor.exec_cmd(repeat_cmd);
}
MotionCmd(count, Motion::RepeatMotionRev) => {
let Some(motion) = self.repeat_motion.clone() else {
return Ok(())
return Ok(());
};
let mut new_motion = motion.invert_char_motion();
new_motion.0 = *count;
@@ -366,18 +348,18 @@ impl FernVi {
verb: None,
motion: Some(new_motion),
raw_seq: format!("{count},"),
flags: CmdFlags::empty()
flags: CmdFlags::empty(),
};
return self.editor.exec_cmd(repeat_cmd);
}
_ => unreachable!()
_ => unreachable!(),
}
}
if cmd.is_repeatable() {
if self.mode.report_mode() == ModeReport::Visual {
// The motion is assigned in the line buffer execution, so we also have to assign it here
// in order to be able to repeat it
// The motion is assigned in the line buffer execution, so we also have to
// assign it here in order to be able to repeat it
let range = self.editor.select_range().unwrap();
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
}
@@ -398,4 +380,3 @@ impl FernVi {
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) {
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) {
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)]
@@ -82,7 +86,7 @@ impl Registers {
}
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
let Some(ch) = ch else {
return Some(&self.default)
return Some(&self.default);
};
match ch {
'a' => Some(&self.a),
@@ -111,12 +115,12 @@ impl Registers {
'x' => Some(&self.x),
'y' => Some(&self.y),
'z' => Some(&self.z),
_ => None
_ => None,
}
}
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
let Some(ch) = ch else {
return Some(&mut self.default)
return Some(&mut self.default);
};
match ch {
'a' => Some(&mut self.a),
@@ -145,7 +149,7 @@ impl Registers {
'x' => Some(&mut self.x),
'y' => Some(&mut self.y),
'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_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}};
use crate::prelude::*;
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
prompt::readline::keys::{KeyCode, ModKeys},
};
use super::{keys::KeyEvent, linebuf::LineBuf};
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();
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");
RawModeGuard { orig, fd: STDIN_FILENO }
RawModeGuard {
orig,
fd: STDIN_FILENO,
}
}
pub type Row = u16;
@@ -24,7 +48,7 @@ pub type Col = u16;
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Pos {
col: Col,
row: Row
row: Row,
}
// 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;
if cfg!(test) {
return (80,24)
return (80, 24);
}
unsafe {
@@ -55,7 +79,7 @@ pub fn get_win_size(fd: RawFd) -> (Col,Row) {
};
(cols, rows)
}
_ => (80,24)
_ => (80, 24),
}
}
}
@@ -112,9 +136,9 @@ pub fn width_calculator() -> Box<dyn WidthCalculator> {
Ok("WezTerm") => Box::new(UnicodeWidth),
Err(std::env::VarError::NotPresent) => match std::env::var("TERM").as_deref() {
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 {
left.saturating_mul(10)
.saturating_add(right)
left.saturating_mul(10).saturating_add(right)
}
pub trait WidthCalculator {
fn width(&self, text: &str) -> usize;
}
@@ -150,12 +172,7 @@ pub trait KeyReader {
pub trait LineWriter {
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>;
fn redraw(
&mut self,
prompt: &str,
line: &LineBuf,
new_layout: &Layout,
) -> ShResult<()>;
fn redraw(&mut self, prompt: &str, line: &LineBuf, new_layout: &Layout) -> ShResult<()>;
fn flush_write(&mut self, buf: &str) -> ShResult<()>;
}
@@ -202,15 +219,13 @@ impl WidthCalculator for NoZwj {
}
pub struct TermBuffer {
tty: RawFd
tty: RawFd,
}
impl TermBuffer {
pub fn new(tty: RawFd) -> Self {
assert!(isatty(tty).is_ok_and(|r| r));
Self {
tty
}
Self { tty }
}
}
@@ -221,7 +236,7 @@ impl Read for TermBuffer {
match nix::unistd::read(self.tty, buf) {
Ok(n) => return Ok(n),
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
let mut raw = self.orig.clone();
termios::cfmakeraw(&mut raw);
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw)
.expect("Failed to re-enable raw mode");
termios::tcsetattr(fd, termios::SetArg::TCSANOW, &raw).expect("Failed to re-enable raw mode");
result
}
@@ -258,13 +272,17 @@ impl RawModeGuard {
impl Drop for RawModeGuard {
fn drop(&mut self) {
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 {
buffer: BufReader<TermBuffer>
buffer: BufReader<TermBuffer>,
}
impl Default for TermReader {
@@ -276,18 +294,18 @@ impl Default for TermReader {
impl TermReader {
pub fn new() -> Self {
Self {
buffer: BufReader::new(TermBuffer::new(1))
buffer: BufReader::new(TermBuffer::new(1)),
}
}
/// Execute some logic in raw mode
///
/// 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> {
if !self.buffer.buffer().is_empty() {
return Ok(true)
return Ok(true);
}
let mut fds = [poll::PollFd::new(self.as_fd(), PollFlags::POLLIN)];
@@ -295,7 +313,7 @@ impl TermReader {
match r {
Ok(n) => Ok(n != 0),
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> {
let buf = self.buffer.fill_buf()?;
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 {
Ok(buf[0])
}
@@ -318,8 +339,6 @@ impl TermReader {
self.buffer.consume(1);
}
pub fn parse_esc_seq(&mut self) -> ShResult<KeyEvent> {
let mut seq = vec![0x1b];
@@ -398,7 +417,6 @@ impl TermReader {
_ => Ok(KeyEvent(KeyCode::Esc, ModKeys::empty())),
}
}
}
impl KeyReader for TermReader {
@@ -443,7 +461,7 @@ pub struct Layout {
pub w_calc: Box<dyn WidthCalculator>,
pub prompt_end: Pos,
pub cursor: Pos,
pub end: Pos
pub end: Pos,
}
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 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);
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 {
@@ -530,26 +553,31 @@ impl TermWriter {
t_cols,
buffer: String::new(),
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> {
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) {
std::cmp::Ordering::Greater => {
let shift = new.row - old.row;
match shift {
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 => {
let shift = old.row - new.row;
match shift {
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 */ }
@@ -560,14 +588,14 @@ impl TermWriter {
let shift = new.col - old.col;
match shift {
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 => {
let shift = old.col - new.col;
match shift {
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 */ }
@@ -582,38 +610,40 @@ impl TermWriter {
Ok(())
}
pub fn update_t_cols(&mut self) {
let (t_cols, _) = get_win_size(self.out);
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)?;
if result {
// The terminals reply is going to be stuck behind the currently buffered output
// So let's get out of here
return Ok(())
return Ok(());
}
// Ping the cursor's position
self.flush_write("\x1b[6n\n")?;
if !rdr.poll(PollTimeout::from(255u8))? {
return Ok(())
return Ok(());
}
if rdr.next_byte()? as char != '\x1b' {
return Ok(())
return Ok(());
}
if rdr.next_byte()? as char != '[' {
return Ok(())
return Ok(());
}
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
@@ -622,12 +652,13 @@ impl TermWriter {
// The cursor is not at the leftmost, so let's fix that
if col != Some(1) {
if use_newline {
// We use '\n' instead of '\r' sometimes because if there's a bunch of garbage on this line,
// It might pollute the prompt/line buffer if those are shorter than said garbage
// We use '\n' instead of '\r' sometimes because if there's a bunch of garbage
// on this line, It might pollute the prompt/line buffer if those are
// shorter than said garbage
self.flush_write("\n")?;
} else {
// Sometimes though, we know that there's nothing to the right of the cursor after moving
// So we just move to the left.
// Sometimes though, we know that there's nothing to the right of the cursor
// after moving So we just move to the left.
self.flush_write("\r")?;
}
}
@@ -656,13 +687,13 @@ impl LineWriter for TermWriter {
Ok(())
}
fn redraw(
&mut self,
prompt: &str,
line: &LineBuf,
new_layout: &Layout,
) -> ShResult<()> {
let err = |_| ShErr::simple(ShErrKind::InternalErr, "Failed to write to LineWriter internal buffer");
fn redraw(&mut self, prompt: &str, line: &LineBuf, new_layout: &Layout) -> ShResult<()> {
let err = |_| {
ShErr::simple(
ShErrKind::InternalErr,
"Failed to write to LineWriter internal buffer",
)
};
self.buffer.clear();
let end = new_layout.end;

View File

@@ -2,19 +2,20 @@ use bitflags::bitflags;
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)]
pub struct RegisterName {
name: Option<char>,
count: usize,
append: bool
append: bool,
}
impl RegisterName {
pub fn new(name: Option<char>, count: Option<usize>) -> Self {
let Some(ch) = name else {
return Self::default()
return Self::default();
};
let append = ch.is_uppercase();
@@ -22,7 +23,7 @@ impl RegisterName {
Self {
name: Some(name),
count: count.unwrap_or(1),
append
append,
}
}
pub fn name(&self) -> Option<char> {
@@ -51,7 +52,7 @@ impl Default for RegisterName {
Self {
name: None,
count: 1,
append: false
append: false,
}
}
}
@@ -97,8 +98,12 @@ impl ViCmd {
self.motion.as_ref().map(|m| m.0).unwrap_or(1)
}
pub fn normalize_counts(&mut self) {
let Some(verb) = self.verb.as_mut() else { return };
let Some(motion) = self.motion.as_mut() else { return };
let Some(verb) = self.verb.as_mut() else {
return;
};
let Some(motion) = self.motion.as_mut() else {
return;
};
let VerbCmd(v_count, _) = verb;
let MotionCmd(m_count, _) = motion;
let product = *v_count * *m_count;
@@ -109,31 +114,48 @@ impl ViCmd {
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
}
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 {
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 {
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 {
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 {
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 {
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) &&
self.motion.is_none()
self.verb.as_ref().is_some_and(|v| {
matches!(
v.1,
Verb::ReplaceCharInplace(_, _) | Verb::ToggleCaseInplace(_)
)
}) && self.motion.is_none()
}
pub fn is_line_motion(&self) -> bool {
self.motion.as_ref().is_some_and(|m| {
matches!(m.1,
Motion::LineUp |
Motion::LineDown |
Motion::LineUpCharwise |
Motion::LineDownCharwise
matches!(
m.1,
Motion::LineUp | Motion::LineDown | Motion::LineUpCharwise | Motion::LineDownCharwise
)
})
}
@@ -144,21 +166,22 @@ impl ViCmd {
match motion.1 {
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => unreachable!()
_ => unreachable!(),
}
}
}
}
pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| {
matches!(v.1,
Verb::Change |
Verb::InsertMode |
Verb::InsertModeLineBreak(_) |
Verb::NormalMode |
Verb::VisualModeSelectLast |
Verb::VisualMode |
Verb::ReplaceMode
matches!(
v.1,
Verb::Change
| Verb::InsertMode
| Verb::InsertModeLineBreak(_)
| Verb::NormalMode
| Verb::VisualModeSelectLast
| Verb::VisualMode
| Verb::ReplaceMode
)
})
}
@@ -217,59 +240,58 @@ pub enum Verb {
Dedent,
Equalize,
AcceptLineOrNewline,
EndOfFile
EndOfFile,
}
impl Verb {
pub fn is_repeatable(&self) -> bool {
matches!(self,
Self::Delete |
Self::Change |
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_) |
Self::ToLower |
Self::ToUpper |
Self::ToggleCaseRange |
Self::ToggleCaseInplace(_) |
Self::Put(_) |
Self::ReplaceMode |
Self::InsertModeLineBreak(_) |
Self::JoinLines |
Self::InsertChar(_) |
Self::Insert(_) |
Self::Indent |
Self::Dedent |
Self::Equalize
matches!(
self,
Self::Delete
| Self::Change
| Self::ReplaceChar(_)
| Self::ReplaceCharInplace(_, _)
| Self::ToLower
| Self::ToUpper
| Self::ToggleCaseRange
| Self::ToggleCaseInplace(_)
| Self::Put(_)
| Self::ReplaceMode
| Self::InsertModeLineBreak(_)
| Self::JoinLines
| Self::InsertChar(_)
| Self::Insert(_)
| Self::Indent
| Self::Dedent
| Self::Equalize
)
}
pub fn is_edit(&self) -> bool {
matches!(self,
Self::Delete |
Self::Change |
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_) |
Self::ToggleCaseRange |
Self::ToggleCaseInplace(_) |
Self::ToLower |
Self::ToUpper |
Self::RepeatLast |
Self::Put(_) |
Self::ReplaceMode |
Self::InsertModeLineBreak(_) |
Self::JoinLines |
Self::InsertChar(_) |
Self::Insert(_) |
Self::Rot13 |
Self::EndOfFile
matches!(
self,
Self::Delete
| Self::Change
| Self::ReplaceChar(_)
| Self::ReplaceCharInplace(_, _)
| Self::ToggleCaseRange
| Self::ToggleCaseInplace(_)
| Self::ToLower
| Self::ToUpper
| Self::RepeatLast
| Self::Put(_)
| Self::ReplaceMode
| Self::InsertModeLineBreak(_)
| Self::JoinLines
| Self::InsertChar(_)
| Self::Insert(_)
| Self::Rot13
| Self::EndOfFile
)
}
pub fn is_char_insert(&self) -> bool {
matches!(self,
Self::Change |
Self::InsertChar(_) |
Self::ReplaceChar(_) |
Self::ReplaceCharInplace(_,_)
matches!(
self,
Self::Change | Self::InsertChar(_) | Self::ReplaceChar(_) | Self::ReplaceCharInplace(_, _)
)
}
}
@@ -311,14 +333,14 @@ pub enum Motion {
Range(usize, usize),
RepeatMotion,
RepeatMotionRev,
Null
Null,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MotionBehavior {
Exclusive,
Inclusive,
Linewise
Linewise,
}
impl Motion {
@@ -332,35 +354,33 @@ impl Motion {
}
}
pub fn is_exclusive(&self) -> bool {
matches!(&self,
Self::BeginningOfLine |
Self::BeginningOfFirstWord |
Self::BeginningOfScreenLine |
Self::FirstGraphicalOnScreenLine |
Self::LineDownCharwise |
Self::LineUpCharwise |
Self::ScreenLineUpCharwise |
Self::ScreenLineDownCharwise |
Self::ToColumn |
Self::TextObj(TextObj::Sentence(_)) |
Self::TextObj(TextObj::Paragraph(_)) |
Self::CharSearch(Direction::Backward, _, _) |
Self::WordMotion(To::Start,_,_) |
Self::ToBrace(_) |
Self::ToBracket(_) |
Self::ToParen(_) |
Self::ScreenLineDown |
Self::ScreenLineUp |
Self::Range(_, _)
matches!(
&self,
Self::BeginningOfLine
| Self::BeginningOfFirstWord
| Self::BeginningOfScreenLine
| Self::FirstGraphicalOnScreenLine
| Self::LineDownCharwise
| Self::LineUpCharwise
| Self::ScreenLineUpCharwise
| Self::ScreenLineDownCharwise
| Self::ToColumn
| Self::TextObj(TextObj::Sentence(_))
| Self::TextObj(TextObj::Paragraph(_))
| Self::CharSearch(Direction::Backward, _, _)
| Self::WordMotion(To::Start, _, _)
| Self::ToBrace(_)
| Self::ToBracket(_)
| Self::ToParen(_)
| Self::ScreenLineDown
| Self::ScreenLineUp
| Self::Range(_, _)
)
}
pub fn is_linewise(&self) -> bool {
matches!(self,
Self::WholeLine |
Self::LineUp |
Self::LineDown |
Self::ScreenLineDown |
Self::ScreenLineUp
matches!(
self,
Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
)
}
}
@@ -368,7 +388,7 @@ impl Motion {
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Anchor {
After,
Before
Before,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TextObj {
@@ -410,30 +430,30 @@ pub enum TextObj {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Word {
Big,
Normal
Normal,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Bound {
Inside,
Around
Around,
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction {
#[default]
Forward,
Backward
Backward,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Dest {
On,
Before,
After
After,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum To {
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 crate::{libsh::error::{Note, ShErr, ShErrKind, ShResult}, state::ShFunc};
use crate::{
libsh::error::{Note, ShErr, ShErrKind, ShResult},
state::ShFunc,
};
#[derive(Clone, Copy, Debug)]
pub enum FernBellStyle {
@@ -10,7 +12,6 @@ pub enum FernBellStyle {
Disable,
}
impl FromStr for FernBellStyle {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
@@ -18,12 +19,10 @@ impl FromStr for FernBellStyle {
"audible" => Ok(Self::Audible),
"visible" => Ok(Self::Visible),
"disable" => Ok(Self::Disable),
_ => Err(
ShErr::simple(
_ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("Invalid bell style '{s}'")
)
)
format!("Invalid bell style '{s}'"),
)),
}
}
}
@@ -42,7 +41,7 @@ impl Display for FernBellStyle {
pub enum FernEditMode {
#[default]
Vi,
Emacs
Emacs,
}
impl FromStr for FernEditMode {
@@ -51,12 +50,10 @@ impl FromStr for FernEditMode {
match s.to_ascii_lowercase().as_str() {
"vi" => Ok(Self::Vi),
"emacs" => Ok(Self::Emacs),
_ => Err(
ShErr::simple(
_ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("Invalid edit mode '{s}'")
)
)
format!("Invalid edit mode '{s}'"),
)),
}
}
}
@@ -73,7 +70,7 @@ impl Display for FernEditMode {
#[derive(Clone, Debug)]
pub struct ShOpts {
pub core: ShOptCore,
pub prompt: ShOptPrompt
pub prompt: ShOptPrompt,
}
impl Default for ShOpts {
@@ -99,12 +96,10 @@ impl ShOpts {
pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> {
let mut query = opt.split('.');
let Some(key) = query.next() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: No option given"
)
)
"shopt: No option given",
));
};
let remainder = query.collect::<Vec<_>>().join(".");
@@ -116,14 +111,12 @@ impl ShOpts {
return Err(
ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key"
"shopt: Expected 'core' or 'prompt' in shopt key",
)
.with_note(
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
.with_sub_notes(vec![
"Example: 'shopt core.autocd=true'"
])
)
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
),
)
}
}
@@ -134,32 +127,26 @@ impl ShOpts {
// TODO: handle escapes?
let mut query = query.split('.');
let Some(key) = query.next() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: No option given"
)
)
"shopt: No option given",
));
};
let remainder = query.collect::<Vec<_>>().join(".");
match key {
"core" => self.core.get(&remainder),
"prompt" => self.prompt.get(&remainder),
_ => {
Err(
_ => Err(
ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key"
"shopt: Expected 'core' or 'prompt' in shopt key",
)
.with_note(
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
.with_sub_notes(vec![
"Example: 'shopt core.autocd=true'"
])
)
)
}
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
),
),
}
}
}
@@ -181,67 +168,55 @@ impl ShOptCore {
match opt {
"dotglob" => {
let Ok(val) = val.parse::<bool>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for dotglob value"
)
)
"shopt: expected 'true' or 'false' for dotglob value",
));
};
self.dotglob = val;
}
"autocd" => {
let Ok(val) = val.parse::<bool>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for autocd value"
)
)
"shopt: expected 'true' or 'false' for autocd value",
));
};
self.autocd = val;
}
"hist_ignore_dupes" => {
let Ok(val) = val.parse::<bool>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
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;
}
"max_hist" => {
let Ok(val) = val.parse::<usize>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
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;
}
"interactive_comments" => {
let Ok(val) = val.parse::<bool>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for interactive_comments value"
)
)
"shopt: expected 'true' or 'false' for interactive_comments value",
));
};
self.interactive_comments = val;
}
"auto_hist" => {
let Ok(val) = val.parse::<bool>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for auto_hist value"
)
)
"shopt: expected 'true' or 'false' for auto_hist value",
));
};
self.auto_hist = val;
}
@@ -250,28 +225,22 @@ impl ShOptCore {
return Err(
ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected a bell style for bell_style value"
"shopt: expected a bell style for bell_style value",
)
.with_note(
Note::new("bell_style takes these options as values")
.with_sub_notes(vec![
"audible",
"visible",
"disable"
])
)
)
.with_sub_notes(vec!["audible", "visible", "disable"]),
),
);
};
self.bell_style = val;
}
"max_recurse_depth" => {
let Ok(val) = val.parse::<usize>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
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;
}
@@ -279,12 +248,11 @@ impl ShOptCore {
return Err(
ShErr::simple(
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("'core' contains the following options")
.with_sub_notes(vec![
Note::new("'core' contains the following options").with_sub_notes(vec![
"dotglob",
"autocd",
"hist_ignore_dupes",
@@ -293,9 +261,8 @@ impl ShOptCore {
"auto_hist",
"bell_style",
"max_recurse_depth",
]
)
)
]),
),
)
}
}
@@ -303,7 +270,7 @@ impl ShOptCore {
}
pub fn get(&self, query: &str) -> ShResult<Option<String>> {
if query.is_empty() {
return Ok(Some(format!("{self}")))
return Ok(Some(format!("{self}")));
}
match query {
@@ -313,7 +280,9 @@ impl ShOptCore {
Ok(Some(output))
}
"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));
Ok(Some(output))
}
@@ -323,7 +292,9 @@ impl ShOptCore {
Ok(Some(output))
}
"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));
Ok(Some(output))
}
@@ -333,7 +304,9 @@ impl ShOptCore {
Ok(Some(output))
}
"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));
Ok(Some(output))
}
@@ -347,16 +320,14 @@ impl ShOptCore {
output.push_str(&format!("{}", self.max_recurse_depth));
Ok(Some(output))
}
_ => {
Err(
_ => Err(
ShErr::simple(
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("'core' contains the following options")
.with_sub_notes(vec![
Note::new("'core' contains the following options").with_sub_notes(vec![
"dotglob",
"autocd",
"hist_ignore_dupes",
@@ -365,11 +336,9 @@ impl ShOptCore {
"auto_hist",
"bell_style",
"max_recurse_depth",
]
)
)
)
}
]),
),
),
}
}
}
@@ -381,7 +350,10 @@ impl Display for ShOptCore {
output.push(format!("autocd = {}", self.autocd));
output.push(format!("hist_ignore_dupes = {}", self.hist_ignore_dupes));
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!("bell_style = {}", self.bell_style));
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
@@ -414,7 +386,7 @@ pub struct ShOptPrompt {
pub comp_limit: usize,
pub prompt_highlight: bool,
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 {
@@ -422,56 +394,46 @@ impl ShOptPrompt {
match opt {
"trunc_prompt_path" => {
let Ok(val) = val.parse::<usize>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
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;
}
"edit_mode" => {
let Ok(val) = val.parse::<FernEditMode>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'vi' or 'emacs' for edit_mode value"
)
)
"shopt: expected 'vi' or 'emacs' for edit_mode value",
));
};
self.edit_mode = val;
}
"comp_limit" => {
let Ok(val) = val.parse::<usize>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected a positive integer for comp_limit value"
)
)
"shopt: expected a positive integer for comp_limit value",
));
};
self.comp_limit = val;
}
"prompt_highlight" => {
let Ok(val) = val.parse::<bool>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for prompt_highlight value"
)
)
"shopt: expected 'true' or 'false' for prompt_highlight value",
));
};
self.prompt_highlight = val;
}
"tab_stop" => {
let Ok(val) = val.parse::<usize>() else {
return Err(
ShErr::simple(
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected a positive integer for tab_stop value"
)
)
"shopt: expected a positive integer for tab_stop value",
));
};
self.tab_stop = val;
}
@@ -482,12 +444,11 @@ impl ShOptPrompt {
return Err(
ShErr::simple(
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("'core' contains the following options")
.with_sub_notes(vec![
Note::new("'core' contains the following options").with_sub_notes(vec![
"dotglob",
"autocd",
"hist_ignore_dupes",
@@ -496,9 +457,8 @@ impl ShOptPrompt {
"auto_hist",
"bell_style",
"max_recurse_depth",
]
)
)
]),
),
)
}
}
@@ -506,27 +466,32 @@ impl ShOptPrompt {
}
pub fn get(&self, query: &str) -> ShResult<Option<String>> {
if query.is_empty() {
return Ok(Some(format!("{self}")))
return Ok(Some(format!("{self}")));
}
match query {
"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));
Ok(Some(output))
}
"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));
Ok(Some(output))
}
"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));
Ok(Some(output))
}
"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));
Ok(Some(output))
}
@@ -536,23 +501,23 @@ impl ShOptPrompt {
Ok(Some(output))
}
"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");
for key in self.custom.keys() {
output.push_str(&format!(" - {key}\n"));
}
Ok(Some(output.trim().to_string()))
}
_ => {
Err(
_ => Err(
ShErr::simple(
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("'core' contains the following options")
.with_sub_notes(vec![
Note::new("'core' contains the following options").with_sub_notes(vec![
"dotglob",
"autocd",
"hist_ignore_dupes",
@@ -561,11 +526,9 @@ impl ShOptPrompt {
"auto_hist",
"bell_style",
"max_recurse_depth",
]
)
)
)
}
]),
),
),
}
}
}
@@ -598,7 +561,7 @@ impl Default for ShOptPrompt {
comp_limit: 100,
prompt_highlight: true,
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() {
unsafe {
@@ -12,7 +17,6 @@ pub fn sig_setup() {
}
}
extern "C" fn handle_sighup(_: libc::c_int) {
write_jobs(|j| {
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::Continued(pid) => child_continued(pid),
WtStat::StillAlive => break,
_ => unimplemented!()
_ => unimplemented!(),
} {
eprintln!("{}", e)
}
@@ -76,7 +80,11 @@ pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| {
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);
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);
write_jobs(|j| {
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);
child.set_stat(status);
} 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<()> {
/*
* Here we are going to get metadata on the exited process by querying the job table with the pid.
* Then if the discovered job is the fg task, return terminal control to rsh
* If it is not the fg task, print the 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
* Here we are going to get metadata on the exited process by querying the
* job table with the pid. Then if the discovered job is the fg task,
* return terminal control to rsh If it is not the fg task, print the
* 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.
*/
write_jobs(|j| j.close_job_fds(pid));
if let Some((
pgid,
is_fg,
is_finished
)) = write_jobs(|j| {
if let Some((pgid, is_fg, is_finished)) = write_jobs(|j| {
let fg_pgid = j.get_fg().map(|job| job.pgid());
if let Some(job) = j.query_mut(JobID::Pid(pid)) {
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();
let is_finished = !job.running();
if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) {
child.set_stat(status);
}
@@ -143,7 +151,6 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
None
}
}) {
if is_finished {
if is_fg {
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 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()));
@@ -16,9 +31,9 @@ pub static SHOPTS: LazyLock<RwLock<ShOpts>> = LazyLock::new(|| RwLock::new(ShOpt
/// A shell function
///
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers to
/// The Node must be stored with the ParsedSrc because the tokens of the node contain an Arc<String>
/// Which refers to the String held in ParsedSrc
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers
/// to The Node must be stored with the ParsedSrc because the tokens of the node
/// contain an Arc<String> Which refers to the String held in ParsedSrc
///
/// Can be dereferenced to pull out the wrapped Node
#[derive(Clone, Debug)]
@@ -54,7 +69,7 @@ impl Deref for ShFunc {
#[derive(Default, Clone, Debug)]
pub struct LogTab {
functions: HashMap<String, ShFunc>,
aliases: HashMap<String,String>
aliases: HashMap<String, String>,
}
impl LogTab {
@@ -96,12 +111,15 @@ impl LogTab {
#[derive(Clone, Debug)]
pub struct Var {
export: bool,
value: String
value: String,
}
impl Var {
pub fn new(value: String) -> Self {
Self { export: false, value }
Self {
export: false,
value,
}
}
pub fn mark_for_export(&mut self) {
self.export = true;
@@ -119,7 +137,8 @@ impl Deref for Var {
pub struct VarTab {
vars: HashMap<String, Var>,
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 {
@@ -127,7 +146,11 @@ impl VarTab {
let vars = HashMap::new();
let params = Self::init_params();
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
}
@@ -135,13 +158,21 @@ impl VarTab {
let mut params = HashMap::new();
params.insert("?".into(), "0".into()); // Last command exit status
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(), "".into()); // PID of the last background job (if any)
params
}
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
let term = {
if isatty(1).unwrap() {
@@ -167,7 +198,9 @@ impl VarTab {
uid = 0.into();
}
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("HOST", hostname.clone());
@@ -258,11 +291,10 @@ impl VarTab {
}
}
pub fn get_var(&self, var: &str) -> String {
if var.chars().count() == 1 ||
var.parse::<usize>().is_ok() {
if var.chars().count() == 1 || var.parse::<usize>().is_ok() {
let param = self.get_param(var);
if !param.is_empty() {
return param
return param;
}
}
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() {
return self.params.contains_key(var_name);
}
self.vars.contains_key(var_name) ||
(
var_name.len() == 1 &&
self.params.contains_key(var_name)
)
self.vars.contains_key(var_name) || (var_name.len() == 1 && self.params.contains_key(var_name))
}
pub fn set_param(&mut self, param: &str, val: &str) {
self.params.insert(param.to_string(), val.to_string());
}
pub fn get_param(&self, param: &str) -> String {
if param.parse::<usize>().is_ok() {
let argv_idx = param
.to_string()
.parse::<usize>()
.unwrap();
let arg = self.sh_argv.get(argv_idx).map(|s| s.to_string()).unwrap_or_default();
let argv_idx = param.to_string().parse::<usize>().unwrap();
let arg = self
.sh_argv
.get(argv_idx)
.map(|s| s.to_string())
.unwrap_or_default();
arg
} 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 {
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
#[derive(Default, Debug)]
pub struct MetaTab {
runtime_start: Option<Instant>
runtime_start: Option<Instant>,
}
impl MetaTab {
@@ -329,7 +366,8 @@ impl MetaTab {
self.runtime_start = Some(Instant::now());
}
pub fn stop_timer(&mut self) -> Option<Duration> {
self.runtime_start
self
.runtime_start
.take() // runtime_start returns to None
.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()))
}
/// 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) {
(
read_logic(|l| l.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"))
};
if !path.exists() {
return Err(
ShErr::simple(ShErrKind::InternalErr, ".fernrc not found")
)
return Err(ShErr::simple(ShErrKind::InternalErr, ".fernrc not found"));
}
source_file(path)
}
pub fn source_file(path: PathBuf) -> ShResult<()> {
let mut file = OpenOptions::new()
.read(true)
.open(path)?;
let mut file = OpenOptions::new().read(true).open(path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;

View File

@@ -3,7 +3,10 @@ use super::*;
#[test]
fn cmd_not_found() {
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_fmt = format!("{err}");
@@ -13,7 +16,9 @@ fn cmd_not_found() {
#[test]
fn unclosed_subsh() {
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 {
panic!("{:?}", token);
};
@@ -25,7 +30,9 @@ fn unclosed_subsh() {
#[test]
fn unclosed_dquote() {
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 {
panic!();
};
@@ -37,7 +44,9 @@ fn unclosed_dquote() {
#[test]
fn unclosed_squote() {
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 {
panic!();
};
@@ -160,14 +169,7 @@ fn error_with_notes() {
fn error_with_notes_and_sub_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason"))
.with_note(
Note::new("Here is how to fix it:")
.with_sub_notes(vec![
"blah",
"blah",
"blah"
])
);
.with_note(Note::new("Here is how to fix it:").with_sub_notes(vec!["blah", "blah", "blah"]));
let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt)

View File

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

View File

@@ -6,10 +6,16 @@ use super::super::*;
#[test]
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()
.unwrap();
let NdRule::Command { assignments: _, argv } = node.class else {
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
panic!()
};
@@ -20,7 +26,10 @@ fn getopt_from_argv() {
#[test]
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);
insta::assert_debug_snapshot!(words);
@@ -29,7 +38,10 @@ fn getopt_simple() {
#[test]
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);
insta::assert_debug_snapshot!(words);

View File

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

View File

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

View File

@@ -1,6 +1,16 @@
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;
@@ -8,7 +18,7 @@ use super::super::*;
#[derive(Default, Debug)]
struct TestReader {
pub bytes: VecDeque<u8>
pub bytes: VecDeque<u8>,
}
impl TestReader {
@@ -114,7 +124,7 @@ impl KeyReader for TestReader {
println!("found escape seq");
let seq = self.parse_esc_seq_from_bytes();
println!("{seq:?}");
return seq
return seq;
}
}
@@ -132,8 +142,7 @@ impl KeyReader for TestReader {
}
}
pub struct TestWriter {
}
pub struct TestWriter {}
impl TestWriter {
pub fn new() -> Self {
@@ -171,7 +180,7 @@ impl FernVi {
repeat_action: None,
repeat_motion: None,
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) {
let cmd = ViNormal::new()
.cmds_from_raw(cmd)
.pop()
.unwrap();
let cmd = ViNormal::new().cmds_from_raw(cmd).pop().unwrap();
let mut buf = LineBuf::new().with_initial(buf, cursor);
buf.exec_cmd(cmd).unwrap();
(buf.as_str().to_string(), buf.cursor.get())
@@ -340,7 +346,8 @@ fn linebuf_prev_line_only_newlines() {
#[test]
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();
let total = buf.grapheme_indices.as_ref().unwrap().len();
@@ -348,17 +355,33 @@ fn linebuf_cursor_motion() {
for i in 0..total {
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 {
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 {
// 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 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())
};
@@ -382,216 +405,171 @@ fn linebuf_cursor_motion() {
#[test]
fn editor_delete_word() {
assert_eq!(normal_cmd(
"dw",
"The quick brown fox jumps over the lazy dog",
16),
assert_eq!(
normal_cmd("dw", "The quick brown fox jumps over the lazy dog", 16),
("The quick brown jumps over the lazy dog".into(), 16)
);
}
#[test]
fn editor_delete_backwards() {
assert_eq!(normal_cmd(
"2db",
"The quick brown fox jumps over the lazy dog",
16),
assert_eq!(
normal_cmd("2db", "The quick brown fox jumps over the lazy dog", 16),
("The fox jumps over the lazy dog".into(), 4)
);
}
#[test]
fn editor_rot13_five_words_backwards() {
assert_eq!(normal_cmd(
"g?5b",
"The quick brown fox jumps over the lazy dog",
31),
assert_eq!(
normal_cmd("g?5b", "The quick brown fox jumps over the lazy dog", 31),
("The dhvpx oebja sbk whzcf bire the lazy dog".into(), 4)
);
}
#[test]
fn editor_delete_word_on_whitespace() {
assert_eq!(normal_cmd(
"dw",
"The quick brown fox",
10), //on the whitespace between "quick" and "brown"
assert_eq!(
normal_cmd("dw", "The quick brown fox", 10), //on the whitespace between "quick" and "brown"
("The quick brown fox".into(), 10)
);
}
#[test]
fn editor_delete_5_words() {
assert_eq!(normal_cmd(
"5dw",
"The quick brown fox jumps over the lazy dog",
16,),
assert_eq!(
normal_cmd("5dw", "The quick brown fox jumps over the lazy dog", 16,),
("The quick brown dog".into(), 16)
);
}
#[test]
fn editor_delete_end_includes_last() {
assert_eq!(normal_cmd(
"de",
"The quick brown fox::::jumps over the lazy dog",
16),
assert_eq!(
normal_cmd("de", "The quick brown fox::::jumps over the lazy dog", 16),
("The quick brown ::::jumps over the lazy dog".into(), 16)
);
}
#[test]
fn editor_delete_end_unicode_word() {
assert_eq!(normal_cmd(
"de",
"naïve café world",
0),
assert_eq!(
normal_cmd("de", "naïve café world", 0),
(" café world".into(), 0)
);
}
#[test]
fn editor_inplace_edit_cursor_position() {
assert_eq!(normal_cmd(
"5~",
"foobar",
0),
("FOOBAr".into(), 4)
);
assert_eq!(normal_cmd(
"5rg",
"foobar",
0),
("gggggr".into(), 4)
);
assert_eq!(normal_cmd("5~", "foobar", 0), ("FOOBAr".into(), 4));
assert_eq!(normal_cmd("5rg", "foobar", 0), ("gggggr".into(), 4));
}
#[test]
fn editor_insert_mode_not_clamped() {
assert_eq!(normal_cmd(
"a",
"foobar",
5),
("foobar".into(), 6)
)
assert_eq!(normal_cmd("a", "foobar", 5), ("foobar".into(), 6))
}
#[test]
fn editor_overshooting_motions() {
assert_eq!(normal_cmd(
"5dw",
"foo bar",
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)
);
assert_eq!(normal_cmd("5dw", "foo bar", 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]
fn editor_textobj_quoted() {
assert_eq!(normal_cmd(
"di\"",
"this buffer has \"some \\\"quoted\" text",
0),
assert_eq!(
normal_cmd("di\"", "this buffer has \"some \\\"quoted\" text", 0),
("this buffer has \"\" text".into(), 17)
);
assert_eq!(normal_cmd(
"da\"",
"this buffer has \"some \\\"quoted\" text",
0),
assert_eq!(
normal_cmd("da\"", "this buffer has \"some \\\"quoted\" text", 0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di'",
"this buffer has 'some \\'quoted' text",
0),
assert_eq!(
normal_cmd("di'", "this buffer has 'some \\'quoted' text", 0),
("this buffer has '' text".into(), 17)
);
assert_eq!(normal_cmd(
"da'",
"this buffer has 'some \\'quoted' text",
0),
assert_eq!(
normal_cmd("da'", "this buffer has 'some \\'quoted' text", 0),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
"di`",
"this buffer has `some \\`quoted` text",
0),
assert_eq!(
normal_cmd("di`", "this buffer has `some \\`quoted` text", 0),
("this buffer has `` text".into(), 17)
);
assert_eq!(normal_cmd(
"da`",
"this buffer has `some \\`quoted` text",
0),
assert_eq!(
normal_cmd("da`", "this buffer has `some \\`quoted` text", 0),
("this buffer has text".into(), 16)
);
}
#[test]
fn editor_textobj_delimited() {
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"di)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0),
0
),
("this buffer has () text".into(), 17)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"da)",
"this buffer has (some \\(\\)(inner) \\(\\)delimited) text",
0),
0
),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"di]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0),
0
),
("this buffer has [] text".into(), 17)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"da]",
"this buffer has [some \\[\\][inner] \\[\\]delimited] text",
0),
0
),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"di}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0),
0
),
("this buffer has {} text".into(), 17)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"da}",
"this buffer has {some \\{\\}{inner} \\{\\}delimited} text",
0),
0
),
("this buffer has text".into(), 16)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"di>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0),
0
),
("this buffer has <> text".into(), 17)
);
assert_eq!(normal_cmd(
assert_eq!(
normal_cmd(
"da>",
"this buffer has <some \\<\\><inner> \\<\\>delimited> text",
0),
0
),
("this buffer has text".into(), 16)
);
}
@@ -610,18 +588,13 @@ fn editor_delete_line_up() {
#[test]
fn fernvi_test_simple() {
assert_eq!(fernvi_test(
"foo bar\x1bbdw\r",
""),
"foo "
)
assert_eq!(fernvi_test("foo bar\x1bbdw\r", ""), "foo ")
}
#[test]
fn fernvi_test_mode_change() {
assert_eq!(fernvi_test(
"foo bar biz buzz\x1bbbb2cwbiz buzz bar\r",
""),
assert_eq!(
fernvi_test("foo bar biz buzz\x1bbbb2cwbiz buzz bar\r", ""),
"foo biz buzz bar buzz"
)
}
@@ -637,9 +610,11 @@ fn fernvi_test_lorem_ipsum_1() {
#[test]
fn fernvi_test_lorem_ipsum_undo() {
assert_eq!(fernvi_test(
assert_eq!(
fernvi_test(
"\x1bwwwwwwwwainserting some characters now...\x1bu\r",
LOREM_IPSUM),
LOREM_IPSUM
),
LOREM_IPSUM
)
}

View File

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

View File

@@ -29,7 +29,9 @@ fn styled_background() {
#[test]
fn styled_set() {
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);
insta::assert_snapshot!(styled);
}