Added rustfmt.toml, formatted codebase

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

7
rustfmt.toml Normal file
View File

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

View File

@@ -1,95 +1,103 @@
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, read_logic, write_logic}}; use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state::{self, read_logic, write_logic},
};
use super::setup_builtin; use super::setup_builtin;
pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
let mut alias_output = read_logic(|l| { let mut alias_output = read_logic(|l| {
l.aliases() l.aliases()
.iter() .iter()
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1)) .map(|ent| format!("{} = \"{}\"", ent.0, ent.1))
.collect::<Vec<_>>() .collect::<Vec<_>>()
}); });
alias_output.sort(); // Sort them alphabetically alias_output.sort(); // Sort them alphabetically
let mut alias_output = alias_output.join("\n"); // Join them with newlines let mut alias_output = alias_output.join("\n"); // Join them with newlines
alias_output.push('\n'); // Push a final newline alias_output.push('\n'); // Push a final newline
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, alias_output.as_bytes())?; // Write it write(stdout, alias_output.as_bytes())?; // Write it
} else { } else {
for (arg,span) in argv { for (arg, span) in argv {
if arg == "command" || arg == "builtin" { if arg == "command" || arg == "builtin" {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("alias: Cannot assign alias to reserved name '{arg}'"),
format!("alias: Cannot assign alias to reserved name '{arg}'"), span,
span ));
) }
)
}
let Some((name,body)) = arg.split_once('=') else { let Some((name, body)) = arg.split_once('=') else {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, "alias: Expected an assignment in alias args",
"alias: Expected an assignment in alias args", span,
span ));
) };
) write_logic(|l| l.insert_alias(name, body));
}; }
write_logic(|l| l.insert_alias(name, body)); }
} io_frame.unwrap().restore()?;
} state::set_status(0);
io_frame.unwrap().restore()?; Ok(())
state::set_status(0);
Ok(())
} }
pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() {
// Display the environment variables
let mut alias_output = read_logic(|l| {
l.aliases()
.iter()
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1))
.collect::<Vec<_>>()
});
alias_output.sort(); // Sort them alphabetically
let mut alias_output = alias_output.join("\n"); // Join them with newlines
alias_output.push('\n'); // Push a final newline
if argv.is_empty() { let stdout = borrow_fd(STDOUT_FILENO);
// Display the environment variables write(stdout, alias_output.as_bytes())?; // Write it
let mut alias_output = read_logic(|l| { } else {
l.aliases() for (arg, span) in argv {
.iter() flog!(DEBUG, arg);
.map(|ent| format!("{} = \"{}\"", ent.0, ent.1)) if read_logic(|l| l.get_alias(&arg)).is_none() {
.collect::<Vec<_>>() return Err(ShErr::full(
}); ShErrKind::SyntaxErr,
alias_output.sort(); // Sort them alphabetically format!("unalias: alias '{arg}' not found"),
let mut alias_output = alias_output.join("\n"); // Join them with newlines span,
alias_output.push('\n'); // Push a final newline ));
};
let stdout = borrow_fd(STDOUT_FILENO); write_logic(|l| l.remove_alias(&arg))
write(stdout, alias_output.as_bytes())?; // Write it }
} else { }
for (arg,span) in argv { io_frame.unwrap().restore()?;
flog!(DEBUG, arg); state::set_status(0);
if read_logic(|l| l.get_alias(&arg)).is_none() { Ok(())
return Err(
ShErr::full(
ShErrKind::SyntaxErr,
format!("unalias: alias '{arg}' not found"),
span
)
)
};
write_logic(|l| l.remove_alias(&arg))
}
}
io_frame.unwrap().restore()?;
state::set_status(0);
Ok(())
} }

View File

@@ -1,45 +1,51 @@
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, state::{self}}; use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
prelude::*,
state::{self},
};
use super::setup_builtin; use super::setup_builtin;
pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span(); let span = node.get_span();
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,_) = setup_builtin(argv,job,None)?; let (argv, _) = setup_builtin(argv, job, None)?;
let new_dir = if let Some((arg,_)) = argv.into_iter().next() { let new_dir = if let Some((arg, _)) = argv.into_iter().next() {
PathBuf::from(arg) PathBuf::from(arg)
} else { } else {
PathBuf::from(env::var("HOME").unwrap()) PathBuf::from(env::var("HOME").unwrap())
}; };
if !new_dir.exists() { if !new_dir.exists() {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("cd: No such file or directory '{}'", new_dir.display()),
format!("cd: No such file or directory '{}'",new_dir.display()), span,
span, ));
) }
)
}
if !new_dir.is_dir() { if !new_dir.is_dir() {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("cd: Not a directory '{}'", new_dir.display()),
format!("cd: Not a directory '{}'",new_dir.display()), span,
span, ));
) }
)
}
env::set_current_dir(new_dir).unwrap(); env::set_current_dir(new_dir).unwrap();
let new_dir = env::current_dir().unwrap(); let new_dir = env::current_dir().unwrap();
env::set_var("PWD", new_dir); env::set_var("PWD", new_dir);
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -1,77 +1,90 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::{builtin::setup_builtin, getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state}; use crate::{
builtin::setup_builtin,
getopt::{get_opts_from_tokens, Opt, OptSet},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state,
};
pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {[ pub static ECHO_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
Opt::Short('n'), [
Opt::Short('E'), Opt::Short('n'),
Opt::Short('e'), Opt::Short('E'),
Opt::Short('r'), Opt::Short('e'),
].into()}); Opt::Short('r'),
]
.into()
});
bitflags! { bitflags! {
pub struct EchoFlags: u32 { pub struct EchoFlags: u32 {
const NO_NEWLINE = 0b000001; const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010; const USE_STDERR = 0b000010;
const USE_ESCAPE = 0b000100; const USE_ESCAPE = 0b000100;
} }
} }
pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
assert!(!argv.is_empty()); } = node.class
let (argv,opts) = get_opts_from_tokens(argv); else {
let flags = get_echo_flags(opts).blame(blame)?; unreachable!()
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; };
assert!(!argv.is_empty());
let (argv, opts) = get_opts_from_tokens(argv);
let flags = get_echo_flags(opts).blame(blame)?;
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let output_channel = if flags.contains(EchoFlags::USE_STDERR) { let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
borrow_fd(STDERR_FILENO) borrow_fd(STDERR_FILENO)
} else { } else {
borrow_fd(STDOUT_FILENO) borrow_fd(STDOUT_FILENO)
}; };
let mut echo_output = argv.into_iter() let mut echo_output = argv
.map(|a| a.0) // Extract the String from the tuple of (String,Span) .into_iter()
.collect::<Vec<_>>() .map(|a| a.0) // Extract the String from the tuple of (String,Span)
.join(" "); .collect::<Vec<_>>()
.join(" ");
if !flags.contains(EchoFlags::NO_NEWLINE) { if !flags.contains(EchoFlags::NO_NEWLINE) {
echo_output.push('\n') echo_output.push('\n')
} }
write(output_channel, echo_output.as_bytes())?; write(output_channel, echo_output.as_bytes())?;
io_frame.unwrap().restore()?; io_frame.unwrap().restore()?;
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> { pub fn get_echo_flags(mut opts: Vec<Opt>) -> ShResult<EchoFlags> {
let mut flags = EchoFlags::empty(); let mut flags = EchoFlags::empty();
while let Some(opt) = opts.pop() { while let Some(opt) = opts.pop() {
if !ECHO_OPTS.contains(&opt) { if !ECHO_OPTS.contains(&opt) {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("echo: Unexpected flag '{opt}'"),
format!("echo: Unexpected flag '{opt}'"), ));
) }
) let Opt::Short(opt) = opt else { unreachable!() };
}
let Opt::Short(opt) = opt else {
unreachable!()
};
match opt { match opt {
'n' => flags |= EchoFlags::NO_NEWLINE, 'n' => flags |= EchoFlags::NO_NEWLINE,
'r' => flags |= EchoFlags::USE_STDERR, 'r' => flags |= EchoFlags::USE_STDERR,
'e' => flags |= EchoFlags::USE_ESCAPE, 'e' => flags |= EchoFlags::USE_ESCAPE,
_ => unreachable!() _ => unreachable!(),
} }
} }
Ok(flags) Ok(flags)
} }

View File

@@ -1,35 +1,48 @@
use crate::{jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, write_vars}}; use crate::{
jobs::JobBldr,
libsh::error::ShResult,
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state::{self, write_vars},
};
use super::setup_builtin; use super::setup_builtin;
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
let mut env_output = env::vars() let mut env_output = env::vars()
.map(|var| format!("{}={}",var.0,var.1)) // Get all of them, zip them into one string .map(|var| format!("{}={}", var.0, var.1)) // Get all of them, zip them into one string
.collect::<Vec<_>>(); .collect::<Vec<_>>();
env_output.sort(); // Sort them alphabetically env_output.sort(); // Sort them alphabetically
let mut env_output = env_output.join("\n"); // Join them with newlines let mut env_output = env_output.join("\n"); // Join them with newlines
env_output.push('\n'); // Push a final newline env_output.push('\n'); // Push a final newline
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, env_output.as_bytes())?; // Write it write(stdout, env_output.as_bytes())?; // Write it
} else { } else {
for (arg,_) in argv { for (arg, _) in argv {
if let Some((var,val)) = arg.split_once('=') { if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, true)); // Export an assignment like 'foo=bar' write_vars(|v| v.set_var(var, val, true)); // Export an assignment like
} else { // 'foo=bar'
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any } else {
} write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
} // any
} }
io_frame.unwrap().restore()?; }
state::set_status(0); }
Ok(()) io_frame.unwrap().restore()?;
state::set_status(0);
Ok(())
} }

View File

@@ -1,39 +1,46 @@
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*}; use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{execute::prepare_argv, NdRule, Node},
prelude::*,
};
pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
use ShErrKind::*; use ShErrKind::*;
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
let mut code = 0; } = node.class
else {
unreachable!()
};
let mut code = 0;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
let cmd = argv.remove(0).0; let cmd = argv.remove(0).0;
if !argv.is_empty() { if !argv.is_empty() {
let (arg,span) = argv let (arg, span) = argv.into_iter().next().unwrap();
.into_iter()
.next()
.unwrap();
let Ok(status) = arg.parse::<i32>() else { let Ok(status) = arg.parse::<i32>() else {
return Err( return Err(ShErr::full(
ShErr::full(ShErrKind::SyntaxErr, format!("{cmd}: Expected a number"), span) ShErrKind::SyntaxErr,
) format!("{cmd}: Expected a number"),
}; span,
));
};
code = status; code = status;
} }
flog!(DEBUG,code); flog!(DEBUG, code);
let kind = match kind { let kind = match kind {
LoopContinue(_) => LoopContinue(code), LoopContinue(_) => LoopContinue(code),
LoopBreak(_) => LoopBreak(code), LoopBreak(_) => LoopBreak(code),
FuncReturn(_) => FuncReturn(code), FuncReturn(_) => FuncReturn(code),
CleanExit(_) => CleanExit(code), CleanExit(_) => CleanExit(code),
_ => unreachable!() _ => unreachable!(),
}; };
Err(ShErr::simple(kind, "")) Err(ShErr::simple(kind, ""))
} }

View File

@@ -1,183 +1,182 @@
use crate::{jobs::{JobBldr, JobCmdFlags, JobID}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{lex::Span, NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::{self, read_jobs, write_jobs}}; use crate::{
jobs::{JobBldr, JobCmdFlags, JobID},
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{lex::Span, NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state::{self, read_jobs, write_jobs},
};
use super::setup_builtin; use super::setup_builtin;
pub enum JobBehavior { pub enum JobBehavior {
Foregound, Foregound,
Background Background,
} }
pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> { pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let cmd = match behavior { let cmd = match behavior {
JobBehavior::Foregound => "fg", JobBehavior::Foregound => "fg",
JobBehavior::Background => "bg" JobBehavior::Background => "bg",
}; };
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,_) = setup_builtin(argv, job, None)?; let (argv, _) = setup_builtin(argv, job, None)?;
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) { if read_jobs(|j| j.get_fg().is_some()) {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::InternalErr,
ShErrKind::InternalErr, format!("Somehow called '{}' with an existing foreground job", cmd),
format!("Somehow called '{}' with an existing foreground job",cmd), blame,
blame ));
) }
)
}
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id id
} else { } else {
return Err( return Err(ShErr::full(ShErrKind::ExecFail, "No jobs found", blame));
ShErr::full( };
ShErrKind::ExecFail,
"No jobs found",
blame
)
)
};
let tabid = match argv.next() { let tabid = match argv.next() {
Some((arg,blame)) => parse_job_id(&arg, blame)?, Some((arg, blame)) => parse_job_id(&arg, blame)?,
None => curr_job_id None => curr_job_id,
}; };
let mut job = write_jobs(|j| { let mut job = write_jobs(|j| {
let id = JobID::TableID(tabid); let id = JobID::TableID(tabid);
let query_result = j.query(id.clone()); let query_result = j.query(id.clone());
if query_result.is_some() { if query_result.is_some() {
Ok(j.remove_job(id.clone()).unwrap()) Ok(j.remove_job(id.clone()).unwrap())
} else { } else {
Err( Err(ShErr::full(
ShErr::full( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("Job id `{}' not found", tabid),
format!("Job id `{}' not found", tabid), blame,
blame ))
) }
) })?;
}
})?;
job.killpg(Signal::SIGCONT)?; job.killpg(Signal::SIGCONT)?;
match behavior { match behavior {
JobBehavior::Foregound => { JobBehavior::Foregound => {
write_jobs(|j| j.new_fg(job))?; write_jobs(|j| j.new_fg(job))?;
} }
JobBehavior::Background => { JobBehavior::Background => {
let job_order = read_jobs(|j| j.order().to_vec()); let job_order = read_jobs(|j| j.order().to_vec());
write(borrow_fd(1), job.display(&job_order, JobCmdFlags::PIDS).as_bytes())?; write(
write_jobs(|j| j.insert_job(job, true))?; borrow_fd(1),
} job.display(&job_order, JobCmdFlags::PIDS).as_bytes(),
} )?;
state::set_status(0); write_jobs(|j| j.insert_job(job, true))?;
Ok(()) }
}
state::set_status(0);
Ok(())
} }
fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> { fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
if arg.starts_with('%') { if arg.starts_with('%') {
let arg = arg.strip_prefix('%').unwrap(); let arg = arg.strip_prefix('%').unwrap();
if arg.chars().all(|ch| ch.is_ascii_digit()) { if arg.chars().all(|ch| ch.is_ascii_digit()) {
Ok(arg.parse::<usize>().unwrap()) Ok(arg.parse::<usize>().unwrap())
} else { } else {
let result = write_jobs(|j| { let result = write_jobs(|j| {
let query_result = j.query(JobID::Command(arg.into())); let query_result = j.query(JobID::Command(arg.into()));
query_result.map(|job| job.tabid().unwrap()) query_result.map(|job| job.tabid().unwrap())
}); });
match result { match result {
Some(id) => Ok(id), Some(id) => Ok(id),
None => Err( None => Err(ShErr::full(
ShErr::full( ShErrKind::InternalErr,
ShErrKind::InternalErr, "Found a job but no table id in parse_job_id()",
"Found a job but no table id in parse_job_id()", blame,
blame )),
) }
) }
} } else if arg.chars().all(|ch| ch.is_ascii_digit()) {
} let result = write_jobs(|j| {
} else if arg.chars().all(|ch| ch.is_ascii_digit()) { let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap())));
let result = write_jobs(|j| { if let Some(job) = pgid_query_result {
let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap()))); return Some(job.tabid().unwrap());
if let Some(job) = pgid_query_result { }
return Some(job.tabid().unwrap())
}
if arg.parse::<i32>().unwrap() > 0 { if arg.parse::<i32>().unwrap() > 0 {
let table_id_query_result = j.query(JobID::TableID(arg.parse::<usize>().unwrap())); let table_id_query_result = j.query(JobID::TableID(arg.parse::<usize>().unwrap()));
return table_id_query_result.map(|job| job.tabid().unwrap()); return table_id_query_result.map(|job| job.tabid().unwrap());
} }
None None
}); });
match result { match result {
Some(id) => Ok(id), Some(id) => Ok(id),
None => Err( None => Err(ShErr::full(
ShErr::full( ShErrKind::InternalErr,
ShErrKind::InternalErr, "Found a job but no table id in parse_job_id()",
"Found a job but no table id in parse_job_id()", blame,
blame )),
) }
) } else {
} Err(ShErr::full(
} else { ShErrKind::SyntaxErr,
Err( format!("Invalid fd arg: {}", arg),
ShErr::full( blame,
ShErrKind::SyntaxErr, ))
format!("Invalid fd arg: {}", arg), }
blame
)
)
}
} }
pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut flags = JobCmdFlags::empty(); let mut flags = JobCmdFlags::empty();
for (arg,span) in argv { for (arg, span) in argv {
let mut chars = arg.chars().peekable(); let mut chars = arg.chars().peekable();
if chars.peek().is_none_or(|ch| *ch != '-') { if chars.peek().is_none_or(|ch| *ch != '-') {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, "Invalid flag in jobs call",
"Invalid flag in jobs call", span,
span ));
) }
) chars.next();
} for ch in chars {
chars.next(); let flag = match ch {
for ch in chars { 'l' => JobCmdFlags::LONG,
let flag = match ch { 'p' => JobCmdFlags::PIDS,
'l' => JobCmdFlags::LONG, 'n' => JobCmdFlags::NEW_ONLY,
'p' => JobCmdFlags::PIDS, 'r' => JobCmdFlags::RUNNING,
'n' => JobCmdFlags::NEW_ONLY, 's' => JobCmdFlags::STOPPED,
'r' => JobCmdFlags::RUNNING, _ => {
's' => JobCmdFlags::STOPPED, return Err(ShErr::full(
_ => return Err( ShErrKind::SyntaxErr,
ShErr::full( "Invalid flag in jobs call",
ShErrKind::SyntaxErr, span,
"Invalid flag in jobs call", ))
span }
) };
) flags |= flag
}
}
write_jobs(|j| j.print_jobs(flags))?;
io_frame.unwrap().restore()?;
state::set_status(0);
}; Ok(())
flags |= flag
}
}
write_jobs(|j| j.print_jobs(flags))?;
io_frame.unwrap().restore()?;
state::set_status(0);
Ok(())
} }

View File

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

View File

@@ -1,21 +1,32 @@
use crate::{jobs::JobBldr, libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state}; use crate::{
jobs::JobBldr,
libsh::error::ShResult,
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state,
};
use super::setup_builtin; use super::setup_builtin;
pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (_,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; let (_, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string(); let mut curr_dir = env::current_dir().unwrap().to_str().unwrap().to_string();
curr_dir.push('\n'); curr_dir.push('\n');
write(stdout, curr_dir.as_bytes())?; write(stdout, curr_dir.as_bytes())?;
io_frame.unwrap().restore().unwrap(); io_frame.unwrap().restore().unwrap();
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -1,30 +1,37 @@
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, state::{self, write_vars}}; use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
state::{self, write_vars},
};
use super::setup_builtin; use super::setup_builtin;
pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,_) = setup_builtin(argv, job, None)?; let (argv, _) = setup_builtin(argv, job, None)?;
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if let Some((arg,span)) = argv.next() { if let Some((arg, span)) = argv.next() {
let Ok(count) = arg.parse::<usize>() else { let Ok(count) = arg.parse::<usize>() else {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::ExecFail,
ShErrKind::ExecFail, "Expected a number in shift args",
"Expected a number in shift args", span,
span ));
) };
) for _ in 0..count {
}; write_vars(|v| v.fpop_arg());
for _ in 0..count { }
write_vars(|v| v.fpop_arg()); }
}
}
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -1,30 +1,41 @@
use crate::{jobs::JobBldr, libsh::error::{ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state::write_shopts}; use crate::{
jobs::JobBldr,
libsh::error::{ShResult, ShResultExt},
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
state::write_shopts,
};
use super::setup_builtin; use super::setup_builtin;
pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut io_frame = io_frame.unwrap(); let mut io_frame = io_frame.unwrap();
io_frame.redirect()?; io_frame.redirect()?;
for (arg,span) in argv { for (arg, span) in argv {
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
continue continue;
}; };
let output_channel = borrow_fd(STDOUT_FILENO); let output_channel = borrow_fd(STDOUT_FILENO);
output.push('\n'); output.push('\n');
if let Err(e) = write(output_channel, output.as_bytes()) { if let Err(e) = write(output_channel, output.as_bytes()) {
io_frame.restore()?; io_frame.restore()?;
return Err(e.into()) return Err(e.into());
} }
} }
io_frame.restore()?; io_frame.restore()?;
Ok(()) Ok(())
} }

View File

@@ -1,37 +1,43 @@
use crate::{jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, state::{self, source_file}}; use crate::{
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
prelude::*,
state::{self, source_file},
};
use super::setup_builtin; use super::setup_builtin;
pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
} = node.class
else {
unreachable!()
};
let (argv,_) = setup_builtin(argv, job, None)?; let (argv, _) = setup_builtin(argv, job, None)?;
for (arg,span) in argv { for (arg, span) in argv {
let path = PathBuf::from(arg); let path = PathBuf::from(arg);
if !path.exists() { if !path.exists() {
return Err( return Err(ShErr::full(
ShErr::full( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("source: File '{}' not found", path.display()),
format!("source: File '{}' not found",path.display()), span,
span ));
) }
); if !path.is_file() {
} return Err(ShErr::full(
if !path.is_file() { ShErrKind::ExecFail,
return Err( format!("source: Given path '{}' is not a file", path.display()),
ShErr::full( span,
ShErrKind::ExecFail, ));
format!("source: Given path '{}' is not a file",path.display()), }
span source_file(path)?;
) }
);
}
source_file(path)?;
}
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -1,328 +1,321 @@
use std::{fs::metadata, path::PathBuf, str::FromStr}; use std::{fs::metadata, path::PathBuf, str::FromStr};
use nix::{sys::stat::{self, SFlag}, unistd::AccessFlags}; use nix::{
sys::stat::{self, SFlag},
unistd::AccessFlags,
};
use regex::Regex; use regex::Regex;
use crate::{libsh::error::{ShErr, ShErrKind, ShResult},prelude::*, parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}}; use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
prelude::*,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum UnaryOp { pub enum UnaryOp {
Exists, // -e Exists, // -e
Directory, // -d Directory, // -d
File, // -f File, // -f
Symlink, // -h or -L Symlink, // -h or -L
Readable, // -r Readable, // -r
Writable, // -w Writable, // -w
Executable, // -x Executable, // -x
NonEmpty, // -s NonEmpty, // -s
NamedPipe, // -p NamedPipe, // -p
Socket, // -S Socket, // -S
BlockSpecial, // -b BlockSpecial, // -b
CharSpecial, // -c CharSpecial, // -c
Sticky, // -k Sticky, // -k
UIDOwner, // -O UIDOwner, // -O
GIDOwner, // -G GIDOwner, // -G
ModifiedSinceStatusChange, // -N ModifiedSinceStatusChange, // -N
SetUID, // -u SetUID, // -u
SetGID, // -g SetGID, // -g
Terminal, // -t Terminal, // -t
NonNull, // -n NonNull, // -n
Null, // -z Null, // -z
} }
impl FromStr for UnaryOp { impl FromStr for UnaryOp {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"-e" => Ok(Self::Exists), "-e" => Ok(Self::Exists),
"-d" => Ok(Self::Directory), "-d" => Ok(Self::Directory),
"-f" => Ok(Self::File), "-f" => Ok(Self::File),
"-h" | "-L" => Ok(Self::Symlink), // -h or -L "-h" | "-L" => Ok(Self::Symlink), // -h or -L
"-r" => Ok(Self::Readable), "-r" => Ok(Self::Readable),
"-w" => Ok(Self::Writable), "-w" => Ok(Self::Writable),
"-x" => Ok(Self::Executable), "-x" => Ok(Self::Executable),
"-s" => Ok(Self::NonEmpty), "-s" => Ok(Self::NonEmpty),
"-p" => Ok(Self::NamedPipe), "-p" => Ok(Self::NamedPipe),
"-S" => Ok(Self::Socket), "-S" => Ok(Self::Socket),
"-b" => Ok(Self::BlockSpecial), "-b" => Ok(Self::BlockSpecial),
"-c" => Ok(Self::CharSpecial), "-c" => Ok(Self::CharSpecial),
"-k" => Ok(Self::Sticky), "-k" => Ok(Self::Sticky),
"-O" => Ok(Self::UIDOwner), "-O" => Ok(Self::UIDOwner),
"-G" => Ok(Self::GIDOwner), "-G" => Ok(Self::GIDOwner),
"-N" => Ok(Self::ModifiedSinceStatusChange), "-N" => Ok(Self::ModifiedSinceStatusChange),
"-u" => Ok(Self::SetUID), "-u" => Ok(Self::SetUID),
"-g" => Ok(Self::SetGID), "-g" => Ok(Self::SetGID),
"-t" => Ok(Self::Terminal), "-t" => Ok(Self::Terminal),
"-n" => Ok(Self::NonNull), "-n" => Ok(Self::NonNull),
"-z" => Ok(Self::Null), "-z" => Ok(Self::Null),
_ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] }) _ => Err(ShErr::Simple {
} kind: ShErrKind::SyntaxErr,
} msg: "Invalid test operator".into(),
notes: vec![],
}),
}
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum TestOp { pub enum TestOp {
Unary(UnaryOp), Unary(UnaryOp),
StringEq, // == StringEq, // ==
StringNeq, // != StringNeq, // !=
IntEq, // -eq IntEq, // -eq
IntNeq, // -ne IntNeq, // -ne
IntGt, // -gt IntGt, // -gt
IntLt, // -lt IntLt, // -lt
IntGe, // -ge IntGe, // -ge
IntLe, // -le IntLe, // -le
RegexMatch, // =~ RegexMatch, // =~
} }
impl FromStr for TestOp { impl FromStr for TestOp {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"==" => Ok(Self::StringEq), "==" => Ok(Self::StringEq),
"!=" => Ok(Self::StringNeq), "!=" => Ok(Self::StringNeq),
"=~" => Ok(Self::RegexMatch), "=~" => Ok(Self::RegexMatch),
"-eq" => Ok(Self::IntEq), "-eq" => Ok(Self::IntEq),
"-ne" => Ok(Self::IntNeq), "-ne" => Ok(Self::IntNeq),
"-gt" => Ok(Self::IntGt), "-gt" => Ok(Self::IntGt),
"-lt" => Ok(Self::IntLt), "-lt" => Ok(Self::IntLt),
"-ge" => Ok(Self::IntGe), "-ge" => Ok(Self::IntGe),
"-le" => Ok(Self::IntLe), "-le" => Ok(Self::IntLe),
_ if TEST_UNARY_OPS.contains(&s) => { _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
Ok(Self::Unary(s.parse::<UnaryOp>()?)) _ => Err(ShErr::Simple {
} kind: ShErrKind::SyntaxErr,
_ => Err(ShErr::Simple { kind: ShErrKind::SyntaxErr, msg: "Invalid test operator".into(), notes: vec![] }) msg: "Invalid test operator".into(),
} notes: vec![],
} }),
}
}
} }
fn replace_posix_classes(pat: &str) -> String { fn replace_posix_classes(pat: &str) -> String {
pat.replace("[[:alnum:]]", r"[A-Za-z0-9]") pat
.replace("[[:alpha:]]", r"[A-Za-z]") .replace("[[:alnum:]]", r"[A-Za-z0-9]")
.replace("[[:blank:]]", r"[ \t]") .replace("[[:alpha:]]", r"[A-Za-z]")
.replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]") .replace("[[:blank:]]", r"[ \t]")
.replace("[[:digit:]]", r"[0-9]") .replace("[[:cntrl:]]", r"[\x00-\x1F\x7F]")
.replace("[[:graph:]]", r"[!-~]") .replace("[[:digit:]]", r"[0-9]")
.replace("[[:lower:]]", r"[a-z]") .replace("[[:graph:]]", r"[!-~]")
.replace("[[:print:]]", r"[\x20-\x7E]") .replace("[[:lower:]]", r"[a-z]")
.replace("[[:space:]]", r"[ \t\r\n\x0B\x0C]") // vertical tab (\x0B), form feed (\x0C) .replace("[[:print:]]", r"[\x20-\x7E]")
.replace("[[:upper:]]", r"[A-Z]") .replace("[[:space:]]", r"[ \t\r\n\x0B\x0C]") // vertical tab (\x0B), form feed (\x0C)
.replace("[[:xdigit:]]", r"[0-9A-Fa-f]") .replace("[[:upper:]]", r"[A-Z]")
.replace("[[:xdigit:]]", r"[0-9A-Fa-f]")
} }
pub fn double_bracket_test(node: Node) -> ShResult<bool> { pub fn double_bracket_test(node: Node) -> ShResult<bool> {
let err_span = node.get_span(); let err_span = node.get_span();
let NdRule::Test { cases } = node.class else { let NdRule::Test { cases } = node.class else {
unreachable!() unreachable!()
}; };
let mut last_result = false; let mut last_result = false;
let mut conjunct_op: Option<ConjunctOp>; let mut conjunct_op: Option<ConjunctOp>;
for case in cases { for case in cases {
let result = match case { let result = match case {
TestCase::Unary { operator, operand, conjunct } => { TestCase::Unary {
let operand = operand.expand()?.get_words().join(" "); operator,
conjunct_op = conjunct; operand,
let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { conjunct,
return Err( } => {
ShErr::Full { kind: ShErrKind::SyntaxErr, msg: "Invalid unary operator".into(), notes: vec![], span: err_span } let operand = operand.expand()?.get_words().join(" ");
) conjunct_op = conjunct;
}; let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else {
match op { return Err(ShErr::Full {
UnaryOp::Exists => { kind: ShErrKind::SyntaxErr,
let path = PathBuf::from(operand.as_str()); msg: "Invalid unary operator".into(),
path.exists() notes: vec![],
} span: err_span,
UnaryOp::Directory => { });
let path = PathBuf::from(operand.as_str()); };
if path.exists() { match op {
path.metadata() UnaryOp::Exists => {
.unwrap() let path = PathBuf::from(operand.as_str());
.is_dir() path.exists()
} else { }
false UnaryOp::Directory => {
} let path = PathBuf::from(operand.as_str());
} if path.exists() {
UnaryOp::File => { path.metadata().unwrap().is_dir()
let path = PathBuf::from(operand.as_str()); } else {
if path.exists() { false
path.metadata() }
.unwrap() }
.is_file() UnaryOp::File => {
} else { let path = PathBuf::from(operand.as_str());
false if path.exists() {
} path.metadata().unwrap().is_file()
} } else {
UnaryOp::Symlink => { false
let path = PathBuf::from(operand.as_str()); }
if path.exists() { }
path.metadata() UnaryOp::Symlink => {
.unwrap() let path = PathBuf::from(operand.as_str());
.file_type() if path.exists() {
.is_symlink() path.metadata().unwrap().file_type().is_symlink()
} else { } else {
false false
} }
} }
UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(), UnaryOp::Readable => nix::unistd::access(operand.as_str(), AccessFlags::R_OK).is_ok(),
UnaryOp::Writable => nix::unistd::access(operand.as_str(), AccessFlags::W_OK).is_ok(), UnaryOp::Writable => nix::unistd::access(operand.as_str(), AccessFlags::W_OK).is_ok(),
UnaryOp::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(), UnaryOp::Executable => nix::unistd::access(operand.as_str(), AccessFlags::X_OK).is_ok(),
UnaryOp::NonEmpty => { UnaryOp::NonEmpty => match metadata(operand.as_str()) {
match metadata(operand.as_str()) { Ok(meta) => meta.len() > 0,
Ok(meta) => meta.len() > 0, Err(_) => false,
Err(_) => false },
} UnaryOp::NamedPipe => match stat::stat(operand.as_str()) {
} Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO),
UnaryOp::NamedPipe => { Err(_) => false,
match stat::stat(operand.as_str()) { },
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFIFO), UnaryOp::Socket => match stat::stat(operand.as_str()) {
Err(_) => false, Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK),
} Err(_) => false,
} },
UnaryOp::Socket => { UnaryOp::BlockSpecial => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) { Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK),
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFSOCK), Err(_) => false,
Err(_) => false, },
} UnaryOp::CharSpecial => match stat::stat(operand.as_str()) {
} Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR),
UnaryOp::BlockSpecial => { Err(_) => false,
match stat::stat(operand.as_str()) { },
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFBLK), UnaryOp::Sticky => match stat::stat(operand.as_str()) {
Err(_) => false, Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0,
} Err(_) => false,
} },
UnaryOp::CharSpecial => { UnaryOp::UIDOwner => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) { Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(),
Ok(stat) => SFlag::from_bits_truncate(stat.st_mode).contains(SFlag::S_IFCHR), Err(_) => false,
Err(_) => false, },
}
}
UnaryOp::Sticky => {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_mode & nix::libc::S_ISVTX != 0,
Err(_) => false,
}
}
UnaryOp::UIDOwner => {
match stat::stat(operand.as_str()) {
Ok(stat) => stat.st_uid == nix::unistd::geteuid().as_raw(),
Err(_) => false,
}
}
UnaryOp::GIDOwner => { UnaryOp::GIDOwner => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) { Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(),
Ok(stat) => stat.st_gid == nix::unistd::getegid().as_raw(), Err(_) => false,
Err(_) => false, },
}
}
UnaryOp::ModifiedSinceStatusChange => { UnaryOp::ModifiedSinceStatusChange => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) { Ok(stat) => stat.st_mtime > stat.st_ctime,
Ok(stat) => stat.st_mtime > stat.st_ctime, Err(_) => false,
Err(_) => false, },
}
}
UnaryOp::SetUID => { UnaryOp::SetUID => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) { Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0,
Ok(stat) => stat.st_mode & nix::libc::S_ISUID != 0, Err(_) => false,
Err(_) => false, },
}
}
UnaryOp::SetGID => { UnaryOp::SetGID => match stat::stat(operand.as_str()) {
match stat::stat(operand.as_str()) { Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0,
Ok(stat) => stat.st_mode & nix::libc::S_ISGID != 0, Err(_) => false,
Err(_) => false, },
}
}
UnaryOp::Terminal => { UnaryOp::Terminal => match operand.as_str().parse::<nix::libc::c_int>() {
match operand.as_str().parse::<nix::libc::c_int>() { Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 },
Ok(fd) => unsafe { nix::libc::isatty(fd) == 1 }, Err(_) => false,
Err(_) => false, },
} UnaryOp::NonNull => !operand.is_empty(),
} UnaryOp::Null => operand.is_empty(),
UnaryOp::NonNull => !operand.is_empty(), }
UnaryOp::Null => operand.is_empty(), }
} TestCase::Binary {
} lhs,
TestCase::Binary { lhs, operator, rhs, conjunct } => { operator,
let lhs = lhs.expand()?.get_words().join(" "); rhs,
let rhs = rhs.expand()?.get_words().join(" "); conjunct,
conjunct_op = conjunct; } => {
let test_op = operator.as_str().parse::<TestOp>()?; let lhs = lhs.expand()?.get_words().join(" ");
flog!(DEBUG, lhs); let rhs = rhs.expand()?.get_words().join(" ");
flog!(DEBUG, rhs); conjunct_op = conjunct;
flog!(DEBUG, test_op); let test_op = operator.as_str().parse::<TestOp>()?;
match test_op { flog!(DEBUG, lhs);
TestOp::Unary(_) => { flog!(DEBUG, rhs);
return Err( flog!(DEBUG, test_op);
ShErr::Full { match test_op {
kind: ShErrKind::SyntaxErr, TestOp::Unary(_) => {
msg: "Expected a binary operator in this test call; found a unary operator".into(), return Err(ShErr::Full {
notes: vec![], kind: ShErrKind::SyntaxErr,
span: err_span msg: "Expected a binary operator in this test call; found a unary operator".into(),
} notes: vec![],
) span: err_span,
} })
TestOp::StringEq => rhs.trim() == lhs.trim(), }
TestOp::StringNeq => rhs.trim() != lhs.trim(), TestOp::StringEq => rhs.trim() == lhs.trim(),
TestOp::IntNeq | TestOp::StringNeq => rhs.trim() != lhs.trim(),
TestOp::IntGt | TestOp::IntNeq
TestOp::IntLt | | TestOp::IntGt
TestOp::IntGe | | TestOp::IntLt
TestOp::IntLe | | TestOp::IntGe
TestOp::IntEq => { | TestOp::IntLe
let err = ShErr::Full { | TestOp::IntEq => {
kind: ShErrKind::SyntaxErr, let err = ShErr::Full {
msg: format!("Expected an integer with '{}' operator", operator.as_str()), kind: ShErrKind::SyntaxErr,
notes: vec![], msg: format!("Expected an integer with '{}' operator", operator.as_str()),
span: err_span.clone() notes: vec![],
}; span: err_span.clone(),
let Ok(lhs) = lhs.trim().parse::<i32>() else { };
return Err(err) let Ok(lhs) = lhs.trim().parse::<i32>() else {
}; return Err(err);
let Ok(rhs) = rhs.trim().parse::<i32>() else { };
return Err(err) let Ok(rhs) = rhs.trim().parse::<i32>() else {
}; return Err(err);
match test_op { };
TestOp::IntNeq => lhs != rhs, match test_op {
TestOp::IntGt => lhs > rhs, TestOp::IntNeq => lhs != rhs,
TestOp::IntLt => lhs < rhs, TestOp::IntGt => lhs > rhs,
TestOp::IntGe => lhs >= rhs, TestOp::IntLt => lhs < rhs,
TestOp::IntLe => lhs <= rhs, TestOp::IntGe => lhs >= rhs,
TestOp::IntEq => lhs == rhs, TestOp::IntLe => lhs <= rhs,
_ => unreachable!() TestOp::IntEq => lhs == rhs,
} _ => unreachable!(),
} }
TestOp::RegexMatch => { }
// FIXME: Imagine doing all of this in every single iteration of a loop TestOp::RegexMatch => {
let cleaned = replace_posix_classes(&rhs); // FIXME: Imagine doing all of this in every single iteration of a loop
let regex = Regex::new(&cleaned).unwrap(); let cleaned = replace_posix_classes(&rhs);
regex.is_match(&lhs) let regex = Regex::new(&cleaned).unwrap();
} regex.is_match(&lhs)
} }
} }
}; }
flog!(DEBUG, last_result); };
flog!(DEBUG, last_result);
if let Some(op) = conjunct_op { if let Some(op) = conjunct_op {
match op { match op {
ConjunctOp::And if !last_result => { ConjunctOp::And if !last_result => {
last_result = result; last_result = result;
break break;
} }
ConjunctOp::Or if last_result => { ConjunctOp::Or if last_result => {
last_result = result; last_result = result;
break break;
} }
_ => {} _ => {}
} }
} else { } else {
last_result = result; last_result = result;
} }
} }
flog!(DEBUG, last_result); flog!(DEBUG, last_result);
Ok(last_result) Ok(last_result)
} }

View File

@@ -1,190 +1,191 @@
use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock}; use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock};
use crate::{getopt::{get_opts_from_tokens, Opt, OptSet}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}}; use crate::{
getopt::{get_opts_from_tokens, Opt, OptSet},
jobs::JobBldr,
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
prelude::*,
procio::{borrow_fd, IoStack},
};
use super::setup_builtin; use super::setup_builtin;
pub static ZOLTRAAK_OPTS: LazyLock<OptSet> = LazyLock::new(|| { pub static ZOLTRAAK_OPTS: LazyLock<OptSet> = LazyLock::new(|| {
[ [
Opt::Long("dry-run".into()), Opt::Long("dry-run".into()),
Opt::Long("confirm".into()), Opt::Long("confirm".into()),
Opt::Long("no-preserve-root".into()), Opt::Long("no-preserve-root".into()),
Opt::Short('r'), Opt::Short('r'),
Opt::Short('f'), Opt::Short('f'),
Opt::Short('v') Opt::Short('v'),
].into() ]
.into()
}); });
bitflags! { bitflags! {
#[derive(Clone,Copy,Debug,PartialEq,Eq)] #[derive(Clone,Copy,Debug,PartialEq,Eq)]
struct ZoltFlags: u32 { struct ZoltFlags: u32 {
const DRY = 0b000001; const DRY = 0b000001;
const CONFIRM = 0b000010; const CONFIRM = 0b000010;
const NO_PRESERVE_ROOT = 0b000100; const NO_PRESERVE_ROOT = 0b000100;
const RECURSIVE = 0b001000; const RECURSIVE = 0b001000;
const FORCE = 0b010000; const FORCE = 0b010000;
const VERBOSE = 0b100000; const VERBOSE = 0b100000;
} }
} }
/// Annihilate a file /// Annihilate a file
/// ///
/// This command works similarly to 'rm', but behaves more destructively. /// This command works similarly to 'rm', but behaves more destructively.
/// The file given as an argument is completely destroyed. The command works by shredding all of the data contained in the file, before truncating the length of the file to 0 to ensure that not even any metadata remains. /// The file given as an argument is completely destroyed. The command works by
/// shredding all of the data contained in the file, before truncating the
/// length of the file to 0 to ensure that not even any metadata remains.
pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let NdRule::Command { assignments: _, argv } = node.class else { let NdRule::Command {
unreachable!() assignments: _,
}; argv,
let mut flags = ZoltFlags::empty(); } = node.class
else {
unreachable!()
};
let mut flags = ZoltFlags::empty();
let (argv,opts) = get_opts_from_tokens(argv); let (argv, opts) = get_opts_from_tokens(argv);
for opt in opts { for opt in opts {
if !ZOLTRAAK_OPTS.contains(&opt) { if !ZOLTRAAK_OPTS.contains(&opt) {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, format!("zoltraak: unrecognized option '{opt}'"),
format!("zoltraak: unrecognized option '{opt}'") ));
) }
) match opt {
} Opt::Long(flag) => match flag.as_str() {
match opt { "no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT,
Opt::Long(flag) => { "confirm" => flags |= ZoltFlags::CONFIRM,
match flag.as_str() { "dry-run" => flags |= ZoltFlags::DRY,
"no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT, _ => unreachable!(),
"confirm" => flags |= ZoltFlags::CONFIRM, },
"dry-run" => flags |= ZoltFlags::DRY, Opt::Short(flag) => match flag {
_ => unreachable!() 'r' => flags |= ZoltFlags::RECURSIVE,
} 'f' => flags |= ZoltFlags::FORCE,
} 'v' => flags |= ZoltFlags::VERBOSE,
Opt::Short(flag) => { _ => unreachable!(),
match flag { },
'r' => flags |= ZoltFlags::RECURSIVE, }
'f' => flags |= ZoltFlags::FORCE, }
'v' => flags |= ZoltFlags::VERBOSE,
_ => unreachable!()
}
}
}
}
let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut io_frame = io_frame.unwrap(); let mut io_frame = io_frame.unwrap();
io_frame.redirect()?; io_frame.redirect()?;
for (arg,span) in argv { for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
return Err( return Err(
ShErr::simple( ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"zoltraak: Attempted to destroy root directory '/'" "zoltraak: Attempted to destroy root directory '/'",
) )
.with_note( .with_note(
Note::new("If you really want to do this, you can use the --no-preserve-root flag") Note::new("If you really want to do this, you can use the --no-preserve-root flag")
.with_sub_notes(vec![ .with_sub_notes(vec!["Example: 'zoltraak --no-preserve-root /'"]),
"Example: 'zoltraak --no-preserve-root /'" ),
]) );
) }
) if let Err(e) = annihilate(&arg, flags).blame(span) {
} io_frame.restore()?;
if let Err(e) = annihilate(&arg, flags).blame(span) { return Err(e);
io_frame.restore()?; }
return Err(e); }
}
}
io_frame.restore()?; io_frame.restore()?;
Ok(()) Ok(())
} }
fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> { fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
let path_buf = PathBuf::from(path); let path_buf = PathBuf::from(path);
let is_recursive = flags.contains(ZoltFlags::RECURSIVE); let is_recursive = flags.contains(ZoltFlags::RECURSIVE);
let is_verbose = flags.contains(ZoltFlags::VERBOSE); let is_verbose = flags.contains(ZoltFlags::VERBOSE);
const BLOCK_SIZE: u64 = 4096; const BLOCK_SIZE: u64 = 4096;
if !path_buf.exists() { if !path_buf.exists() {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("zoltraak: File '{path}' not found"),
format!("zoltraak: File '{path}' not found") ));
) }
)
}
if path_buf.is_file() { if path_buf.is_file() {
let mut file = OpenOptions::new() let mut file = OpenOptions::new()
.write(true) .write(true)
.custom_flags(libc::O_DIRECT) .custom_flags(libc::O_DIRECT)
.open(path_buf)?; .open(path_buf)?;
let meta = file.metadata()?; let meta = file.metadata()?;
let file_size = meta.len(); let file_size = meta.len();
let full_blocks = file_size / BLOCK_SIZE; let full_blocks = file_size / BLOCK_SIZE;
let byte_remainder = file_size % BLOCK_SIZE; let byte_remainder = file_size % BLOCK_SIZE;
let full_buf = vec![0; BLOCK_SIZE as usize]; let full_buf = vec![0; BLOCK_SIZE as usize];
let remainder_buf = vec![0; byte_remainder as usize]; let remainder_buf = vec![0; byte_remainder as usize];
for _ in 0..full_blocks { for _ in 0..full_blocks {
file.write_all(&full_buf)?; file.write_all(&full_buf)?;
} }
if byte_remainder > 0 { if byte_remainder > 0 {
file.write_all(&remainder_buf)?; file.write_all(&remainder_buf)?;
} }
file.set_len(0)?; file.set_len(0)?;
mem::drop(file); mem::drop(file);
fs::remove_file(path)?; fs::remove_file(path)?;
if is_verbose { if is_verbose {
let stderr = borrow_fd(STDERR_FILENO); let stderr = borrow_fd(STDERR_FILENO);
write(stderr, format!("shredded file '{path}'\n").as_bytes())?; write(stderr, format!("shredded file '{path}'\n").as_bytes())?;
} }
} else if path_buf.is_dir() {
if is_recursive {
annihilate_recursive(path, flags)?; // scary
} else {
return Err(
ShErr::simple(
ShErrKind::ExecFail,
format!("zoltraak: '{path}' is a directory"),
)
.with_note(
Note::new("Use the '-r' flag to recursively shred directories")
.with_sub_notes(vec!["Example: 'zoltraak -r directory'"]),
),
);
}
}
} else if path_buf.is_dir() { Ok(())
if is_recursive {
annihilate_recursive(path, flags)?; // scary
} else {
return Err(
ShErr::simple(
ShErrKind::ExecFail,
format!("zoltraak: '{path}' is a directory")
)
.with_note(
Note::new("Use the '-r' flag to recursively shred directories")
.with_sub_notes(vec![
"Example: 'zoltraak -r directory'"
])
)
)
}
}
Ok(())
} }
fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> { fn annihilate_recursive(dir: &str, flags: ZoltFlags) -> ShResult<()> {
let dir_path = PathBuf::from(dir); let dir_path = PathBuf::from(dir);
let is_verbose = flags.contains(ZoltFlags::VERBOSE); let is_verbose = flags.contains(ZoltFlags::VERBOSE);
for dir_entry in fs::read_dir(&dir_path)? { for dir_entry in fs::read_dir(&dir_path)? {
let entry = dir_entry?.path(); let entry = dir_entry?.path();
let file = entry.to_str().unwrap(); let file = entry.to_str().unwrap();
if entry.is_file() { if entry.is_file() {
annihilate(file, flags)?; annihilate(file, flags)?;
} else if entry.is_dir() { } else if entry.is_dir() {
annihilate_recursive(file, flags)?; annihilate_recursive(file, flags)?;
} }
} }
fs::remove_dir(dir)?; fs::remove_dir(dir)?;
if is_verbose { if is_verbose {
let stderr = borrow_fd(STDERR_FILENO); let stderr = borrow_fd(STDERR_FILENO);
write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?; write(stderr, format!("shredded directory '{dir}'\n").as_bytes())?;
} }
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,127 +1,130 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator
)] )]
pub mod prelude;
pub mod libsh;
pub mod prompt;
pub mod procio;
pub mod parse;
pub mod expand;
pub mod state;
pub mod builtin; pub mod builtin;
pub mod jobs; pub mod expand;
pub mod signal;
pub mod getopt; pub mod getopt;
pub mod jobs;
pub mod libsh;
pub mod parse;
pub mod prelude;
pub mod procio;
pub mod prompt;
pub mod shopt; pub mod shopt;
pub mod signal;
pub mod state;
#[cfg(test)] #[cfg(test)]
pub mod tests; pub mod tests;
use crate::libsh::sys::{save_termios, set_termios}; use crate::libsh::sys::{save_termios, set_termios};
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*;
use crate::signal::sig_setup; use crate::signal::sig_setup;
use crate::state::source_rc; use crate::state::source_rc;
use crate::prelude::*;
use clap::Parser; use clap::Parser;
use shopt::FernEditMode; use shopt::FernEditMode;
use state::{read_shopts, read_vars, write_shopts, write_vars}; use state::{read_vars, write_shopts, write_vars};
#[derive(Parser,Debug)] #[derive(Parser, Debug)]
struct FernArgs { struct FernArgs {
script: Option<String>, script: Option<String>,
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool version: bool,
} }
/// Force evaluation of lazily-initialized values early in shell startup. /// Force evaluation of lazily-initialized values early in shell startup.
/// ///
/// In particular, this ensures that the variable table is initialized, which populates /// In particular, this ensures that the variable table is initialized, which
/// environment variables from the system. If this initialization is deferred too long, /// populates environment variables from the system. If this initialization is
/// features like prompt expansion may fail due to missing environment variables. /// deferred too long, features like prompt expansion may fail due to missing
/// environment variables.
/// ///
/// This function triggers initialization by calling `read_vars` with a no-op closure, /// This function triggers initialization by calling `read_vars` with a no-op
/// which forces access to the variable table and causes its `LazyLock` constructor to run. /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
fn main() { fn main() {
kickstart_lazy_evals(); kickstart_lazy_evals();
let args = FernArgs::parse(); let args = FernArgs::parse();
if args.version { if args.version {
println!("fern {}", env!("CARGO_PKG_VERSION")); println!("fern {}", env!("CARGO_PKG_VERSION"));
return; return;
} }
if let Some(path) = args.script { if let Some(path) = args.script {
run_script(path, args.script_args); run_script(path, args.script_args);
} else { } else {
fern_interactive(); fern_interactive();
} }
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display()); eprintln!("fern: Failed to open input file: {}", path.display());
exit(1); exit(1);
} }
let Ok(input) = fs::read_to_string(path) else { let Ok(input) = fs::read_to_string(path) else {
eprintln!("fern: Failed to read input file: {}", path.display()); eprintln!("fern: Failed to read input file: {}", path.display());
exit(1); exit(1);
}; };
write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string())); write_vars(|v| v.bpush_arg(path.to_string_lossy().to_string()));
for arg in args { for arg in args {
write_vars(|v| v.bpush_arg(arg)) write_vars(|v| v.bpush_arg(arg))
} }
if let Err(e) = exec_input(input,None) { if let Err(e) = exec_input(input, None) {
eprintln!("{e}"); eprintln!("{e}");
exit(1); exit(1);
} }
} }
fn fern_interactive() { fn fern_interactive() {
save_termios(); save_termios();
set_termios(); set_termios();
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
let mut readline_err_count: u32 = 0; let mut readline_err_count: u32 = 0;
loop { // Main loop loop {
let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode")) // Main loop
.unwrap() let edit_mode = write_shopts(|opt| opt.query("prompt.edit_mode"))
.map(|mode| mode.parse::<FernEditMode>().unwrap_or_default()) .unwrap()
.unwrap(); .map(|mode| mode.parse::<FernEditMode>().unwrap_or_default())
let input = match prompt::readline(edit_mode) { .unwrap();
Ok(line) => { let input = match prompt::readline(edit_mode) {
readline_err_count = 0; Ok(line) => {
line readline_err_count = 0;
} line
Err(e) => { }
eprintln!("{e}"); Err(e) => {
readline_err_count += 1; eprintln!("{e}");
if readline_err_count == 20 { readline_err_count += 1;
eprintln!("reached maximum readline error count, exiting"); if readline_err_count == 20 {
break eprintln!("reached maximum readline error count, exiting");
} else { break;
continue } else {
} continue;
} }
}; }
};
if let Err(e) = exec_input(input,None) { if let Err(e) = exec_input(input, None) {
eprintln!("{e}"); eprintln!("{e}");
} }
} }
} }

View File

@@ -6,75 +6,74 @@ use crate::{parse::lex::Tk, prelude::*};
pub type OptSet = Arc<[Opt]>; pub type OptSet = Arc<[Opt]>;
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone,PartialEq,Eq,Debug)]
pub enum Opt { pub enum Opt {
Long(String), Long(String),
Short(char) Short(char),
} }
impl Opt { impl Opt {
pub fn parse(s: &str) -> Vec<Self> { pub fn parse(s: &str) -> Vec<Self> {
let mut opts = vec![]; let mut opts = vec![];
if s.starts_with("--") { if s.starts_with("--") {
opts.push(Opt::Long(s.trim_start_matches('-').to_string())) opts.push(Opt::Long(s.trim_start_matches('-').to_string()))
} else if s.starts_with('-') { } else if s.starts_with('-') {
let mut chars = s.trim_start_matches('-').chars(); let mut chars = s.trim_start_matches('-').chars();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
opts.push(Self::Short(ch)) opts.push(Self::Short(ch))
} }
} }
opts opts
} }
} }
impl Display for Opt { impl Display for Opt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Long(opt) => write!(f,"--{}",opt), Self::Long(opt) => write!(f, "--{}", opt),
Self::Short(opt) => write!(f,"-{}",opt), Self::Short(opt) => write!(f, "-{}", opt),
} }
} }
} }
pub fn get_opts(words: Vec<String>) -> (Vec<String>,Vec<Opt>) { pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
let mut words_iter = words.into_iter(); let mut words_iter = words.into_iter();
let mut opts = vec![]; let mut opts = vec![];
let mut non_opts = vec![]; let mut non_opts = vec![];
while let Some(word) = words_iter.next() { while let Some(word) = words_iter.next() {
if &word == "--" { if &word == "--" {
non_opts.extend(words_iter); non_opts.extend(words_iter);
break break;
} }
let parsed_opts = Opt::parse(&word); let parsed_opts = Opt::parse(&word);
if parsed_opts.is_empty() { if parsed_opts.is_empty() {
non_opts.push(word) non_opts.push(word)
} else { } else {
opts.extend(parsed_opts); opts.extend(parsed_opts);
} }
} }
(non_opts,opts) (non_opts, opts)
} }
pub fn get_opts_from_tokens(tokens: Vec<Tk>) -> (Vec<Tk>, Vec<Opt>) { pub fn get_opts_from_tokens(tokens: Vec<Tk>) -> (Vec<Tk>, Vec<Opt>) {
let mut tokens_iter = tokens.into_iter(); let mut tokens_iter = tokens.into_iter();
let mut opts = vec![]; let mut opts = vec![];
let mut non_opts = vec![]; let mut non_opts = vec![];
while let Some(token) = tokens_iter.next() { while let Some(token) = tokens_iter.next() {
if &token.to_string() == "--" { if &token.to_string() == "--" {
non_opts.extend(tokens_iter); non_opts.extend(tokens_iter);
break break;
} }
let parsed_opts = Opt::parse(&token.to_string()); let parsed_opts = Opt::parse(&token.to_string());
if parsed_opts.is_empty() { if parsed_opts.is_empty() {
non_opts.push(token) non_opts.push(token)
} else { } else {
opts.extend(parsed_opts); opts.extend(parsed_opts);
} }
} }
(non_opts,opts) (non_opts, opts)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,356 +1,439 @@
use std::fmt::Display; use std::fmt::Display;
use crate::{ use crate::{
libsh::term::{Style, Styled}, libsh::term::{Style, Styled},
parse::lex::Span, parse::lex::Span,
prelude::* prelude::*,
}; };
pub type ShResult<T> = Result<T,ShErr>; pub type ShResult<T> = Result<T, ShErr>;
pub trait ShResultExt { pub trait ShResultExt {
fn blame(self, span: Span) -> Self; fn blame(self, span: Span) -> Self;
fn try_blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self;
} }
impl<T> ShResultExt for Result<T,ShErr> { impl<T> ShResultExt for Result<T, ShErr> {
/// Blame a span for an error /// Blame a span for an error
fn blame(self, new_span: Span) -> Self { fn blame(self, new_span: Span) -> Self {
let Err(e) = self else { let Err(e) = self else { return self };
return self match e {
}; ShErr::Simple { kind, msg, notes }
match e { | ShErr::Full {
ShErr::Simple { kind, msg, notes } | kind,
ShErr::Full { kind, msg, notes, span: _ } => Err(ShErr::Full { kind: kind.clone(), msg: msg.clone(), notes: notes.clone(), span: new_span }), msg,
} notes,
} span: _,
/// Blame a span if no blame has been assigned yet } => Err(ShErr::Full {
fn try_blame(self, new_span: Span) -> Self { kind: kind.clone(),
let Err(e) = &self else { msg: msg.clone(),
return self notes: notes.clone(),
}; span: new_span,
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 }
} /// Blame a span if no blame has been assigned yet
} fn try_blame(self, new_span: Span) -> Self {
let Err(e) = &self else { return self };
match e {
ShErr::Simple { kind, msg, notes } => Err(ShErr::Full {
kind: kind.clone(),
msg: msg.clone(),
notes: notes.clone(),
span: new_span,
}),
ShErr::Full {
kind: _,
msg: _,
span: _,
notes: _,
} => self,
}
}
} }
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct Note { pub struct Note {
main: String, main: String,
sub_notes: Vec<Note>, sub_notes: Vec<Note>,
depth: usize depth: usize,
} }
impl Note { impl Note {
pub fn new(main: impl Into<String>) -> Self { pub fn new(main: impl Into<String>) -> Self {
Self { Self {
main: main.into(), main: main.into(),
sub_notes: vec![], sub_notes: vec![],
depth: 0 depth: 0,
} }
} }
pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self { pub fn with_sub_notes(self, new_sub_notes: Vec<impl Into<String>>) -> Self {
let Self { main, mut sub_notes, depth } = self; let Self {
for raw_note in new_sub_notes { main,
let mut note = Note::new(raw_note); mut sub_notes,
note.depth = self.depth + 1; depth,
sub_notes.push(note); } = self;
} for raw_note in new_sub_notes {
Self { main, sub_notes, depth } let mut note = Note::new(raw_note);
} note.depth = self.depth + 1;
sub_notes.push(note);
}
Self {
main,
sub_notes,
depth,
}
}
} }
impl Display for Note { impl Display for Note {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let note = "note".styled(Style::Green); let note = "note".styled(Style::Green);
let main = &self.main; let main = &self.main;
if self.depth == 0 { if self.depth == 0 {
writeln!(f, "{note}: {main}")?; writeln!(f, "{note}: {main}")?;
} else { } else {
let bar_break = "-".styled(Style::Cyan | Style::Bold); let bar_break = "-".styled(Style::Cyan | Style::Bold);
let indent = " ".repeat(self.depth); let indent = " ".repeat(self.depth);
writeln!(f, " {indent}{bar_break} {main}")?; writeln!(f, " {indent}{bar_break} {main}")?;
} }
for sub_note in &self.sub_notes { for sub_note in &self.sub_notes {
write!(f, "{sub_note}")?; write!(f, "{sub_note}")?;
} }
Ok(()) Ok(())
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum ShErr { pub enum ShErr {
Simple { kind: ShErrKind, msg: String, notes: Vec<Note> }, Simple {
Full { kind: ShErrKind, msg: String, notes: Vec<Note>, span: Span } kind: ShErrKind,
msg: String,
notes: Vec<Note>,
},
Full {
kind: ShErrKind,
msg: String,
notes: Vec<Note>,
span: Span,
},
} }
impl ShErr { impl ShErr {
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self { pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
let msg = msg.into(); let msg = msg.into();
Self::Simple { kind, msg, notes: vec![] } Self::Simple {
} kind,
pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self { msg,
let msg = msg.into(); notes: vec![],
Self::Full { kind, msg, span, notes: vec![] } }
} }
pub fn unpack(self) -> (ShErrKind,String,Vec<Note>,Option<Span>) { pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span) -> Self {
match self { let msg = msg.into();
ShErr::Simple { kind, msg, notes } => (kind,msg,notes,None), Self::Full {
ShErr::Full { kind, msg, notes, span } => (kind,msg,notes,Some(span)) kind,
} msg,
} span,
pub fn with_note(self, note: Note) -> Self { notes: vec![],
let (kind,msg,mut notes,span) = self.unpack(); }
notes.push(note); }
if let Some(span) = span { pub fn unpack(self) -> (ShErrKind, String, Vec<Note>, Option<Span>) {
Self::Full { kind, msg, notes, span } match self {
} else { ShErr::Simple { kind, msg, notes } => (kind, msg, notes, None),
Self::Simple { kind, msg, notes } ShErr::Full {
} kind,
} msg,
pub fn with_span(sherr: ShErr, span: Span) -> Self { notes,
let (kind,msg,notes,_) = sherr.unpack(); span,
Self::Full { kind, msg, notes, span } } => (kind, msg, notes, Some(span)),
} }
pub fn kind(&self) -> &ShErrKind { }
match self { pub fn with_note(self, note: Note) -> Self {
ShErr::Simple { kind, msg: _, notes: _ } | let (kind, msg, mut notes, span) = self.unpack();
ShErr::Full { kind, msg: _, notes: _, span: _ } => kind notes.push(note);
} if let Some(span) = span {
} Self::Full {
pub fn get_window(&self) -> Vec<(usize,String)> { kind,
let ShErr::Full { kind: _, msg: _, notes: _, span } = self else { msg,
unreachable!() notes,
}; span,
let mut total_len: usize = 0; }
let mut total_lines: usize = 1; } else {
let mut lines = vec![]; Self::Simple { kind, msg, notes }
let mut cur_line = String::new(); }
}
pub fn with_span(sherr: ShErr, span: Span) -> Self {
let (kind, msg, notes, _) = sherr.unpack();
Self::Full {
kind,
msg,
notes,
span,
}
}
pub fn kind(&self) -> &ShErrKind {
match self {
ShErr::Simple {
kind,
msg: _,
notes: _,
}
| ShErr::Full {
kind,
msg: _,
notes: _,
span: _,
} => kind,
}
}
pub fn get_window(&self) -> Vec<(usize, String)> {
let ShErr::Full {
kind: _,
msg: _,
notes: _,
span,
} = self
else {
unreachable!()
};
let mut total_len: usize = 0;
let mut total_lines: usize = 1;
let mut lines = vec![];
let mut cur_line = String::new();
let src = span.get_source(); let src = span.get_source();
let mut chars = src.chars(); let mut chars = src.chars();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
total_len += ch.len_utf8(); total_len += ch.len_utf8();
cur_line.push(ch); cur_line.push(ch);
if ch == '\n' { if ch == '\n' {
if total_len > span.start { if total_len > span.start {
let line = ( let line = (total_lines, mem::take(&mut cur_line));
total_lines, lines.push(line);
mem::take(&mut cur_line) }
); if total_len >= span.end {
lines.push(line); break;
} }
if total_len >= span.end { total_lines += 1;
break
}
total_lines += 1;
cur_line.clear(); cur_line.clear();
} }
} }
if !cur_line.is_empty() { if !cur_line.is_empty() {
let line = ( let line = (total_lines, mem::take(&mut cur_line));
total_lines, lines.push(line);
mem::take(&mut cur_line) }
);
lines.push(line);
}
lines lines
} }
pub fn get_line_col(&self) -> (usize,usize) { pub fn get_line_col(&self) -> (usize, usize) {
let ShErr::Full { kind: _, msg: _, notes: _, span } = self else { let ShErr::Full {
unreachable!() kind: _,
}; msg: _,
notes: _,
span,
} = self
else {
unreachable!()
};
let mut lineno = 1; let mut lineno = 1;
let mut colno = 1; let mut colno = 1;
let src = span.get_source(); let src = span.get_source();
let mut chars = src.chars().enumerate(); let mut chars = src.chars().enumerate();
while let Some((pos,ch)) = chars.next() { while let Some((pos, ch)) = chars.next() {
if pos >= span.start { if pos >= span.start {
break break;
} }
if ch == '\n' { if ch == '\n' {
lineno += 1; lineno += 1;
colno = 1; colno = 1;
} else { } else {
colno += 1; colno += 1;
} }
} }
(lineno,colno) (lineno, colno)
} }
pub fn get_indicator_lines(&self) -> Option<Vec<String>> { pub fn get_indicator_lines(&self) -> Option<Vec<String>> {
match self { match self {
ShErr::Simple { kind: _, msg: _, notes: _ } => None, ShErr::Simple {
ShErr::Full { kind: _, msg: _, notes: _, span } => { kind: _,
let text = span.as_str(); msg: _,
let lines = text.lines(); notes: _,
let mut indicator_lines = vec![]; } => 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 { for line in lines {
let indicator_line = "^".repeat(line.trim().len()).styled(Style::Red | Style::Bold); let indicator_line = "^"
indicator_lines.push(indicator_line); .repeat(line.trim().len())
} .styled(Style::Red | Style::Bold);
indicator_lines.push(indicator_line);
}
Some(indicator_lines) Some(indicator_lines)
} }
} }
} }
} }
impl Display for ShErr { impl Display for ShErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Simple { msg, kind: _, notes } => { Self::Simple {
let mut all_strings = vec![msg.to_string()]; msg,
let mut notes_fmt = vec![]; kind: _,
for note in notes { notes,
let fmt = format!("{note}"); } => {
notes_fmt.push(fmt); let mut all_strings = vec![msg.to_string()];
} let mut notes_fmt = vec![];
all_strings.append(&mut notes_fmt); for note in notes {
let mut output = all_strings.join("\n"); let fmt = format!("{note}");
output.push('\n'); notes_fmt.push(fmt);
}
all_strings.append(&mut notes_fmt);
let mut output = all_strings.join("\n");
output.push('\n');
writeln!(f, "{}", output) writeln!(f, "{}", output)
} }
Self::Full { msg, kind, notes, span: _ } => { Self::Full {
let window = self.get_window(); msg,
let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter(); kind,
let mut lineno_pad_count = 0; notes,
for (lineno,_) in window.clone() { span: _,
if lineno.to_string().len() > lineno_pad_count { } => {
lineno_pad_count = lineno.to_string().len() + 1 let window = self.get_window();
} let mut indicator_lines = self.get_indicator_lines().unwrap().into_iter();
} let mut lineno_pad_count = 0;
let padding = " ".repeat(lineno_pad_count); for (lineno, _) in window.clone() {
writeln!(f)?; if lineno.to_string().len() > lineno_pad_count {
lineno_pad_count = lineno.to_string().len() + 1
}
}
let padding = " ".repeat(lineno_pad_count);
writeln!(f)?;
let (line, col) = self.get_line_col();
let line_fmt = line.styled(Style::Cyan | Style::Bold);
let col_fmt = col.styled(Style::Cyan | Style::Bold);
let kind = kind.styled(Style::Red | Style::Bold);
let arrow = "->".styled(Style::Cyan | Style::Bold);
writeln!(f, "{kind} - {msg}",)?;
writeln!(f, "{padding}{arrow} [{line_fmt};{col_fmt}]",)?;
let (line,col) = self.get_line_col(); let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold);
let line_fmt = line.styled(Style::Cyan | Style::Bold); writeln!(f, "{bar}")?;
let col_fmt = col.styled(Style::Cyan | Style::Bold);
let kind = kind.styled(Style::Red | Style::Bold);
let arrow = "->".styled(Style::Cyan | Style::Bold);
writeln!(f,
"{kind} - {msg}",
)?;
writeln!(f,
"{padding}{arrow} [{line_fmt};{col_fmt}]",
)?;
let bar = format!("{padding}|").styled(Style::Cyan | Style::Bold); let mut first_ind_ln = true;
writeln!(f,"{bar}")?; for (lineno, line) in window {
let lineno = lineno.to_string();
let line = line.trim();
let mut prefix = format!("{padding}|");
prefix.replace_range(0..lineno.len(), &lineno);
prefix = prefix.styled(Style::Cyan | Style::Bold);
writeln!(f, "{prefix} {line}")?;
let mut first_ind_ln = true; if let Some(ind_ln) = indicator_lines.next() {
for (lineno,line) in window { if first_ind_ln {
let lineno = lineno.to_string(); let ind_ln_padding = " ".repeat(col);
let line = line.trim(); let ind_ln = format!("{ind_ln_padding}{ind_ln}");
let mut prefix = format!("{padding}|"); writeln!(f, "{bar}{ind_ln}")?;
prefix.replace_range(0..lineno.len(), &lineno); first_ind_ln = false;
prefix = prefix.styled(Style::Cyan | Style::Bold); } else {
writeln!(f,"{prefix} {line}")?; writeln!(f, "{bar} {ind_ln}")?;
}
}
}
if let Some(ind_ln) = indicator_lines.next() { write!(f, "{bar}")?;
if first_ind_ln {
let ind_ln_padding = " ".repeat(col);
let ind_ln = format!("{ind_ln_padding}{ind_ln}");
writeln!(f, "{bar}{ind_ln}")?;
first_ind_ln = false;
} else {
writeln!(f, "{bar} {ind_ln}")?;
}
}
}
write!(f,"{bar}")?; let bar_break = "-".styled(Style::Cyan | Style::Bold);
if !notes.is_empty() {
writeln!(f)?;
let bar_break = "-".styled(Style::Cyan | Style::Bold); }
if !notes.is_empty() { for note in notes {
writeln!(f)?; write!(f, "{padding}{bar_break} {note}")?;
} }
for note in notes { Ok(())
}
write!(f, }
"{padding}{bar_break} {note}" }
)?;
}
Ok(())
}
}
}
} }
impl From<std::io::Error> for ShErr { impl From<std::io::Error> for ShErr {
fn from(_: std::io::Error) -> Self { fn from(_: std::io::Error) -> Self {
let msg = std::io::Error::last_os_error(); let msg = std::io::Error::last_os_error();
ShErr::simple(ShErrKind::IoErr, msg.to_string()) ShErr::simple(ShErrKind::IoErr, msg.to_string())
} }
} }
impl From<std::env::VarError> for ShErr { impl From<std::env::VarError> for ShErr {
fn from(value: std::env::VarError) -> Self { fn from(value: std::env::VarError) -> Self {
ShErr::simple(ShErrKind::InternalErr, value.to_string()) ShErr::simple(ShErrKind::InternalErr, value.to_string())
} }
} }
impl From<Errno> for ShErr { impl From<Errno> for ShErr {
fn from(value: Errno) -> Self { fn from(value: Errno) -> Self {
ShErr::simple(ShErrKind::Errno, value.to_string()) ShErr::simple(ShErrKind::Errno, value.to_string())
} }
} }
#[derive(Debug,Clone)] #[derive(Debug, Clone)]
pub enum ShErrKind { pub enum ShErrKind {
IoErr, IoErr,
SyntaxErr, SyntaxErr,
ParseErr, ParseErr,
InternalErr, InternalErr,
ExecFail, ExecFail,
HistoryReadErr, HistoryReadErr,
ResourceLimitExceeded, ResourceLimitExceeded,
BadPermission, BadPermission,
Errno, Errno,
FileNotFound(String), FileNotFound(String),
CmdNotFound(String), CmdNotFound(String),
CleanExit(i32), CleanExit(i32),
FuncReturn(i32), FuncReturn(i32),
LoopContinue(i32), LoopContinue(i32),
LoopBreak(i32), LoopBreak(i32),
ReadlineErr, ReadlineErr,
Null Null,
} }
impl Display for ShErrKind { impl Display for ShErrKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let output = match self { let output = match self {
Self::IoErr => "I/O Error", Self::IoErr => "I/O Error",
Self::SyntaxErr => "Syntax Error", Self::SyntaxErr => "Syntax Error",
Self::ParseErr => "Parse Error", Self::ParseErr => "Parse Error",
Self::InternalErr => "Internal Error", Self::InternalErr => "Internal Error",
Self::HistoryReadErr => "History Parse Error", Self::HistoryReadErr => "History Parse Error",
Self::ExecFail => "Execution Failed", Self::ExecFail => "Execution Failed",
Self::ResourceLimitExceeded => "Resource Limit Exceeded", Self::ResourceLimitExceeded => "Resource Limit Exceeded",
Self::BadPermission => "Bad Permissions", Self::BadPermission => "Bad Permissions",
Self::Errno => "ERRNO", Self::Errno => "ERRNO",
Self::FileNotFound(file) => &format!("File not found: {file}"), Self::FileNotFound(file) => &format!("File not found: {file}"),
Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"), Self::CmdNotFound(cmd) => &format!("Command not found: {cmd}"),
Self::CleanExit(_) => "", Self::CleanExit(_) => "",
Self::FuncReturn(_) => "", Self::FuncReturn(_) => "",
Self::LoopContinue(_) => "", Self::LoopContinue(_) => "",
Self::LoopBreak(_) => "", Self::LoopBreak(_) => "",
Self::ReadlineErr => "Line Read Error", Self::ReadlineErr => "Line Read Error",
Self::Null => "", Self::Null => "",
}; };
write!(f,"{output}") write!(f, "{output}")
} }
} }

View File

@@ -2,55 +2,57 @@ use std::fmt::Display;
use super::term::{Style, Styled}; use super::term::{Style, Styled};
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq , Debug)] #[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Debug)]
#[repr(u8)] #[repr(u8)]
pub enum FernLogLevel { pub enum FernLogLevel {
NONE = 0, NONE = 0,
ERROR = 1, ERROR = 1,
WARN = 2, WARN = 2,
INFO = 3, INFO = 3,
DEBUG = 4, DEBUG = 4,
TRACE = 5 TRACE = 5,
} }
impl Display for FernLogLevel { impl Display for FernLogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use FernLogLevel::*; use FernLogLevel::*;
match self { match self {
ERROR => write!(f,"{}","ERROR".styled(Style::Red | Style::Bold)), ERROR => write!(f, "{}", "ERROR".styled(Style::Red | Style::Bold)),
WARN => write!(f,"{}","WARN".styled(Style::Yellow | Style::Bold)), WARN => write!(f, "{}", "WARN".styled(Style::Yellow | Style::Bold)),
INFO => write!(f,"{}","INFO".styled(Style::Green | Style::Bold)), INFO => write!(f, "{}", "INFO".styled(Style::Green | Style::Bold)),
DEBUG => write!(f,"{}","DEBUG".styled(Style::Magenta | Style::Bold)), DEBUG => write!(f, "{}", "DEBUG".styled(Style::Magenta | Style::Bold)),
TRACE => write!(f,"{}","TRACE".styled(Style::Blue | Style::Bold)), TRACE => write!(f, "{}", "TRACE".styled(Style::Blue | Style::Bold)),
NONE => write!(f,"") NONE => write!(f, ""),
} }
} }
} }
pub fn log_level() -> FernLogLevel { pub fn log_level() -> FernLogLevel {
use FernLogLevel::*; use FernLogLevel::*;
let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default(); let level = std::env::var("FERN_LOG_LEVEL").unwrap_or_default();
match level.to_lowercase().as_str() { match level.to_lowercase().as_str() {
"error" => ERROR, "error" => ERROR,
"warn" => WARN, "warn" => WARN,
"info" => INFO, "info" => INFO,
"debug" => DEBUG, "debug" => DEBUG,
"trace" => TRACE, "trace" => TRACE,
_ => NONE _ => NONE,
} }
} }
/// A structured logging macro designed for `fern`. /// A structured logging macro designed for `fern`.
/// ///
/// `flog!` was implemented because `rustyline` uses `env_logger`, which clutters the debug output. /// `flog!` was implemented because `rustyline` uses `env_logger`, which
/// This macro prints log messages in a structured format, including the log level, filename, and line number. /// clutters the debug output. This macro prints log messages in a structured
/// format, including the log level, filename, and line number.
/// ///
/// # Usage /// # Usage
/// ///
/// The macro supports three types of arguments: /// The macro supports three types of arguments:
/// ///
/// ## 1. **Formatted Messages** /// ## 1. **Formatted Messages**
/// Similar to `println!` or `format!`, allows embedding values inside a formatted string. /// Similar to `println!` or `format!`, allows embedding values inside a
/// formatted string.
/// ///
/// ```rust /// ```rust
/// flog!(ERROR, "foo is {}", foo); /// flog!(ERROR, "foo is {}", foo);
@@ -73,7 +75,8 @@ pub fn log_level() -> FernLogLevel {
/// ``` /// ```
/// ///
/// ## 3. **Expressions** /// ## 3. **Expressions**
/// Logs the evaluated result of each given expression, displaying both the expression and its value. /// Logs the evaluated result of each given expression, displaying both the
/// expression and its value.
/// ///
/// ```rust /// ```rust
/// flog!(INFO, 1.min(2)); /// flog!(INFO, 1.min(2));
@@ -84,8 +87,10 @@ pub fn log_level() -> FernLogLevel {
/// ``` /// ```
/// ///
/// # Considerations /// # Considerations
/// - This macro uses `eprintln!()` internally, so its formatting rules must be followed. /// - This macro uses `eprintln!()` internally, so its formatting rules must be
/// - **Literals and formatted messages** require arguments that implement [`std::fmt::Display`]. /// followed.
/// - **Literals and formatted messages** require arguments that implement
/// [`std::fmt::Display`].
/// - **Expressions** require arguments that implement [`std::fmt::Debug`]. /// - **Expressions** require arguments that implement [`std::fmt::Debug`].
#[macro_export] #[macro_export]
macro_rules! flog { macro_rules! flog {

View File

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

View File

@@ -4,70 +4,91 @@ use crate::{prelude::*, state::write_jobs};
/// ///
/// The previous state of the terminal options. /// The previous state of the terminal options.
/// ///
/// This variable stores the terminal settings at the start of the program and restores them when the program exits. /// This variable stores the terminal settings at the start of the program and
/// It is initialized exactly once at the start of the program and accessed exactly once at the end of the program. /// restores them when the program exits. It is initialized exactly once at the
/// It will not be mutated or accessed under any other circumstances. /// start of the program and accessed exactly once at the end of the program. It
/// will not be mutated or accessed under any other circumstances.
/// ///
/// This ended up being necessary because wrapping Termios in a thread-safe way was unreasonably tricky. /// This ended up being necessary because wrapping Termios in a thread-safe way
/// was unreasonably tricky.
/// ///
/// The possible states of this variable are: /// The possible states of this variable are:
/// - `None`: The terminal options have not been set yet (before initialization). /// - `None`: The terminal options have not been set yet (before
/// - `Some(None)`: There were no terminal options to save (i.e., no terminal input detected). /// initialization).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been saved. /// - `Some(None)`: There were no terminal options to save (i.e., no terminal
/// input detected).
/// - `Some(Some(Termios))`: The terminal options (as `Termios`) have been
/// saved.
/// ///
/// **Important:** This static variable is mutable and accessed via unsafe code. It is only safe to use because: /// **Important:** This static variable is mutable and accessed via unsafe code.
/// - It is set once during program startup and accessed once during program exit. /// It is only safe to use because:
/// - It is set once during program startup and accessed once during program
/// exit.
/// - It is not mutated or accessed after the initial setup and final read. /// - It is not mutated or accessed after the initial setup and final read.
/// ///
/// **Caution:** Future changes to this code should respect these constraints to ensure safety. Modifying or accessing this variable outside the defined lifecycle could lead to undefined behavior. /// **Caution:** Future changes to this code should respect these constraints to
/// ensure safety. Modifying or accessing this variable outside the defined
/// lifecycle could lead to undefined behavior.
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None; pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() { pub fn save_termios() {
unsafe { unsafe {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL; termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); termios::tcsetattr(
Some(termios) std::io::stdin(),
} else { nix::sys::termios::SetArg::TCSANOW,
None &termios,
}); )
} .unwrap();
Some(termios)
} else {
None
});
}
} }
#[allow(static_mut_refs)] #[allow(static_mut_refs)]
///Access the saved termios ///Access the saved termios
/// ///
///# Safety ///# Safety
///This function is unsafe because it accesses a public mutable static value. This function should only ever be called after save_termios() has already been called. ///This function is unsafe because it accesses a public mutable static value.
/// This function should only ever be called after save_termios() has already
/// been called.
pub unsafe fn get_saved_termios() -> Option<Termios> { pub unsafe fn get_saved_termios() -> Option<Termios> {
// SAVED_TERMIOS should *only ever* be set once and accessed once // SAVED_TERMIOS should *only ever* be set once and accessed once
// Set at the start of the program, and accessed during the exit of the program to reset the termios. // Set at the start of the program, and accessed during the exit of the program
// Do not use this variable anywhere else // to reset the termios. Do not use this variable anywhere else
SAVED_TERMIOS.clone().flatten() SAVED_TERMIOS.clone().flatten()
} }
/// Set termios to not echo control characters, like ^Z for instance /// Set termios to not echo control characters, like ^Z for instance
pub fn set_termios() { pub fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() { if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL; termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); termios::tcsetattr(
} std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW,
&termios,
)
.unwrap();
}
} }
pub fn sh_quit(code: i32) -> ! { pub fn sh_quit(code: i32) -> ! {
write_jobs(|j| { write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() { for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok(); job.killpg(Signal::SIGTERM).ok();
} }
}); });
if let Some(termios) = unsafe { get_saved_termios() } { if let Some(termios) = unsafe { get_saved_termios() } {
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap(); termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
} }
if code == 0 { if code == 0 {
eprintln!("exit"); eprintln!("exit");
} else { } else {
eprintln!("exit {code}"); eprintln!("exit {code}");
} }
exit(code); exit(code);
} }

View File

@@ -1,11 +1,11 @@
use std::{fmt::Display, ops::BitOr}; use std::{fmt::Display, ops::BitOr};
pub trait Styled: Sized + Display { pub trait Styled: Sized + Display {
fn styled<S: Into<StyleSet>>(self, style: S) -> String { fn styled<S: Into<StyleSet>>(self, style: S) -> String {
let styles: StyleSet = style.into(); let styles: StyleSet = style.into();
let reset = Style::Reset; let reset = Style::Reset;
format!("{styles}{self}{reset}") format!("{styles}{self}{reset}")
} }
} }
impl<T: Display> Styled for T {} impl<T: Display> Styled for T {}
@@ -13,157 +13,157 @@ impl<T: Display> Styled for T {}
/// Enum representing a single ANSI style /// Enum representing a single ANSI style
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Style { pub enum Style {
// Undoes all styles // Undoes all styles
Reset, Reset,
// Foreground Colors // Foreground Colors
Black, Black,
Red, Red,
Green, Green,
Yellow, Yellow,
Blue, Blue,
Magenta, Magenta,
Cyan, Cyan,
White, White,
BrightBlack, BrightBlack,
BrightRed, BrightRed,
BrightGreen, BrightGreen,
BrightYellow, BrightYellow,
BrightBlue, BrightBlue,
BrightMagenta, BrightMagenta,
BrightCyan, BrightCyan,
BrightWhite, BrightWhite,
RGB(u8, u8, u8), // Custom foreground color RGB(u8, u8, u8), // Custom foreground color
// Background Colors // Background Colors
BgBlack, BgBlack,
BgRed, BgRed,
BgGreen, BgGreen,
BgYellow, BgYellow,
BgBlue, BgBlue,
BgMagenta, BgMagenta,
BgCyan, BgCyan,
BgWhite, BgWhite,
BgBrightBlack, BgBrightBlack,
BgBrightRed, BgBrightRed,
BgBrightGreen, BgBrightGreen,
BgBrightYellow, BgBrightYellow,
BgBrightBlue, BgBrightBlue,
BgBrightMagenta, BgBrightMagenta,
BgBrightCyan, BgBrightCyan,
BgBrightWhite, BgBrightWhite,
BgRGB(u8, u8, u8), // Custom background color BgRGB(u8, u8, u8), // Custom background color
// Text Attributes // Text Attributes
Bold, Bold,
Dim, Dim,
Italic, Italic,
Underline, Underline,
Strikethrough, Strikethrough,
Reversed, Reversed,
} }
impl Display for Style { impl Display for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Style::Reset => write!(f, "\x1b[0m"), Style::Reset => write!(f, "\x1b[0m"),
// Foreground colors // Foreground colors
Style::Black => write!(f, "\x1b[30m"), Style::Black => write!(f, "\x1b[30m"),
Style::Red => write!(f, "\x1b[31m"), Style::Red => write!(f, "\x1b[31m"),
Style::Green => write!(f, "\x1b[32m"), Style::Green => write!(f, "\x1b[32m"),
Style::Yellow => write!(f, "\x1b[33m"), Style::Yellow => write!(f, "\x1b[33m"),
Style::Blue => write!(f, "\x1b[34m"), Style::Blue => write!(f, "\x1b[34m"),
Style::Magenta => write!(f, "\x1b[35m"), Style::Magenta => write!(f, "\x1b[35m"),
Style::Cyan => write!(f, "\x1b[36m"), Style::Cyan => write!(f, "\x1b[36m"),
Style::White => write!(f, "\x1b[37m"), Style::White => write!(f, "\x1b[37m"),
Style::BrightBlack => write!(f, "\x1b[90m"), Style::BrightBlack => write!(f, "\x1b[90m"),
Style::BrightRed => write!(f, "\x1b[91m"), Style::BrightRed => write!(f, "\x1b[91m"),
Style::BrightGreen => write!(f, "\x1b[92m"), Style::BrightGreen => write!(f, "\x1b[92m"),
Style::BrightYellow => write!(f, "\x1b[93m"), Style::BrightYellow => write!(f, "\x1b[93m"),
Style::BrightBlue => write!(f, "\x1b[94m"), Style::BrightBlue => write!(f, "\x1b[94m"),
Style::BrightMagenta => write!(f, "\x1b[95m"), Style::BrightMagenta => write!(f, "\x1b[95m"),
Style::BrightCyan => write!(f, "\x1b[96m"), Style::BrightCyan => write!(f, "\x1b[96m"),
Style::BrightWhite => write!(f, "\x1b[97m"), Style::BrightWhite => write!(f, "\x1b[97m"),
Style::RGB(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"), Style::RGB(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"),
// Background colors // Background colors
Style::BgBlack => write!(f, "\x1b[40m"), Style::BgBlack => write!(f, "\x1b[40m"),
Style::BgRed => write!(f, "\x1b[41m"), Style::BgRed => write!(f, "\x1b[41m"),
Style::BgGreen => write!(f, "\x1b[42m"), Style::BgGreen => write!(f, "\x1b[42m"),
Style::BgYellow => write!(f, "\x1b[43m"), Style::BgYellow => write!(f, "\x1b[43m"),
Style::BgBlue => write!(f, "\x1b[44m"), Style::BgBlue => write!(f, "\x1b[44m"),
Style::BgMagenta => write!(f, "\x1b[45m"), Style::BgMagenta => write!(f, "\x1b[45m"),
Style::BgCyan => write!(f, "\x1b[46m"), Style::BgCyan => write!(f, "\x1b[46m"),
Style::BgWhite => write!(f, "\x1b[47m"), Style::BgWhite => write!(f, "\x1b[47m"),
Style::BgBrightBlack => write!(f, "\x1b[100m"), Style::BgBrightBlack => write!(f, "\x1b[100m"),
Style::BgBrightRed => write!(f, "\x1b[101m"), Style::BgBrightRed => write!(f, "\x1b[101m"),
Style::BgBrightGreen => write!(f, "\x1b[102m"), Style::BgBrightGreen => write!(f, "\x1b[102m"),
Style::BgBrightYellow => write!(f, "\x1b[103m"), Style::BgBrightYellow => write!(f, "\x1b[103m"),
Style::BgBrightBlue => write!(f, "\x1b[104m"), Style::BgBrightBlue => write!(f, "\x1b[104m"),
Style::BgBrightMagenta => write!(f, "\x1b[105m"), Style::BgBrightMagenta => write!(f, "\x1b[105m"),
Style::BgBrightCyan => write!(f, "\x1b[106m"), Style::BgBrightCyan => write!(f, "\x1b[106m"),
Style::BgBrightWhite => write!(f, "\x1b[107m"), Style::BgBrightWhite => write!(f, "\x1b[107m"),
Style::BgRGB(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"), Style::BgRGB(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"),
// Text attributes // Text attributes
Style::Bold => write!(f, "\x1b[1m"), Style::Bold => write!(f, "\x1b[1m"),
Style::Dim => write!(f, "\x1b[2m"), // New Style::Dim => write!(f, "\x1b[2m"), // New
Style::Italic => write!(f, "\x1b[3m"), Style::Italic => write!(f, "\x1b[3m"),
Style::Underline => write!(f, "\x1b[4m"), Style::Underline => write!(f, "\x1b[4m"),
Style::Strikethrough => write!(f, "\x1b[9m"), // New Style::Strikethrough => write!(f, "\x1b[9m"), // New
Style::Reversed => write!(f, "\x1b[7m"), Style::Reversed => write!(f, "\x1b[7m"),
} }
} }
} }
/// Struct representing a **set** of styles /// Struct representing a **set** of styles
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct StyleSet { pub struct StyleSet {
styles: Vec<Style>, styles: Vec<Style>,
} }
impl StyleSet { impl StyleSet {
pub fn new() -> Self { pub fn new() -> Self {
Self { styles: vec![] } Self { styles: vec![] }
} }
pub fn add_style(mut self, style: Style) -> Self { pub fn add_style(mut self, style: Style) -> Self {
if !self.styles.contains(&style) { if !self.styles.contains(&style) {
self.styles.push(style); self.styles.push(style);
} }
self self
} }
} }
impl Display for StyleSet { impl Display for StyleSet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for style in &self.styles { for style in &self.styles {
style.fmt(f)? style.fmt(f)?
} }
Ok(()) Ok(())
} }
} }
/// Allow OR (`|`) operator to combine multiple `Style` values into a `StyleSet` /// Allow OR (`|`) operator to combine multiple `Style` values into a `StyleSet`
impl BitOr for Style { impl BitOr for Style {
type Output = StyleSet; type Output = StyleSet;
fn bitor(self, rhs: Self) -> Self::Output { fn bitor(self, rhs: Self) -> Self::Output {
StyleSet::new().add_style(self).add_style(rhs) StyleSet::new().add_style(self).add_style(rhs)
} }
} }
/// Allow OR (`|`) operator to combine `StyleSet` with `Style` /// Allow OR (`|`) operator to combine `StyleSet` with `Style`
impl BitOr<Style> for StyleSet { impl BitOr<Style> for StyleSet {
type Output = StyleSet; type Output = StyleSet;
fn bitor(self, rhs: Style) -> Self::Output { fn bitor(self, rhs: Style) -> Self::Output {
self.add_style(rhs) self.add_style(rhs)
} }
} }
impl From<Style> for StyleSet { impl From<Style> for StyleSet {
fn from(style: Style) -> Self { fn from(style: Style) -> Self {
StyleSet::new().add_style(style) StyleSet::new().add_style(style)
} }
} }

View File

@@ -5,107 +5,105 @@ use crate::parse::{Redir, RedirType};
use crate::prelude::*; use crate::prelude::*;
pub trait VecDequeExt<T> { pub trait VecDequeExt<T> {
fn to_vec(self) -> Vec<T>; fn to_vec(self) -> Vec<T>;
} }
pub trait CharDequeUtils { pub trait CharDequeUtils {
fn to_string(self) -> String; fn to_string(self) -> String;
fn ends_with(&self, pat: &str) -> bool; fn ends_with(&self, pat: &str) -> bool;
fn starts_with(&self, pat: &str) -> bool; fn starts_with(&self, pat: &str) -> bool;
} }
pub trait TkVecUtils<Tk> { pub trait TkVecUtils<Tk> {
fn get_span(&self) -> Option<Span>; fn get_span(&self) -> Option<Span>;
fn debug_tokens(&self); fn debug_tokens(&self);
} }
pub trait RedirVecUtils<Redir> { pub trait RedirVecUtils<Redir> {
/// Splits the vector of redirections into two vectors /// Splits the vector of redirections into two vectors
/// ///
/// One vector contains input redirs, the other contains output redirs /// One vector contains input redirs, the other contains output redirs
fn split_by_channel(self) -> (Vec<Redir>,Vec<Redir>); fn split_by_channel(self) -> (Vec<Redir>, Vec<Redir>);
} }
impl<T> VecDequeExt<T> for VecDeque<T> { impl<T> VecDequeExt<T> for VecDeque<T> {
fn to_vec(self) -> Vec<T> { fn to_vec(self) -> Vec<T> {
self.into_iter().collect::<Vec<T>>() self.into_iter().collect::<Vec<T>>()
} }
} }
impl CharDequeUtils for VecDeque<char> { impl CharDequeUtils for VecDeque<char> {
fn to_string(mut self) -> String { fn to_string(mut self) -> String {
let mut result = String::with_capacity(self.len()); let mut result = String::with_capacity(self.len());
while let Some(ch) = self.pop_front() { while let Some(ch) = self.pop_front() {
result.push(ch); result.push(ch);
} }
result result
} }
fn ends_with(&self, pat: &str) -> bool { fn ends_with(&self, pat: &str) -> bool {
let pat_chars = pat.chars(); let pat_chars = pat.chars();
let self_len = self.len(); let self_len = self.len();
// If pattern is longer than self, return false // If pattern is longer than self, return false
if pat_chars.clone().count() > self_len { if pat_chars.clone().count() > self_len {
return false; return false;
} }
// Compare from the back // Compare from the back
self.iter().rev().zip(pat_chars.rev()).all(|(c1, c2)| c1 == &c2) self
} .iter()
.rev()
.zip(pat_chars.rev())
.all(|(c1, c2)| c1 == &c2)
}
fn starts_with(&self, pat: &str) -> bool { fn starts_with(&self, pat: &str) -> bool {
let pat_chars = pat.chars(); let pat_chars = pat.chars();
let self_len = self.len(); let self_len = self.len();
// If pattern is longer than self, return false // If pattern is longer than self, return false
if pat_chars.clone().count() > self_len { if pat_chars.clone().count() > self_len {
return false; return false;
} }
// Compare from the front // Compare from the front
self.iter().zip(pat_chars).all(|(c1, c2)| c1 == &c2) self.iter().zip(pat_chars).all(|(c1, c2)| c1 == &c2)
} }
} }
impl TkVecUtils<Tk> for Vec<Tk> { impl TkVecUtils<Tk> for Vec<Tk> {
fn get_span(&self) -> Option<Span> { fn get_span(&self) -> Option<Span> {
if let Some(first_tk) = self.first() { if let Some(first_tk) = self.first() {
self.last().map(|last_tk| { self
Span::new( .last()
first_tk.span.start..last_tk.span.end, .map(|last_tk| Span::new(first_tk.span.start..last_tk.span.end, first_tk.source()))
first_tk.source() } else {
) None
}) }
} else { }
None fn debug_tokens(&self) {
} for token in self {
} flog!(DEBUG, "token: {}", token)
fn debug_tokens(&self) { }
for token in self { }
flog!(DEBUG, "token: {}",token)
}
}
} }
impl RedirVecUtils<Redir> for Vec<Redir> { impl RedirVecUtils<Redir> for Vec<Redir> {
fn split_by_channel(self) -> (Vec<Redir>,Vec<Redir>) { fn split_by_channel(self) -> (Vec<Redir>, Vec<Redir>) {
let mut input = vec![]; let mut input = vec![];
let mut output = vec![]; let mut output = vec![];
for redir in self { for redir in self {
match redir.class { match redir.class {
RedirType::Input => input.push(redir), RedirType::Input => input.push(redir),
RedirType::Pipe => { RedirType::Pipe => match redir.io_mode.tgt_fd() {
match redir.io_mode.tgt_fd() { STDIN_FILENO => input.push(redir),
STDIN_FILENO => input.push(redir), STDOUT_FILENO | STDERR_FILENO => output.push(redir),
STDOUT_FILENO | _ => unreachable!(),
STDERR_FILENO => output.push(redir), },
_ => unreachable!() _ => output.push(redir),
} }
} }
_ => output.push(redir) (input, output)
} }
}
(input,output)
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,266 +1,312 @@
use std::{fmt::Debug, ops::{Deref, DerefMut}}; use std::{
fmt::Debug,
ops::{Deref, DerefMut},
};
use crate::{libsh::{error::{ShErr, ShErrKind, ShResult}, utils::RedirVecUtils}, parse::{get_redir_file, Redir, RedirType}, prelude::*}; use crate::{
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::RedirVecUtils,
},
parse::{get_redir_file, Redir, RedirType},
prelude::*,
};
// Credit to fish-shell for many of the implementation ideas present in this module // Credit to fish-shell for many of the implementation ideas present in this
// https://fishshell.com/ // module https://fishshell.com/
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub enum IoMode { pub enum IoMode {
Fd { tgt_fd: RawFd, src_fd: Arc<OwnedFd> }, Fd {
File { tgt_fd: RawFd, path: PathBuf, mode: RedirType }, tgt_fd: RawFd,
Pipe { tgt_fd: RawFd, pipe: Arc<OwnedFd> }, src_fd: Arc<OwnedFd>,
Buffer { buf: String, pipe: Arc<OwnedFd> } },
File {
tgt_fd: RawFd,
path: PathBuf,
mode: RedirType,
},
Pipe {
tgt_fd: RawFd,
pipe: Arc<OwnedFd>,
},
Buffer {
buf: String,
pipe: Arc<OwnedFd>,
},
} }
impl IoMode { impl IoMode {
pub fn fd(tgt_fd: RawFd, src_fd: RawFd) -> Self { pub fn fd(tgt_fd: RawFd, src_fd: RawFd) -> Self {
let src_fd = unsafe { OwnedFd::from_raw_fd(src_fd).into() }; let src_fd = unsafe { OwnedFd::from_raw_fd(src_fd).into() };
Self::Fd { tgt_fd, src_fd } Self::Fd { tgt_fd, src_fd }
} }
pub fn file(tgt_fd: RawFd, path: PathBuf, mode: RedirType) -> Self { pub fn file(tgt_fd: RawFd, path: PathBuf, mode: RedirType) -> Self {
Self::File { tgt_fd, path, mode } Self::File { tgt_fd, path, mode }
} }
pub fn pipe(tgt_fd: RawFd, pipe: OwnedFd) -> Self { pub fn pipe(tgt_fd: RawFd, pipe: OwnedFd) -> Self {
let pipe = pipe.into(); let pipe = pipe.into();
Self::Pipe { tgt_fd, pipe } Self::Pipe { tgt_fd, pipe }
} }
pub fn tgt_fd(&self) -> RawFd { pub fn tgt_fd(&self) -> RawFd {
match self { match self {
IoMode::Fd { tgt_fd, .. } | IoMode::Fd { tgt_fd, .. } | IoMode::File { tgt_fd, .. } | IoMode::Pipe { tgt_fd, .. } => {
IoMode::File { tgt_fd, .. } | *tgt_fd
IoMode::Pipe { tgt_fd, .. } => *tgt_fd, }
_ => panic!() _ => panic!(),
} }
} }
pub fn src_fd(&self) -> RawFd { pub fn src_fd(&self) -> RawFd {
match self { match self {
IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(), IoMode::Fd { tgt_fd: _, src_fd } => src_fd.as_raw_fd(),
IoMode::File {..} => panic!("Attempted to obtain src_fd from file before opening"), IoMode::File { .. } => panic!("Attempted to obtain src_fd from file before opening"),
IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(), IoMode::Pipe { tgt_fd: _, pipe } => pipe.as_raw_fd(),
_ => panic!() _ => panic!(),
} }
} }
pub fn open_file(mut self) -> ShResult<Self> { pub fn open_file(mut self) -> ShResult<Self> {
if let IoMode::File { tgt_fd, path, mode } = self { if let IoMode::File { tgt_fd, path, mode } = self {
let file = get_redir_file(mode, path)?; let file = get_redir_file(mode, path)?;
self = IoMode::Fd { tgt_fd, src_fd: Arc::new(OwnedFd::from(file)) } self = IoMode::Fd {
} tgt_fd,
Ok(self) src_fd: Arc::new(OwnedFd::from(file)),
} }
pub fn get_pipes() -> (Self,Self) { }
let (rpipe,wpipe) = pipe().unwrap(); Ok(self)
( }
Self::Pipe { tgt_fd: STDIN_FILENO, pipe: rpipe.into() }, pub fn get_pipes() -> (Self, Self) {
Self::Pipe { tgt_fd: STDOUT_FILENO, pipe: wpipe.into() } let (rpipe, wpipe) = pipe().unwrap();
) (
} Self::Pipe {
tgt_fd: STDIN_FILENO,
pipe: rpipe.into(),
},
Self::Pipe {
tgt_fd: STDOUT_FILENO,
pipe: wpipe.into(),
},
)
}
} }
impl Read for IoMode { impl Read for IoMode {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let src_fd = self.src_fd(); let src_fd = self.src_fd();
Ok(read(src_fd, buf)?) Ok(read(src_fd, buf)?)
} }
} }
pub struct IoBuf<R: Read> { pub struct IoBuf<R: Read> {
buf: Vec<u8>, buf: Vec<u8>,
reader: R, reader: R,
} }
impl<R: Read> IoBuf<R> { impl<R: Read> IoBuf<R> {
pub fn new(reader: R) -> Self { pub fn new(reader: R) -> Self {
Self { Self {
buf: Vec::new(), buf: Vec::new(),
reader, reader,
} }
} }
/// Reads exactly `size` bytes (or fewer if EOF) into the buffer /// Reads exactly `size` bytes (or fewer if EOF) into the buffer
pub fn read_buffer(&mut self, size: usize) -> io::Result<()> { pub fn read_buffer(&mut self, size: usize) -> io::Result<()> {
let mut temp_buf = vec![0; size]; // Temporary buffer let mut temp_buf = vec![0; size]; // Temporary buffer
let bytes_read = self.reader.read(&mut temp_buf)?; let bytes_read = self.reader.read(&mut temp_buf)?;
self.buf.extend_from_slice(&temp_buf[..bytes_read]); // Append only what was read self.buf.extend_from_slice(&temp_buf[..bytes_read]); // Append only what was read
Ok(()) Ok(())
} }
/// Continuously reads until EOF /// Continuously reads until EOF
pub fn fill_buffer(&mut self) -> io::Result<()> { pub fn fill_buffer(&mut self) -> io::Result<()> {
let mut temp_buf = vec![0; 1024]; // Read in chunks let mut temp_buf = vec![0; 1024]; // Read in chunks
loop { loop {
flog!(DEBUG, "reading bytes"); flog!(DEBUG, "reading bytes");
let bytes_read = self.reader.read(&mut temp_buf)?; let bytes_read = self.reader.read(&mut temp_buf)?;
flog!(DEBUG, bytes_read); flog!(DEBUG, bytes_read);
if bytes_read == 0 { if bytes_read == 0 {
break; // EOF reached break; // EOF reached
} }
self.buf.extend_from_slice(&temp_buf[..bytes_read]); self.buf.extend_from_slice(&temp_buf[..bytes_read]);
} }
Ok(()) Ok(())
} }
/// Get current buffer contents as a string (if valid UTF-8) /// Get current buffer contents as a string (if valid UTF-8)
pub fn as_str(&self) -> ShResult<&str> { pub fn as_str(&self) -> ShResult<&str> {
std::str::from_utf8(&self.buf).map_err(|_| { std::str::from_utf8(&self.buf)
ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf") .map_err(|_| ShErr::simple(ShErrKind::InternalErr, "Invalid utf-8 in IoBuf"))
}) }
}
} }
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and `stderr` respectively /// A struct wrapping three fildescs representing `stdin`, `stdout`, and
#[derive(Debug,Clone)] /// `stderr` respectively
pub struct IoGroup(RawFd,RawFd,RawFd); #[derive(Debug, Clone)]
pub struct IoGroup(RawFd, RawFd, RawFd);
/// A single stack frame used with the IoStack /// A single stack frame used with the IoStack
/// Each stack frame represents the redirections of a single command /// Each stack frame represents the redirections of a single command
#[derive(Default,Clone,Debug)] #[derive(Default, Clone, Debug)]
pub struct IoFrame { pub struct IoFrame {
redirs: Vec<Redir>, redirs: Vec<Redir>,
saved_io: Option<IoGroup>, saved_io: Option<IoGroup>,
} }
impl<'e> IoFrame { impl<'e> IoFrame {
pub fn new() -> Self { pub fn new() -> Self {
Default::default() Default::default()
} }
pub fn from_redirs(redirs: Vec<Redir>) -> Self { pub fn from_redirs(redirs: Vec<Redir>) -> Self {
Self { redirs, saved_io: None } Self {
} redirs,
pub fn from_redir(redir: Redir) -> Self { saved_io: None,
Self { redirs: vec![redir], saved_io: None } }
} }
pub fn from_redir(redir: Redir) -> Self {
Self {
redirs: vec![redir],
saved_io: None,
}
}
/// Splits the frame into two frames /// Splits the frame into two frames
/// ///
/// One frame contains input redirections, the other contains output redirections /// One frame contains input redirections, the other contains output
/// This is used in shell structures to route redirections either *to* the condition, or *from* the body /// redirections This is used in shell structures to route redirections
/// The first field of the tuple contains input redirections (used for the condition) /// either *to* the condition, or *from* the body The first field of the
/// The second field contains output redirections (used for the body) /// tuple contains input redirections (used for the condition) The second
pub fn split_frame(self) -> (Self,Self) { /// field contains output redirections (used for the body)
let Self { redirs, saved_io: _ } = self; pub fn split_frame(self) -> (Self, Self) {
let (input_redirs,output_redirs) = redirs.split_by_channel(); let Self {
( redirs,
Self::from_redirs(input_redirs), saved_io: _,
Self::from_redirs(output_redirs) } = self;
) let (input_redirs, output_redirs) = redirs.split_by_channel();
} (
pub fn save(&'e mut self) { Self::from_redirs(input_redirs),
let saved_in = dup(STDIN_FILENO).unwrap(); Self::from_redirs(output_redirs),
let saved_out = dup(STDOUT_FILENO).unwrap(); )
let saved_err = dup(STDERR_FILENO).unwrap(); }
self.saved_io = Some(IoGroup(saved_in,saved_out,saved_err)); pub fn save(&'e mut self) {
} let saved_in = dup(STDIN_FILENO).unwrap();
pub fn redirect(&mut self) -> ShResult<()> { let saved_out = dup(STDOUT_FILENO).unwrap();
self.save(); let saved_err = dup(STDERR_FILENO).unwrap();
for redir in &mut self.redirs { self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
let io_mode = &mut redir.io_mode; }
flog!(DEBUG, io_mode); pub fn redirect(&mut self) -> ShResult<()> {
if let IoMode::File {..} = io_mode { self.save();
*io_mode = io_mode.clone().open_file()?; for redir in &mut self.redirs {
}; let io_mode = &mut redir.io_mode;
flog!(DEBUG, io_mode); flog!(DEBUG, io_mode);
let tgt_fd = io_mode.tgt_fd(); if let IoMode::File { .. } = io_mode {
let src_fd = io_mode.src_fd(); *io_mode = io_mode.clone().open_file()?;
dup2(src_fd, tgt_fd)?; };
} flog!(DEBUG, io_mode);
Ok(()) let tgt_fd = io_mode.tgt_fd();
} let src_fd = io_mode.src_fd();
pub fn restore(&mut self) -> ShResult<()> { dup2(src_fd, tgt_fd)?;
if let Some(saved) = self.saved_io.take() { }
dup2(saved.0, STDIN_FILENO)?; Ok(())
close(saved.0)?; }
dup2(saved.1, STDOUT_FILENO)?; pub fn restore(&mut self) -> ShResult<()> {
close(saved.1)?; if let Some(saved) = self.saved_io.take() {
dup2(saved.2, STDERR_FILENO)?; dup2(saved.0, STDIN_FILENO)?;
close(saved.2)?; close(saved.0)?;
} dup2(saved.1, STDOUT_FILENO)?;
Ok(()) close(saved.1)?;
} dup2(saved.2, STDERR_FILENO)?;
close(saved.2)?;
}
Ok(())
}
} }
impl Deref for IoFrame { impl Deref for IoFrame {
type Target = Vec<Redir>; type Target = Vec<Redir>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.redirs &self.redirs
} }
} }
impl DerefMut for IoFrame { impl DerefMut for IoFrame {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.redirs &mut self.redirs
} }
} }
/// A stack that maintains the current state of I/O for commands /// A stack that maintains the current state of I/O for commands
/// ///
/// This struct maintains the current state of I/O for the `Dispatcher` struct /// This struct maintains the current state of I/O for the `Dispatcher` struct
/// Each executed command requires an `IoFrame` in order to perform redirections. /// Each executed command requires an `IoFrame` in order to perform
/// As nodes are walked through by the `Dispatcher`, it pushes new frames in certain contexts, and pops frames in others. /// redirections. As nodes are walked through by the `Dispatcher`, it pushes new
/// Each command calls pop_frame() in order to get the current IoFrame in order to perform redirection /// frames in certain contexts, and pops frames in others. Each command calls
#[derive(Debug,Default)] /// pop_frame() in order to get the current IoFrame in order to perform
/// redirection
#[derive(Debug, Default)]
pub struct IoStack { pub struct IoStack {
stack: Vec<IoFrame>, stack: Vec<IoFrame>,
} }
impl IoStack { impl IoStack {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
stack: vec![IoFrame::new()], stack: vec![IoFrame::new()],
} }
} }
pub fn curr_frame(&self) -> &IoFrame { pub fn curr_frame(&self) -> &IoFrame {
self.stack.last().unwrap() self.stack.last().unwrap()
} }
pub fn curr_frame_mut(&mut self) -> &mut IoFrame { pub fn curr_frame_mut(&mut self) -> &mut IoFrame {
self.stack.last_mut().unwrap() self.stack.last_mut().unwrap()
} }
pub fn push_to_frame(&mut self, redir: Redir) { pub fn push_to_frame(&mut self, redir: Redir) {
self.curr_frame_mut().push(redir) self.curr_frame_mut().push(redir)
} }
pub fn append_to_frame(&mut self, mut other: Vec<Redir>) { pub fn append_to_frame(&mut self, mut other: Vec<Redir>) {
self.curr_frame_mut().append(&mut other) self.curr_frame_mut().append(&mut other)
} }
/// Pop the current stack frame /// Pop the current stack frame
/// This differs from using `pop()` because it always returns a stack frame /// This differs from using `pop()` because it always returns a stack frame
/// If `self.pop()` would empty the `IoStack`, it instead uses `std::mem::take()` to take the last frame /// If `self.pop()` would empty the `IoStack`, it instead uses
/// There will always be at least one frame in the `IoStack`. /// `std::mem::take()` to take the last frame There will always be at least
pub fn pop_frame(&mut self) -> IoFrame { /// one frame in the `IoStack`.
if self.stack.len() > 1 { pub fn pop_frame(&mut self) -> IoFrame {
self.pop().unwrap() if self.stack.len() > 1 {
} else { self.pop().unwrap()
std::mem::take(self.curr_frame_mut()) } else {
} std::mem::take(self.curr_frame_mut())
} }
/// Push a new stack frame. }
pub fn push_frame(&mut self, frame: IoFrame) { /// Push a new stack frame.
self.push(frame) pub fn push_frame(&mut self, frame: IoFrame) {
} self.push(frame)
/// Flatten the `IoStack` }
/// All of the current stack frames will be flattened into a single one /// Flatten the `IoStack`
/// Not sure what use this will serve, but my gut said this was worthy of writing /// All of the current stack frames will be flattened into a single one
pub fn flatten(&mut self) { /// Not sure what use this will serve, but my gut said this was worthy of
let mut flat_frame = IoFrame::new(); /// writing
while let Some(mut frame) = self.pop() { pub fn flatten(&mut self) {
flat_frame.append(&mut frame) let mut flat_frame = IoFrame::new();
} while let Some(mut frame) = self.pop() {
self.push(flat_frame); flat_frame.append(&mut frame)
} }
self.push(flat_frame);
}
} }
impl Deref for IoStack { impl Deref for IoStack {
type Target = Vec<IoFrame>; type Target = Vec<IoFrame>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.stack &self.stack
} }
} }
impl DerefMut for IoStack { impl DerefMut for IoStack {
fn deref_mut(&mut self) -> &mut Self::Target { fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.stack &mut self.stack
} }
} }
pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {
unsafe { BorrowedFd::borrow_raw(fd) } unsafe { BorrowedFd::borrow_raw(fd) }
} }

View File

@@ -0,0 +1 @@

View File

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

View File

@@ -1,350 +1,393 @@
use std::{env, fmt::{Write,Display}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}}; use std::{
env,
fmt::{Display, Write},
fs::{self, OpenOptions},
io::Write as IoWrite,
path::{Path, PathBuf},
str::FromStr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::prelude::*; use crate::prelude::*;
use super::vicmd::Direction; // surprisingly useful use super::vicmd::Direction; // surprisingly useful
#[derive(Default,Clone,Copy,Debug)] #[derive(Default, Clone, Copy, Debug)]
pub enum SearchKind { pub enum SearchKind {
Fuzzy, Fuzzy,
#[default] #[default]
Prefix Prefix,
} }
#[derive(Default,Clone,Debug)] #[derive(Default, Clone, Debug)]
pub struct SearchConstraint { pub struct SearchConstraint {
kind: SearchKind, kind: SearchKind,
term: String, term: String,
} }
impl SearchConstraint { impl SearchConstraint {
pub fn new(kind: SearchKind, term: String) -> Self { pub fn new(kind: SearchKind, term: String) -> Self {
Self { kind, term } Self { kind, term }
} }
} }
#[derive(Debug,Clone)] #[derive(Debug, Clone)]
pub struct HistEntry { pub struct HistEntry {
id: u32, id: u32,
timestamp: SystemTime, timestamp: SystemTime,
command: String, command: String,
new: bool new: bool,
} }
impl HistEntry { impl HistEntry {
pub fn id(&self) -> u32 { pub fn id(&self) -> u32 {
self.id self.id
} }
pub fn timestamp(&self) -> &SystemTime { pub fn timestamp(&self) -> &SystemTime {
&self.timestamp &self.timestamp
} }
pub fn command(&self) -> &str { pub fn command(&self) -> &str {
&self.command &self.command
} }
fn with_escaped_newlines(&self) -> String { fn with_escaped_newlines(&self) -> String {
let mut escaped = String::new(); let mut escaped = String::new();
let mut chars = self.command.chars(); let mut chars = self.command.chars();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' => { '\\' => {
escaped.push(ch); escaped.push(ch);
if let Some(ch) = chars.next() { if let Some(ch) = chars.next() {
escaped.push(ch) escaped.push(ch)
} }
} }
'\n' => { '\n' => {
escaped.push_str("\\\n"); escaped.push_str("\\\n");
} }
_ => escaped.push(ch), _ => escaped.push(ch),
} }
} }
escaped escaped
} }
pub fn is_new(&self) -> bool { pub fn is_new(&self) -> bool {
self.new self.new
} }
} }
impl FromStr for HistEntry { impl FromStr for HistEntry {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = Err( let err = Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on history entry '{s}'"), notes: vec![] } kind: ShErrKind::HistoryReadErr,
); msg: format!("Bad formatting on history entry '{s}'"),
notes: vec![],
});
//: 248972349;148;echo foo; echo bar //: 248972349;148;echo foo; echo bar
let Some(cleaned) = s.strip_prefix(": ") else { return err }; let Some(cleaned) = s.strip_prefix(": ") else {
//248972349;148;echo foo; echo bar return err;
let Some((timestamp,id_and_command)) = cleaned.split_once(';') else { return err }; };
//("248972349","148;echo foo; echo bar") //248972349;148;echo foo; echo bar
let Some((id,command)) = id_and_command.split_once(';') else { return err }; let Some((timestamp, id_and_command)) = cleaned.split_once(';') else {
//("148","echo foo; echo bar") return err;
let Ok(ts_seconds) = timestamp.parse::<u64>() else { return err }; };
let Ok(id) = id.parse::<u32>() else { return err }; //("248972349","148;echo foo; echo bar")
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds); let Some((id, command)) = id_and_command.split_once(';') else {
let command = command.to_string(); return err;
Ok(Self { id, timestamp, command, new: false }) };
} //("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 timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
let command = command.to_string();
Ok(Self {
id,
timestamp,
command,
new: false,
})
}
} }
impl Display for HistEntry { impl Display for HistEntry {
/// Similar to zsh's history format, but not entirely /// Similar to zsh's history format, but not entirely
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let command = self.with_escaped_newlines(); let command = self.with_escaped_newlines();
let HistEntry { id, timestamp, command: _, new: _ } = self; let HistEntry {
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs(); id,
writeln!(f, ": {timestamp};{id};{command}") timestamp,
} command: _,
new: _,
} = self;
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
writeln!(f, ": {timestamp};{id};{command}")
}
} }
pub struct HistEntries(Vec<HistEntry>); pub struct HistEntries(Vec<HistEntry>);
impl FromStr for HistEntries { impl FromStr for HistEntries {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut entries = vec![]; let mut entries = vec![];
let mut lines = s.lines().enumerate().peekable(); let mut lines = s.lines().enumerate().peekable();
let mut cur_line = String::new(); let mut cur_line = String::new();
while let Some((i,line)) = lines.next() { while let Some((i, line)) = lines.next() {
if !line.starts_with(": ") { if !line.starts_with(": ") {
return Err( return Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } kind: ShErrKind::HistoryReadErr,
) msg: format!("Bad formatting on line {i}"),
} notes: vec![],
let mut chars = line.chars().peekable(); });
let mut feeding_lines = true; }
while feeding_lines { let mut chars = line.chars().peekable();
feeding_lines = false; let mut feeding_lines = true;
while let Some(ch) = chars.next() { while feeding_lines {
match ch { feeding_lines = false;
'\\' => { while let Some(ch) = chars.next() {
if let Some(esc_ch) = chars.next() { match ch {
cur_line.push(esc_ch); '\\' => {
} else { if let Some(esc_ch) = chars.next() {
cur_line.push('\n'); cur_line.push(esc_ch);
feeding_lines = true; } else {
} cur_line.push('\n');
} feeding_lines = true;
'\n' => { }
break }
} '\n' => break,
_ => { _ => {
cur_line.push(ch); cur_line.push(ch);
} }
} }
} }
if feeding_lines { if feeding_lines {
let Some((_,line)) = lines.next() else { let Some((_, line)) = lines.next() else {
return Err( return Err(ShErr::Simple {
ShErr::Simple { kind: ShErrKind::HistoryReadErr, msg: format!("Bad formatting on line {i}"), notes: vec![] } kind: ShErrKind::HistoryReadErr,
) msg: format!("Bad formatting on line {i}"),
}; notes: vec![],
chars = line.chars().peekable(); });
} };
} chars = line.chars().peekable();
let entry = cur_line.parse::<HistEntry>()?; }
entries.push(entry); }
cur_line.clear(); let entry = cur_line.parse::<HistEntry>()?;
} entries.push(entry);
cur_line.clear();
}
Ok(Self(entries))
Ok(Self(entries)) }
}
} }
fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> { fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
if !path.exists() { if !path.exists() {
fs::File::create(path)?; fs::File::create(path)?;
} }
let raw = fs::read_to_string(path)?; let raw = fs::read_to_string(path)?;
Ok(raw.parse::<HistEntries>()?.0) Ok(raw.parse::<HistEntries>()?.0)
} }
pub struct History { pub struct History {
path: PathBuf, path: PathBuf,
entries: Vec<HistEntry>, entries: Vec<HistEntry>,
search_mask: Vec<HistEntry>, search_mask: Vec<HistEntry>,
cursor: usize, cursor: usize,
search_direction: Direction, search_direction: Direction,
ignore_dups: bool, ignore_dups: bool,
max_size: Option<u32>, max_size: Option<u32>,
} }
impl History { impl History {
pub fn new() -> ShResult<Self> { pub fn new() -> ShResult<Self> {
let path = PathBuf::from(env::var("FERNHIST").unwrap_or({ let path = PathBuf::from(env::var("FERNHIST").unwrap_or({
let home = env::var("HOME").unwrap(); let home = env::var("HOME").unwrap();
format!("{home}/.fern_history") format!("{home}/.fern_history")
})); }));
let mut entries = read_hist_file(&path)?; let mut entries = read_hist_file(&path)?;
{ {
let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0); let id = entries.last().map(|ent| ent.id + 1).unwrap_or(0);
let timestamp = SystemTime::now(); let timestamp = SystemTime::now();
let command = "".into(); let command = "".into();
entries.push(HistEntry { id, timestamp, command, new: true }) entries.push(HistEntry {
} id,
let search_mask = entries.clone(); timestamp,
let cursor = entries.len() - 1; command,
let mut new = Self { new: true,
path, })
entries, }
search_mask, let search_mask = entries.clone();
cursor, let cursor = entries.len() - 1;
search_direction: Direction::Backward, let mut new = Self {
ignore_dups: true, path,
max_size: None, entries,
}; search_mask,
new.push_empty_entry(); // Current pending command cursor,
Ok(new) search_direction: Direction::Backward,
} ignore_dups: true,
max_size: None,
};
new.push_empty_entry(); // Current pending command
Ok(new)
}
pub fn entries(&self) -> &[HistEntry] { pub fn entries(&self) -> &[HistEntry] {
&self.entries &self.entries
} }
pub fn masked_entries(&self) -> &[HistEntry] { pub fn masked_entries(&self) -> &[HistEntry] {
&self.search_mask &self.search_mask
} }
pub fn push_empty_entry(&mut self) { pub fn push_empty_entry(&mut self) {}
}
pub fn cursor_entry(&self) -> Option<&HistEntry> { pub fn cursor_entry(&self) -> Option<&HistEntry> {
self.search_mask.get(self.cursor) self.search_mask.get(self.cursor)
} }
pub fn update_pending_cmd(&mut self, command: &str) { pub fn update_pending_cmd(&mut self, command: &str) {
let Some(ent) = self.last_mut() else { let Some(ent) = self.last_mut() else { return };
return let cmd = command.to_string();
}; let constraint = SearchConstraint {
let cmd = command.to_string(); kind: SearchKind::Prefix,
let constraint = SearchConstraint { kind: SearchKind::Prefix, term: cmd.clone() }; term: cmd.clone(),
};
ent.command = cmd;
self.constrain_entries(constraint);
}
ent.command = cmd; pub fn last_mut(&mut self) -> Option<&mut HistEntry> {
self.constrain_entries(constraint); self.entries.last_mut()
} }
pub fn last_mut(&mut self) -> Option<&mut HistEntry> { pub fn get_new_id(&self) -> u32 {
self.entries.last_mut() let Some(ent) = self.entries.last() else {
} return 0;
};
ent.id + 1
}
pub fn get_new_id(&self) -> u32 { pub fn ignore_dups(&mut self, yn: bool) {
let Some(ent) = self.entries.last() else { self.ignore_dups = yn
return 0 }
};
ent.id + 1
}
pub fn ignore_dups(&mut self, yn: bool) { pub fn max_hist_size(&mut self, size: Option<u32>) {
self.ignore_dups = yn self.max_size = size
} }
pub fn max_hist_size(&mut self, size: Option<u32>) { pub fn constrain_entries(&mut self, constraint: SearchConstraint) {
self.max_size = size flog!(DEBUG, constraint);
} let SearchConstraint { kind, term } = constraint;
match kind {
SearchKind::Prefix => {
if term.is_empty() {
self.search_mask = self.entries.clone();
} else {
let filtered = self
.entries
.clone()
.into_iter()
.filter(|ent| ent.command().starts_with(&term));
pub fn constrain_entries(&mut self, constraint: SearchConstraint) { self.search_mask = filtered.collect();
flog!(DEBUG,constraint); }
let SearchConstraint { kind, term } = constraint; self.cursor = self.search_mask.len().saturating_sub(1);
match kind { }
SearchKind::Prefix => { SearchKind::Fuzzy => todo!(),
if term.is_empty() { }
self.search_mask = self.entries.clone(); }
} else {
let filtered = self.entries
.clone()
.into_iter()
.filter(|ent| ent.command().starts_with(&term));
self.search_mask = filtered.collect(); pub fn hint_entry(&self) -> Option<&HistEntry> {
} let second_to_last = self.search_mask.len().checked_sub(2)?;
self.cursor = self.search_mask.len().saturating_sub(1); self.search_mask.get(second_to_last)
} }
SearchKind::Fuzzy => todo!(),
}
}
pub fn hint_entry(&self) -> Option<&HistEntry> { pub fn get_hint(&self) -> Option<String> {
let second_to_last = self.search_mask.len().checked_sub(2)?; if self
self.search_mask.get(second_to_last) .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())
} else {
None
}
}
pub fn get_hint(&self) -> Option<String> { pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
if self.cursor_entry().is_some_and(|ent| ent.is_new() && !ent.command().is_empty()) { let new_idx = self
let entry = self.hint_entry()?; .cursor
let prefix = self.cursor_entry()?.command(); .saturating_add_signed(offset)
Some(entry.command().to_string()) .clamp(0, self.search_mask.len().saturating_sub(1));
} else { let ent = self.search_mask.get(new_idx)?;
None
}
}
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { self.cursor = new_idx;
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)?;
self.cursor = new_idx; Some(ent)
}
Some(ent) pub fn push(&mut self, command: String) {
} let timestamp = SystemTime::now();
let id = self.get_new_id();
if self.ignore_dups && self.is_dup(&command) {
return;
}
self.entries.push(HistEntry {
id,
timestamp,
command,
new: true,
});
}
pub fn push(&mut self, command: String) { pub fn is_dup(&self, other: &str) -> bool {
let timestamp = SystemTime::now(); let Some(ent) = self.entries.last() else {
let id = self.get_new_id(); return false;
if self.ignore_dups && self.is_dup(&command) { };
return let ent_cmd = &ent.command;
} ent_cmd == other
self.entries.push(HistEntry { id, timestamp, command, new: true }); }
}
pub fn is_dup(&self, other: &str) -> bool { pub fn save(&mut self) -> ShResult<()> {
let Some(ent) = self.entries.last() else { let mut file = OpenOptions::new()
return false .create(true)
}; .append(true)
let ent_cmd = &ent.command; .open(&self.path)?;
ent_cmd == other
}
pub fn save(&mut self) -> ShResult<()> { let last_file_entry = self
let mut file = OpenOptions::new() .entries
.create(true) .iter()
.append(true) .filter(|ent| !ent.new)
.open(&self.path)?; .next_back()
.map(|ent| ent.command.clone())
.unwrap_or_default();
let last_file_entry = self.entries let entries = self.entries.iter_mut().filter(|ent| {
.iter() ent.new
.filter(|ent| !ent.new) && !ent.command.is_empty()
.next_back() && if self.ignore_dups {
.map(|ent| ent.command.clone()) ent.command() != last_file_entry
.unwrap_or_default(); } else {
true
}
});
let entries = self.entries let mut data = String::new();
.iter_mut() for ent in entries {
.filter(|ent| { ent.new = false;
ent.new && write!(data, "{ent}").unwrap();
!ent.command.is_empty() && }
if self.ignore_dups {
ent.command() != last_file_entry
} else {
true
}
});
let mut data = String::new(); file.write_all(data.as_bytes())?;
for ent in entries {
ent.new = false;
write!(data, "{ent}").unwrap();
}
file.write_all(data.as_bytes())?; Ok(())
}
Ok(())
}
} }

View File

@@ -3,140 +3,137 @@ use unicode_segmentation::UnicodeSegmentation;
// Credit to Rustyline for the design ideas in this module // Credit to Rustyline for the design ideas in this module
// https://github.com/kkawakam/rustyline // https://github.com/kkawakam/rustyline
#[derive(Clone,PartialEq,Eq,Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct KeyEvent(pub KeyCode, pub ModKeys); pub struct KeyEvent(pub KeyCode, pub ModKeys);
impl KeyEvent { impl KeyEvent {
pub fn new(ch: &str, mut mods: ModKeys) -> Self { pub fn new(ch: &str, mut mods: ModKeys) -> Self {
use {KeyCode as K, KeyEvent as E, ModKeys as M}; use {KeyCode as K, KeyEvent as E, ModKeys as M};
let mut graphemes = ch.graphemes(true); let mut graphemes = ch.graphemes(true);
let first = match graphemes.next() { let first = match graphemes.next() {
Some(g) => g, Some(g) => g,
None => return E(K::Null, mods), None => return E(K::Null, mods),
}; };
// If more than one grapheme, it's not a single key event // If more than one grapheme, it's not a single key event
if graphemes.next().is_some() { if graphemes.next().is_some() {
return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired return E(K::Null, mods); // Or panic, or wrap in Grapheme if desired
} }
let mut chars = first.chars(); let mut chars = first.chars();
let single_char = chars.next(); let single_char = chars.next();
let is_single_char = chars.next().is_none(); let is_single_char = chars.next().is_none();
match single_char { match single_char {
Some(c) if is_single_char && c.is_control() => { Some(c) if is_single_char && c.is_control() => match c {
match c { '\x00' => E(K::Char('@'), mods | M::CTRL),
'\x00' => E(K::Char('@'), mods | M::CTRL), '\x01' => E(K::Char('A'), mods | M::CTRL),
'\x01' => E(K::Char('A'), mods | M::CTRL), '\x02' => E(K::Char('B'), mods | M::CTRL),
'\x02' => E(K::Char('B'), mods | M::CTRL), '\x03' => E(K::Char('C'), mods | M::CTRL),
'\x03' => E(K::Char('C'), mods | M::CTRL), '\x04' => E(K::Char('D'), mods | M::CTRL),
'\x04' => E(K::Char('D'), mods | M::CTRL), '\x05' => E(K::Char('E'), mods | M::CTRL),
'\x05' => E(K::Char('E'), mods | M::CTRL), '\x06' => E(K::Char('F'), mods | M::CTRL),
'\x06' => E(K::Char('F'), mods | M::CTRL), '\x07' => E(K::Char('G'), mods | M::CTRL),
'\x07' => E(K::Char('G'), mods | M::CTRL), '\x08' => E(K::Backspace, mods),
'\x08' => E(K::Backspace, mods), '\x09' => {
'\x09' => { if mods.contains(M::SHIFT) {
if mods.contains(M::SHIFT) { mods.remove(M::SHIFT);
mods.remove(M::SHIFT); E(K::BackTab, mods)
E(K::BackTab, mods) } else {
} else { E(K::Tab, mods)
E(K::Tab, mods) }
} }
} '\x0a' => E(K::Char('J'), mods | M::CTRL),
'\x0a' => E(K::Char('J'), mods | M::CTRL), '\x0b' => E(K::Char('K'), mods | M::CTRL),
'\x0b' => E(K::Char('K'), mods | M::CTRL), '\x0c' => E(K::Char('L'), mods | M::CTRL),
'\x0c' => E(K::Char('L'), mods | M::CTRL), '\x0d' => E(K::Enter, mods),
'\x0d' => E(K::Enter, mods), '\x0e' => E(K::Char('N'), mods | M::CTRL),
'\x0e' => E(K::Char('N'), mods | M::CTRL), '\x0f' => E(K::Char('O'), mods | M::CTRL),
'\x0f' => E(K::Char('O'), mods | M::CTRL), '\x10' => E(K::Char('P'), mods | M::CTRL),
'\x10' => E(K::Char('P'), mods | M::CTRL), '\x11' => E(K::Char('Q'), mods | M::CTRL),
'\x11' => E(K::Char('Q'), mods | M::CTRL), '\x12' => E(K::Char('R'), mods | M::CTRL),
'\x12' => E(K::Char('R'), mods | M::CTRL), '\x13' => E(K::Char('S'), mods | M::CTRL),
'\x13' => E(K::Char('S'), mods | M::CTRL), '\x14' => E(K::Char('T'), mods | M::CTRL),
'\x14' => E(K::Char('T'), mods | M::CTRL), '\x15' => E(K::Char('U'), mods | M::CTRL),
'\x15' => E(K::Char('U'), mods | M::CTRL), '\x16' => E(K::Char('V'), mods | M::CTRL),
'\x16' => E(K::Char('V'), mods | M::CTRL), '\x17' => E(K::Char('W'), mods | M::CTRL),
'\x17' => E(K::Char('W'), mods | M::CTRL), '\x18' => E(K::Char('X'), mods | M::CTRL),
'\x18' => E(K::Char('X'), mods | M::CTRL), '\x19' => E(K::Char('Y'), mods | M::CTRL),
'\x19' => E(K::Char('Y'), mods | M::CTRL), '\x1a' => E(K::Char('Z'), mods | M::CTRL),
'\x1a' => E(K::Char('Z'), mods | M::CTRL), '\x1b' => E(K::Esc, mods),
'\x1b' => E(K::Esc, mods), '\x1c' => E(K::Char('\\'), mods | M::CTRL),
'\x1c' => E(K::Char('\\'), mods | M::CTRL), '\x1d' => E(K::Char(']'), mods | M::CTRL),
'\x1d' => E(K::Char(']'), mods | M::CTRL), '\x1e' => E(K::Char('^'), mods | M::CTRL),
'\x1e' => E(K::Char('^'), mods | M::CTRL), '\x1f' => E(K::Char('_'), mods | M::CTRL),
'\x1f' => E(K::Char('_'), mods | M::CTRL), '\x7f' => E(K::Backspace, mods),
'\x7f' => E(K::Backspace, mods), '\u{9b}' => E(K::Esc, mods | M::SHIFT),
'\u{9b}' => E(K::Esc, mods | M::SHIFT), _ => E(K::Null, mods),
_ => E(K::Null, mods), },
} Some(c) if is_single_char => {
} if !mods.is_empty() {
Some(c) if is_single_char => { mods.remove(M::SHIFT);
if !mods.is_empty() { }
mods.remove(M::SHIFT); E(K::Char(c), mods)
} }
E(K::Char(c), mods) _ => {
} // multi-char grapheme (emoji, accented, etc)
_ => { if !mods.is_empty() {
// multi-char grapheme (emoji, accented, etc) mods.remove(M::SHIFT);
if !mods.is_empty() { }
mods.remove(M::SHIFT); E(K::Grapheme(Arc::from(first)), mods)
} }
E(K::Grapheme(Arc::from(first)), mods) }
} }
}
}
} }
#[derive(Clone,PartialEq,Eq,Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum KeyCode { pub enum KeyCode {
UnknownEscSeq, UnknownEscSeq,
Backspace, Backspace,
BackTab, BackTab,
BracketedPasteStart, BracketedPasteStart,
BracketedPasteEnd, BracketedPasteEnd,
Char(char), Char(char),
Grapheme(Arc<str>), Grapheme(Arc<str>),
Delete, Delete,
Down, Down,
End, End,
Enter, Enter,
Esc, Esc,
F(u8), F(u8),
Home, Home,
Insert, Insert,
Left, Left,
Null, Null,
PageDown, PageDown,
PageUp, PageUp,
Right, Right,
Tab, Tab,
Up, Up,
} }
bitflags::bitflags! { bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct ModKeys: u8 { pub struct ModKeys: u8 {
/// Control modifier /// Control modifier
const CTRL = 1<<3; const CTRL = 1<<3;
/// Escape or Alt modifier /// Escape or Alt modifier
const ALT = 1<<2; const ALT = 1<<2;
/// Shift modifier /// Shift modifier
const SHIFT = 1<<1; const SHIFT = 1<<1;
/// No modifier /// No modifier
const NONE = 0; const NONE = 0;
/// Ctrl + Shift /// Ctrl + Shift
const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits(); const CTRL_SHIFT = Self::CTRL.bits() | Self::SHIFT.bits();
/// Alt + Shift /// Alt + Shift
const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits(); const ALT_SHIFT = Self::ALT.bits() | Self::SHIFT.bits();
/// Ctrl + Alt /// Ctrl + Alt
const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits(); const CTRL_ALT = Self::CTRL.bits() | Self::ALT.bits();
/// Ctrl + Alt + Shift /// Ctrl + Alt + Shift
const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits(); const CTRL_ALT_SHIFT = Self::CTRL.bits() | Self::ALT.bits() | Self::SHIFT.bits();
} }
} }

View File

@@ -0,0 +1 @@

File diff suppressed because it is too large Load Diff

View File

@@ -6,396 +6,377 @@ use term::{get_win_size, raw_mode, KeyReader, Layout, LineWriter, TermReader, Te
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd}; use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual}; use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
use crate::libsh::{error::{ShErr, ShErrKind, ShResult}, sys::sh_quit, term::{Style, Styled}}; use crate::libsh::{
error::{ShErr, ShErrKind, ShResult},
sys::sh_quit,
term::{Style, Styled},
};
use crate::prelude::*; use crate::prelude::*;
pub mod term;
pub mod linebuf;
pub mod layout;
pub mod keys;
pub mod vicmd;
pub mod register;
pub mod vimode;
pub mod history; pub mod history;
pub mod keys;
pub mod layout;
pub mod linebuf;
pub mod register;
pub mod term;
pub mod vicmd;
pub mod vimode;
// Very useful for testing // Very useful for testing
const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."; const LOREM_IPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra.";
pub trait Readline { pub trait Readline {
fn readline(&mut self) -> ShResult<String>; fn readline(&mut self) -> ShResult<String>;
} }
pub struct FernVi { pub struct FernVi {
pub reader: Box<dyn KeyReader>, pub reader: Box<dyn KeyReader>,
pub writer: Box<dyn LineWriter>, pub writer: Box<dyn LineWriter>,
pub prompt: String, pub prompt: String,
pub mode: Box<dyn ViMode>, pub mode: Box<dyn ViMode>,
pub old_layout: Option<Layout>, pub old_layout: Option<Layout>,
pub repeat_action: Option<CmdReplay>, pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>, pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf, pub editor: LineBuf,
pub history: History pub history: History,
} }
impl Readline for FernVi { impl Readline for FernVi {
fn readline(&mut self) -> ShResult<String> { fn readline(&mut self) -> ShResult<String> {
let raw_mode_guard = raw_mode(); // Restores termios state on drop let raw_mode_guard = raw_mode(); // Restores termios state on drop
loop { loop {
raw_mode_guard.disable_for(|| self.print_line())?; raw_mode_guard.disable_for(|| self.print_line())?;
let Some(key) = self.reader.read_key() else { let Some(key) = self.reader.read_key() else {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")) return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF"));
}; };
flog!(DEBUG, key); flog!(DEBUG, key);
if self.should_accept_hint(&key) { if self.should_accept_hint(&key) {
self.editor.accept_hint(); self.editor.accept_hint();
self.history.update_pending_cmd(self.editor.as_str()); self.history.update_pending_cmd(self.editor.as_str());
self.print_line()?; self.print_line()?;
continue continue;
} }
let Some(mut cmd) = self.mode.handle_key(key) else { let Some(mut cmd) = self.mode.handle_key(key) else {
flog!(DEBUG, "got none??"); flog!(DEBUG, "got none??");
continue continue;
}; };
flog!(DEBUG,cmd); flog!(DEBUG, cmd);
cmd.alter_line_motion_if_no_verb(); cmd.alter_line_motion_if_no_verb();
if self.should_grab_history(&cmd) { if self.should_grab_history(&cmd) {
self.scroll_history(cmd); self.scroll_history(cmd);
self.print_line()?; self.print_line()?;
continue continue;
} }
if cmd.should_submit() { if cmd.should_submit() {
raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?;
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
return Ok(self.editor.take_buf()) return Ok(self.editor.take_buf());
} }
if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) {
if self.editor.buffer.is_empty() { if self.editor.buffer.is_empty() {
std::mem::drop(raw_mode_guard); std::mem::drop(raw_mode_guard);
sh_quit(0); sh_quit(0);
} else { } else {
self.editor.buffer.clear(); self.editor.buffer.clear();
continue continue;
} }
} }
flog!(DEBUG,cmd); flog!(DEBUG, cmd);
let before = self.editor.buffer.clone(); let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?; self.exec_cmd(cmd)?;
let after = self.editor.as_str(); let after = self.editor.as_str();
if before != after { if before != after {
self.history.update_pending_cmd(self.editor.as_str()); self.history.update_pending_cmd(self.editor.as_str());
} }
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
} }
} }
} }
impl FernVi { impl FernVi {
pub fn new(prompt: Option<String>) -> ShResult<Self> { pub fn new(prompt: Option<String>) -> ShResult<Self> {
Ok(Self { Ok(Self {
reader: Box::new(TermReader::new()), reader: Box::new(TermReader::new()),
writer: Box::new(TermWriter::new(STDOUT_FILENO)), writer: Box::new(TermWriter::new(STDOUT_FILENO)),
prompt: prompt.unwrap_or("$ ".styled(Style::Green)), prompt: prompt.unwrap_or("$ ".styled(Style::Green)),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
old_layout: None, old_layout: None,
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
editor: LineBuf::new().with_initial(LOREM_IPSUM, 0), editor: LineBuf::new().with_initial(LOREM_IPSUM, 0),
history: History::new()? history: History::new()?,
}) })
} }
pub fn get_layout(&mut self) -> Layout { pub fn get_layout(&mut self) -> Layout {
let line = self.editor.to_string(); let line = self.editor.to_string();
flog!(DEBUG,line); flog!(DEBUG, line);
let to_cursor = self.editor.slice_to_cursor().unwrap_or_default(); let to_cursor = self.editor.slice_to_cursor().unwrap_or_default();
let (cols,_) = get_win_size(STDIN_FILENO); let (cols, _) = get_win_size(STDIN_FILENO);
Layout::from_parts( Layout::from_parts(/* tab_stop: */ 8, cols, &self.prompt, to_cursor, &line)
/*tab_stop:*/ 8, }
cols, pub fn scroll_history(&mut self, cmd: ViCmd) {
&self.prompt, flog!(DEBUG, "scrolling");
to_cursor, /*
&line if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) {
) let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string());
} self.history.constrain_entries(constraint);
pub fn scroll_history(&mut self, cmd: ViCmd) { }
flog!(DEBUG,"scrolling"); */
/* let count = &cmd.motion().unwrap().0;
if self.history.cursor_entry().is_some_and(|ent| ent.is_new()) { let motion = &cmd.motion().unwrap().1;
let constraint = SearchConstraint::new(SearchKind::Prefix, self.editor.to_string()); flog!(DEBUG, count, motion);
self.history.constrain_entries(constraint); flog!(DEBUG, self.history.masked_entries());
} let entry = match motion {
*/ Motion::LineUpCharwise => {
let count = &cmd.motion().unwrap().0; let Some(hist_entry) = self.history.scroll(-(*count as isize)) else {
let motion = &cmd.motion().unwrap().1; return;
flog!(DEBUG,count,motion); };
flog!(DEBUG,self.history.masked_entries()); flog!(DEBUG, "found entry");
let entry = match motion { flog!(DEBUG, hist_entry.command());
Motion::LineUpCharwise => { hist_entry
let Some(hist_entry) = self.history.scroll(-(*count as isize)) else { }
return Motion::LineDownCharwise => {
}; let Some(hist_entry) = self.history.scroll(*count as isize) else {
flog!(DEBUG,"found entry"); return;
flog!(DEBUG,hist_entry.command()); };
hist_entry flog!(DEBUG, "found entry");
} flog!(DEBUG, hist_entry.command());
Motion::LineDownCharwise => { hist_entry
let Some(hist_entry) = self.history.scroll(*count as isize) else { }
return _ => unreachable!(),
}; };
flog!(DEBUG,"found entry"); let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col());
flog!(DEBUG,hist_entry.command()); let mut buf = LineBuf::new().with_initial(entry.command(), 0);
hist_entry let line_end = buf.end_of_line();
} if let Some(dest) = self.mode.hist_scroll_start_pos() {
_ => unreachable!() match dest {
}; To::Start => { /* Already at 0 */ }
let col = self.editor.saved_col.unwrap_or(self.editor.cursor_col()); To::End => {
let mut buf = LineBuf::new().with_initial(entry.command(),0); // History entries cannot be empty
let line_end = buf.end_of_line(); // So this subtraction is safe (maybe)
if let Some(dest) = self.mode.hist_scroll_start_pos() { buf.cursor.add(line_end);
match dest { }
To::Start => { }
/* Already at 0 */ } else {
} let target = (col).min(line_end);
To::End => { buf.cursor.add(target);
// History entries cannot be empty }
// So this subtraction is safe (maybe)
buf.cursor.add(line_end);
}
}
} else {
let target = (col).min(line_end);
buf.cursor.add(target);
}
self.editor = buf self.editor = buf
} }
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
flog!(DEBUG,self.editor.cursor_at_max()); flog!(DEBUG, self.editor.cursor_at_max());
flog!(DEBUG,self.editor.cursor); flog!(DEBUG, self.editor.cursor);
if self.editor.cursor_at_max() && self.editor.has_hint() { if self.editor.cursor_at_max() && self.editor.has_hint() {
match self.mode.report_mode() { match self.mode.report_mode() {
ModeReport::Replace | ModeReport::Replace | ModeReport::Insert => {
ModeReport::Insert => { matches!(event, KeyEvent(KeyCode::Right, ModKeys::NONE))
matches!( }
event, ModeReport::Visual | ModeReport::Normal => {
KeyEvent(KeyCode::Right, ModKeys::NONE) 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 => { _ => unimplemented!(),
matches!( }
event, } else {
KeyEvent(KeyCode::Right, ModKeys::NONE) false
) || }
( }
self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() &&
matches!(
event,
KeyEvent(KeyCode::Char('l'), ModKeys::NONE)
)
)
}
_ => unimplemented!()
}
} else {
false
}
}
pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool { pub fn should_grab_history(&mut self, cmd: &ViCmd) -> bool {
cmd.verb().is_none() && cmd.verb().is_none()
( && (cmd
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise))) && .motion()
self.editor.start_of_line() == 0 .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineUpCharwise)))
) || && self.editor.start_of_line() == 0)
( || (cmd
cmd.motion().is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise))) && .motion()
self.editor.end_of_line() == self.editor.cursor_max() && .is_some_and(|m| matches!(m, MotionCmd(_, Motion::LineDownCharwise)))
!self.history.cursor_entry().is_some_and(|ent| ent.is_new()) && self.editor.end_of_line() == self.editor.cursor_max()
) && !self.history.cursor_entry().is_some_and(|ent| ent.is_new()))
} }
pub fn print_line(&mut self) -> ShResult<()> { pub fn print_line(&mut self) -> ShResult<()> {
let new_layout = self.get_layout(); let new_layout = self.get_layout();
if let Some(layout) = self.old_layout.as_ref() { if let Some(layout) = self.old_layout.as_ref() {
self.writer.clear_rows(layout)?; self.writer.clear_rows(layout)?;
} }
self.writer.redraw( self
&self.prompt, .writer
&self.editor, .redraw(&self.prompt, &self.editor, &new_layout)?;
&new_layout
)?;
self.writer.flush_write(&self.mode.cursor_style())?; self.writer.flush_write(&self.mode.cursor_style())?;
self.old_layout = Some(new_layout); self.old_layout = Some(new_layout);
Ok(()) Ok(())
} }
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
let mut selecting = false; let mut selecting = false;
let mut is_insert_mode = false; let mut is_insert_mode = false;
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
let count = cmd.verb_count(); let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 { let mut mode: Box<dyn ViMode> = match cmd.verb().unwrap().1 {
Verb::Change | Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
Verb::InsertModeLineBreak(_) | is_insert_mode = true;
Verb::InsertMode => { Box::new(ViInsert::new().with_count(count as u16))
is_insert_mode = true; }
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::NormalMode => { Verb::NormalMode => Box::new(ViNormal::new()),
Box::new(ViNormal::new())
}
Verb::ReplaceMode => Box::new(ViReplace::new()), Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::VisualModeSelectLast => { Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual { if self.mode.report_mode() != ModeReport::Visual {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); self
} .editor
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new()); .start_selecting(SelectMode::Char(SelectAnchor::End));
std::mem::swap(&mut mode, &mut self.mode); }
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); 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 => { Verb::VisualMode => {
selecting = true; selecting = true;
Box::new(ViVisual::new()) Box::new(ViVisual::new())
} }
_ => unreachable!() _ => unreachable!(),
}; };
std::mem::swap(&mut mode, &mut self.mode); std::mem::swap(&mut mode, &mut self.mode);
if mode.is_repeatable() { if mode.is_repeatable() {
self.repeat_action = mode.as_replay(); self.repeat_action = mode.as_replay();
} }
self.editor.exec_cmd(cmd)?; self.editor.exec_cmd(cmd)?;
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
if selecting { if selecting {
self.editor.start_selecting(SelectMode::Char(SelectAnchor::End)); self
} else { .editor
self.editor.stop_selecting(); .start_selecting(SelectMode::Char(SelectAnchor::End));
} } else {
if is_insert_mode { self.editor.stop_selecting();
self.editor.mark_insert_mode_start_pos(); }
} else { if is_insert_mode {
self.editor.clear_insert_mode_start_pos(); self.editor.mark_insert_mode_start_pos();
} } else {
return Ok(()) self.editor.clear_insert_mode_start_pos();
} else if cmd.is_cmd_repeat() { }
let Some(replay) = self.repeat_action.clone() else { return Ok(());
return Ok(()) } else if cmd.is_cmd_repeat() {
}; let Some(replay) = self.repeat_action.clone() else {
let ViCmd { verb, .. } = cmd; return Ok(());
let VerbCmd(count,_) = verb.unwrap(); };
match replay { let ViCmd { verb, .. } = cmd;
CmdReplay::ModeReplay { cmds, mut repeat } => { let VerbCmd(count, _) = verb.unwrap();
if count > 1 { match replay {
repeat = count as u16; CmdReplay::ModeReplay { cmds, mut repeat } => {
} if count > 1 {
for _ in 0..repeat { repeat = count as u16;
let cmds = cmds.clone(); }
for cmd in cmds { for _ in 0..repeat {
self.editor.exec_cmd(cmd)? let cmds = cmds.clone();
} for cmd in cmds {
} self.editor.exec_cmd(cmd)?
} }
CmdReplay::Single(mut cmd) => { }
if count > 1 { }
// Override the counts with the one passed to the '.' command CmdReplay::Single(mut cmd) => {
if cmd.verb.is_some() { if count > 1 {
if let Some(v_mut) = cmd.verb.as_mut() { // Override the counts with the one passed to the '.' command
v_mut.0 = count if cmd.verb.is_some() {
} if let Some(v_mut) = cmd.verb.as_mut() {
if let Some(m_mut) = cmd.motion.as_mut() { v_mut.0 = count
m_mut.0 = 1 }
} if let Some(m_mut) = cmd.motion.as_mut() {
} else { m_mut.0 = 1
return Ok(()) // it has to have a verb to be repeatable, something weird happened }
} } else {
} return Ok(()); // it has to have a verb to be repeatable,
self.editor.exec_cmd(cmd)?; // something weird happened
} }
_ => unreachable!("motions should be handled in the other branch") }
} self.editor.exec_cmd(cmd)?;
return Ok(()) }
} else if cmd.is_motion_repeat() { _ => unreachable!("motions should be handled in the other branch"),
match cmd.motion.as_ref().unwrap() { }
MotionCmd(count,Motion::RepeatMotion) => { return Ok(());
let Some(motion) = self.repeat_motion.clone() else { } else if cmd.is_motion_repeat() {
return Ok(()) match cmd.motion.as_ref().unwrap() {
}; MotionCmd(count, Motion::RepeatMotion) => {
let repeat_cmd = ViCmd { let Some(motion) = self.repeat_motion.clone() else {
register: RegisterName::default(), return Ok(());
verb: None, };
motion: Some(motion), let repeat_cmd = ViCmd {
raw_seq: format!("{count};"), register: RegisterName::default(),
flags: CmdFlags::empty() verb: None,
}; motion: Some(motion),
return self.editor.exec_cmd(repeat_cmd); raw_seq: format!("{count};"),
} flags: CmdFlags::empty(),
MotionCmd(count,Motion::RepeatMotionRev) => { };
let Some(motion) = self.repeat_motion.clone() else { return self.editor.exec_cmd(repeat_cmd);
return Ok(()) }
}; MotionCmd(count, Motion::RepeatMotionRev) => {
let mut new_motion = motion.invert_char_motion(); let Some(motion) = self.repeat_motion.clone() else {
new_motion.0 = *count; return Ok(());
let repeat_cmd = ViCmd { };
register: RegisterName::default(), let mut new_motion = motion.invert_char_motion();
verb: None, new_motion.0 = *count;
motion: Some(new_motion), let repeat_cmd = ViCmd {
raw_seq: format!("{count},"), register: RegisterName::default(),
flags: CmdFlags::empty() verb: None,
}; motion: Some(new_motion),
return self.editor.exec_cmd(repeat_cmd); raw_seq: format!("{count},"),
} flags: CmdFlags::empty(),
_ => unreachable!() };
} return self.editor.exec_cmd(repeat_cmd);
} }
_ => unreachable!(),
}
}
if cmd.is_repeatable() { if cmd.is_repeatable() {
if self.mode.report_mode() == ModeReport::Visual { if self.mode.report_mode() == ModeReport::Visual {
// The motion is assigned in the line buffer execution, so we also have to assign it here // The motion is assigned in the line buffer execution, so we also have to
// in order to be able to repeat it // assign it here in order to be able to repeat it
let range = self.editor.select_range().unwrap(); let range = self.editor.select_range().unwrap();
cmd.motion = Some(MotionCmd(1,Motion::Range(range.0, range.1))) cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
} }
self.repeat_action = Some(CmdReplay::Single(cmd.clone())); self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
} }
if cmd.is_char_search() { if cmd.is_char_search() {
self.repeat_motion = cmd.motion.clone() self.repeat_motion = cmd.motion.clone()
} }
self.editor.exec_cmd(cmd.clone())?; self.editor.exec_cmd(cmd.clone())?;
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
self.editor.stop_selecting(); self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
std::mem::swap(&mut mode, &mut self.mode); std::mem::swap(&mut mode, &mut self.mode);
} }
Ok(()) Ok(())
} }
} }

View File

@@ -3,166 +3,170 @@ use std::sync::Mutex;
pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new()); pub static REGISTERS: Mutex<Registers> = Mutex::new(Registers::new());
pub fn read_register(ch: Option<char>) -> Option<String> { pub fn read_register(ch: Option<char>) -> Option<String> {
let lock = REGISTERS.lock().unwrap(); let lock = REGISTERS.lock().unwrap();
lock.get_reg(ch).map(|r| r.buf().clone()) lock.get_reg(ch).map(|r| r.buf().clone())
} }
pub fn write_register(ch: Option<char>, buf: String) { pub fn write_register(ch: Option<char>, buf: String) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.write(buf) } if let Some(r) = lock.get_reg_mut(ch) {
r.write(buf)
}
} }
pub fn append_register(ch: Option<char>, buf: String) { pub fn append_register(ch: Option<char>, buf: String) {
let mut lock = REGISTERS.lock().unwrap(); let mut lock = REGISTERS.lock().unwrap();
if let Some(r) = lock.get_reg_mut(ch) { r.append(buf) } if let Some(r) = lock.get_reg_mut(ch) {
r.append(buf)
}
} }
#[derive(Default,Debug)] #[derive(Default, Debug)]
pub struct Registers { pub struct Registers {
default: Register, default: Register,
a: Register, a: Register,
b: Register, b: Register,
c: Register, c: Register,
d: Register, d: Register,
e: Register, e: Register,
f: Register, f: Register,
g: Register, g: Register,
h: Register, h: Register,
i: Register, i: Register,
j: Register, j: Register,
k: Register, k: Register,
l: Register, l: Register,
m: Register, m: Register,
n: Register, n: Register,
o: Register, o: Register,
p: Register, p: Register,
q: Register, q: Register,
r: Register, r: Register,
s: Register, s: Register,
t: Register, t: Register,
u: Register, u: Register,
v: Register, v: Register,
w: Register, w: Register,
x: Register, x: Register,
y: Register, y: Register,
z: Register, z: Register,
} }
impl Registers { impl Registers {
pub const fn new() -> Self { pub const fn new() -> Self {
Self { Self {
default: Register(String::new()), default: Register(String::new()),
a: Register(String::new()), a: Register(String::new()),
b: Register(String::new()), b: Register(String::new()),
c: Register(String::new()), c: Register(String::new()),
d: Register(String::new()), d: Register(String::new()),
e: Register(String::new()), e: Register(String::new()),
f: Register(String::new()), f: Register(String::new()),
g: Register(String::new()), g: Register(String::new()),
h: Register(String::new()), h: Register(String::new()),
i: Register(String::new()), i: Register(String::new()),
j: Register(String::new()), j: Register(String::new()),
k: Register(String::new()), k: Register(String::new()),
l: Register(String::new()), l: Register(String::new()),
m: Register(String::new()), m: Register(String::new()),
n: Register(String::new()), n: Register(String::new()),
o: Register(String::new()), o: Register(String::new()),
p: Register(String::new()), p: Register(String::new()),
q: Register(String::new()), q: Register(String::new()),
r: Register(String::new()), r: Register(String::new()),
s: Register(String::new()), s: Register(String::new()),
t: Register(String::new()), t: Register(String::new()),
u: Register(String::new()), u: Register(String::new()),
v: Register(String::new()), v: Register(String::new()),
w: Register(String::new()), w: Register(String::new()),
x: Register(String::new()), x: Register(String::new()),
y: Register(String::new()), y: Register(String::new()),
z: Register(String::new()), z: Register(String::new()),
} }
} }
pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> { pub fn get_reg(&self, ch: Option<char>) -> Option<&Register> {
let Some(ch) = ch else { let Some(ch) = ch else {
return Some(&self.default) return Some(&self.default);
}; };
match ch { match ch {
'a' => Some(&self.a), 'a' => Some(&self.a),
'b' => Some(&self.b), 'b' => Some(&self.b),
'c' => Some(&self.c), 'c' => Some(&self.c),
'd' => Some(&self.d), 'd' => Some(&self.d),
'e' => Some(&self.e), 'e' => Some(&self.e),
'f' => Some(&self.f), 'f' => Some(&self.f),
'g' => Some(&self.g), 'g' => Some(&self.g),
'h' => Some(&self.h), 'h' => Some(&self.h),
'i' => Some(&self.i), 'i' => Some(&self.i),
'j' => Some(&self.j), 'j' => Some(&self.j),
'k' => Some(&self.k), 'k' => Some(&self.k),
'l' => Some(&self.l), 'l' => Some(&self.l),
'm' => Some(&self.m), 'm' => Some(&self.m),
'n' => Some(&self.n), 'n' => Some(&self.n),
'o' => Some(&self.o), 'o' => Some(&self.o),
'p' => Some(&self.p), 'p' => Some(&self.p),
'q' => Some(&self.q), 'q' => Some(&self.q),
'r' => Some(&self.r), 'r' => Some(&self.r),
's' => Some(&self.s), 's' => Some(&self.s),
't' => Some(&self.t), 't' => Some(&self.t),
'u' => Some(&self.u), 'u' => Some(&self.u),
'v' => Some(&self.v), 'v' => Some(&self.v),
'w' => Some(&self.w), 'w' => Some(&self.w),
'x' => Some(&self.x), 'x' => Some(&self.x),
'y' => Some(&self.y), 'y' => Some(&self.y),
'z' => Some(&self.z), 'z' => Some(&self.z),
_ => None _ => None,
} }
} }
pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> { pub fn get_reg_mut(&mut self, ch: Option<char>) -> Option<&mut Register> {
let Some(ch) = ch else { let Some(ch) = ch else {
return Some(&mut self.default) return Some(&mut self.default);
}; };
match ch { match ch {
'a' => Some(&mut self.a), 'a' => Some(&mut self.a),
'b' => Some(&mut self.b), 'b' => Some(&mut self.b),
'c' => Some(&mut self.c), 'c' => Some(&mut self.c),
'd' => Some(&mut self.d), 'd' => Some(&mut self.d),
'e' => Some(&mut self.e), 'e' => Some(&mut self.e),
'f' => Some(&mut self.f), 'f' => Some(&mut self.f),
'g' => Some(&mut self.g), 'g' => Some(&mut self.g),
'h' => Some(&mut self.h), 'h' => Some(&mut self.h),
'i' => Some(&mut self.i), 'i' => Some(&mut self.i),
'j' => Some(&mut self.j), 'j' => Some(&mut self.j),
'k' => Some(&mut self.k), 'k' => Some(&mut self.k),
'l' => Some(&mut self.l), 'l' => Some(&mut self.l),
'm' => Some(&mut self.m), 'm' => Some(&mut self.m),
'n' => Some(&mut self.n), 'n' => Some(&mut self.n),
'o' => Some(&mut self.o), 'o' => Some(&mut self.o),
'p' => Some(&mut self.p), 'p' => Some(&mut self.p),
'q' => Some(&mut self.q), 'q' => Some(&mut self.q),
'r' => Some(&mut self.r), 'r' => Some(&mut self.r),
's' => Some(&mut self.s), 's' => Some(&mut self.s),
't' => Some(&mut self.t), 't' => Some(&mut self.t),
'u' => Some(&mut self.u), 'u' => Some(&mut self.u),
'v' => Some(&mut self.v), 'v' => Some(&mut self.v),
'w' => Some(&mut self.w), 'w' => Some(&mut self.w),
'x' => Some(&mut self.x), 'x' => Some(&mut self.x),
'y' => Some(&mut self.y), 'y' => Some(&mut self.y),
'z' => Some(&mut self.z), 'z' => Some(&mut self.z),
_ => None _ => None,
} }
} }
} }
#[derive(Clone,Default,Debug)] #[derive(Clone, Default, Debug)]
pub struct Register(String); pub struct Register(String);
impl Register { impl Register {
pub fn buf(&self) -> &String { pub fn buf(&self) -> &String {
&self.0 &self.0
} }
pub fn write(&mut self, buf: String) { pub fn write(&mut self, buf: String) {
self.0 = buf self.0 = buf
} }
pub fn append(&mut self, buf: String) { pub fn append(&mut self, buf: String) {
self.0.push_str(&buf) self.0.push_str(&buf)
} }
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.0.clear() self.0.clear()
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -2,438 +2,458 @@ use bitflags::bitflags;
use super::register::{append_register, read_register, write_register}; use super::register::{append_register, read_register, write_register};
//TODO: write tests that take edit results and cursor positions from actual neovim edits and test them against the behavior of this editor //TODO: write tests that take edit results and cursor positions from actual
// neovim edits and test them against the behavior of this editor
#[derive(Clone,Copy,Debug)] #[derive(Clone, Copy, Debug)]
pub struct RegisterName { pub struct RegisterName {
name: Option<char>, name: Option<char>,
count: usize, count: usize,
append: bool append: bool,
} }
impl RegisterName { impl RegisterName {
pub fn new(name: Option<char>, count: Option<usize>) -> Self { pub fn new(name: Option<char>, count: Option<usize>) -> Self {
let Some(ch) = name else { let Some(ch) = name else {
return Self::default() return Self::default();
}; };
let append = ch.is_uppercase(); let append = ch.is_uppercase();
let name = ch.to_ascii_lowercase(); let name = ch.to_ascii_lowercase();
Self { Self {
name: Some(name), name: Some(name),
count: count.unwrap_or(1), count: count.unwrap_or(1),
append append,
} }
} }
pub fn name(&self) -> Option<char> { pub fn name(&self) -> Option<char> {
self.name self.name
} }
pub fn is_append(&self) -> bool { pub fn is_append(&self) -> bool {
self.append self.append
} }
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.count self.count
} }
pub fn write_to_register(&self, buf: String) { pub fn write_to_register(&self, buf: String) {
if self.append { if self.append {
append_register(self.name, buf); append_register(self.name, buf);
} else { } else {
write_register(self.name, buf); write_register(self.name, buf);
} }
} }
pub fn read_from_register(&self) -> Option<String> { pub fn read_from_register(&self) -> Option<String> {
read_register(self.name) read_register(self.name)
} }
} }
impl Default for RegisterName { impl Default for RegisterName {
fn default() -> Self { fn default() -> Self {
Self { Self {
name: None, name: None,
count: 1, count: 1,
append: false append: false,
} }
} }
} }
bitflags! { bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CmdFlags: u32 { pub struct CmdFlags: u32 {
const VISUAL = 1<<0; const VISUAL = 1<<0;
const VISUAL_LINE = 1<<1; const VISUAL_LINE = 1<<1;
const VISUAL_BLOCK = 1<<2; const VISUAL_BLOCK = 1<<2;
} }
} }
#[derive(Clone,Default,Debug)] #[derive(Clone, Default, Debug)]
pub struct ViCmd { pub struct ViCmd {
pub register: RegisterName, pub register: RegisterName,
pub verb: Option<VerbCmd>, pub verb: Option<VerbCmd>,
pub motion: Option<MotionCmd>, pub motion: Option<MotionCmd>,
pub raw_seq: String, pub raw_seq: String,
pub flags: CmdFlags, pub flags: CmdFlags,
} }
impl ViCmd { impl ViCmd {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn set_motion(&mut self, motion: MotionCmd) { pub fn set_motion(&mut self, motion: MotionCmd) {
self.motion = Some(motion) self.motion = Some(motion)
} }
pub fn set_verb(&mut self, verb: VerbCmd) { pub fn set_verb(&mut self, verb: VerbCmd) {
self.verb = Some(verb) self.verb = Some(verb)
} }
pub fn verb(&self) -> Option<&VerbCmd> { pub fn verb(&self) -> Option<&VerbCmd> {
self.verb.as_ref() self.verb.as_ref()
} }
pub fn motion(&self) -> Option<&MotionCmd> { pub fn motion(&self) -> Option<&MotionCmd> {
self.motion.as_ref() self.motion.as_ref()
} }
pub fn verb_count(&self) -> usize { pub fn verb_count(&self) -> usize {
self.verb.as_ref().map(|v| v.0).unwrap_or(1) self.verb.as_ref().map(|v| v.0).unwrap_or(1)
} }
pub fn motion_count(&self) -> usize { pub fn motion_count(&self) -> usize {
self.motion.as_ref().map(|m| m.0).unwrap_or(1) self.motion.as_ref().map(|m| m.0).unwrap_or(1)
} }
pub fn normalize_counts(&mut self) { pub fn normalize_counts(&mut self) {
let Some(verb) = self.verb.as_mut() else { return }; let Some(verb) = self.verb.as_mut() else {
let Some(motion) = self.motion.as_mut() else { return }; return;
let VerbCmd(v_count, _) = verb; };
let MotionCmd(m_count, _) = motion; let Some(motion) = self.motion.as_mut() else {
let product = *v_count * *m_count; return;
verb.0 = 1; };
motion.0 = product; let VerbCmd(v_count, _) = verb;
} let MotionCmd(m_count, _) = motion;
pub fn is_repeatable(&self) -> bool { let product = *v_count * *m_count;
self.verb.as_ref().is_some_and(|v| v.1.is_repeatable()) verb.0 = 1;
} motion.0 = product;
pub fn is_cmd_repeat(&self) -> bool { }
self.verb.as_ref().is_some_and(|v| matches!(v.1,Verb::RepeatLast)) pub fn is_repeatable(&self) -> bool {
} self.verb.as_ref().is_some_and(|v| v.1.is_repeatable())
pub fn is_motion_repeat(&self) -> bool { }
self.motion.as_ref().is_some_and(|m| matches!(m.1,Motion::RepeatMotion | Motion::RepeatMotionRev)) pub fn is_cmd_repeat(&self) -> bool {
} self
pub fn is_char_search(&self) -> bool { .verb
self.motion.as_ref().is_some_and(|m| matches!(m.1, Motion::CharSearch(..))) .as_ref()
} .is_some_and(|v| matches!(v.1, Verb::RepeatLast))
pub fn should_submit(&self) -> bool { }
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline)) pub fn is_motion_repeat(&self) -> bool {
} self
pub fn is_undo_op(&self) -> bool { .motion
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo)) .as_ref()
} .is_some_and(|m| matches!(m.1, Motion::RepeatMotion | Motion::RepeatMotionRev))
pub fn is_inplace_edit(&self) -> bool { }
self.verb.as_ref().is_some_and(|v| matches!(v.1, Verb::ReplaceCharInplace(_,_) | Verb::ToggleCaseInplace(_))) && pub fn is_char_search(&self) -> bool {
self.motion.is_none() self
} .motion
pub fn is_line_motion(&self) -> bool { .as_ref()
self.motion.as_ref().is_some_and(|m| { .is_some_and(|m| matches!(m.1, Motion::CharSearch(..)))
matches!(m.1, }
Motion::LineUp | pub fn should_submit(&self) -> bool {
Motion::LineDown | self
Motion::LineUpCharwise | .verb
Motion::LineDownCharwise .as_ref()
) .is_some_and(|v| matches!(v.1, Verb::AcceptLineOrNewline))
}) }
} pub fn is_undo_op(&self) -> bool {
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise self
pub fn alter_line_motion_if_no_verb(&mut self) { .verb
if self.is_line_motion() && self.verb.is_none() { .as_ref()
if let Some(motion) = self.motion.as_mut() { .is_some_and(|v| matches!(v.1, Verb::Undo | Verb::Redo))
match motion.1 { }
Motion::LineUp => motion.1 = Motion::LineUpCharwise, pub fn is_inplace_edit(&self) -> bool {
Motion::LineDown => motion.1 = Motion::LineDownCharwise, self.verb.as_ref().is_some_and(|v| {
_ => unreachable!() matches!(
} v.1,
} Verb::ReplaceCharInplace(_, _) | Verb::ToggleCaseInplace(_)
} )
} }) && self.motion.is_none()
pub fn is_mode_transition(&self) -> bool { }
self.verb.as_ref().is_some_and(|v| { pub fn is_line_motion(&self) -> bool {
matches!(v.1, self.motion.as_ref().is_some_and(|m| {
Verb::Change | matches!(
Verb::InsertMode | m.1,
Verb::InsertModeLineBreak(_) | Motion::LineUp | Motion::LineDown | Motion::LineUpCharwise | Motion::LineDownCharwise
Verb::NormalMode | )
Verb::VisualModeSelectLast | })
Verb::VisualMode | }
Verb::ReplaceMode /// If a ViCmd has a linewise motion, but no verb, we change it to charwise
) pub fn alter_line_motion_if_no_verb(&mut self) {
}) if self.is_line_motion() && self.verb.is_none() {
} if let Some(motion) = self.motion.as_mut() {
match motion.1 {
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => 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
)
})
}
} }
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct VerbCmd(pub usize,pub Verb); pub struct VerbCmd(pub usize, pub Verb);
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct MotionCmd(pub usize,pub Motion); pub struct MotionCmd(pub usize, pub Motion);
impl MotionCmd { impl MotionCmd {
pub fn invert_char_motion(self) -> Self { pub fn invert_char_motion(self) -> Self {
let MotionCmd(count,Motion::CharSearch(dir, dest, ch)) = self else { let MotionCmd(count, Motion::CharSearch(dir, dest, ch)) = self else {
unreachable!() unreachable!()
}; };
let new_dir = match dir { let new_dir = match dir {
Direction::Forward => Direction::Backward, Direction::Forward => Direction::Backward,
Direction::Backward => Direction::Forward, Direction::Backward => Direction::Forward,
}; };
MotionCmd(count,Motion::CharSearch(new_dir, dest, ch)) MotionCmd(count, Motion::CharSearch(new_dir, dest, ch))
} }
} }
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
pub enum Verb { pub enum Verb {
Delete, Delete,
Change, Change,
Yank, Yank,
Rot13, // lol Rot13, // lol
ReplaceChar(char), // char to replace with, number of chars to replace ReplaceChar(char), // char to replace with, number of chars to replace
ReplaceCharInplace(char,u16), // char to replace with, number of chars to replace ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
ToggleCaseInplace(u16), // Number of chars to toggle ToggleCaseInplace(u16), // Number of chars to toggle
ToggleCaseRange, ToggleCaseRange,
ToLower, ToLower,
ToUpper, ToUpper,
Complete, Complete,
CompleteBackward, CompleteBackward,
Undo, Undo,
Redo, Redo,
RepeatLast, RepeatLast,
Put(Anchor), Put(Anchor),
ReplaceMode, ReplaceMode,
InsertMode, InsertMode,
InsertModeLineBreak(Anchor), InsertModeLineBreak(Anchor),
NormalMode, NormalMode,
VisualMode, VisualMode,
VisualModeLine, VisualModeLine,
VisualModeBlock, // dont even know if im going to implement this VisualModeBlock, // dont even know if im going to implement this
VisualModeSelectLast, VisualModeSelectLast,
SwapVisualAnchor, SwapVisualAnchor,
JoinLines, JoinLines,
InsertChar(char), InsertChar(char),
Insert(String), Insert(String),
Indent, Indent,
Dedent, Dedent,
Equalize, Equalize,
AcceptLineOrNewline, AcceptLineOrNewline,
EndOfFile EndOfFile,
} }
impl Verb { impl Verb {
pub fn is_repeatable(&self) -> bool { pub fn is_repeatable(&self) -> bool {
matches!(self, matches!(
Self::Delete | self,
Self::Change | Self::Delete
Self::ReplaceChar(_) | | Self::Change
Self::ReplaceCharInplace(_,_) | | Self::ReplaceChar(_)
Self::ToLower | | Self::ReplaceCharInplace(_, _)
Self::ToUpper | | Self::ToLower
Self::ToggleCaseRange | | Self::ToUpper
Self::ToggleCaseInplace(_) | | Self::ToggleCaseRange
Self::Put(_) | | Self::ToggleCaseInplace(_)
Self::ReplaceMode | | Self::Put(_)
Self::InsertModeLineBreak(_) | | Self::ReplaceMode
Self::JoinLines | | Self::InsertModeLineBreak(_)
Self::InsertChar(_) | | Self::JoinLines
Self::Insert(_) | | Self::InsertChar(_)
Self::Indent | | Self::Insert(_)
Self::Dedent | | Self::Indent
Self::Equalize | Self::Dedent
) | Self::Equalize
} )
pub fn is_edit(&self) -> bool { }
matches!(self, pub fn is_edit(&self) -> bool {
Self::Delete | matches!(
Self::Change | self,
Self::ReplaceChar(_) | Self::Delete
Self::ReplaceCharInplace(_,_) | | Self::Change
Self::ToggleCaseRange | | Self::ReplaceChar(_)
Self::ToggleCaseInplace(_) | | Self::ReplaceCharInplace(_, _)
Self::ToLower | | Self::ToggleCaseRange
Self::ToUpper | | Self::ToggleCaseInplace(_)
Self::RepeatLast | | Self::ToLower
Self::Put(_) | | Self::ToUpper
Self::ReplaceMode | | Self::RepeatLast
Self::InsertModeLineBreak(_) | | Self::Put(_)
Self::JoinLines | | Self::ReplaceMode
Self::InsertChar(_) | | Self::InsertModeLineBreak(_)
Self::Insert(_) | | Self::JoinLines
Self::Rot13 | | Self::InsertChar(_)
Self::EndOfFile | Self::Insert(_)
) | Self::Rot13
} | Self::EndOfFile
pub fn is_char_insert(&self) -> bool { )
matches!(self, }
Self::Change | pub fn is_char_insert(&self) -> bool {
Self::InsertChar(_) | matches!(
Self::ReplaceChar(_) | self,
Self::ReplaceCharInplace(_,_) Self::Change | Self::InsertChar(_) | Self::ReplaceChar(_) | Self::ReplaceCharInplace(_, _)
) )
} }
} }
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum Motion { pub enum Motion {
WholeLine, WholeLine,
TextObj(TextObj), TextObj(TextObj),
EndOfLastWord, EndOfLastWord,
BeginningOfFirstWord, BeginningOfFirstWord,
BeginningOfLine, BeginningOfLine,
EndOfLine, EndOfLine,
WordMotion(To,Word,Direction), WordMotion(To, Word, Direction),
CharSearch(Direction,Dest,char), CharSearch(Direction, Dest, char),
BackwardChar, BackwardChar,
ForwardChar, ForwardChar,
BackwardCharForced, // These two variants can cross line boundaries BackwardCharForced, // These two variants can cross line boundaries
ForwardCharForced, ForwardCharForced,
LineUp, LineUp,
LineUpCharwise, LineUpCharwise,
ScreenLineUp, ScreenLineUp,
ScreenLineUpCharwise, ScreenLineUpCharwise,
LineDown, LineDown,
LineDownCharwise, LineDownCharwise,
ScreenLineDown, ScreenLineDown,
ScreenLineDownCharwise, ScreenLineDownCharwise,
BeginningOfScreenLine, BeginningOfScreenLine,
FirstGraphicalOnScreenLine, FirstGraphicalOnScreenLine,
HalfOfScreen, HalfOfScreen,
HalfOfScreenLineText, HalfOfScreenLineText,
WholeBuffer, WholeBuffer,
BeginningOfBuffer, BeginningOfBuffer,
EndOfBuffer, EndOfBuffer,
ToColumn, ToColumn,
ToDelimMatch, ToDelimMatch,
ToBrace(Direction), ToBrace(Direction),
ToBracket(Direction), ToBracket(Direction),
ToParen(Direction), ToParen(Direction),
Range(usize,usize), Range(usize, usize),
RepeatMotion, RepeatMotion,
RepeatMotionRev, RepeatMotionRev,
Null Null,
} }
#[derive(Clone,Copy,PartialEq,Eq,Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum MotionBehavior { pub enum MotionBehavior {
Exclusive, Exclusive,
Inclusive, Inclusive,
Linewise Linewise,
} }
impl Motion { impl Motion {
pub fn behavior(&self) -> MotionBehavior { pub fn behavior(&self) -> MotionBehavior {
if self.is_linewise() { if self.is_linewise() {
MotionBehavior::Linewise MotionBehavior::Linewise
} else if self.is_exclusive() { } else if self.is_exclusive() {
MotionBehavior::Exclusive MotionBehavior::Exclusive
} else { } else {
MotionBehavior::Inclusive MotionBehavior::Inclusive
} }
} }
pub fn is_exclusive(&self) -> bool { pub fn is_exclusive(&self) -> bool {
matches!(&self, matches!(
Self::BeginningOfLine | &self,
Self::BeginningOfFirstWord | Self::BeginningOfLine
Self::BeginningOfScreenLine | | Self::BeginningOfFirstWord
Self::FirstGraphicalOnScreenLine | | Self::BeginningOfScreenLine
Self::LineDownCharwise | | Self::FirstGraphicalOnScreenLine
Self::LineUpCharwise | | Self::LineDownCharwise
Self::ScreenLineUpCharwise | | Self::LineUpCharwise
Self::ScreenLineDownCharwise | | Self::ScreenLineUpCharwise
Self::ToColumn | | Self::ScreenLineDownCharwise
Self::TextObj(TextObj::Sentence(_)) | | Self::ToColumn
Self::TextObj(TextObj::Paragraph(_)) | | Self::TextObj(TextObj::Sentence(_))
Self::CharSearch(Direction::Backward, _, _) | | Self::TextObj(TextObj::Paragraph(_))
Self::WordMotion(To::Start,_,_) | | Self::CharSearch(Direction::Backward, _, _)
Self::ToBrace(_) | | Self::WordMotion(To::Start, _, _)
Self::ToBracket(_) | | Self::ToBrace(_)
Self::ToParen(_) | | Self::ToBracket(_)
Self::ScreenLineDown | | Self::ToParen(_)
Self::ScreenLineUp | | Self::ScreenLineDown
Self::Range(_, _) | Self::ScreenLineUp
) | Self::Range(_, _)
} )
pub fn is_linewise(&self) -> bool { }
matches!(self, pub fn is_linewise(&self) -> bool {
Self::WholeLine | matches!(
Self::LineUp | self,
Self::LineDown | Self::WholeLine | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp
Self::ScreenLineDown | )
Self::ScreenLineUp }
)
}
} }
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum Anchor { pub enum Anchor {
After, After,
Before Before,
} }
#[derive(Debug, Clone, Eq, PartialEq)] #[derive(Debug, Clone, Eq, PartialEq)]
pub enum TextObj { pub enum TextObj {
/// `iw`, `aw` — inner word, around word /// `iw`, `aw` — inner word, around word
Word(Word, Bound), Word(Word, Bound),
/// `)`, `(` — forward, backward /// `)`, `(` — forward, backward
Sentence(Direction), Sentence(Direction),
/// `}`, `{` — forward, backward /// `}`, `{` — forward, backward
Paragraph(Direction), Paragraph(Direction),
WholeSentence(Bound), WholeSentence(Bound),
WholeParagraph(Bound), WholeParagraph(Bound),
/// `i"`, `a"` — inner/around double quotes /// `i"`, `a"` — inner/around double quotes
DoubleQuote(Bound), DoubleQuote(Bound),
/// `i'`, `a'` /// `i'`, `a'`
SingleQuote(Bound), SingleQuote(Bound),
/// `i\``, `a\`` /// `i\``, `a\``
BacktickQuote(Bound), BacktickQuote(Bound),
/// `i)`, `a)` — round parens /// `i)`, `a)` — round parens
Paren(Bound), Paren(Bound),
/// `i]`, `a]` /// `i]`, `a]`
Bracket(Bound), Bracket(Bound),
/// `i}`, `a}` /// `i}`, `a}`
Brace(Bound), Brace(Bound),
/// `i<`, `a<` /// `i<`, `a<`
Angle(Bound), Angle(Bound),
/// `it`, `at` — HTML/XML tags /// `it`, `at` — HTML/XML tags
Tag(Bound), Tag(Bound),
/// Custom user-defined objects maybe? /// Custom user-defined objects maybe?
Custom(char), Custom(char),
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Word { pub enum Word {
Big, Big,
Normal Normal,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Bound { pub enum Bound {
Inside, Inside,
Around Around,
} }
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)] #[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub enum Direction { pub enum Direction {
#[default] #[default]
Forward, Forward,
Backward Backward,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Dest { pub enum Dest {
On, On,
Before, Before,
After After,
} }
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum To { pub enum To {
Start, Start,
End End,
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,161 +1,168 @@
use crate::{jobs::{take_term, JobCmdFlags, JobID}, libsh::{error::ShResult, sys::sh_quit}, prelude::*, state::{read_jobs, write_jobs}}; use crate::{
jobs::{take_term, JobCmdFlags, JobID},
libsh::{error::ShResult, sys::sh_quit},
prelude::*,
state::{read_jobs, write_jobs},
};
pub fn sig_setup() { pub fn sig_setup() {
unsafe { unsafe {
signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap(); signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap();
signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap(); signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap();
signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap(); signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap();
signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap(); signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap();
signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap(); signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap();
signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap(); signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap();
signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap(); signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap();
} }
} }
extern "C" fn handle_sighup(_: libc::c_int) { extern "C" fn handle_sighup(_: libc::c_int) {
write_jobs(|j| { write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() { for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok(); job.killpg(Signal::SIGTERM).ok();
} }
}); });
std::process::exit(0); std::process::exit(0);
} }
extern "C" fn handle_sigtstp(_: libc::c_int) { extern "C" fn handle_sigtstp(_: libc::c_int) {
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.get_fg_mut() { if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGTSTP).ok(); job.killpg(Signal::SIGTSTP).ok();
} }
}); });
} }
extern "C" fn handle_sigint(_: libc::c_int) { extern "C" fn handle_sigint(_: libc::c_int) {
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.get_fg_mut() { if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGINT).ok(); job.killpg(Signal::SIGINT).ok();
} }
}); });
} }
pub extern "C" fn ignore_sigchld(_: libc::c_int) { pub extern "C" fn ignore_sigchld(_: libc::c_int) {
/* /*
Do nothing Do nothing
This function exists because using SIGIGN to ignore SIGCHLD This function exists because using SIGIGN to ignore SIGCHLD
will cause the kernel to automatically reap the child process, which is not what we want. will cause the kernel to automatically reap the child process, which is not what we want.
This handler will leave the signaling process as a zombie, allowing us This handler will leave the signaling process as a zombie, allowing us
to handle it somewhere else. to handle it somewhere else.
This handler is used when we want to handle SIGCHLD explicitly, This handler is used when we want to handle SIGCHLD explicitly,
like in the case of handling foreground jobs like in the case of handling foreground jobs
*/ */
} }
extern "C" fn handle_sigquit(_: libc::c_int) { extern "C" fn handle_sigquit(_: libc::c_int) {
sh_quit(0) sh_quit(0)
} }
pub extern "C" fn handle_sigchld(_: libc::c_int) { pub extern "C" fn handle_sigchld(_: libc::c_int) {
let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED; let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED;
while let Ok(status) = waitpid(None, Some(flags)) { while let Ok(status) = waitpid(None, Some(flags)) {
if let Err(e) = match status { if let Err(e) = match status {
WtStat::Exited(pid, _) => child_exited(pid, status), WtStat::Exited(pid, _) => child_exited(pid, status),
WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal), WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal),
WtStat::Stopped(pid, signal) => child_stopped(pid, signal), WtStat::Stopped(pid, signal) => child_stopped(pid, signal),
WtStat::Continued(pid) => child_continued(pid), WtStat::Continued(pid) => child_continued(pid),
WtStat::StillAlive => break, WtStat::StillAlive => break,
_ => unimplemented!() _ => unimplemented!(),
} { } {
eprintln!("{}",e) eprintln!("{}", e)
} }
} }
} }
pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> { pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid); let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) { if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap(); let child = job
let stat = WtStat::Signaled(pid, sig, false); .children_mut()
child.set_stat(stat); .iter_mut()
} .find(|chld| pid == chld.pid())
}); .unwrap();
if sig == Signal::SIGINT { let stat = WtStat::Signaled(pid, sig, false);
take_term().unwrap() child.set_stat(stat);
} }
Ok(()) });
if sig == Signal::SIGINT {
take_term().unwrap()
}
Ok(())
} }
pub fn child_stopped(pid: Pid, sig: Signal) -> ShResult<()> { pub fn child_stopped(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid); let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) { if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap(); let child = job
let status = WtStat::Stopped(pid, sig); .children_mut()
child.set_stat(status); .iter_mut()
} else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) { .find(|chld| pid == chld.pid())
j.fg_to_bg(WtStat::Stopped(pid, sig)).unwrap(); .unwrap();
} let status = WtStat::Stopped(pid, sig);
}); child.set_stat(status);
take_term()?; } else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) {
Ok(()) j.fg_to_bg(WtStat::Stopped(pid, sig)).unwrap();
}
});
take_term()?;
Ok(())
} }
pub fn child_continued(pid: Pid) -> ShResult<()> { pub fn child_continued(pid: Pid) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid); let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| { write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) { if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
job.killpg(Signal::SIGCONT).ok(); job.killpg(Signal::SIGCONT).ok();
} }
}); });
Ok(()) Ok(())
} }
pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
/* /*
* Here we are going to get metadata on the exited process by querying the job table with the pid. * Here we are going to get metadata on the exited process by querying the
* Then if the discovered job is the fg task, return terminal control to rsh * job table with the pid. Then if the discovered job is the fg task,
* If it is not the fg task, print the display info for the job in the job table * return terminal control to rsh If it is not the fg task, print the
* We can reasonably assume that if it is not a foreground job, then it exists in the job table * display info for the job in the job table We can reasonably assume that
* If this assumption is incorrect, the code has gone wrong somewhere. * 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(( write_jobs(|j| j.close_job_fds(pid));
pgid, if let Some((pgid, is_fg, is_finished)) = write_jobs(|j| {
is_fg, let fg_pgid = j.get_fg().map(|job| job.pgid());
is_finished if let Some(job) = j.query_mut(JobID::Pid(pid)) {
)) = write_jobs(|j| { let pgid = job.pgid();
let fg_pgid = j.get_fg().map(|job| job.pgid()); let is_fg = fg_pgid.is_some_and(|fg| fg == pgid);
if let Some(job) = j.query_mut(JobID::Pid(pid)) { job.update_by_id(JobID::Pid(pid), status).unwrap();
let pgid = job.pgid(); let is_finished = !job.running();
let is_fg = fg_pgid.is_some_and(|fg| fg == pgid);
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);
}
if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) { Some((pgid, is_fg, is_finished))
child.set_stat(status); } else {
} None
}
Some((pgid, is_fg, is_finished)) }) {
} else { if is_finished {
None if is_fg {
} take_term()?;
}) { } else {
println!();
if is_finished { let job_order = read_jobs(|j| j.order().to_vec());
if is_fg { let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
take_term()?; if let Some(job) = result {
} else { println!("{}", job.display(&job_order, JobCmdFlags::PIDS))
println!(); }
let job_order = read_jobs(|j| j.order().to_vec()); }
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); }
if let Some(job) = result { }
println!("{}",job.display(&job_order,JobCmdFlags::PIDS)) Ok(())
}
}
}
}
Ok(())
} }

View File

@@ -1,8 +1,23 @@
use std::{collections::{HashMap, VecDeque}, ops::Deref, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}, time::Duration}; use std::{
collections::{HashMap, VecDeque},
ops::Deref,
sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard},
time::Duration,
};
use nix::unistd::{gethostname, getppid, User}; use nix::unistd::{gethostname, getppid, User};
use crate::{exec_input, jobs::JobTab, libsh::{error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts}; use crate::{
exec_input,
jobs::JobTab,
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::VecDequeExt,
},
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
prelude::*,
shopt::ShOpts,
};
pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new())); pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new()));
@@ -16,438 +31,458 @@ pub static SHOPTS: LazyLock<RwLock<ShOpts>> = LazyLock::new(|| RwLock::new(ShOpt
/// A shell function /// A shell function
/// ///
/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers to /// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers
/// The Node must be stored with the ParsedSrc because the tokens of the node contain an Arc<String> /// to The Node must be stored with the ParsedSrc because the tokens of the node
/// Which refers to the String held in ParsedSrc /// contain an Arc<String> Which refers to the String held in ParsedSrc
/// ///
/// Can be dereferenced to pull out the wrapped Node /// Can be dereferenced to pull out the wrapped Node
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct ShFunc(Node); pub struct ShFunc(Node);
impl ShFunc { impl ShFunc {
pub fn new(mut src: ParsedSrc) -> Self { pub fn new(mut src: ParsedSrc) -> Self {
let body = Self::extract_brc_grp_hack(src.extract_nodes()); let body = Self::extract_brc_grp_hack(src.extract_nodes());
Self(body) Self(body)
} }
fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node { fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node {
// FIXME: find a better way to do this // FIXME: find a better way to do this
let conjunction = tree.pop().unwrap(); let conjunction = tree.pop().unwrap();
let NdRule::Conjunction { mut elements } = conjunction.class else { let NdRule::Conjunction { mut elements } = conjunction.class else {
unreachable!() unreachable!()
}; };
let conjunct_node = elements.pop().unwrap(); let conjunct_node = elements.pop().unwrap();
let ConjunctNode { cmd, operator: _ } = conjunct_node; let ConjunctNode { cmd, operator: _ } = conjunct_node;
*cmd *cmd
} }
} }
impl Deref for ShFunc { impl Deref for ShFunc {
type Target = Node; type Target = Node;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
} }
} }
/// The logic table for the shell /// The logic table for the shell
/// ///
/// Contains aliases and functions /// Contains aliases and functions
#[derive(Default,Clone,Debug)] #[derive(Default, Clone, Debug)]
pub struct LogTab { pub struct LogTab {
functions: HashMap<String,ShFunc>, functions: HashMap<String, ShFunc>,
aliases: HashMap<String,String> aliases: HashMap<String, String>,
} }
impl LogTab { impl LogTab {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn insert_func(&mut self, name: &str, src: ShFunc) { pub fn insert_func(&mut self, name: &str, src: ShFunc) {
self.functions.insert(name.into(), src); self.functions.insert(name.into(), src);
} }
pub fn get_func(&self, name: &str) -> Option<ShFunc> { pub fn get_func(&self, name: &str) -> Option<ShFunc> {
self.functions.get(name).cloned() self.functions.get(name).cloned()
} }
pub fn funcs(&self) -> &HashMap<String,ShFunc> { pub fn funcs(&self) -> &HashMap<String, ShFunc> {
&self.functions &self.functions
} }
pub fn aliases(&self) -> &HashMap<String,String> { pub fn aliases(&self) -> &HashMap<String, String> {
&self.aliases &self.aliases
} }
pub fn insert_alias(&mut self, name: &str, body: &str) { pub fn insert_alias(&mut self, name: &str, body: &str) {
self.aliases.insert(name.into(), body.into()); self.aliases.insert(name.into(), body.into());
} }
pub fn get_alias(&self, name: &str) -> Option<String> { pub fn get_alias(&self, name: &str) -> Option<String> {
self.aliases.get(name).cloned() self.aliases.get(name).cloned()
} }
pub fn remove_alias(&mut self, name: &str) { pub fn remove_alias(&mut self, name: &str) {
flog!(DEBUG, self.aliases); flog!(DEBUG, self.aliases);
flog!(DEBUG, name); flog!(DEBUG, name);
self.aliases.remove(name); self.aliases.remove(name);
flog!(DEBUG, self.aliases); flog!(DEBUG, self.aliases);
} }
pub fn clear_aliases(&mut self) { pub fn clear_aliases(&mut self) {
self.aliases.clear() self.aliases.clear()
} }
pub fn clear_functions(&mut self) { pub fn clear_functions(&mut self) {
self.functions.clear() self.functions.clear()
} }
} }
#[derive(Clone,Debug)] #[derive(Clone, Debug)]
pub struct Var { pub struct Var {
export: bool, export: bool,
value: String value: String,
} }
impl Var { impl Var {
pub fn new(value: String) -> Self { pub fn new(value: String) -> Self {
Self { export: false, value } Self {
} export: false,
pub fn mark_for_export(&mut self) { value,
self.export = true; }
} }
pub fn mark_for_export(&mut self) {
self.export = true;
}
} }
impl Deref for Var { impl Deref for Var {
type Target = String; type Target = String;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.value &self.value
} }
} }
#[derive(Default,Clone,Debug)] #[derive(Default, Clone, Debug)]
pub struct VarTab { pub struct VarTab {
vars: HashMap<String,Var>, vars: HashMap<String, Var>,
params: HashMap<String,String>, params: HashMap<String, String>,
sh_argv: VecDeque<String>, // Using a VecDeque makes the implementation of `shift` straightforward sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift`
* straightforward */
} }
impl VarTab { impl VarTab {
pub fn new() -> Self { pub fn new() -> Self {
let vars = HashMap::new(); let vars = HashMap::new();
let params = Self::init_params(); let params = Self::init_params();
Self::init_env(); Self::init_env();
let mut var_tab = Self { vars, params, sh_argv: VecDeque::new() }; let mut var_tab = Self {
var_tab.init_sh_argv(); vars,
var_tab params,
} sh_argv: VecDeque::new(),
fn init_params() -> HashMap<String, String> { };
let mut params = HashMap::new(); var_tab.init_sh_argv();
params.insert("?".into(), "0".into()); // Last command exit status var_tab
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 fn init_params() -> HashMap<String, String> {
params.insert("$".into(), Pid::this().to_string()); // PID of the shell let mut params = HashMap::new();
params.insert("!".into(), "".into()); // PID of the last background job (if any) params.insert("?".into(), "0".into()); // Last command exit status
params params.insert("#".into(), "0".into()); // Number of positional parameters
} params.insert(
fn init_env() { "0".into(),
let pathbuf_to_string = |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string(); std::env::current_exe()
// First, inherit any env vars from the parent process .unwrap()
let term = { .to_str()
if isatty(1).unwrap() { .unwrap()
if let Ok(term) = std::env::var("TERM") { .to_string(),
term ); // Name of the shell
} else { params.insert("$".into(), Pid::this().to_string()); // PID of the shell
"linux".to_string() params.insert("!".into(), "".into()); // PID of the last background job (if any)
} params
} else { }
"xterm-256color".to_string() fn init_env() {
} let pathbuf_to_string =
}; |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
let home; // First, inherit any env vars from the parent process
let username; let term = {
let uid; if isatty(1).unwrap() {
if let Some(user) = User::from_uid(nix::unistd::Uid::current()).ok().flatten() { if let Ok(term) = std::env::var("TERM") {
home = user.dir; term
username = user.name; } else {
uid = user.uid; "linux".to_string()
} else { }
home = PathBuf::new(); } else {
username = "unknown".into(); "xterm-256color".to_string()
uid = 0.into(); }
} };
let home = pathbuf_to_string(Ok(home)); let home;
let hostname = gethostname().map(|hname| hname.to_string_lossy().to_string()).unwrap_or_default(); let username;
let uid;
if let Some(user) = User::from_uid(nix::unistd::Uid::current()).ok().flatten() {
home = user.dir;
username = user.name;
uid = user.uid;
} else {
home = PathBuf::new();
username = "unknown".into();
uid = 0.into();
}
let home = pathbuf_to_string(Ok(home));
let hostname = gethostname()
.map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default();
env::set_var("IFS", " \t\n"); env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone()); env::set_var("HOST", hostname.clone());
env::set_var("UID", uid.to_string()); env::set_var("UID", uid.to_string());
env::set_var("PPID", getppid().to_string()); env::set_var("PPID", getppid().to_string());
env::set_var("TMPDIR", "/tmp"); env::set_var("TMPDIR", "/tmp");
env::set_var("TERM", term); env::set_var("TERM", term);
env::set_var("LANG", "en_US.UTF-8"); env::set_var("LANG", "en_US.UTF-8");
env::set_var("USER", username.clone()); env::set_var("USER", username.clone());
env::set_var("LOGNAME", username); env::set_var("LOGNAME", username);
env::set_var("PWD", pathbuf_to_string(std::env::current_dir())); env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
env::set_var("HOME", home.clone()); env::set_var("HOME", home.clone());
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env::set_var("FERN_HIST",format!("{}/.fernhist",home)); env::set_var("FERN_HIST", format!("{}/.fernhist", home));
env::set_var("FERN_RC",format!("{}/.fernrc",home)); env::set_var("FERN_RC", format!("{}/.fernrc", home));
} }
pub fn init_sh_argv(&mut self) { pub fn init_sh_argv(&mut self) {
for arg in env::args() { for arg in env::args() {
self.bpush_arg(arg); self.bpush_arg(arg);
} }
} }
pub fn update_exports(&mut self) { pub fn update_exports(&mut self) {
for var_name in self.vars.keys() { for var_name in self.vars.keys() {
let var = self.vars.get(var_name).unwrap(); let var = self.vars.get(var_name).unwrap();
if var.export { if var.export {
env::set_var(var_name, &var.value); env::set_var(var_name, &var.value);
} else { } else {
env::set_var(var_name, ""); env::set_var(var_name, "");
} }
} }
} }
pub fn sh_argv(&self) -> &VecDeque<String> { pub fn sh_argv(&self) -> &VecDeque<String> {
&self.sh_argv &self.sh_argv
} }
pub fn sh_argv_mut(&mut self) -> &mut VecDeque<String> { pub fn sh_argv_mut(&mut self) -> &mut VecDeque<String> {
&mut self.sh_argv &mut self.sh_argv
} }
pub fn clear_args(&mut self) { pub fn clear_args(&mut self) {
self.sh_argv.clear(); self.sh_argv.clear();
// Push the current exe again // Push the current exe again
// This makes sure that $0 is always the current shell, no matter what // This makes sure that $0 is always the current shell, no matter what
// It also updates the arg parameters '@' and '#' as well // It also updates the arg parameters '@' and '#' as well
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string()); self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
} }
fn update_arg_params(&mut self) { fn update_arg_params(&mut self) {
self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" ")); self.set_param("@", &self.sh_argv.clone().to_vec()[1..].join(" "));
self.set_param("#", &(self.sh_argv.len() - 1).to_string()); self.set_param("#", &(self.sh_argv.len() - 1).to_string());
} }
/// Push an arg to the front of the arg deque /// Push an arg to the front of the arg deque
pub fn fpush_arg(&mut self, arg: String) { pub fn fpush_arg(&mut self, arg: String) {
self.sh_argv.push_front(arg); self.sh_argv.push_front(arg);
self.update_arg_params(); self.update_arg_params();
} }
/// Push an arg to the back of the arg deque /// Push an arg to the back of the arg deque
pub fn bpush_arg(&mut self, arg: String) { pub fn bpush_arg(&mut self, arg: String) {
self.sh_argv.push_back(arg); self.sh_argv.push_back(arg);
self.update_arg_params(); self.update_arg_params();
} }
/// Pop an arg from the front of the arg deque /// Pop an arg from the front of the arg deque
pub fn fpop_arg(&mut self) -> Option<String> { pub fn fpop_arg(&mut self) -> Option<String> {
let arg = self.sh_argv.pop_front(); let arg = self.sh_argv.pop_front();
self.update_arg_params(); self.update_arg_params();
arg arg
} }
/// Pop an arg from the back of the arg deque /// Pop an arg from the back of the arg deque
pub fn bpop_arg(&mut self) -> Option<String> { pub fn bpop_arg(&mut self) -> Option<String> {
let arg = self.sh_argv.pop_back(); let arg = self.sh_argv.pop_back();
self.update_arg_params(); self.update_arg_params();
arg arg
} }
pub fn vars(&self) -> &HashMap<String,Var> { pub fn vars(&self) -> &HashMap<String, Var> {
&self.vars &self.vars
} }
pub fn vars_mut(&mut self) -> &mut HashMap<String,Var> { pub fn vars_mut(&mut self) -> &mut HashMap<String, Var> {
&mut self.vars &mut self.vars
} }
pub fn params(&self) -> &HashMap<String,String> { pub fn params(&self) -> &HashMap<String, String> {
&self.params &self.params
} }
pub fn params_mut(&mut self) -> &mut HashMap<String,String> { pub fn params_mut(&mut self) -> &mut HashMap<String, String> {
&mut self.params &mut self.params
} }
pub fn export_var(&mut self, var_name: &str) { pub fn export_var(&mut self, var_name: &str) {
if let Some(var) = self.vars.get_mut(var_name) { if let Some(var) = self.vars.get_mut(var_name) {
var.mark_for_export(); var.mark_for_export();
env::set_var(var_name, &var.value); env::set_var(var_name, &var.value);
} }
} }
pub fn get_var(&self, var: &str) -> String { pub fn get_var(&self, var: &str) -> String {
if var.chars().count() == 1 || if var.chars().count() == 1 || var.parse::<usize>().is_ok() {
var.parse::<usize>().is_ok() { let param = self.get_param(var);
let param = self.get_param(var); if !param.is_empty() {
if !param.is_empty() { return param;
return param }
} }
} if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) { var
var } else {
} else { std::env::var(var).unwrap_or_default()
std::env::var(var).unwrap_or_default() }
} }
} pub fn set_var(&mut self, var_name: &str, val: &str, export: bool) {
pub fn set_var(&mut self, var_name: &str, val: &str, export: bool) { if let Some(var) = self.vars.get_mut(var_name) {
if let Some(var) = self.vars.get_mut(var_name) { var.value = val.to_string();
var.value = val.to_string(); if var.export {
if var.export { env::set_var(var_name, val);
env::set_var(var_name, val); }
} } else {
} else { let mut var = Var::new(val.to_string());
let mut var = Var::new(val.to_string()); if export {
if export { var.mark_for_export();
var.mark_for_export(); env::set_var(var_name, &*var);
env::set_var(var_name, &*var); }
} self.vars.insert(var_name.to_string(), var);
self.vars.insert(var_name.to_string(), var); }
} }
} pub fn var_exists(&self, var_name: &str) -> bool {
pub fn var_exists(&self, var_name: &str) -> bool { if var_name.parse::<usize>().is_ok() {
if var_name.parse::<usize>().is_ok() { return self.params.contains_key(var_name);
return self.params.contains_key(var_name); }
} self.vars.contains_key(var_name) || (var_name.len() == 1 && self.params.contains_key(var_name))
self.vars.contains_key(var_name) || }
( pub fn set_param(&mut self, param: &str, val: &str) {
var_name.len() == 1 && self.params.insert(param.to_string(), val.to_string());
self.params.contains_key(var_name) }
) pub fn get_param(&self, param: &str) -> String {
} if param.parse::<usize>().is_ok() {
pub fn set_param(&mut self, param: &str, val: &str) { let argv_idx = param.to_string().parse::<usize>().unwrap();
self.params.insert(param.to_string(),val.to_string()); let arg = self
} .sh_argv
pub fn get_param(&self, param: &str) -> String { .get(argv_idx)
if param.parse::<usize>().is_ok() { .map(|s| s.to_string())
let argv_idx = param .unwrap_or_default();
.to_string() arg
.parse::<usize>() } else if param == "?" {
.unwrap(); self
let arg = self.sh_argv.get(argv_idx).map(|s| s.to_string()).unwrap_or_default(); .params
arg .get(param)
} else if param == "?" { .map(|s| s.to_string())
self.params.get(param).map(|s| s.to_string()).unwrap_or("0".into()) .unwrap_or("0".into())
} else { } else {
self.params.get(param).map(|s| s.to_string()).unwrap_or_default() self
} .params
} .get(param)
.map(|s| s.to_string())
.unwrap_or_default()
}
}
} }
/// A table of metadata for the shell /// A table of metadata for the shell
#[derive(Default,Debug)] #[derive(Default, Debug)]
pub struct MetaTab { pub struct MetaTab {
runtime_start: Option<Instant> runtime_start: Option<Instant>,
} }
impl MetaTab { impl MetaTab {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn start_timer(&mut self) { pub fn start_timer(&mut self) {
self.runtime_start = Some(Instant::now()); self.runtime_start = Some(Instant::now());
} }
pub fn stop_timer(&mut self) -> Option<Duration> { pub fn stop_timer(&mut self) -> Option<Duration> {
self.runtime_start self
.take() // runtime_start returns to None .runtime_start
.map(|start| start.elapsed()) // return the duration, if any .take() // runtime_start returns to None
} .map(|start| start.elapsed()) // return the duration, if any
}
} }
/// Read from the job table /// Read from the job table
pub fn read_jobs<T, F: FnOnce(RwLockReadGuard<JobTab>) -> T>(f: F) -> T { pub fn read_jobs<T, F: FnOnce(RwLockReadGuard<JobTab>) -> T>(f: F) -> T {
let lock = JOB_TABLE.read().unwrap(); let lock = JOB_TABLE.read().unwrap();
f(lock) f(lock)
} }
/// Write to the job table /// Write to the job table
pub fn write_jobs<T, F: FnOnce(&mut RwLockWriteGuard<JobTab>) -> T>(f: F) -> T { pub fn write_jobs<T, F: FnOnce(&mut RwLockWriteGuard<JobTab>) -> T>(f: F) -> T {
let lock = &mut JOB_TABLE.write().unwrap(); let lock = &mut JOB_TABLE.write().unwrap();
f(lock) f(lock)
} }
/// Read from the variable table /// Read from the variable table
pub fn read_vars<T, F: FnOnce(RwLockReadGuard<VarTab>) -> T>(f: F) -> T { pub fn read_vars<T, F: FnOnce(RwLockReadGuard<VarTab>) -> T>(f: F) -> T {
let lock = VAR_TABLE.read().unwrap(); let lock = VAR_TABLE.read().unwrap();
f(lock) f(lock)
} }
/// Write to the variable table /// Write to the variable table
pub fn write_vars<T, F: FnOnce(&mut RwLockWriteGuard<VarTab>) -> T>(f: F) -> T { pub fn write_vars<T, F: FnOnce(&mut RwLockWriteGuard<VarTab>) -> T>(f: F) -> T {
let lock = &mut VAR_TABLE.write().unwrap(); let lock = &mut VAR_TABLE.write().unwrap();
f(lock) f(lock)
} }
pub fn read_meta<T, F: FnOnce(RwLockReadGuard<MetaTab>) -> T>(f: F) -> T { pub fn read_meta<T, F: FnOnce(RwLockReadGuard<MetaTab>) -> T>(f: F) -> T {
let lock = META_TABLE.read().unwrap(); let lock = META_TABLE.read().unwrap();
f(lock) f(lock)
} }
/// Write to the variable table /// Write to the variable table
pub fn write_meta<T, F: FnOnce(&mut RwLockWriteGuard<MetaTab>) -> T>(f: F) -> T { pub fn write_meta<T, F: FnOnce(&mut RwLockWriteGuard<MetaTab>) -> T>(f: F) -> T {
let lock = &mut META_TABLE.write().unwrap(); let lock = &mut META_TABLE.write().unwrap();
f(lock) f(lock)
} }
/// Read from the logic table /// Read from the logic table
pub fn read_logic<T, F: FnOnce(RwLockReadGuard<LogTab>) -> T>(f: F) -> T { pub fn read_logic<T, F: FnOnce(RwLockReadGuard<LogTab>) -> T>(f: F) -> T {
let lock = LOGIC_TABLE.read().unwrap(); let lock = LOGIC_TABLE.read().unwrap();
f(lock) f(lock)
} }
/// Write to the logic table /// Write to the logic table
pub fn write_logic<T, F: FnOnce(&mut RwLockWriteGuard<LogTab>) -> T>(f: F) -> T { pub fn write_logic<T, F: FnOnce(&mut RwLockWriteGuard<LogTab>) -> T>(f: F) -> T {
let lock = &mut LOGIC_TABLE.write().unwrap(); let lock = &mut LOGIC_TABLE.write().unwrap();
f(lock) f(lock)
} }
pub fn read_shopts<T, F: FnOnce(RwLockReadGuard<ShOpts>) -> T>(f: F) -> T { pub fn read_shopts<T, F: FnOnce(RwLockReadGuard<ShOpts>) -> T>(f: F) -> T {
let lock = SHOPTS.read().unwrap(); let lock = SHOPTS.read().unwrap();
f(lock) f(lock)
} }
pub fn write_shopts<T, F: FnOnce(&mut RwLockWriteGuard<ShOpts>) -> T>(f: F) -> T { pub fn write_shopts<T, F: FnOnce(&mut RwLockWriteGuard<ShOpts>) -> T>(f: F) -> T {
let lock = &mut SHOPTS.write().unwrap(); let lock = &mut SHOPTS.write().unwrap();
f(lock) f(lock)
} }
/// This function is used internally and ideally never sees user input /// This function is used internally and ideally never sees user input
/// ///
/// It will panic if you give it an invalid path. /// It will panic if you give it an invalid path.
pub fn get_shopt(path: &str) -> String { pub fn get_shopt(path: &str) -> String {
read_shopts(|s| s.get(path)).unwrap().unwrap() read_shopts(|s| s.get(path)).unwrap().unwrap()
} }
pub fn get_status() -> i32 { pub fn get_status() -> i32 {
read_vars(|v| v.get_param("?")).parse::<i32>().unwrap() read_vars(|v| v.get_param("?")).parse::<i32>().unwrap()
} }
#[track_caller] #[track_caller]
pub fn set_status(code: i32) { pub fn set_status(code: i32) {
write_vars(|v| v.set_param("?", &code.to_string())) write_vars(|v| v.set_param("?", &code.to_string()))
} }
/// Save the current state of the logic and variable table, and the working directory path /// Save the current state of the logic and variable table, and the working
/// directory path
pub fn get_snapshots() -> (LogTab, VarTab, String) { pub fn get_snapshots() -> (LogTab, VarTab, String) {
( (
read_logic(|l| l.clone()), read_logic(|l| l.clone()),
read_vars(|v| v.clone()), read_vars(|v| v.clone()),
env::var("PWD").unwrap_or_default() env::var("PWD").unwrap_or_default(),
) )
} }
pub fn restore_snapshot(snapshot: (LogTab, VarTab, String)) { pub fn restore_snapshot(snapshot: (LogTab, VarTab, String)) {
write_logic(|l| **l = snapshot.0); write_logic(|l| **l = snapshot.0);
write_vars(|v| { write_vars(|v| {
**v = snapshot.1; **v = snapshot.1;
v.update_exports(); v.update_exports();
}); });
env::set_current_dir(&snapshot.2).unwrap(); env::set_current_dir(&snapshot.2).unwrap();
env::set_var("PWD", &snapshot.2); env::set_var("PWD", &snapshot.2);
} }
pub fn source_rc() -> ShResult<()> { pub fn source_rc() -> ShResult<()> {
let path = if let Ok(path) = env::var("FERN_RC") { let path = if let Ok(path) = env::var("FERN_RC") {
PathBuf::from(&path) PathBuf::from(&path)
} else { } else {
let home = env::var("HOME").unwrap(); let home = env::var("HOME").unwrap();
PathBuf::from(format!("{home}/.fernrc")) PathBuf::from(format!("{home}/.fernrc"))
}; };
if !path.exists() { if !path.exists() {
return Err( return Err(ShErr::simple(ShErrKind::InternalErr, ".fernrc not found"));
ShErr::simple(ShErrKind::InternalErr, ".fernrc not found") }
) source_file(path)
}
source_file(path)
} }
pub fn source_file(path: PathBuf) -> ShResult<()> { pub fn source_file(path: PathBuf) -> ShResult<()> {
let mut file = OpenOptions::new() let mut file = OpenOptions::new().read(true).open(path)?;
.read(true)
.open(path)?;
let mut buf = String::new(); let mut buf = String::new();
file.read_to_string(&mut buf)?; file.read_to_string(&mut buf)?;
exec_input(buf,None)?; exec_input(buf, None)?;
Ok(()) Ok(())
} }

View File

@@ -2,173 +2,175 @@ use super::*;
#[test] #[test]
fn cmd_not_found() { fn cmd_not_found() {
let input = "foo"; let input = "foo";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).next().unwrap().unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
let err = ShErr::full(ShErrKind::CmdNotFound("foo".into()), "", token.span); .next()
.unwrap()
.unwrap();
let err = ShErr::full(ShErrKind::CmdNotFound("foo".into()), "", token.span);
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn unclosed_subsh() { fn unclosed_subsh() {
let input = "(foo"; let input = "(foo";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
let Err(err) = token else { .nth(1)
panic!("{:?}",token); .unwrap();
}; let Err(err) = token else {
panic!("{:?}", token);
};
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn unclosed_dquote() { fn unclosed_dquote() {
let input = "\"foo bar"; let input = "\"foo bar";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
let Err(err) = token else { .nth(1)
panic!(); .unwrap();
}; let Err(err) = token else {
panic!();
};
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn unclosed_squote() { fn unclosed_squote() {
let input = "'foo bar"; let input = "'foo bar";
let token = LexStream::new(Arc::new(input.into()), LexFlags::empty()).nth(1).unwrap(); let token = LexStream::new(Arc::new(input.into()), LexFlags::empty())
let Err(err) = token else { .nth(1)
panic!(); .unwrap();
}; let Err(err) = token else {
panic!();
};
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn unclosed_brc_grp() { fn unclosed_brc_grp() {
let input = "{ foo bar"; let input = "{ foo bar";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(err) = node else { let Err(err) = node else {
panic!(); panic!();
}; };
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn if_no_fi() { fn if_no_fi() {
let input = "if foo; then bar;"; let input = "if foo; then bar;";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() }; let Err(e) = node else { panic!() };
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn if_no_then() { fn if_no_then() {
let input = "if foo; bar; fi"; let input = "if foo; bar; fi";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() }; let Err(e) = node else { panic!() };
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn loop_no_done() { fn loop_no_done() {
let input = "while true; do echo foo;"; let input = "while true; do echo foo;";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() }; let Err(e) = node else { panic!() };
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn loop_no_do() { fn loop_no_do() {
let input = "while true; echo foo; done"; let input = "while true; echo foo; done";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() }; let Err(e) = node else { panic!() };
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn case_no_esac() { fn case_no_esac() {
let input = "case foo in foo) bar;; bar) foo;;"; let input = "case foo in foo) bar;; bar) foo;;";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() }; let Err(e) = node else { panic!() };
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn case_no_in() { fn case_no_in() {
let input = "case foo foo) bar;; bar) foo;; esac"; let input = "case foo foo) bar;; bar) foo;; esac";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let node = ParseStream::new(tokens).next().unwrap(); let node = ParseStream::new(tokens).next().unwrap();
let Err(e) = node else { panic!() }; let Err(e) = node else { panic!() };
let err_fmt = format!("{e}"); let err_fmt = format!("{e}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn error_with_notes() { fn error_with_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed") let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason")) .with_note(Note::new("Execution failed for this reason"))
.with_note(Note::new("Here is how to fix it: blah blah blah")); .with_note(Note::new("Here is how to fix it: blah blah blah"));
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }
#[test] #[test]
fn error_with_notes_and_sub_notes() { fn error_with_notes_and_sub_notes() {
let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed") let err = ShErr::simple(ShErrKind::ExecFail, "Execution failed")
.with_note(Note::new("Execution failed for this reason")) .with_note(Note::new("Execution failed for this reason"))
.with_note( .with_note(Note::new("Here is how to fix it:").with_sub_notes(vec!["blah", "blah", "blah"]));
Note::new("Here is how to fix it:")
.with_sub_notes(vec![
"blah",
"blah",
"blah"
])
);
let err_fmt = format!("{err}"); let err_fmt = format!("{err}");
insta::assert_snapshot!(err_fmt) insta::assert_snapshot!(err_fmt)
} }

View File

@@ -6,297 +6,300 @@ use super::*;
#[test] #[test]
fn simple_expansion() { fn simple_expansion() {
let varsub = "$foo"; let varsub = "$foo";
write_vars(|v| v.set_var("foo", "this is the value of the variable", false)); write_vars(|v| v.set_var("foo", "this is the value of the variable", false));
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty()) let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI)) .filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI))
.collect(); .collect();
let var_tk = tokens.pop().unwrap(); let var_tk = tokens.pop().unwrap();
let exp_tk = var_tk.expand().unwrap(); let exp_tk = var_tk.expand().unwrap();
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
insta::assert_debug_snapshot!(exp_tk.get_words()) insta::assert_debug_snapshot!(exp_tk.get_words())
} }
#[test] #[test]
fn unescape_string() { fn unescape_string() {
let string = "echo $foo \\$bar"; let string = "echo $foo \\$bar";
let unescaped = unescape_str(string); let unescaped = unescape_str(string);
insta::assert_snapshot!(unescaped) insta::assert_snapshot!(unescaped)
} }
#[test] #[test]
fn expand_alias_simple() { fn expand_alias_simple() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
let input = String::from("foo"); let input = String::from("foo");
let result = expand_aliases(input, HashSet::new(), l); let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(),"echo foo"); assert_eq!(result.as_str(), "echo foo");
l.clear_aliases(); l.clear_aliases();
}); });
} }
#[test] #[test]
fn expand_alias_in_if() { fn expand_alias_in_if() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
let input = String::from("if foo; then echo bar; fi"); let input = String::from("if foo; then echo bar; fi");
let result = expand_aliases(input, HashSet::new(), l); let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(),"if echo foo; then echo bar; fi"); assert_eq!(result.as_str(), "if echo foo; then echo bar; fi");
l.clear_aliases(); l.clear_aliases();
}); });
} }
#[test] #[test]
fn expand_alias_multiline() { fn expand_alias_multiline() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "echo bar"); l.insert_alias("bar", "echo bar");
let input = String::from(" let input = String::from(
"
foo foo
if true; then if true; then
bar bar
fi fi
"); ",
let expected = String::from(" );
let expected = String::from(
"
echo foo echo foo
if true; then if true; then
echo bar echo bar
fi fi
"); ",
);
let result = expand_aliases(input, HashSet::new(), l); let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result,expected) assert_eq!(result, expected)
}); });
} }
#[test] #[test]
fn expand_multiple_aliases() { fn expand_multiple_aliases() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "echo bar"); l.insert_alias("bar", "echo bar");
l.insert_alias("biz", "echo biz"); l.insert_alias("biz", "echo biz");
let input = String::from("foo; bar; biz"); let input = String::from("foo; bar; biz");
let result = expand_aliases(input, HashSet::new(), l); let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(),"echo foo; echo bar; echo biz"); assert_eq!(result.as_str(), "echo foo; echo bar; echo biz");
}); });
} }
#[test] #[test]
fn alias_in_arg_position() { fn alias_in_arg_position() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
let input = String::from("echo foo"); let input = String::from("echo foo");
let result = expand_aliases(input.clone(), HashSet::new(), l); let result = expand_aliases(input.clone(), HashSet::new(), l);
assert_eq!(input,result); assert_eq!(input, result);
l.clear_aliases(); l.clear_aliases();
}); });
} }
#[test] #[test]
fn expand_recursive_alias() { fn expand_recursive_alias() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "echo foo"); l.insert_alias("foo", "echo foo");
l.insert_alias("bar", "foo bar"); l.insert_alias("bar", "foo bar");
let input = String::from("bar"); let input = String::from("bar");
let result = expand_aliases(input, HashSet::new(), l); let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(),"echo foo bar"); assert_eq!(result.as_str(), "echo foo bar");
}); });
} }
#[test] #[test]
fn test_infinite_recursive_alias() { fn test_infinite_recursive_alias() {
write_logic(|l| { write_logic(|l| {
l.insert_alias("foo", "foo bar"); l.insert_alias("foo", "foo bar");
let input = String::from("foo");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(),"foo bar");
l.clear_aliases();
});
let input = String::from("foo");
let result = expand_aliases(input, HashSet::new(), l);
assert_eq!(result.as_str(), "foo bar");
l.clear_aliases();
});
} }
#[test] #[test]
fn param_expansion_defaultunsetornull() { fn param_expansion_defaultunsetornull() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
v.set_var("set_var", "value", false); v.set_var("set_var", "value", false);
}); });
let result = perform_param_expansion("unset:-default").unwrap(); let result = perform_param_expansion("unset:-default").unwrap();
assert_eq!(result, "default"); assert_eq!(result, "default");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_defaultunset() { fn param_expansion_defaultunset() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
v.set_var("set_var", "value", false); v.set_var("set_var", "value", false);
}); });
let result = perform_param_expansion("unset-default").unwrap(); let result = perform_param_expansion("unset-default").unwrap();
assert_eq!(result, "default"); assert_eq!(result, "default");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_setdefaultunsetornull() { fn param_expansion_setdefaultunsetornull() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
v.set_var("set_var", "value", false); v.set_var("set_var", "value", false);
}); });
let result = perform_param_expansion("unset:=assigned").unwrap(); let result = perform_param_expansion("unset:=assigned").unwrap();
assert_eq!(result, "assigned"); assert_eq!(result, "assigned");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_setdefaultunset() { fn param_expansion_setdefaultunset() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
v.set_var("set_var", "value", false); v.set_var("set_var", "value", false);
}); });
let result = perform_param_expansion("unset=assigned").unwrap(); let result = perform_param_expansion("unset=assigned").unwrap();
assert_eq!(result, "assigned"); assert_eq!(result, "assigned");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_altsetnotnull() { fn param_expansion_altsetnotnull() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
v.set_var("set_var", "value", false); v.set_var("set_var", "value", false);
}); });
let result = perform_param_expansion("set_var:+alt").unwrap(); let result = perform_param_expansion("set_var:+alt").unwrap();
assert_eq!(result, "alt"); assert_eq!(result, "alt");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_altnotnull() { fn param_expansion_altnotnull() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
v.set_var("set_var", "value", false); v.set_var("set_var", "value", false);
}); });
let result = perform_param_expansion("set_var+alt").unwrap(); let result = perform_param_expansion("set_var+alt").unwrap();
assert_eq!(result, "alt"); assert_eq!(result, "alt");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_len() { fn param_expansion_len() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("#foo").unwrap(); let result = perform_param_expansion("#foo").unwrap();
assert_eq!(result, "3"); assert_eq!(result, "3");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_substr() { fn param_expansion_substr() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo:1").unwrap(); let result = perform_param_expansion("foo:1").unwrap();
assert_eq!(result, "oo"); assert_eq!(result, "oo");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_substrlen() { fn param_expansion_substrlen() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo:0:2").unwrap(); let result = perform_param_expansion("foo:0:2").unwrap();
assert_eq!(result, "fo"); assert_eq!(result, "fo");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_remshortestprefix() { fn param_expansion_remshortestprefix() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo#f*").unwrap(); let result = perform_param_expansion("foo#f*").unwrap();
assert_eq!(result, "oo"); assert_eq!(result, "oo");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_remlongestprefix() { fn param_expansion_remlongestprefix() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo##f*").unwrap(); let result = perform_param_expansion("foo##f*").unwrap();
assert_eq!(result, ""); assert_eq!(result, "");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_remshortestsuffix() { fn param_expansion_remshortestsuffix() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo%*o").unwrap(); let result = perform_param_expansion("foo%*o").unwrap();
assert_eq!(result, "fo"); assert_eq!(result, "fo");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_remlongestsuffix() { fn param_expansion_remlongestsuffix() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo%%*o").unwrap(); let result = perform_param_expansion("foo%%*o").unwrap();
assert_eq!(result, ""); assert_eq!(result, "");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_replacefirstmatch() { fn param_expansion_replacefirstmatch() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo/foo/X").unwrap(); let result = perform_param_expansion("foo/foo/X").unwrap();
assert_eq!(result, "X"); assert_eq!(result, "X");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_replaceallmatches() { fn param_expansion_replaceallmatches() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo//o/X").unwrap(); let result = perform_param_expansion("foo//o/X").unwrap();
assert_eq!(result, "fXX"); assert_eq!(result, "fXX");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_replaceprefix() { fn param_expansion_replaceprefix() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo/#f/X").unwrap(); let result = perform_param_expansion("foo/#f/X").unwrap();
assert_eq!(result, "Xoo"); assert_eq!(result, "Xoo");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }
#[test] #[test]
fn param_expansion_replacesuffix() { fn param_expansion_replacesuffix() {
write_vars(|v| { write_vars(|v| {
v.set_var("foo", "foo", false); v.set_var("foo", "foo", false);
}); });
let result = perform_param_expansion("foo/%o/X").unwrap(); let result = perform_param_expansion("foo/%o/X").unwrap();
assert_eq!(result, "foX"); assert_eq!(result, "foX");
write_vars(|v| v.vars_mut().clear()); write_vars(|v| v.vars_mut().clear());
} }

View File

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

View File

@@ -1,52 +1,52 @@
use super::*; use super::*;
#[test] #[test]
fn lex_simple() { fn lex_simple() {
let input = "echo hello world"; let input = "echo hello world";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }
#[test] #[test]
fn lex_redir() { fn lex_redir() {
let input = "echo foo > bar.txt"; let input = "echo foo > bar.txt";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }
#[test] #[test]
fn lex_redir_fds() { fn lex_redir_fds() {
let input = "echo foo 1>&2"; let input = "echo foo 1>&2";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }
#[test] #[test]
fn lex_quote_str() { fn lex_quote_str() {
let input = "echo \"foo bar\" biz baz"; let input = "echo \"foo bar\" biz baz";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }
#[test] #[test]
fn lex_with_keywords() { fn lex_with_keywords() {
let input = "if true; then echo foo; fi"; let input = "if true; then echo foo; fi";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }
#[test] #[test]
fn lex_multiline() { fn lex_multiline() {
let input = "echo hello world\necho foo bar\necho boo biz"; let input = "echo hello world\necho foo bar\necho boo biz";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }
#[test] #[test]
fn lex_case() { fn lex_case() {
let input = "case $foo in foo) bar;; bar) foo;; biz) baz;; esac"; let input = "case $foo in foo) bar;; bar) foo;; biz) baz;; esac";
let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect(); let tokens: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()).collect();
insta::assert_debug_snapshot!(tokens) insta::assert_debug_snapshot!(tokens)
} }

View File

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

View File

@@ -2,95 +2,95 @@ use super::*;
#[test] #[test]
fn parse_simple() { fn parse_simple() {
let input = "echo hello world"; let input = "echo hello world";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_pipeline() { fn parse_pipeline() {
let input = "echo foo | sed s/foo/bar"; let input = "echo foo | sed s/foo/bar";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_conjunction() { fn parse_conjunction() {
let input = "echo foo && echo bar"; let input = "echo foo && echo bar";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_conjunction_and_pipeline() { fn parse_conjunction_and_pipeline() {
let input = "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/"; let input = "echo foo | sed s/foo/bar/ && echo bar | sed s/bar/foo/ || echo foo bar | sed s/foo bar/bar foo/";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_multiline() { fn parse_multiline() {
let input = " let input = "
echo hello world echo hello world
echo foo bar echo foo bar
echo boo biz"; echo boo biz";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_if_simple() { fn parse_if_simple() {
let input = "if foo; then echo bar; fi"; let input = "if foo; then echo bar; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_if_with_elif() { fn parse_if_with_elif() {
let input = "if foo; then echo bar; elif bar; then echo foo; fi"; let input = "if foo; then echo bar; elif bar; then echo foo; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_if_multiple_elif() { fn parse_if_multiple_elif() {
let input = "if foo; then echo bar; elif bar; then echo foo; elif biz; then echo baz; fi"; let input = "if foo; then echo bar; elif bar; then echo foo; elif biz; then echo baz; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_if_multiline() { fn parse_if_multiline() {
let input = " let input = "
if foo; then if foo; then
echo bar echo bar
elif bar; then elif bar; then
@@ -98,59 +98,59 @@ elif bar; then
elif biz; then elif biz; then
echo baz echo baz
fi"; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_loop_simple() { fn parse_loop_simple() {
let input = "while foo; do bar; done"; let input = "while foo; do bar; done";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_loop_until() { fn parse_loop_until() {
let input = "until foo; do bar; done"; let input = "until foo; do bar; done";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_loop_multiline() { fn parse_loop_multiline() {
let input = " let input = "
until foo; do until foo; do
bar bar
done"; done";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_case_simple() { fn parse_case_simple() {
let input = "case foo in foo) bar;; bar) foo;; biz) baz;; esac"; let input = "case foo in foo) bar;; bar) foo;; biz) baz;; esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_case_multiline() { fn parse_case_multiline() {
let input = "case foo in let input = "case foo in
foo) bar foo) bar
;; ;;
bar) foo bar) foo
@@ -158,16 +158,16 @@ fn parse_case_multiline() {
biz) baz biz) baz
;; ;;
esac"; esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_case_nested() { fn parse_case_nested() {
let input = "case foo in let input = "case foo in
foo) foo)
if true; then if true; then
while true; do while true; do
@@ -194,41 +194,41 @@ fn parse_case_nested() {
fi fi
;; ;;
esac"; esac";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn parse_cursed() { fn parse_cursed() {
let input = "if if if if case foo in foo) if true; then true; fi;; esac; then case foo in foo) until true; do true; done;; esac; fi; then until if case foo in foo) true;; esac; then if true; then true; fi; fi; do until until true; do true; done; do case foo in foo) true;; esac; done; done; fi; then until until case foo in foo) true;; esac; do if true; then true; fi; done; do until true; do true; done; done; fi; then until case foo in foo) case foo in foo) true;; esac;; esac; do if if true; then true; fi; then until true; do true; done; fi; done; elif until until case foo in foo) true;; esac; do if true; then true; fi; done; do case foo in foo) until true; do true; done;; esac; done; then case foo in foo) if case foo in foo) true;; esac; then if true; then true; fi; fi;; esac; else case foo in foo) until until true; do true; done; do case foo in foo) true;; esac; done;; esac; fi"; let input = "if if if if case foo in foo) if true; then true; fi;; esac; then case foo in foo) until true; do true; done;; esac; fi; then until if case foo in foo) true;; esac; then if true; then true; fi; fi; do until until true; do true; done; do case foo in foo) true;; esac; done; done; fi; then until until case foo in foo) true;; esac; do if true; then true; fi; done; do until true; do true; done; done; fi; then until case foo in foo) case foo in foo) true;; esac;; esac; do if if true; then true; fi; then until true; do true; done; fi; done; elif until until case foo in foo) true;; esac; do if true; then true; fi; done; do case foo in foo) until true; do true; done;; esac; done; then case foo in foo) if case foo in foo) true;; esac; then if true; then true; fi; fi;; esac; else case foo in foo) until until true; do true; done; do case foo in foo) true;; esac; done;; esac; fi";
let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty()) let tk_stream: Vec<_> = LexStream::new(Arc::new(input.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect(); let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
// 15,000 line snapshot file btw // 15,000 line snapshot file btw
insta::assert_debug_snapshot!(nodes) insta::assert_debug_snapshot!(nodes)
} }
#[test] #[test]
fn test_node_operation() { fn test_node_operation() {
let input = String::from("echo hello world; echo foo bar"); let input = String::from("echo hello world; echo foo bar");
let mut check_nodes = vec![]; let mut check_nodes = vec![];
let tokens: Vec<Tk> = LexStream::new(input.into(), LexFlags::empty()) let tokens: Vec<Tk> = LexStream::new(input.into(), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
.collect(); .collect();
let nodes = ParseStream::new(tokens) let nodes = ParseStream::new(tokens).map(|nd| nd.unwrap());
.map(|nd| nd.unwrap());
for mut node in nodes { for mut node in nodes {
node_operation(&mut node, node_operation(
&|node: &Node| matches!(node.class, NdRule::Command {..}), &mut node,
&mut |node: &mut Node| check_nodes.push(node.clone()), &|node: &Node| matches!(node.class, NdRule::Command { .. }),
); &mut |node: &mut Node| check_nodes.push(node.clone()),
} );
insta::assert_debug_snapshot!(check_nodes) }
insta::assert_debug_snapshot!(check_nodes)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -4,49 +4,48 @@ use pretty_assertions::assert_eq;
use super::super::*; use super::super::*;
fn get_script_output(name: &str, args: &[&str]) -> Output { fn get_script_output(name: &str, args: &[&str]) -> Output {
// Resolve the path to the fern binary. // Resolve the path to the fern binary.
// Do not question me. // Do not question me.
let mut fern_path = env::current_exe() let mut fern_path = env::current_exe().expect("Failed to get test executable"); // The path to the test executable
.expect("Failed to get test executable"); // The path to the test executable fern_path.pop(); // Hocus pocus
fern_path.pop(); // Hocus pocus fern_path.pop();
fern_path.pop(); fern_path.push("fern"); // Abra Kadabra
fern_path.push("fern"); // Abra Kadabra
if !fern_path.is_file() { if !fern_path.is_file() {
fern_path.pop(); fern_path.pop();
fern_path.pop(); fern_path.pop();
fern_path.push("release"); fern_path.push("release");
fern_path.push("fern"); fern_path.push("fern");
} }
if !fern_path.is_file() { if !fern_path.is_file() {
panic!("where the hell is the binary") panic!("where the hell is the binary")
} }
process::Command::new(fern_path) // Alakazam process::Command::new(fern_path) // Alakazam
.arg(name) .arg(name)
.args(args) .args(args)
.output() .output()
.expect("Failed to run script") .expect("Failed to run script")
} }
#[test] #[test]
fn script_hello_world() { fn script_hello_world() {
let output = get_script_output("./test_scripts/hello.sh", &[]); let output = get_script_output("./test_scripts/hello.sh", &[]);
assert!(output.status.success()); assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "Hello, World!") assert_eq!(stdout.trim(), "Hello, World!")
} }
#[test] #[test]
fn script_cmdsub() { fn script_cmdsub() {
let output = get_script_output("./test_scripts/cmdsub.sh", &[]); let output = get_script_output("./test_scripts/cmdsub.sh", &[]);
assert!(output.status.success()); assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "foo Hello bar") assert_eq!(stdout.trim(), "foo Hello bar")
} }
#[test] #[test]
fn script_multiline() { fn script_multiline() {
let output = get_script_output("./test_scripts/multiline.sh", &[]); let output = get_script_output("./test_scripts/multiline.sh", &[]);
assert!(output.status.success()); assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
assert_eq!(stdout.trim(), "foo\nbar\nbiz\nbuzz") assert_eq!(stdout.trim(), "foo\nbar\nbiz\nbuzz")
} }

View File

@@ -3,39 +3,41 @@ use libsh::term::{Style, StyleSet, Styled};
use super::super::*; use super::super::*;
#[test] #[test]
fn styled_simple() { fn styled_simple() {
let input = "hello world"; let input = "hello world";
let styled = input.styled(Style::Green); let styled = input.styled(Style::Green);
insta::assert_snapshot!(styled) insta::assert_snapshot!(styled)
} }
#[test] #[test]
fn styled_multiple() { fn styled_multiple() {
let input = "styled text"; let input = "styled text";
let styled = input.styled(Style::Red | Style::Bold | Style::Underline); let styled = input.styled(Style::Red | Style::Bold | Style::Underline);
insta::assert_snapshot!(styled); insta::assert_snapshot!(styled);
} }
#[test] #[test]
fn styled_rgb() { fn styled_rgb() {
let input = "RGB styled text"; let input = "RGB styled text";
let styled = input.styled(Style::RGB(255, 99, 71)); // Tomato color let styled = input.styled(Style::RGB(255, 99, 71)); // Tomato color
insta::assert_snapshot!(styled); insta::assert_snapshot!(styled);
} }
#[test] #[test]
fn styled_background() { fn styled_background() {
let input = "text with background"; let input = "text with background";
let styled = input.styled(Style::BgBlue | Style::Bold); let styled = input.styled(Style::BgBlue | Style::Bold);
insta::assert_snapshot!(styled); insta::assert_snapshot!(styled);
} }
#[test] #[test]
fn styled_set() { fn styled_set() {
let input = "multi-style text"; let input = "multi-style text";
let style_set = StyleSet::new().add_style(Style::Magenta).add_style(Style::Italic); let style_set = StyleSet::new()
let styled = input.styled(style_set); .add_style(Style::Magenta)
insta::assert_snapshot!(styled); .add_style(Style::Italic);
let styled = input.styled(style_set);
insta::assert_snapshot!(styled);
} }
#[test] #[test]
fn styled_reset() { fn styled_reset() {
let input = "reset test"; let input = "reset test";
let styled = input.styled(Style::Bold | Style::Reset); let styled = input.styled(Style::Bold | Style::Reset);
insta::assert_snapshot!(styled); insta::assert_snapshot!(styled);
} }