This commit is contained in:
2025-03-15 00:02:05 -04:00
parent 34cc2b3976
commit 97b4b1835d
75 changed files with 4335 additions and 7918 deletions

View File

@@ -1,19 +0,0 @@
use crate::prelude::*;
pub fn alias(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let argv = argv.drop_first();
let mut argv_iter = argv.into_iter();
while let Some(arg) = argv_iter.next() {
let arg_raw = shenv.input_slice(arg.span()).to_string();
if let Some((alias,body)) = arg_raw.split_once('=') {
let clean_body = clean_string(&body);
shenv.logic_mut().set_alias(alias, &clean_body);
} else {
return Err(ShErr::full(ShErrKind::SyntaxErr, "Expected an assignment in alias args", shenv.get_input(), arg.span().clone()))
}
}
} else { unreachable!() }
Ok(())
}

View File

@@ -1,16 +0,0 @@
use crate::prelude::*;
pub fn cd(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let mut argv_iter = argv.into_iter();
argv_iter.next(); // Ignore 'cd'
let dir_raw = argv_iter.next().map(|arg| shenv.input_slice(arg.span()).into()).unwrap_or(std::env::var("HOME")?);
let dir = PathBuf::from(&dir_raw);
std::env::set_current_dir(dir)?;
let new_dir = std::env::current_dir()?;
shenv.vars_mut().export("PWD",new_dir.to_str().unwrap());
shenv.set_code(0);
}
Ok(())
}

View File

@@ -1,20 +0,0 @@
use crate::prelude::*;
pub fn sh_flow(node: Node, shenv: &mut ShEnv, kind: ShErrKind) -> ShResult<()> {
let rule = node.into_rule();
let mut code: i32 = 0;
if let NdRule::Command { argv, redirs } = rule {
let mut argv_iter = argv.into_iter();
while let Some(arg) = argv_iter.next() {
if let Ok(code_arg) = shenv.input_slice(arg.span()).parse() {
code = code_arg
}
}
} else { unreachable!() }
shenv.set_code(code);
// Our control flow keywords are used as ShErrKinds
// This design will halt the execution flow and start heading straight back upward
// Function returns and loop breaks/continues will be caught in the proper context to allow
// Execution to continue at the proper return point.
Err(ShErr::simple(kind, ""))
}

View File

@@ -1,86 +0,0 @@
use shellenv::jobs::{ChildProc, JobBldr};
use crate::prelude::*;
bitflags! {
#[derive(Debug,Clone,Copy)]
pub struct EchoFlags: u32 {
const USE_ESCAPE = 0b0001;
const NO_ESCAPE = 0b0010;
const STDERR = 0b0100;
const NO_NEWLINE = 0b1000;
}
}
pub fn echo(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let mut argv_iter = argv.into_iter().skip(1).peekable();
let mut echo_flags = EchoFlags::empty();
while let Some(arg) = argv_iter.peek() {
let blame = arg.span();
let raw = arg.as_raw(shenv);
if raw.starts_with('-') {
let _ = argv_iter.next();
let mut options = raw.strip_prefix('-').unwrap().chars();
while let Some(opt) = options.next() {
match opt {
'r' => echo_flags |= EchoFlags::STDERR,
'n' => echo_flags |= EchoFlags::NO_NEWLINE,
'e' => {
if echo_flags.contains(EchoFlags::NO_ESCAPE) {
return Err(
ShErr::full(
ShErrKind::ExecFail,
"the 'e' and 'E' flags are mutually exclusive",
shenv.get_input(),
blame
)
)
}
echo_flags |= EchoFlags::USE_ESCAPE;
}
'E' => {
if echo_flags.contains(EchoFlags::USE_ESCAPE) {
return Err(
ShErr::full(
ShErrKind::ExecFail,
"the 'e' and 'E' flags are mutually exclusive",
shenv.get_input(),
blame
)
)
}
echo_flags |= EchoFlags::NO_ESCAPE;
}
_ => return Err(
ShErr::full(
ShErrKind::ExecFail,
format!("Unrecognized echo option"),
shenv.get_input(),
blame
)
)
}
}
} else {
break
}
}
let mut argv = argv_iter.collect::<Vec<_>>().as_strings(shenv);
argv.retain(|arg| arg != "\n");
log!(DEBUG,argv);
let mut formatted = argv.join(" ");
if !echo_flags.contains(EchoFlags::NO_NEWLINE) {
formatted.push('\n');
}
shenv.collect_redirs(redirs);
log!(DEBUG,"{:?}",shenv.ctx().redirs());
shenv.ctx_mut().activate_rdrs()?;
write_out(formatted)?;
} else { unreachable!() }
Ok(())
}

View File

@@ -1,18 +0,0 @@
use crate::prelude::*;
pub fn export(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let mut argv_iter = argv.into_iter();
argv_iter.next(); // Ignore 'export'
while let Some(arg) = argv_iter.next() {
let arg_raw = arg.as_raw(shenv);
if let Some((var,val)) = arg_raw.split_once('=') {
shenv.vars_mut().export(var, &clean_string(val));
} else {
eprintln!("Expected an assignment in export args, found this: {}", arg_raw)
}
}
} else { unreachable!() }
Ok(())
}

View File

@@ -1,170 +0,0 @@
use shellenv::jobs::JobCmdFlags;
use crate::prelude::*;
pub fn continue_job(node: Node, shenv: &mut ShEnv, fg: bool) -> ShResult<()> {
let blame = node.span();
let cmd = if fg { "fg" } else { "bg" };
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let mut argv_s = argv.drop_first().as_strings(shenv).into_iter();
if read_jobs(|j| j.get_fg().is_some()) {
return Err(
ShErr::full(
ShErrKind::InternalErr,
format!("Somehow called {} with an existing foreground job",cmd),
shenv.get_input(),
blame
)
)
}
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id
} else {
return Err(ShErr::full(ShErrKind::ExecFail, "No jobs found", shenv.get_input(), blame))
};
let tabid = match argv_s.next() {
Some(arg) => parse_job_id(&arg, blame.clone(),shenv)?,
None => curr_job_id
};
let mut job = write_jobs(|j| {
let id = JobID::TableID(tabid);
let query_result = j.query(id.clone());
if query_result.is_some() {
Ok(j.remove_job(id.clone()).unwrap())
} else {
Err(
ShErr::full(
ShErrKind::ExecFail,
format!("Job id `{}' not found", tabid),
shenv.get_input(),
blame
)
)
}
})?;
job.killpg(Signal::SIGCONT)?;
if fg {
write_jobs(|j| j.new_fg(job))?;
} else {
let job_order = read_jobs(|j| j.order().to_vec());
write(borrow_fd(1), job.display(&job_order, JobCmdFlags::PIDS).as_bytes())?;
write_jobs(|j| j.insert_job(job, true))?;
}
shenv.set_code(0);
} else { unreachable!() }
Ok(())
}
fn parse_job_id(arg: &str, blame: Rc<RefCell<Span>>, shenv: &mut ShEnv) -> ShResult<usize> {
if arg.starts_with('%') {
let arg = arg.strip_prefix('%').unwrap();
if arg.chars().all(|ch| ch.is_ascii_digit()) {
Ok(arg.parse::<usize>().unwrap())
} else {
let result = write_jobs(|j| {
let query_result = j.query(JobID::Command(arg.into()));
query_result.map(|job| job.tabid().unwrap())
});
match result {
Some(id) => Ok(id),
None => Err(
ShErr::full(
ShErrKind::InternalErr,
"Found a job but no table id in parse_job_id()",
shenv.get_input(),
blame
)
)
}
}
} else if arg.chars().all(|ch| ch.is_ascii_digit()) {
let result = write_jobs(|j| {
let pgid_query_result = j.query(JobID::Pgid(Pid::from_raw(arg.parse::<i32>().unwrap())));
if let Some(job) = pgid_query_result {
return Some(job.tabid().unwrap())
}
if arg.parse::<i32>().unwrap() > 0 {
let table_id_query_result = j.query(JobID::TableID(arg.parse::<usize>().unwrap()));
return table_id_query_result.map(|job| job.tabid().unwrap());
}
None
});
match result {
Some(id) => Ok(id),
None => Err(
ShErr::full(
ShErrKind::InternalErr,
"Found a job but no table id in parse_job_id()",
shenv.get_input(),
blame
)
)
}
} else {
Err(
ShErr::full(
ShErrKind::SyntaxErr,
format!("Invalid fd arg: {}", arg),
shenv.get_input(),
blame
)
)
}
}
pub fn jobs(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let mut argv = argv.drop_first().into_iter();
let mut flags = JobCmdFlags::empty();
while let Some(arg) = argv.next() {
let arg_s = shenv.input_slice(arg.span());
let mut chars = arg_s.chars().peekable();
if chars.peek().is_none_or(|ch| *ch != '-') {
return Err(
ShErr::full(
ShErrKind::SyntaxErr,
"Invalid flag in jobs call",
shenv.get_input(),
arg.span()
)
)
}
chars.next();
while let Some(ch) = chars.next() {
let flag = match ch {
'l' => JobCmdFlags::LONG,
'p' => JobCmdFlags::PIDS,
'n' => JobCmdFlags::NEW_ONLY,
'r' => JobCmdFlags::RUNNING,
's' => JobCmdFlags::STOPPED,
_ => return Err(
ShErr::full(
ShErrKind::SyntaxErr,
"Invalid flag in jobs call",
shenv.get_input(),
arg.span()
)
)
};
flags |= flag
}
}
write_jobs(|j| j.print_jobs(flags))?;
shenv.set_code(0);
} else { unreachable!() }
Ok(())
}

View File

@@ -1,26 +0,0 @@
pub mod echo;
pub mod cd;
pub mod pwd;
pub mod export;
pub mod jobctl;
pub mod read;
pub mod alias;
pub mod control_flow;
pub mod source;
pub const BUILTINS: [&str;14] = [
"echo",
"cd",
"pwd",
"export",
"fg",
"bg",
"jobs",
"read",
"alias",
"exit",
"continue",
"return",
"break",
"source",
];

View File

@@ -1,17 +0,0 @@
use shellenv::jobs::{ChildProc, JobBldr};
use crate::prelude::*;
pub fn pwd(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv: _, redirs } = rule {
let mut pwd = shenv.vars().get_var("PWD").to_string();
pwd.push('\n');
shenv.collect_redirs(redirs);
shenv.ctx_mut().activate_rdrs()?;
write_out(pwd)?;
} else { unreachable!() }
Ok(())
}

View File

@@ -1,40 +0,0 @@
use crate::prelude::*;
pub fn read_builtin(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs: _ } = rule {
let argv = argv.drop_first();
let mut argv_iter = argv.iter();
// TODO: properly implement redirections
// using activate_redirs() was causing issues, may require manual handling
let mut buf = vec![0u8; 1024];
let bytes_read = read(0, &mut buf)?;
buf.truncate(bytes_read);
let read_input = String::from_utf8_lossy(&buf).trim_end().to_string();
if let Some(var) = argv_iter.next() {
/*
let words: Vec<&str> = read_input.split_whitespace().collect();
for (var, value) in argv_iter.zip(words.iter().chain(std::iter::repeat(&""))) {
shenv.vars_mut().set_var(&var.to_string(), value);
}
// Assign the rest of the string to the first variable if there's only one
if argv.len() == 1 {
shenv.vars_mut().set_var(&first_var.to_string(), &read_input);
}
*/
let var_name = shenv.input_slice(var.span()).to_string();
shenv.vars_mut().set_var(&var_name, &read_input);
}
} else {
unreachable!()
}
log!(TRACE, "leaving read");
shenv.set_code(0);
Ok(())
}

View File

@@ -1,15 +0,0 @@
use crate::prelude::*;
pub fn source(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
shenv.collect_redirs(redirs);
let mut argv_iter = argv.into_iter().skip(1);
while let Some(arg) = argv_iter.next() {
let arg_raw = arg.as_raw(shenv);
let arg_path = PathBuf::from(arg_raw);
shenv.source_file(arg_path)?;
}
} else { unreachable!() }
Ok(())
}

View File

@@ -1,514 +0,0 @@
use crate::{expand::{arithmetic::expand_arith_string, tilde::expand_tilde_string, vars::expand_string}, prelude::*};
use shellenv::jobs::{ChildProc, JobBldr};
pub mod shellcmd;
pub fn exec_input<S: Into<String>>(input: S, shenv: &mut ShEnv) -> ShResult<()> {
let input = input.into();
shenv.new_input(&input);
let total_time = std::time::Instant::now();
let token_time = std::time::Instant::now();
let token_stream = Lexer::new(input,shenv).lex();
let token_stream = expand_aliases(token_stream, shenv);
for token in &token_stream {
log!(TRACE, token);
log!(TRACE, "{}",token.as_raw(shenv));
}
log!(INFO, "Tokenizing done in {:?}", token_time.elapsed());
let parse_time = std::time::Instant::now();
let syn_tree = Parser::new(token_stream,shenv).parse()?;
log!(TRACE,syn_tree);
log!(INFO, "Parsing done in {:?}", parse_time.elapsed());
if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) {
shenv.save_io()?;
}
let exec_time = std::time::Instant::now();
if let Err(e) = Executor::new(syn_tree, shenv).walk() {
if let ShErrKind::CleanExit = e.kind() {
let code = shenv.get_code();
sh_quit(code);
} else {
if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) {
shenv.reset_io()?;
}
return Err(e.into())
}
}
log!(INFO, "Executing done in {:?}", exec_time.elapsed());
log!(INFO, "Total time spent: {:?}", total_time.elapsed());
if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) {
shenv.reset_io()?;
}
log!(INFO, "Io reset");
Ok(())
}
pub struct Executor<'a> {
ast: SynTree,
shenv: &'a mut ShEnv
}
impl<'a> Executor<'a> {
pub fn new(ast: SynTree, shenv: &'a mut ShEnv) -> Self {
Self { ast, shenv }
}
pub fn walk(&mut self) -> ShResult<()> {
self.shenv.inputman_mut().push_state();
log!(TRACE, "Starting walk");
while let Some(node) = self.ast.next_node() {
if let NdRule::CmdList { cmds } = node.clone().into_rule() {
log!(TRACE, "{:?}", cmds);
exec_list(cmds, self.shenv).try_blame(node.as_raw(self.shenv),node.span())?
} else { unreachable!() }
}
self.shenv.inputman_mut().pop_state();
log!(TRACE, "passed");
Ok(())
}
}
fn exec_list(list: Vec<(Option<CmdGuard>, Node)>, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Executing list");
let mut list = VecDeque::from(list);
while let Some(cmd_info) = list.fpop() {
let guard = cmd_info.0;
let cmd = cmd_info.1;
if let Some(guard) = guard {
let code = shenv.get_code();
match guard {
CmdGuard::And => {
if code != 0 { break; }
}
CmdGuard::Or => {
if code == 0 { break; }
}
}
}
dispatch_node(cmd, shenv)?;
}
Ok(())
}
fn dispatch_node(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let node_raw = node.as_raw(shenv);
let span = node.span();
match *node.rule() {
NdRule::Command {..} |
NdRule::Subshell {..} |
NdRule::Assignment {..} => dispatch_command(node, shenv).try_blame(node_raw, span)?,
NdRule::IfThen {..} => shellcmd::exec_if(node, shenv).try_blame(node_raw, span)?,
NdRule::Loop {..} => shellcmd::exec_loop(node, shenv).try_blame(node_raw, span)?,
NdRule::ForLoop {..} => shellcmd::exec_for(node, shenv).try_blame(node_raw, span)?,
NdRule::Case {..} => shellcmd::exec_case(node, shenv).try_blame(node_raw, span)?,
NdRule::FuncDef {..} => exec_funcdef(node,shenv).try_blame(node_raw, span)?,
NdRule::Pipeline {..} => exec_pipeline(node, shenv).try_blame(node_raw, span)?,
_ => unimplemented!("No support for NdRule::{:?} yet", node.rule())
}
Ok(())
}
fn dispatch_command(mut node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let mut is_builtin = false;
let mut is_func = false;
let mut is_subsh = false;
let mut is_assign = false;
if let NdRule::Command { ref mut argv, redirs: _ } = node.rule_mut() {
if !shenv.ctx().flags().contains(ExecFlags::NO_EXPAND) {
*argv = expand_argv(argv.to_vec(), shenv)?;
}
let cmd = argv.first().unwrap().as_raw(shenv);
if shenv.logic().get_function(&cmd).is_some() {
is_func = true;
} else if node.flags().contains(NdFlag::BUILTIN) {
is_builtin = true;
}
} else if let NdRule::Subshell { body: _, ref mut argv, redirs: _ } = node.rule_mut() {
if !shenv.ctx().flags().contains(ExecFlags::NO_EXPAND) {
*argv = expand_argv(argv.to_vec(), shenv)?;
}
is_subsh = true;
} else if let NdRule::Assignment { assignments: _, cmd: _ } = node.rule() {
is_assign = true;
} else { unreachable!() }
if is_builtin {
exec_builtin(node, shenv)?;
} else if is_func {
exec_func(node, shenv)?;
} else if is_subsh {
exec_subshell(node, shenv)?;
} else if is_assign {
exec_assignment(node, shenv)?;
} else {
exec_cmd(node, shenv)?;
}
Ok(())
}
fn exec_func(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let mut argv_iter = argv.into_iter();
let func_name = argv_iter.next().unwrap().as_raw(shenv);
let body = shenv.logic().get_function(&func_name).unwrap().to_string();
let snapshot = shenv.clone();
shenv.vars_mut().reset_params();
shenv.ctx_mut().set_flag(ExecFlags::IN_FUNC);
while let Some(arg) = argv_iter.next() {
let arg_raw = shenv.input_slice(arg.span()).to_string();
shenv.vars_mut().bpush_arg(&arg_raw);
}
shenv.collect_redirs(redirs);
match exec_input(body, shenv) {
Ok(()) => {
*shenv = snapshot;
return Ok(())
}
Err(e) if e.kind() == ShErrKind::FuncReturn => {
let code = shenv.get_code();
*shenv = snapshot;
shenv.set_code(code);
return Ok(())
}
Err(e) => {
*shenv = snapshot;
return Err(e.into())
}
}
}
Ok(())
}
fn exec_funcdef(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::FuncDef { name, body } = rule {
let name_raw = name.as_raw(shenv);
let name = name_raw.trim_end_matches("()");
let body_raw = body.as_raw(shenv);
let body = body_raw[1..body_raw.len() - 1].trim();
shenv.logic_mut().set_function(name, body);
} else { unreachable!() }
Ok(())
}
fn exec_subshell(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let snapshot = shenv.clone();
shenv.vars_mut().reset_params();
let is_bg = node.flags().contains(NdFlag::BACKGROUND);
let rule = node.into_rule();
if let NdRule::Subshell { body, argv, redirs } = rule {
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.ctx_mut().unset_flag(ExecFlags::NO_FORK); // Allow sub-forks in this case
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
write_err(e)?;
exit(1);
}
for arg in argv {
let arg_raw = &arg.as_raw(shenv);
shenv.vars_mut().bpush_arg(arg_raw);
}
let body_raw = body.as_raw(shenv);
match exec_input(body_raw, shenv) {
Ok(()) => exit(0),
Err(e) => {
eprintln!("{}",e);
exit(1);
}
}
} else {
match unsafe { fork()? } {
Child => {
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
write_err(e)?;
exit(1);
}
for arg in argv {
let arg_raw = &arg.as_raw(shenv);
shenv.vars_mut().bpush_arg(arg_raw);
}
let body_raw = body.as_raw(shenv);
match exec_input(body_raw, shenv) {
Ok(()) => exit(0),
Err(e) => {
eprintln!("{}",e);
exit(1);
}
}
}
Parent { child } => {
*shenv = snapshot;
let children = vec![
ChildProc::new(child, Some("anonymous subshell"), Some(child))?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(child)
.build();
dispatch_job(job, is_bg, shenv)?;
}
}
}
} else { unreachable!() }
Ok(())
}
fn exec_builtin(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Executing builtin");
let command = if let NdRule::Command { argv, redirs: _ } = node.rule() {
argv.first().unwrap().as_raw(shenv)
} else { unreachable!() };
log!(TRACE, "{}", command.as_str());
match command.as_str() {
"echo" => echo(node, shenv)?,
"cd" => cd(node,shenv)?,
"pwd" => pwd(node, shenv)?,
"export" => export(node, shenv)?,
"jobs" => jobs(node, shenv)?,
"fg" => continue_job(node, shenv, true)?,
"bg" => continue_job(node, shenv, false)?,
"read" => read_builtin(node, shenv)?,
"alias" => alias(node, shenv)?,
"exit" => sh_flow(node, shenv, ShErrKind::CleanExit)?,
"return" => sh_flow(node, shenv, ShErrKind::FuncReturn)?,
"break" => sh_flow(node, shenv, ShErrKind::LoopBreak)?,
"continue" => sh_flow(node, shenv, ShErrKind::LoopContinue)?,
"source" => source(node, shenv)?,
_ => unimplemented!("Have not yet implemented support for builtin `{}'",command)
}
log!(TRACE, "done");
Ok(())
}
fn exec_assignment(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Executing assignment");
let rule = node.into_rule();
if let NdRule::Assignment { assignments, cmd } = rule {
log!(TRACE, "Assignments: {:?}", assignments);
log!(TRACE, "Command: {:?}", cmd);
let mut assigns = assignments.into_iter();
if let Some(cmd) = cmd {
let saved_env = shenv.vars().env().clone();
while let Some(token) = assigns.next() {
let raw = token.as_raw(shenv);
if let Some((var,val)) = raw.split_once('=') {
let val_rule = Lexer::get_rule(&val);
if EXPANSIONS.contains(&val_rule) {
let exp = match val_rule {
TkRule::ArithSub => expand_arith_string(val,shenv)?,
TkRule::DQuote => expand_string(val, shenv)?,
TkRule::TildeSub => expand_tilde_string(val),
TkRule::VarSub => {
let val = shenv.vars().get_var(var);
val.to_string()
}
_ => unimplemented!()
};
shenv.vars_mut().export(var, &exp);
} else {
shenv.vars_mut().export(var, val);
}
}
}
dispatch_command(*cmd, shenv)?;
*shenv.vars_mut().env_mut() = saved_env;
} else {
while let Some(token) = assigns.next() {
let raw = token.as_raw(shenv);
if let Some((var,val)) = raw.split_once('=') {
let val_rule = Lexer::get_rule(&val);
if EXPANSIONS.contains(&val_rule) {
let exp = match val_rule {
TkRule::ArithSub => expand_arith_string(val,shenv)?,
TkRule::DQuote => expand_string(val, shenv)?,
TkRule::TildeSub => expand_tilde_string(val),
TkRule::VarSub => {
let val = shenv.vars().get_var(var);
val.to_string()
}
_ => unimplemented!()
};
shenv.vars_mut().set_var(var, &exp);
} else {
shenv.vars_mut().set_var(var, val);
}
}
}
}
} else { unreachable!() }
Ok(())
}
fn exec_pipeline(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Executing pipeline");
let is_bg = node.flags().contains(NdFlag::BACKGROUND);
let rule = node.into_rule();
if let NdRule::Pipeline { cmds } = rule {
let mut prev_rpipe: Option<i32> = None;
let mut cmds = VecDeque::from(cmds);
let mut pgid = None;
let mut cmd_names = vec![];
let mut pids = vec![];
while let Some(cmd) = cmds.pop_front() {
let (mut r_pipe, mut w_pipe) = if cmds.is_empty() {
// If we are on the last command, don't make new pipes
(None,None)
} else {
let (r_pipe, w_pipe) = c_pipe()?;
(Some(r_pipe),Some(w_pipe))
};
if let NdRule::Command { argv, redirs: _ } = cmd.rule() {
let cmd_name = argv.first().unwrap().as_raw(shenv);
cmd_names.push(cmd_name);
} else if let NdRule::Subshell {..} = cmd.rule() {
cmd_names.push("subshell".to_string());
} else {
cmd_names.push("shell cmd".to_string());
}
match unsafe { fork()? } {
Child => {
// Set NO_FORK since we are already in a fork, to prevent unnecessarily forking again
shenv.ctx_mut().set_flag(ExecFlags::NO_FORK);
// We close this r_pipe since it's the one the next command will use, so not useful here
if let Some(r_pipe) = r_pipe.take() {
close(r_pipe)?;
}
// Create some redirections
if let Some(w_pipe) = w_pipe.take() {
if !cmds.is_empty() {
let wpipe_redir = Redir::output(1, w_pipe);
shenv.ctx_mut().push_rdr(wpipe_redir);
}
}
// Use the r_pipe created in the last iteration
if let Some(prev_rpipe) = prev_rpipe.take() {
let rpipe_redir = Redir::input(0, prev_rpipe);
shenv.ctx_mut().push_rdr(rpipe_redir);
}
if let Err(e) = dispatch_node(cmd, shenv) {
eprintln!("{}",e);
exit(1);
}
exit(0);
}
Parent { child } => {
// Close the write pipe out here to signal EOF
if let Some(w_pipe) = w_pipe.take() {
close(w_pipe)?;
}
if pgid.is_none() {
pgid = Some(child);
}
pids.push(child);
if let Some(pipe) = prev_rpipe {
close(pipe)?;
}
prev_rpipe = r_pipe;
}
}
}
let mut children = vec![];
for (i,pid) in pids.iter().enumerate() {
let command = cmd_names.get(i).unwrap();
let child = ChildProc::new(*pid, Some(&command), pgid)?;
children.push(child);
}
let job = JobBldr::new()
.with_children(children)
.with_pgid(pgid.unwrap())
.build();
dispatch_job(job, is_bg, shenv)?;
} else { unreachable!() }
Ok(())
}
fn exec_cmd(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Executing command");
let blame = node.span();
let is_bg = node.flags().contains(NdFlag::BACKGROUND);
let rule = node.into_rule();
if let NdRule::Command { argv, redirs } = rule {
let (argv,envp) = prep_execve(argv, shenv);
let command = argv.first().unwrap().to_string();
if get_bin_path(&command, shenv).is_some() {
log!(TRACE, "{:?}",shenv.ctx().flags());
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
log!(TRACE, "Not forking");
shenv.collect_redirs(redirs);
log!(TRACE, "{:?}",shenv.ctx().redirs());
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
if let Err(errno) = execvpe(command, argv, envp) {
if errno != Errno::EFAULT {
exit(errno as i32);
}
}
} else {
log!(TRACE, "Forking");
match unsafe { fork()? } {
Child => {
log!(TRACE, redirs);
shenv.collect_redirs(redirs);
if let Err(e) = shenv.ctx_mut().activate_rdrs() {
eprintln!("{:?}",e);
exit(1);
}
execvpe(command, argv, envp)?;
exit(1);
}
Parent { child } => {
let children = vec![
ChildProc::new(child, Some(&command), Some(child))?
];
let job = JobBldr::new()
.with_children(children)
.with_pgid(child)
.build();
log!(TRACE, "New job: {:?}", job);
dispatch_job(job, is_bg, shenv)?;
}
}
}
} else {
return Err(ShErr::full(ShErrKind::CmdNotFound, format!("{}", command), shenv.get_input(), blame))
}
} else { unreachable!("Found this rule in exec_cmd: {:?}", rule) }
Ok(())
}
fn prep_execve(argv: Vec<Token>, shenv: &mut ShEnv) -> (Vec<String>, Vec<String>) {
log!(TRACE, "Preparing execvpe args");
let argv_s = argv.as_strings(shenv);
log!(TRACE, argv_s);
let mut envp = vec![];
let mut env_vars = shenv.vars().env().iter();
while let Some(entry) = env_vars.next() {
let key = entry.0;
let val = entry.1;
let formatted = format!("{}={}",key,val);
envp.push(formatted);
}
log!(TRACE, argv_s);
log!(DEBUG, argv_s);
(argv_s, envp)
}

View File

@@ -1,149 +0,0 @@
use crate::prelude::*;
pub fn exec_if(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::IfThen { cond_blocks, else_block, redirs } = rule {
shenv.collect_redirs(redirs);
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.ctx_mut().unset_flag(ExecFlags::NO_FORK);
}
let mut cond_blocks = cond_blocks.into_iter();
while let Some(block) = cond_blocks.next() {
let cond = block.0;
let body = block.1;
let ret = shenv.exec_as_cond(cond)?;
if ret == 0 {
shenv.exec_as_body(body)?;
return Ok(())
}
}
if let Some(block) = else_block {
shenv.exec_as_body(block)?;
}
} else { unreachable!() }
Ok(())
}
pub fn exec_loop(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Loop { kind, cond, body, redirs } = rule {
shenv.collect_redirs(redirs);
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.ctx_mut().unset_flag(ExecFlags::NO_FORK);
}
loop {
let ret = shenv.exec_as_cond(cond.clone())?;
match kind {
LoopKind::While => {
if ret == 0 {
match shenv.exec_as_body(body.clone()) {
Ok(_) => continue,
Err(e) => {
match e.kind() {
ShErrKind::LoopContinue => continue,
ShErrKind::LoopBreak => break,
_ => return Err(e.into())
}
}
}
} else { break }
}
LoopKind::Until => {
if ret != 0 {
match shenv.exec_as_body(body.clone()) {
Ok(_) => continue,
Err(e) => {
match e.kind() {
ShErrKind::LoopContinue => continue,
ShErrKind::LoopBreak => break,
_ => return Err(e.into())
}
}
}
} else { break }
}
}
}
} else { unreachable!() }
Ok(())
}
pub fn exec_for(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::ForLoop { vars, arr, body, redirs } = rule {
shenv.collect_redirs(redirs);
let saved_vars = shenv.vars().clone();
if shenv.ctx().flags().contains(ExecFlags::NO_FORK) {
shenv.ctx_mut().unset_flag(ExecFlags::NO_FORK);
}
log!(DEBUG, vars);
log!(DEBUG, arr);
for chunk in arr.chunks(vars.len()) {
log!(DEBUG, "input: {}", shenv.get_input());
for (var,value) in vars.iter().zip(chunk.iter()) {
let var = var.as_raw(shenv);
let val = value.as_raw(shenv);
log!(DEBUG,var);
log!(DEBUG,val);
shenv.vars_mut().set_var(&var, &val);
}
if chunk.len() < vars.len() {
for var in &vars[chunk.len()..] { // If 'vars' is longer than the chunk, then unset the orphaned vars
let var = var.as_raw(shenv);
log!(DEBUG, "unsetting");
log!(DEBUG, var);
shenv.vars_mut().unset_var(&var);
}
}
shenv.exec_as_body(body.clone())?;
}
*shenv.vars_mut() = saved_vars;
} else { unreachable!() }
Ok(())
}
pub fn exec_case(node: Node, shenv: &mut ShEnv) -> ShResult<()> {
let rule = node.into_rule();
if let NdRule::Case { pat, blocks, redirs } = rule {
shenv.collect_redirs(redirs);
let mut blocks_iter = blocks.into_iter();
let pat_raw = expand_token(pat, shenv)?
.iter()
.map(|tk| tk.as_raw(shenv))
.collect::<Vec<_>>()
.join(" ");
while let Some((block_pat, block)) = blocks_iter.next() {
let block_pat_raw = block_pat.as_raw(shenv);
let block_pat_raw = block_pat_raw.trim_end_matches(')');
if block_pat_raw == "*" {
let _ret = shenv.exec_as_body(block)?;
return Ok(())
} else if block_pat_raw.contains('|') {
let pats = block_pat_raw.split('|');
for pat in pats {
if pat_raw.trim() == pat.trim() {
let _ret = shenv.exec_as_body(block)?;
return Ok(())
}
}
} else if pat_raw.trim() == block_pat_raw.trim() {
let _ret = shenv.exec_as_body(block)?;
return Ok(())
}
}
} else { unreachable!() }
Ok(())
}

102
src/expand.rs Normal file
View File

@@ -0,0 +1,102 @@
use crate::{libsh::error::{ShErr, ShErrKind}, parse::lex::{is_hard_sep, LexFlags, LexStream, Tk, Span, TkErr, TkFlags, TkState, TkRule}, prelude::*, state::read_vars};
/// Variable substitution marker
pub const VAR_SUB: char = '\u{fdd0}';
impl<'t> Tk<'t> {
/// Create a new expanded token
///
/// params
/// tokens: A vector of raw tokens lexed from the expansion result
/// span: The span of the original token that is being expanded
/// flags: some TkFlags
pub fn expand(self, span: Span<'t>, flags: TkFlags) -> Self {
let exp = Expander::new(self).expand();
let class = TkRule::Expanded { exp };
Self { class, span, err_span: None, flags, err: TkErr::Null }
}
pub fn get_words(&self) -> Vec<String> {
match &self.class {
TkRule::Expanded { exp } => exp.clone(),
_ => vec![self.to_string()]
}
}
}
pub struct Expander {
raw: String,
}
impl<'t> Expander {
pub fn new(raw: Tk<'t>) -> Self {
let unescaped = unescape_str(raw.span.as_str());
Self { raw: unescaped }
}
pub fn expand(&'t mut self) -> Vec<String> {
self.raw = self.expand_raw();
let tokens: Vec<_> = LexStream::new(&self.raw, LexFlags::RAW)
.filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI))
.map(|tk| tk.to_string())
.collect();
tokens
}
pub fn expand_raw(&self) -> String {
let mut chars = self.raw.chars();
let mut result = String::new();
let mut var_name = String::new();
let mut in_brace = false;
// TODO: implement error handling for unclosed braces
while let Some(ch) = chars.next() {
match ch {
VAR_SUB => {
while let Some(ch) = chars.next() {
match ch {
'{' => in_brace = true,
'}' if in_brace => {
let var_val = read_vars(|v| v.get_var(&var_name));
result.push_str(&var_val);
var_name.clear();
break
}
_ if is_hard_sep(ch) => {
let var_val = read_vars(|v| v.get_var(&var_name));
result.push_str(&var_val);
result.push(ch);
var_name.clear();
break
}
_ => var_name.push(ch),
}
}
if !var_name.is_empty() {
let var_val = read_vars(|v| v.get_var(&var_name));
result.push_str(&var_val);
var_name.clear();
}
}
_ => result.push(ch)
}
}
result
}
}
/// Clean up a single layer of escape characters, and then replace control characters like '$' with a non-character unicode representation that is unmistakable by the rest of the code
pub fn unescape_str(raw: &str) -> String {
let mut chars = raw.chars();
let mut result = String::new();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if let Some(next_ch) = chars.next() {
result.push(next_ch)
}
}
'$' => result.push(VAR_SUB),
_ => result.push(ch)
}
}
result
}

View File

@@ -1,73 +0,0 @@
use crate::{parse::lex::SEPARATORS, prelude::*};
pub fn expand_alias(candidate: Token, shenv: &mut ShEnv) -> Vec<Token> {
let mut tokens = vec![];
let mut work_stack = VecDeque::new();
let logic = shenv.logic().clone();
let mut done = false;
// Start with the candidate token in the work queue
work_stack.bpush(candidate);
// Process until there are no more tokens in the queue
while !done {
done = true;
while let Some(token) = work_stack.fpop() {
if token.rule() == TkRule::Ident {
let cand_str = token.as_raw(shenv);
if let Some(alias) = logic.get_alias(&cand_str) {
done = false;
if !token.span().borrow().expanded {
let mut new_tokens = shenv.expand_input(alias, token.span());
new_tokens.retain(|tk| tk.rule() != TkRule::Whitespace);
for token in &new_tokens {
tokens.push(token.clone());
}
}
} else {
tokens.push(token);
}
} else {
tokens.push(token);
}
}
if !done {
work_stack.extend(tokens.drain(..));
}
}
tokens
}
pub fn expand_aliases(tokens: Vec<Token>, shenv: &mut ShEnv) -> Vec<Token> {
let mut stream = tokens.iter();
let mut processed = vec![];
let mut is_command = true;
while let Some(token) = stream.next() {
match token.rule() {
_ if SEPARATORS.contains(&token.rule()) => {
is_command = true;
processed.push(token.clone());
}
TkRule::Case | TkRule::For => {
processed.push(token.clone());
while let Some(token) = stream.next() {
processed.push(token.clone());
if token.rule() == TkRule::Sep {
break
}
}
}
TkRule::Ident if is_command => {
is_command = false;
let mut alias_tokens = expand_alias(token.clone(), shenv);
if !alias_tokens.is_empty() {
processed.append(&mut alias_tokens);
} else {
processed.push(token.clone());
}
}
_ => processed.push(token.clone()),
}
}
processed
}

View File

@@ -1,192 +0,0 @@
use crate::prelude::*;
use super::vars::expand_string;
#[derive(Clone,PartialEq,Debug)]
pub enum ExprToken {
Number(f64),
Operator(Op),
OpenParen,
CloseParen
}
#[derive(Clone,PartialEq,Debug)]
pub enum Op {
Add,
Sub,
Mul,
Div,
IntDiv,
Mod,
Pow
}
impl Op {
pub fn precedence(&self) -> u8 {
match self {
Op::Add | Op::Sub => 1,
Op::Mul | Op::Div | Op::IntDiv | Op::Mod => 2,
Op::Pow => 3
}
}
pub fn is_left_associative(&self) -> bool {
*self != Op::Pow
}
}
fn tokenize_expr(expr: &str) -> ShResult<Vec<ExprToken>> {
let mut chars = expr.chars().peekable();
let mut tokens = vec![];
while let Some(ch) = chars.next() {
match ch {
'+' => tokens.push(ExprToken::Operator(Op::Add)),
'-' => tokens.push(ExprToken::Operator(Op::Sub)),
'*' => {
if chars.peek() == Some(&'*') {
chars.next();
tokens.push(ExprToken::Operator(Op::Pow));
} else {
tokens.push(ExprToken::Operator(Op::Mul));
}
}
'/' => {
if chars.peek() == Some(&'/') {
chars.next();
tokens.push(ExprToken::Operator(Op::IntDiv));
} else {
tokens.push(ExprToken::Operator(Op::Div));
}
}
'%' => tokens.push(ExprToken::Operator(Op::Mod)),
'(' => tokens.push(ExprToken::OpenParen),
')' => tokens.push(ExprToken::CloseParen),
'0'..='9' => {
let mut number = ch.to_string();
while let Some(next_ch) = chars.peek() {
if next_ch.is_ascii_digit() {
number.push(chars.next().unwrap());
} else {
break;
}
}
let value = number.parse::<f64>().unwrap();
tokens.push(ExprToken::Number(value));
}
' ' | '\t' => continue, // Skip whitespace
_ => return Err(ShErr::simple(ShErrKind::ParseErr, format!("Unexpected character in arithmetic expansion: {}",ch))), // Handle unexpected characters
}
}
Ok(tokens)
}
fn shunting_yard(tokens: Vec<ExprToken>) -> ShResult<Vec<ExprToken>> {
let mut sorted = vec![];
let mut operators = vec![];
for token in tokens {
match token {
ExprToken::Number(_) => sorted.push(token.clone()),
ExprToken::Operator(ref op) => {
while let Some(top) = operators.last() {
if let ExprToken::Operator(top_op) = top {
if (op.is_left_associative() && op.precedence() <= top_op.precedence())
|| (!op.is_left_associative() && op.precedence() < top_op.precedence())
{
sorted.push(operators.pop().unwrap())
} else {
break
}
} else {
break
}
}
operators.push(token.clone())
}
ExprToken::OpenParen => operators.push(token.clone()),
ExprToken::CloseParen => {
while let Some(top) = operators.pop() {
if matches!(top, ExprToken::OpenParen) {
break;
}
sorted.push(top);
}
}
}
}
while let Some(op) = operators.pop() {
if matches!(op, ExprToken::OpenParen | ExprToken::CloseParen) {
return Err(ShErr::simple(ShErrKind::ParseErr, "Mismatched parenthesis in arithmetic expansion"))
}
sorted.push(op);
}
Ok(sorted)
}
pub fn eval_rpn(tokens: Vec<ExprToken>) -> ShResult<f64> {
let mut stack = vec![];
for token in tokens {
match token {
ExprToken::Number(num) => stack.push(num),
ExprToken::Operator(op) => {
if stack.len() < 2 {
return Err(ShErr::simple(ShErrKind::ParseErr, "Not enough operands in arithmetic expansion"))
}
let rhs = stack.pop().unwrap();
let lhs = stack.pop().unwrap();
let result = match op {
Op::Add => lhs + rhs,
Op::Sub => lhs - rhs,
Op::Mul => lhs * rhs,
Op::Mod => lhs % rhs,
Op::Pow => lhs.powf(rhs),
Op::Div => {
if rhs == 0.0 {
return Err(ShErr::simple(ShErrKind::ParseErr, "Attempt to divide by zero in arithmetic expansion"))
}
lhs / rhs
}
Op::IntDiv => {
if rhs == 0.0 {
return Err(ShErr::simple(ShErrKind::ParseErr, "Attempt to divide by zero in arithmetic expansion"))
}
(lhs as i64 / rhs as i64) as f64
}
};
stack.push(result);
}
ExprToken::OpenParen => todo!(),
ExprToken::CloseParen => todo!(),
}
}
Ok(stack.pop().unwrap())
}
pub fn expand_arith_token(token: Token, shenv: &mut ShEnv) -> ShResult<Token> {
// I mean hey it works
let token_raw = token.as_raw(shenv);
let arith_raw = token_raw.trim_matches('`');
let result = expand_arith_string(arith_raw,shenv)?;
let mut final_expansion = shenv.expand_input(&result, token.span());
Ok(final_expansion.pop().unwrap_or(token))
}
pub fn expand_arith_string(s: &str,shenv: &mut ShEnv) -> ShResult<String> {
let mut exp = expand_string(s,shenv)?;
if exp.starts_with('`') && s.ends_with('`') {
exp = exp[1..exp.len() - 1].to_string();
}
let expr_tokens = shunting_yard(tokenize_expr(&exp)?)?;
log!(DEBUG,expr_tokens);
let result = eval_rpn(expr_tokens)?.to_string();
Ok(result)
}

View File

@@ -1,149 +0,0 @@
use crate::{expand::vars::expand_string, prelude::*};
pub fn expand_brace_token(token: Token, shenv: &mut ShEnv) -> ShResult<Vec<Token>> {
let raw = token.as_raw(shenv);
let raw_exp = expand_string(&raw, shenv)?;
log!(DEBUG, raw_exp);
let expanded = expand_brace_string(&raw_exp);
log!(DEBUG, expanded);
let mut new_tokens = shenv.expand_input(&expanded, token.span());
new_tokens.retain(|tk| tk.rule() != TkRule::Whitespace);
log!(DEBUG, new_tokens);
Ok(new_tokens)
}
pub fn expand_brace_string(raw: &str) -> String {
let mut result = VecDeque::new();
let mut stack = vec![];
stack.push(raw.to_string());
while let Some(current) = stack.pop() {
if let Some((prefix,braces,suffix)) = get_brace_positions(&current) {
let expanded = expand_brace_inner(&braces);
for part in expanded {
let formatted = format!("{prefix}{part}{suffix}");
stack.push(formatted);
}
} else {
result.fpush(current);
}
}
result.into_iter().collect::<Vec<_>>().join(" ")
}
pub fn get_brace_positions(slice: &str) -> Option<(String, String, String)> {
let mut chars = slice.chars().enumerate();
let mut start = None;
let mut brc_count = 0;
while let Some((i,ch)) = chars.next() {
match ch {
'{' => {
if brc_count == 0 {
start = Some(i);
}
brc_count += 1;
}
'}' => {
brc_count -= 1;
if brc_count == 0 {
if let Some(start) = start {
let prefix = slice[..start].to_string();
let braces = slice[start+1..i].to_string();
let suffix = slice[i+1..].to_string();
return Some((prefix,braces,suffix))
}
}
}
_ => continue
}
}
None
}
fn expand_brace_inner(inner: &str) -> Vec<String> {
if inner.split_once("..").is_some() && !inner.contains(['{','}']) {
expand_range(inner)
} else {
split_list(inner)
}
}
fn split_list(list: &str) -> Vec<String> {
log!(DEBUG, list);
let mut chars = list.chars();
let mut items = vec![];
let mut curr_item = String::new();
let mut brc_count = 0;
while let Some(ch) = chars.next() {
match ch {
',' if brc_count == 0 => {
if !curr_item.is_empty() {
items.push(std::mem::take(&mut curr_item));
}
}
'{' => {
brc_count += 1;
curr_item.push(ch);
}
'}' => {
if brc_count == 0 {
return vec![list.to_string()];
}
brc_count -= 1;
curr_item.push(ch);
}
_ => curr_item.push(ch),
}
}
if !curr_item.is_empty() {
items.push(std::mem::take(&mut curr_item))
}
log!(DEBUG,items);
items
}
fn expand_range(range: &str) -> Vec<String> {
if let Some((left,right)) = range.split_once("..") {
// I know, I know
// This is checking to see if the range looks like "a..b" or "A..B"
// one character on both sides, both are letters, AND (both are uppercase OR both are lowercase)
if (left.len() == 1 && right.len() == 1) &&
(left.chars().all(|ch| ch.is_ascii_alphanumeric() && right.chars().all(|ch| ch.is_ascii_alphanumeric()))) &&
(
(left.chars().all(|ch| ch.is_uppercase()) && right.chars().all(|ch| ch.is_uppercase())) ||
(left.chars().all(|ch| ch.is_lowercase()) && right.chars().all(|ch| ch.is_lowercase()))
)
{
expand_range_alpha(left, right)
}
else if right.chars().all(|ch| ch.is_ascii_digit()) && left.chars().all(|ch| ch.is_ascii_digit())
{
expand_range_numeric(left, right)
}
else
{
vec![range.to_string()]
}
} else {
vec![range.to_string()]
}
}
fn expand_range_alpha(left: &str, right: &str) -> Vec<String> {
let start = left.chars().next().unwrap() as u8;
let end = right.chars().next().unwrap() as u8;
if start > end {
(end..=start).rev().map(|c| (c as char).to_string()).collect()
} else {
(start..=end).map(|c| (c as char).to_string()).collect()
}
}
fn expand_range_numeric(left: &str, right: &str) -> Vec<String> {
let start = left.parse::<i32>().unwrap();
let end = right.parse::<i32>().unwrap();
(start..=end).map(|i| i.to_string()).collect()
}

View File

@@ -1,35 +0,0 @@
use crate::prelude::*;
pub fn expand_cmdsub_token(token: Token, shenv: &mut ShEnv) -> ShResult<Vec<Token>> {
let cmdsub_raw = token.as_raw(shenv);
let output = expand_cmdsub_string(&cmdsub_raw, shenv)?;
let new_tokens = shenv.expand_input(&output, token.span());
Ok(new_tokens)
}
pub fn expand_cmdsub_string(mut s: &str, shenv: &mut ShEnv) -> ShResult<String> {
if s.starts_with("$(") && s.ends_with(')') {
s = &s[2..s.len() - 1]; // From '$(this)' to 'this'
}
let (r_pipe,w_pipe) = c_pipe()?;
let pipe_redir = Redir::output(1, w_pipe);
let mut sub_shenv = shenv.clone();
sub_shenv.ctx_mut().set_flag(ExecFlags::NO_FORK);
sub_shenv.collect_redirs(vec![pipe_redir]);
match unsafe { fork()? } {
Child => {
close(r_pipe).ok();
exec_input(s, &mut sub_shenv).abort_if_err();
exit(0);
}
Parent { child: _ } => {
close(w_pipe).ok();
}
}
let result = read_to_string(r_pipe);
close(r_pipe)?;
Ok(result?)
}

View File

@@ -1,64 +0,0 @@
pub mod vars;
pub mod tilde;
pub mod alias;
pub mod cmdsub;
pub mod arithmetic;
pub mod prompt;
pub mod brace;
use arithmetic::expand_arith_token;
use brace::expand_brace_token;
use cmdsub::expand_cmdsub_token;
use vars::{expand_string, expand_var};
use tilde::expand_tilde_token;
use crate::prelude::*;
pub fn expand_argv(argv: Vec<Token>, shenv: &mut ShEnv) -> ShResult<Vec<Token>> {
let mut processed = vec![];
for arg in argv {
log!(TRACE, "{}",arg.as_raw(shenv));
log!(TRACE, processed);
let mut expanded = expand_token(arg, shenv)?;
processed.append(&mut expanded);
}
Ok(processed)
}
pub fn expand_token(token: Token, shenv: &mut ShEnv) -> ShResult<Vec<Token>> {
let mut processed = vec![];
match token.rule() {
TkRule::DQuote => {
let dquote_exp = expand_string(&token.as_raw(shenv), shenv)?;
let mut expanded = shenv.expand_input(&dquote_exp, token.span());
processed.append(&mut expanded);
}
TkRule::VarSub => {
let mut varsub_exp = expand_var(token.clone(), shenv);
processed.append(&mut varsub_exp);
}
TkRule::TildeSub => {
let tilde_exp = expand_tilde_token(token.clone(), shenv);
processed.push(tilde_exp);
}
TkRule::ArithSub => {
let arith_exp = expand_arith_token(token.clone(), shenv)?;
processed.push(arith_exp);
}
TkRule::BraceExp => {
let mut brace_exp = expand_brace_token(token, shenv)?;
processed.append(&mut brace_exp);
}
TkRule::CmdSub => {
let mut cmdsub_exp = expand_cmdsub_token(token.clone(), shenv)?;
processed.append(&mut cmdsub_exp);
}
_ => {
if token.rule() != TkRule::Ident {
log!(WARN, "found this in expand_token: {:?}", token.rule());
}
processed.push(token.clone())
}
}
Ok(processed)
}

View File

@@ -1,385 +0,0 @@
use crate::prelude::*;
#[derive(Debug)]
pub enum PromptTk {
AsciiOct(i32),
Text(String),
AnsiSeq(String),
VisGrp,
UserSeq,
Runtime,
Weekday,
Dquote,
Squote,
Return,
Newline,
Pwd,
PwdShort,
Hostname,
HostnameShort,
ShellName,
Username,
PromptSymbol,
ExitCode,
SuccessSymbol,
FailureSymbol,
JobCount
}
pub fn format_cmd_runtime(dur: std::time::Duration) -> String {
const ETERNITY: u128 = f32::INFINITY as u128;
let mut micros = dur.as_micros();
let mut millis = 0;
let mut seconds = 0;
let mut minutes = 0;
let mut hours = 0;
let mut days = 0;
let mut weeks = 0;
let mut months = 0;
let mut years = 0;
let mut decades = 0;
let mut centuries = 0;
let mut millennia = 0;
let mut epochs = 0;
let mut aeons = 0;
let mut eternities = 0;
if micros >= 1000 {
millis = micros / 1000;
micros %= 1000;
}
if millis >= 1000 {
seconds = millis / 1000;
millis %= 1000;
}
if seconds >= 60 {
minutes = seconds / 60;
seconds %= 60;
}
if minutes >= 60 {
hours = minutes / 60;
minutes %= 60;
}
if hours >= 24 {
days = hours / 24;
hours %= 24;
}
if days >= 7 {
weeks = days / 7;
days %= 7;
}
if weeks >= 4 {
months = weeks / 4;
weeks %= 4;
}
if months >= 12 {
years = months / 12;
weeks %= 12;
}
if years >= 10 {
decades = years / 10;
years %= 10;
}
if decades >= 10 {
centuries = decades / 10;
decades %= 10;
}
if centuries >= 10 {
millennia = centuries / 10;
centuries %= 10;
}
if millennia >= 1000 {
epochs = millennia / 1000;
millennia %= 1000;
}
if epochs >= 1000 {
aeons = epochs / 1000;
epochs %= aeons;
}
if aeons == ETERNITY {
eternities = aeons / ETERNITY;
aeons %= ETERNITY;
}
// Format the result
let mut result = Vec::new();
if eternities > 0 {
let mut string = format!("{} eternit", eternities);
if eternities > 1 {
string.push_str("ies");
} else {
string.push('y');
}
result.push(string)
}
if aeons > 0 {
let mut string = format!("{} aeon", aeons);
if aeons > 1 {
string.push('s')
}
result.push(string)
}
if epochs > 0 {
let mut string = format!("{} epoch", epochs);
if epochs > 1 {
string.push('s')
}
result.push(string)
}
if millennia > 0 {
let mut string = format!("{} millenni", millennia);
if millennia > 1 {
string.push_str("um")
} else {
string.push('a')
}
result.push(string)
}
if centuries > 0 {
let mut string = format!("{} centur", centuries);
if centuries > 1 {
string.push_str("ies")
} else {
string.push('y')
}
result.push(string)
}
if decades > 0 {
let mut string = format!("{} decade", decades);
if decades > 1 {
string.push('s')
}
result.push(string)
}
if years > 0 {
let mut string = format!("{} year", years);
if years > 1 {
string.push('s')
}
result.push(string)
}
if months > 0 {
let mut string = format!("{} month", months);
if months > 1 {
string.push('s')
}
result.push(string)
}
if weeks > 0 {
let mut string = format!("{} week", weeks);
if weeks > 1 {
string.push('s')
}
result.push(string)
}
if days > 0 {
let mut string = format!("{} day", days);
if days > 1 {
string.push('s')
}
result.push(string)
}
if hours > 0 {
let string = format!("{}h", hours);
result.push(string);
}
if minutes > 0 {
let string = format!("{}m", minutes);
result.push(string);
}
if seconds > 0 {
let string = format!("{}s", seconds);
result.push(string);
}
if millis > 0 {
let string = format!("{}ms",millis);
result.push(string);
}
if result.is_empty() && micros > 0 {
let string = format!("{}µs",micros);
result.push(string);
}
result.join(" ")
}
fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
let mut chars = raw.chars().peekable();
let mut tk_text = String::new();
let mut tokens = vec![];
while let Some(ch) = chars.next() {
match ch {
'\\' => {
// Push any accumulated text as a token
if !tk_text.is_empty() {
tokens.push(PromptTk::Text(std::mem::take(&mut tk_text)));
}
// Handle the escape sequence
if let Some(ch) = chars.next() {
match ch {
'w' => tokens.push(PromptTk::Pwd),
'W' => tokens.push(PromptTk::PwdShort),
'h' => tokens.push(PromptTk::Hostname),
'H' => tokens.push(PromptTk::HostnameShort),
's' => tokens.push(PromptTk::ShellName),
'u' => tokens.push(PromptTk::Username),
'$' => tokens.push(PromptTk::PromptSymbol),
'n' => tokens.push(PromptTk::Text("\n".into())),
'r' => tokens.push(PromptTk::Text("\r".into())),
'T' => tokens.push(PromptTk::Runtime),
'\\' => tokens.push(PromptTk::Text("\\".into())),
'"' => tokens.push(PromptTk::Text("\"".into())),
'\'' => tokens.push(PromptTk::Text("'".into())),
'e' => {
if chars.next() == Some('[') {
let mut params = String::new();
// Collect parameters and final character
while let Some(ch) = chars.next() {
match ch {
'0'..='9' | ';' | '?' | ':' => params.push(ch), // Valid parameter characters
'A'..='Z' | 'a'..='z' => { // Final character (letter)
params.push(ch);
break;
}
_ => {
// Invalid character in ANSI sequence
tokens.push(PromptTk::Text(format!("\x1b[{params}")));
break;
}
}
}
tokens.push(PromptTk::AnsiSeq(format!("\x1b[{params}")));
} else {
// Handle case where 'e' is not followed by '['
tokens.push(PromptTk::Text("\\e".into()));
}
}
'0'..='7' => {
// Handle octal escape
let mut octal_str = String::new();
octal_str.push(ch);
// Collect up to 2 more octal digits
for _ in 0..2 {
if let Some(&next_ch) = chars.peek() {
if next_ch >= '0' && next_ch <= '7' {
octal_str.push(chars.next().unwrap());
} else {
break;
}
} else {
break;
}
}
// Parse the octal string into an integer
if let Ok(octal) = i32::from_str_radix(&octal_str, 8) {
tokens.push(PromptTk::AsciiOct(octal));
} else {
// Fallback: treat as raw text
tokens.push(PromptTk::Text(format!("\\{octal_str}")));
}
}
_ => {
// Unknown escape sequence: treat as raw text
tokens.push(PromptTk::Text(format!("\\{ch}")));
}
}
} else {
// Handle trailing backslash
tokens.push(PromptTk::Text("\\".into()));
}
}
_ => {
// Accumulate non-escape characters
tk_text.push(ch);
}
}
}
// Push any remaining text as a token
if !tk_text.is_empty() {
tokens.push(PromptTk::Text(tk_text));
}
tokens
}
pub fn expand_prompt(raw: &str, shenv: &mut ShEnv) -> ShResult<String> {
let mut tokens = tokenize_prompt(raw).into_iter();
let mut result = String::new();
while let Some(token) = tokens.next() {
match token {
PromptTk::AsciiOct(_) => todo!(),
PromptTk::Text(txt) => result.push_str(&txt),
PromptTk::AnsiSeq(params) => result.push_str(&params),
PromptTk::Runtime => {
log!(INFO, "getting runtime");
if let Some(runtime) = shenv.meta().get_runtime() {
log!(DEBUG, runtime);
let runtime_fmt = format_cmd_runtime(runtime);
result.push_str(&runtime_fmt);
}
}
PromptTk::Pwd => {
let mut pwd = std::env::var("PWD")?;
let home = std::env::var("HOME")?;
if pwd.starts_with(&home) {
pwd = pwd.replacen(&home, "~", 1);
}
result.push_str(&pwd);
}
PromptTk::PwdShort => {
let mut path = std::env::var("PWD")?;
let home = std::env::var("HOME")?;
if path.starts_with(&home) {
path = path.replacen(&home, "~", 1);
}
let pathbuf = PathBuf::from(&path);
let mut segments = pathbuf.iter().count();
let mut path_iter = pathbuf.into_iter();
while segments > 4 {
path_iter.next();
segments -= 1;
}
let path_rebuilt: PathBuf = path_iter.collect();
let mut path_rebuilt = path_rebuilt.to_str().unwrap().to_string();
if path_rebuilt.starts_with(&home) {
path_rebuilt = path_rebuilt.replacen(&home, "~", 1);
}
result.push_str(&path_rebuilt);
}
PromptTk::Hostname => {
let hostname = std::env::var("HOSTNAME")?;
result.push_str(&hostname);
}
PromptTk::HostnameShort => todo!(),
PromptTk::ShellName => result.push_str("fern"),
PromptTk::Username => {
let username = std::env::var("USER")?;
result.push_str(&username);
}
PromptTk::PromptSymbol => {
let uid = std::env::var("UID")?;
let symbol = if &uid == "0" {
'#'
} else {
'$'
};
result.push(symbol);
}
PromptTk::ExitCode => todo!(),
PromptTk::SuccessSymbol => todo!(),
PromptTk::FailureSymbol => todo!(),
PromptTk::JobCount => todo!(),
_ => unimplemented!()
}
}
Ok(result)
}

View File

@@ -1,19 +0,0 @@
use crate::prelude::*;
pub fn expand_tilde_token(tilde_sub: Token, shenv: &mut ShEnv) -> Token {
let tilde_sub_raw = tilde_sub.as_raw(shenv);
let result = expand_tilde_string(&tilde_sub_raw);
if result == tilde_sub_raw {
return tilde_sub
}
shenv.expand_input(&result, tilde_sub.span()).pop().unwrap_or(tilde_sub)
}
pub fn expand_tilde_string(s: &str) -> String {
if s.starts_with('~') {
let home = std::env::var("HOME").unwrap_or_default();
s.replacen('~', &home, 1)
} else {
s.to_string()
}
}

View File

@@ -1,104 +0,0 @@
use crate::{parse::lex::Token, prelude::*};
use super::cmdsub::expand_cmdsub_string;
pub fn expand_var(var_sub: Token, shenv: &mut ShEnv) -> Vec<Token> {
let var_name = var_sub.as_raw(shenv);
let var_name = var_name.trim_start_matches('$').trim_matches(['{','}']);
let value = shenv.vars().get_var(var_name).to_string();
shenv.expand_input(&value, var_sub.span())
}
pub fn expand_string(s: &str, shenv: &mut ShEnv) -> ShResult<String> {
log!(DEBUG, s);
let mut result = String::new();
let mut var_name = String::new();
let mut chars = s.chars().peekable();
let mut in_brace = false;
while let Some(ch) = chars.next() {
match ch {
'\\' => {
result.push(ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch)
}
}
'$' => {
let mut expanded = false;
while let Some(ch) = chars.peek() {
if *ch == '"' || *ch == '`' {
break
}
let ch = chars.next().unwrap();
log!(DEBUG,var_name);
match ch {
'{' if var_name.is_empty() => {
in_brace = true;
}
'}' if in_brace => {
let value = shenv.vars().get_var(&var_name);
result.push_str(value);
expanded = true;
break
}
'(' if var_name.is_empty() => {
let mut paren_count = 1;
var_name.push_str("$(");
while let Some(ch) = chars.next() {
match ch {
'(' => {
paren_count += 1;
var_name.push(ch);
}
')' => {
paren_count -= 1;
var_name.push(ch);
if paren_count == 0 {
break
}
}
_ => var_name.push(ch)
}
}
let value = expand_cmdsub_string(&var_name, shenv)?;
result.push_str(&value);
expanded = true;
break
}
_ if ch.is_ascii_digit() && var_name.is_empty() && !in_brace => {
var_name.push(ch);
let value = shenv.vars().get_var(&var_name);
result.push_str(value);
expanded = true;
break
}
'@' | '#' | '*' | '-' | '?' | '!' | '$' if var_name.is_empty() => {
var_name.push(ch);
let value = shenv.vars().get_var(&var_name);
result.push_str(value);
expanded = true;
break
}
' ' | '\t' | '\n' | ';' | ',' | '{' => {
let value = shenv.vars().get_var(&var_name);
result.push_str(value);
result.push(ch);
expanded = true;
break
}
_ => var_name.push(ch)
}
}
if !expanded {
let value = shenv.vars().get_var(&var_name);
result.push_str(value);
}
var_name.clear();
}
_ => result.push(ch)
}
}
Ok(result)
}

43
src/fern.rs Normal file
View File

@@ -0,0 +1,43 @@
pub mod prelude;
pub mod libsh;
pub mod prompt;
pub mod procio;
pub mod parse;
pub mod expand;
pub mod state;
#[cfg(test)]
pub mod tests;
use std::process::exit;
use parse::{execute::{get_pipe_stack, Dispatcher}, lex::{LexFlags, LexStream}, ParseResult, ParseStream};
use state::write_vars;
fn main() {
loop {
let input = prompt::read_line().unwrap();
if input == "quit" { break };
write_vars(|v| v.new_var("foo", "bar"));
let mut tokens = vec![];
for token in LexStream::new(&input, LexFlags::empty()) {
if token.is_err() {
let error = format!("{:?}: {}",token.err,token.err_span.unwrap().as_str());
panic!("{error}");
}
tokens.push(token);
}
let mut nodes = vec![];
for result in ParseStream::new(tokens) {
match result {
ParseResult::Error(e) => panic!("{}",e),
ParseResult::Match(node) => nodes.push(node),
_ => unreachable!()
}
}
let mut dispatcher = Dispatcher::new(nodes);
dispatcher.begin_dispatch().unwrap();
}
}

View File

@@ -1 +0,0 @@
foobar

View File

@@ -1 +0,0 @@
foo

View File

@@ -1,36 +0,0 @@
use std::collections::VecDeque;
pub trait VecDequeAliases<T> {
fn fpop(&mut self) -> Option<T>;
fn fpush(&mut self, value: T);
fn bpop(&mut self) -> Option<T>;
fn bpush(&mut self, value: T);
fn to_vec(self) -> Vec<T>;
}
impl<T> VecDequeAliases<T> for VecDeque<T> {
/// Alias for pop_front()
fn fpop(&mut self) -> Option<T> {
self.pop_front()
}
/// Alias for push_front()
fn fpush(&mut self, value: T) {
self.push_front(value);
}
/// Alias for pop_back()
fn bpop(&mut self) -> Option<T> {
self.pop_back()
}
/// Alias for push_back()
fn bpush(&mut self, value: T) {
self.push_back(value);
}
/// Just turns the deque into a vector
fn to_vec(mut self) -> Vec<T> {
let mut vec = vec![];
while let Some(item) = self.fpop() {
vec.push(item)
}
vec
}
}

View File

@@ -1,119 +1,81 @@
use std::fmt::Display;
use std::{fmt::Display, str::FromStr};
use crate::parse::lex::Span;
use crate::prelude::*;
use crate::{parse::lex::Span, prelude::*};
pub type ShResult<T> = Result<T,ShErr>;
pub type ShResult<'s,T> = Result<T,ShErr<'s>>;
pub trait ResultExt {
fn eprint(self) -> Self;
fn abort_if_err(&self);
#[derive(Debug)]
pub enum ShErr<'s> {
Simple { kind: ShErrKind, msg: String },
Full { kind: ShErrKind, msg: String, span: Span<'s> }
}
#[derive(Clone,Debug)]
pub struct BlamePair {
input: String,
span: Rc<RefCell<Span>>
impl<'s> ShErr<'s> {
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
let msg = msg.into();
Self::Simple { kind, msg }
}
impl BlamePair {
pub fn new(input: String, span: Rc<RefCell<Span>>) -> Self {
Self { input, span }
pub fn full(kind: ShErrKind, msg: impl Into<String>, span: Span<'s>) -> Self {
let msg = msg.into();
Self::Full { kind, msg, span }
}
pub fn start(&self) -> usize {
self.span.borrow().start()
pub fn unpack(self) -> (ShErrKind,String,Option<Span<'s>>) {
match self {
ShErr::Simple { kind, msg } => (kind,msg,None),
ShErr::Full { kind, msg, span } => (kind,msg,Some(span))
}
pub fn end(&self) -> usize {
self.span.borrow().end()
}
pub fn len(&self) -> usize {
self.input.len()
pub fn with_span(sherr: ShErr, span: Span<'s>) -> Self {
let (kind,msg,_) = sherr.unpack();
Self::Full { kind, msg, span }
}
}
impl Into<String> for BlamePair {
fn into(self) -> String {
self.input
}
}
impl<T, E: Display> ResultExt for Result<T, E> {
fn eprint(self) -> Self {
if let Err(err) = &self {
eprintln!("{}", err);
}
self
}
fn abort_if_err(&self) {
if let Err(err) = &self {
eprintln!("{}", err);
sh_quit(1)
impl<'s> Display for ShErr<'s> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Simple { msg, kind: _ } => writeln!(f, "{}", msg),
Self::Full { msg, kind: _, span: _ } => writeln!(f, "{}", msg)
}
}
}
pub trait Blame {
/// Blame a span for a propagated error. This will convert a ShErr::Simple into a ShErr::Full
/// This will also set the span on a ShErr::Builder
fn blame(self, input: String, span: Rc<RefCell<Span>>) -> Self;
/// If an error is propagated to this point, then attempt to blame a span.
/// If the error in question has already blamed a span, don't overwrite it.
/// Used as a last resort in higher level contexts in case an error somehow goes unblamed
fn try_blame(self, input: String, span: Rc<RefCell<Span>>) -> Self;
}
impl From<std::io::Error> for ShErr {
impl<'s> From<std::io::Error> for ShErr<'s> {
fn from(_: std::io::Error) -> Self {
ShErr::io()
let msg = std::io::Error::last_os_error();
ShErr::simple(ShErrKind::IoErr, msg.to_string())
}
}
impl From<std::env::VarError> for ShErr {
impl<'s> From<std::env::VarError> for ShErr<'s> {
fn from(value: std::env::VarError) -> Self {
ShErr::simple(ShErrKind::InternalErr, &value.to_string())
}
}
impl From<rustyline::error::ReadlineError> for ShErr {
impl<'s> From<rustyline::error::ReadlineError> for ShErr<'s> {
fn from(value: rustyline::error::ReadlineError) -> Self {
ShErr::simple(ShErrKind::ParseErr, &value.to_string())
}
}
impl From<Errno> for ShErr {
impl<'s> From<Errno> for ShErr<'s> {
fn from(value: Errno) -> Self {
ShErr::simple(ShErrKind::Errno, &value.to_string())
}
}
impl<T> Blame for Result<T,ShErr> {
fn blame(self, input: String, span: Rc<RefCell<Span>>) -> Self {
if let Err(mut e) = self {
e.blame(input,span);
Err(e)
} else {
self
}
}
fn try_blame(self, input: String, span: Rc<RefCell<Span>>) -> Self {
if let Err(mut e) = self {
e.try_blame(input,span);
Err(e)
} else {
self
}
}
}
#[derive(Debug,Copy,Clone,PartialEq,Eq)]
#[derive(Debug)]
pub enum ShErrKind {
IoErr,
SyntaxErr,
ParseErr,
InternalErr,
ExecFail,
ResourceLimitExceeded,
BadPermission,
Errno,
FileNotFound,
CmdNotFound,
CleanExit,
FuncReturn,
@@ -121,156 +83,3 @@ pub enum ShErrKind {
LoopBreak,
Null
}
impl Default for ShErrKind {
fn default() -> Self {
Self::Null
}
}
#[derive(Clone,Debug)]
pub enum ShErr {
Simple { kind: ShErrKind, message: String },
Full { kind: ShErrKind, message: String, blame: BlamePair },
}
impl ShErr {
pub fn simple<S: Into<String>>(kind: ShErrKind, message: S) -> Self {
Self::Simple { kind, message: message.into() }
}
pub fn io() -> Self {
io::Error::last_os_error().into()
}
pub fn full<S: Into<String>>(kind: ShErrKind, message: S, input: String, span: Rc<RefCell<Span>>) -> Self {
let blame = BlamePair::new(input.to_string(), span);
Self::Full { kind, message: message.into(), blame }
}
pub fn try_blame(&mut self, input: String, span: Rc<RefCell<Span>>) {
let blame_pair = BlamePair::new(input, span);
match self {
Self::Full {..} => {
/* Do not overwrite */
}
Self::Simple { kind, message } => {
*self = Self::Full { kind: core::mem::take(kind), message: core::mem::take(message), blame: blame_pair }
}
}
}
pub fn blame(&mut self, input: String, span: Rc<RefCell<Span>>) {
let blame_pair = BlamePair::new(input, span);
match self {
Self::Full { kind: _, message: _, blame } => {
*blame = blame_pair;
}
Self::Simple { kind, message } => {
*self = Self::Full { kind: core::mem::take(kind), message: core::mem::take(message), blame: blame_pair }
}
}
}
pub fn with_msg(&mut self, new_message: String) {
match self {
Self::Full { kind: _, message, blame: _ } => {
*message = new_message
}
Self::Simple { kind: _, message } => {
*message = new_message
}
}
}
pub fn kind(&self) -> ShErrKind {
match self {
ShErr::Simple { kind, message: _ } => {
*kind
}
ShErr::Full { kind, message: _, blame: _ } => {
*kind
}
}
}
pub fn with_kind(&mut self, new_kind: ShErrKind) {
match self {
Self::Full { kind, message: _, blame: _ } => {
*kind = new_kind
}
Self::Simple { kind, message: _ } => {
*kind = new_kind
}
}
}
pub fn display_kind(&self) -> String {
match self {
ShErr::Simple { kind, message: _ } |
ShErr::Full { kind, message: _, blame: _ } => {
match kind {
ShErrKind::IoErr => "I/O Error: ".into(),
ShErrKind::SyntaxErr => "Syntax Error: ".into(),
ShErrKind::ParseErr => "Parse Error: ".into(),
ShErrKind::InternalErr => "Internal Error: ".into(),
ShErrKind::ExecFail => "Execution Failed: ".into(),
ShErrKind::Errno => "ERRNO: ".into(),
ShErrKind::CmdNotFound => "Command not found: ".into(),
ShErrKind::CleanExit |
ShErrKind::FuncReturn |
ShErrKind::LoopContinue |
ShErrKind::LoopBreak |
ShErrKind::Null => "".into()
}
}
}
}
pub fn get_line(&self) -> (usize,usize,String) {
if let ShErr::Full { kind: _, message: _, blame } = self {
unsafe {
let mut dist = 0;
let mut line_no = 0;
let window = self.get_window();
let mut lines = window.lines();
while let Some(line) = lines.next() {
line_no += 1;
dist += line.len();
if dist > blame.start() {
dist -= line.len();
let offset = blame.start() - dist;
return (offset,line_no,line.to_string())
}
}
}
(0,0,String::new())
} else {
(0,0,String::new())
}
}
pub fn get_window(&self) -> String {
if let ShErr::Full { kind: _, message: _, blame } = self.clone() {
let window: String = blame.into();
window.split_once('\n').unwrap_or((&window,"")).0.to_string()
} else {
String::new()
}
}
}
impl Display for ShErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_display = match self {
ShErr::Simple { kind: _, message } => format!("{}{}",self.display_kind(),message),
ShErr::Full { kind: _, message, blame } => {
let (offset,line_no,line_text) = self.get_line();
let dist = blame.end().saturating_sub(blame.start());
let padding = " ".repeat(offset);
let line_inner = "~".repeat(dist.saturating_sub(2));
let err_kind = &self.display_kind().styled(Style::Red | Style::Bold);
let stat_line = format!("[{}:{}] - {}{}",line_no,offset,err_kind,message);
let indicator_line = if dist == 1 {
format!("{}^",padding)
} else {
format!("{}^{}^",padding,line_inner)
};
let error_full = format!("\n{}\n{}\n{}\n",stat_line,line_text,indicator_line);
error_full
}
};
write!(f,"{}",error_display)
}
}

View File

@@ -1,6 +1,2 @@
pub mod sys;
#[macro_use]
pub mod utils;
pub mod collections;
pub mod error;
pub mod term;

View File

@@ -1,118 +0,0 @@
use std::{fmt::Display, os::{fd::AsRawFd, unix::fs::PermissionsExt}};
use nix::sys::termios;
use crate::prelude::*;
pub const SIG_EXIT_OFFSET: i32 = 128;
pub fn get_path_cmds() -> ShResult<Vec<String>> {
let mut cmds = vec![];
let path_var = std::env::var("PATH")?;
let paths = path_var.split(':');
for path in paths {
let path = PathBuf::from(&path);
if path.is_dir() {
let path_files = std::fs::read_dir(&path)?;
for file in path_files {
let file_path = file?.path();
if file_path.is_file() {
if let Ok(meta) = std::fs::metadata(&file_path) {
let perms = meta.permissions();
if perms.mode() & 0o111 != 0 {
let file_name = file_path.file_name().unwrap();
cmds.push(file_name.to_str().unwrap().to_string())
}
}
}
}
}
}
Ok(cmds)
}
pub fn get_bin_path(command: &str, shenv: &ShEnv) -> Option<PathBuf> {
let env = shenv.vars().env();
let path_var = env.get("PATH")?;
let mut paths = path_var.split(':');
let script_check = PathBuf::from(command);
if script_check.is_file() {
return Some(script_check)
}
while let Some(raw_path) = paths.next() {
let mut path = PathBuf::from(raw_path);
path.push(command);
//TODO: handle this unwrap
if path.exists() {
return Some(path)
}
}
None
}
pub fn write_out(text: impl Display) -> ShResult<()> {
write(borrow_fd(1), text.to_string().as_bytes())?;
Ok(())
}
pub fn write_err(text: impl Display) -> ShResult<()> {
write(borrow_fd(2), text.to_string().as_bytes())?;
Ok(())
}
/// Return is `readpipe`, `writepipe`
/// Contains all of the necessary boilerplate for grabbing two pipe fds using libc::pipe()
pub fn c_pipe() -> Result<(RawFd,RawFd),Errno> {
let mut pipes: [i32;2] = [0;2];
let ret = unsafe { libc::pipe(pipes.as_mut_ptr()) };
if ret < 0 {
return Err(Errno::from_raw(ret))
}
Ok((pipes[0],pipes[1]))
}
pub fn sh_quit(code: i32) -> ! {
write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok();
}
});
if let Some(termios) = crate::get_saved_termios() {
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
}
if code == 0 {
write_err("exit\n").ok();
} else {
write_err(format!("exit {code}\n")).ok();
}
exit(code);
}
pub fn read_to_string(fd: i32) -> ShResult<String> {
let mut buf = Vec::with_capacity(4096);
let mut temp_buf = [0u8;1024];
loop {
match read(fd, &mut temp_buf) {
Ok(0) => break, // EOF
Ok(n) => buf.extend_from_slice(&temp_buf[..n]),
Err(Errno::EINTR) => continue, // Retry on EINTR
Err(e) => return Err(e.into()), // Return other errors
}
}
Ok(String::from_utf8_lossy(&buf).to_string())
}
pub fn execvpe(cmd: String, argv: Vec<String>, envp: Vec<String>) -> Result<(),Errno> {
let cmd_raw = CString::new(cmd).unwrap();
let argv = argv.into_iter().map(|arg| CString::new(arg).unwrap()).collect::<Vec<CString>>();
let envp = envp.into_iter().map(|var| CString::new(var).unwrap()).collect::<Vec<CString>>();
nix::unistd::execvpe(&cmd_raw, &argv, &envp).unwrap();
Ok(())
}

View File

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

View File

@@ -1,410 +0,0 @@
use core::fmt::{Debug, Display, Write};
use std::{os::fd::{AsRawFd, BorrowedFd}, str::FromStr};
use crate::{parse::lex::EXPANSIONS, prelude::*};
use super::term::StyleSet;
pub trait RedirTargetType {
fn as_tgt(self) -> RedirTarget;
}
impl RedirTargetType for PathBuf {
fn as_tgt(self) -> RedirTarget {
RedirTarget::File(self)
}
}
impl RedirTargetType for i32 {
fn as_tgt(self) -> RedirTarget {
RedirTarget::Fd(self)
}
}
pub trait StrOps {
/// This function operates on anything that implements `AsRef<str>` and `Display`, which is mainly strings.
/// It takes a 'Style' which can be passed as a single Style object like `Style::Cyan` or a Bit OR of many styles,
/// For instance: `Style::Red | Style::Bold | Style::Italic`
fn styled<S: Into<StyleSet>>(self, style: S) -> String;
}
impl<T: AsRef<str> + Display> StrOps for T {
fn styled<S: Into<StyleSet>>(self, style: S) -> String {
style_text(&self, style)
}
}
pub trait ArgVec {
fn as_strings(self, shenv: &mut ShEnv) -> Vec<String>;
fn drop_first(self) -> Vec<Token>;
}
impl ArgVec for Vec<Token> {
/// Converts the contained tokens into strings.
fn as_strings(self, shenv: &mut ShEnv) -> Vec<String> {
let mut argv_iter = self.into_iter();
let mut argv_processed = vec![];
while let Some(arg) = argv_iter.next() {
let cleaned = clean_string(&arg.as_raw(shenv)).trim_matches(' ').to_string();
argv_processed.push(cleaned);
}
argv_processed
}
/// This is used to ignore the first argument
/// Most commonly used in builtins where execvpe is not used
fn drop_first(self) -> Vec<Token> {
self[1..].to_vec()
}
}
#[macro_export]
macro_rules! test {
($test:block) => {
$test
exit(1)
};
}
#[derive(Clone, Copy, PartialEq, PartialOrd, Ord, Eq , Debug)]
#[repr(i32)]
pub enum LogLevel {
ERROR = 1,
WARN = 2,
INFO = 3,
DEBUG = 4,
TRACE = 5,
NULL = 0
}
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ERROR => write!(f,"{}","ERROR".styled(Style::Red | Style::Bold)),
WARN => write!(f,"{}","WARN".styled(Style::Yellow | Style::Bold)),
INFO => write!(f,"{}","INFO".styled(Style::Green | Style::Bold)),
DEBUG => write!(f,"{}","DEBUG".styled(Style::Magenta | Style::Bold)),
TRACE => write!(f,"{}","TRACE".styled(Style::Blue | Style::Bold)),
NULL => write!(f,"")
}
}
}
#[macro_export]
macro_rules! log {
($level:expr, $($var:ident),+) => {{
$(
let var_name = stringify!($var);
if $level <= log_level() {
let file = file!();
let file_styled = file.styled(Style::Cyan);
let line = line!();
let line_styled = line.to_string().styled(Style::Cyan);
let logged = format!("[{}][{}:{}] {} = {:#?}",$level, file_styled,line_styled,var_name, &$var);
write(borrow_fd(2),format!("{}\n",logged).as_bytes()).unwrap();
}
)+
}};
($level:expr, $lit:literal) => {{
if $level <= log_level() {
let file = file!();
let file_styled = file.styled(Style::Cyan);
let line = line!();
let line_styled = line.to_string().styled(Style::Cyan);
let logged = format!("[{}][{}:{}] {}", $level, file_styled, line_styled, $lit);
write(borrow_fd(2), format!("{}\n", logged).as_bytes()).unwrap();
}
}};
($level:expr, $($arg:tt)*) => {{
if $level <= log_level() {
let formatted = format!($($arg)*);
let file = file!();
let file_styled = file.styled(Style::Cyan);
let line = line!();
let line_styled = line.to_string().styled(Style::Cyan);
let logged = format!("[{}][{}:{}] {}", $level, file_styled, line_styled, formatted);
write(borrow_fd(2), format!("{}\n", logged).as_bytes()).unwrap();
}
}};
}
#[macro_export]
macro_rules! bp {
($var:expr) => {
log!($var);
let mut buf = String::new();
readln!("Press enter to continue", buf);
};
($($arg:tt)*) => {
log!($(arg)*);
let mut buf = String::new();
readln!("Press enter to continue", buf);
};
}
pub fn borrow_fd<'a>(fd: i32) -> BorrowedFd<'a> {
unsafe { BorrowedFd::borrow_raw(fd) }
}
// TODO: add more of these
#[derive(Debug,Clone,PartialEq,Copy)]
pub enum RedirType {
Input,
Output,
Append,
HereDoc,
HereString
}
#[derive(Debug,Clone)]
pub enum RedirTarget {
Fd(i32),
File(PathBuf),
HereDoc(String),
HereString(String),
}
#[derive(Debug,Clone)]
pub struct RedirBldr {
src: Option<i32>,
op: Option<RedirType>,
tgt: Option<RedirTarget>,
}
impl RedirBldr {
pub fn new() -> Self {
Self { src: None, op: None, tgt: None }
}
pub fn with_src(self, src: i32) -> Self {
Self { src: Some(src), op: self.op, tgt: self.tgt }
}
pub fn with_op(self, op: RedirType) -> Self {
Self { src: self.src, op: Some(op), tgt: self.tgt }
}
pub fn with_tgt(self, tgt: RedirTarget) -> Self {
Self { src: self.src, op: self.op, tgt: Some(tgt) }
}
pub fn src(&self) -> Option<i32> {
self.src
}
pub fn op(&self) -> Option<RedirType> {
self.op
}
pub fn tgt(&self) -> Option<&RedirTarget> {
self.tgt.as_ref()
}
pub fn build(self) -> Redir {
Redir::new(self.src.unwrap(), self.op.unwrap(), self.tgt.unwrap())
}
}
impl FromStr for RedirBldr {
type Err = ShErr;
fn from_str(raw: &str) -> ShResult<Self> {
let mut redir_bldr = RedirBldr::new().with_src(1);
let mut chars = raw.chars().peekable();
let mut raw_src = String::new();
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
raw_src.push(chars.next().unwrap())
}
if !raw_src.is_empty() {
let src = raw_src.parse::<i32>().unwrap();
redir_bldr = redir_bldr.with_src(src);
}
while let Some(ch) = chars.next() {
match ch {
'<' => {
redir_bldr = redir_bldr.with_src(0);
if chars.peek() == Some(&'<') {
chars.next();
if chars.peek() == Some(&'<') {
chars.next();
redir_bldr = redir_bldr.with_op(RedirType::HereString);
break
} else {
redir_bldr = redir_bldr.with_op(RedirType::HereDoc);
let body = extract_heredoc_body(raw)?;
redir_bldr = redir_bldr.with_tgt(RedirTarget::HereDoc(body));
break
}
} else {
redir_bldr = redir_bldr.with_op(RedirType::Input);
}
}
'>' => {
if chars.peek() == Some(&'>') {
chars.next();
redir_bldr = redir_bldr.with_op(RedirType::Append);
break
} else {
redir_bldr = redir_bldr.with_op(RedirType::Output);
break
}
}
'&' => {
let mut raw_tgt = String::new();
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
raw_tgt.push(chars.next().unwrap())
}
let redir_target = RedirTarget::Fd(raw_tgt.parse::<i32>().unwrap());
redir_bldr = redir_bldr.with_tgt(redir_target);
}
_ => unreachable!()
}
}
Ok(redir_bldr)
}
}
#[derive(Debug,Clone)]
pub struct Redir {
pub src: i32,
pub op: RedirType,
pub tgt: RedirTarget
}
impl Redir {
pub fn new(src: i32, op: RedirType, tgt: RedirTarget) -> Self {
Self { src, op, tgt }
}
pub fn output(src: i32, tgt: impl RedirTargetType) -> Self {
Self::new(src, RedirType::Output, tgt.as_tgt())
}
pub fn input(src: i32, tgt: impl RedirTargetType) -> Self {
Self::new(src, RedirType::Input, tgt.as_tgt())
}
}
#[derive(Debug,Clone)]
pub struct CmdRedirs {
open: Vec<RawFd>,
targets_fd: Vec<Redir>,
targets_file: Vec<Redir>,
targets_text: Vec<Redir>,
}
impl CmdRedirs {
pub fn new(mut redirs: Vec<Redir>) -> Self {
let mut targets_fd = vec![];
let mut targets_file = vec![];
let mut targets_text = vec![];
while let Some(redir) = redirs.pop() {
let Redir { src: _, op: _, tgt } = &redir;
match tgt {
RedirTarget::Fd(_) => targets_fd.push(redir),
RedirTarget::File(_) => targets_file.push(redir),
_ => targets_text.push(redir)
}
}
Self { open: vec![], targets_fd, targets_file, targets_text }
}
pub fn activate(&mut self) -> ShResult<()> {
self.open_file_tgts()?;
self.open_fd_tgts()?;
self.open_text_tgts()?;
Ok(())
}
pub fn open_text_tgts(&mut self) -> ShResult<()> {
while let Some(redir) = self.targets_text.pop() {
let Redir { src, op: _, tgt } = redir;
let (rpipe, wpipe) = c_pipe()?;
let src = borrow_fd(src);
let wpipe_fd = borrow_fd(wpipe);
match tgt {
RedirTarget::HereDoc(body) |
RedirTarget::HereString(body) => {
write(wpipe_fd, body.as_bytes())?;
close(wpipe).ok();
}
_ => unreachable!()
}
dup2(rpipe, src.as_raw_fd())?;
close(rpipe).ok();
}
Ok(())
}
pub fn open_file_tgts(&mut self) -> ShResult<()> {
while let Some(redir) = self.targets_file.pop() {
let Redir { src, op, tgt } = redir;
let src = borrow_fd(src);
let file_fd = if let RedirTarget::File(path) = tgt {
let flags = match op {
RedirType::Input => OFlag::O_RDONLY,
RedirType::Output => OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
RedirType::Append => OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_APPEND,
_ => unimplemented!()
};
let mode = Mode::from_bits(0o644).unwrap();
open(&path,flags,mode)?
} else { unreachable!() };
dup2(file_fd.as_raw_fd(),src.as_raw_fd())?;
close(file_fd.as_raw_fd())?;
self.open.push(src.as_raw_fd());
}
Ok(())
}
pub fn open_fd_tgts(&mut self) -> ShResult<()> {
while let Some(redir) = self.targets_fd.pop() {
let Redir { src, op: _, tgt } = redir;
let tgt = if let RedirTarget::Fd(fd) = tgt {
borrow_fd(fd)
} else { unreachable!() };
let src = borrow_fd(src);
dup2(tgt.as_raw_fd(), src.as_raw_fd())?;
close(tgt.as_raw_fd())?;
self.open.push(src.as_raw_fd());
}
Ok(())
}
}
pub fn extract_heredoc_body(body: &str) -> ShResult<String> {
log!(DEBUG,body);
if let Some(cleaned) = body.strip_prefix("<<") {
if let Some((delim,body)) = cleaned.split_once('\n') {
if let Some(body) = body.trim().strip_suffix(&delim) {
Ok(body.to_string())
} else {
return Err(ShErr::simple(ShErrKind::ParseErr, "Malformed closing delimiter in heredoc"))
}
} else {
return Err(ShErr::simple(ShErrKind::ParseErr, "Invalid heredoc delimiter"))
}
} else {
return Err(ShErr::simple(ShErrKind::ParseErr, "Invalid heredoc operator"))
}
}
pub fn check_expansion(s: &str) -> Option<TkRule> {
let rule = Lexer::get_rule(s);
if EXPANSIONS.contains(&rule) {
Some(rule)
} else {
None
}
}
pub fn clean_string(s: impl ToString) -> String {
let s = s.to_string();
if (s.starts_with('"') && s.ends_with('"')) ||
(s.starts_with('\'') && s.ends_with('\'')) ||
(s.starts_with('`') && s.ends_with('`'))
{
if s.len() > 1 {
s[1..s.len() - 1].to_string()
} else {
s
}
} else if s.starts_with("$(") && s.ends_with(')') {
s[2..s.len() - 1].to_string()
} else {
s
}
}

View File

@@ -1,126 +0,0 @@
#![allow(static_mut_refs,unused_unsafe)]
pub mod libsh;
pub mod shellenv;
pub mod parse;
pub mod prelude;
pub mod execute;
pub mod signal;
pub mod prompt;
pub mod builtin;
pub mod expand;
pub mod tests;
use std::os::fd::AsRawFd;
use nix::sys::termios::{self, LocalFlags, Termios};
use signal::sig_setup;
use crate::prelude::*;
pub static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
bitflags! {
pub struct FernFlags: u32 {
const NO_RC = 0b000001;
const NO_HIST = 0b000010;
const INTERACTIVE = 0b000100;
}
}
pub fn save_termios() {
unsafe {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
Some(termios)
} else {
None
});
}
}
pub fn get_saved_termios() -> Option<Termios> {
unsafe {
// This is only used when the shell exits so it's fine
// SAVED_TERMIOS is only mutated once at the start as well
SAVED_TERMIOS.clone().flatten()
}
}
fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
}
}
fn parse_args(shenv: &mut ShEnv) {
let mut args = std::env::args().skip(1);
let mut script_path: Option<PathBuf> = None;
let mut command: Option<String> = None;
let mut flags = FernFlags::empty();
log!(DEBUG, args);
while let Some(mut arg) = args.next() {
log!(DEBUG, arg);
if arg.starts_with("--") {
arg = arg.strip_prefix("--").unwrap().to_string();
match arg.as_str() {
"no-rc" => flags |= FernFlags::NO_RC,
"no-hist" => flags |= FernFlags::NO_HIST,
_ => eprintln!("Warning - Unrecognized option: {arg}")
}
} else if arg.starts_with('-') {
arg = arg.strip_prefix('-').unwrap().to_string();
match arg.as_str() {
"c" => command = args.next(),
_ => eprintln!("Warning - Unrecognized option: {arg}")
}
} else {
let path_check = PathBuf::from(&arg);
if path_check.is_file() {
script_path = Some(path_check);
}
}
}
if !flags.contains(FernFlags::NO_RC) {
let _ = shenv.source_rc().eprint();
}
if let Some(cmd) = command {
let input = clean_string(cmd);
let _ = exec_input(input, shenv).eprint();
} else if let Some(script) = script_path {
let _ = shenv.source_file(script).eprint();
} else {
interactive(shenv);
}
}
pub fn main() {
sig_setup();
save_termios();
set_termios();
let mut shenv = ShEnv::new();
parse_args(&mut shenv);
}
fn interactive(shenv: &mut ShEnv) {
loop {
log!(TRACE, "Entered loop");
match prompt::read_line(shenv) {
Ok(line) => {
shenv.meta_mut().start_timer();
let _ = exec_input(line, shenv).eprint();
}
Err(e) => {
eprintln!("{}",e);
continue;
}
};
}
}

193
src/parse/execute.rs Normal file
View File

@@ -0,0 +1,193 @@
use std::collections::VecDeque;
use nix::sys::wait::WaitPidFlag;
use crate::{libsh::error::ShResult, prelude::*, procio::{IoFrame, IoPipe, IoStack}, state};
use super::{lex::Tk, ConjunctNode, ConjunctOp, NdRule, Node, Redir, RedirType};
/// Arguments to the execvpe function
pub struct ExecArgs {
pub cmd: CString,
pub argv: Vec<CString>,
pub envp: Vec<CString>
}
impl ExecArgs {
pub fn new(argv: Vec<String>) -> Self {
assert!(!argv.is_empty());
let cmd = Self::get_cmd(&argv);
let argv = Self::get_argv(argv);
let envp = Self::get_envp();
Self { cmd, argv, envp }
}
pub fn get_cmd(argv: &[String]) -> CString {
CString::new(argv[0].as_str()).unwrap()
}
pub fn get_argv(argv: Vec<String>) -> Vec<CString> {
argv.into_iter().map(|s| CString::new(s).unwrap()).collect()
}
pub fn get_envp() -> Vec<CString> {
std::env::vars().map(|v| CString::new(format!("{}={}",v.0,v.1)).unwrap()).collect()
}
}
pub struct Dispatcher<'t> {
nodes: VecDeque<Node<'t>>,
pub io_stack: IoStack
}
impl<'t> Dispatcher<'t> {
pub fn new(nodes: Vec<Node<'t>>) -> Self {
let nodes = VecDeque::from(nodes);
Self { nodes, io_stack: IoStack::new() }
}
pub fn begin_dispatch(&mut self) -> ShResult<'t,()> {
while let Some(list) = self.nodes.pop_front() {
self.dispatch_node(list)?;
}
Ok(())
}
pub fn dispatch_node(&mut self, node: Node<'t>) -> ShResult<'t,()> {
match node.class {
NdRule::CmdList {..} => self.exec_conjunction(node)?,
NdRule::Pipeline {..} => self.exec_pipeline(node)?,
NdRule::Command {..} => self.exec_cmd(node)?,
_ => unreachable!()
}
Ok(())
}
pub fn exec_conjunction(&mut self, conjunction: Node<'t>) -> ShResult<'t,()> {
let NdRule::CmdList { elements } = conjunction.class else {
unreachable!()
};
let mut elem_iter = elements.into_iter();
while let Some(element) = elem_iter.next() {
let ConjunctNode { cmd, operator } = element;
self.dispatch_node(*cmd)?;
let status = state::get_status();
match operator {
ConjunctOp::And => if status != 0 { break },
ConjunctOp::Or => if status == 0 { break },
ConjunctOp::Null => break
}
}
Ok(())
}
pub fn exec_pipeline(&mut self, pipeline: Node<'t>) -> ShResult<'t,()> {
let NdRule::Pipeline { cmds, pipe_err } = pipeline.class else {
unreachable!()
};
// Zip the commands and their respective pipes into an iterator
let pipes_and_cmds = get_pipe_stack(cmds.len())
.into_iter()
.zip(cmds);
for ((rpipe,wpipe), cmd) in pipes_and_cmds {
if let Some(pipe) = rpipe {
self.io_stack.push_to_frame(pipe);
}
if let Some(pipe) = wpipe {
self.io_stack.push_to_frame(pipe);
}
self.dispatch_node(cmd)?;
}
Ok(())
}
pub fn exec_cmd(&mut self, cmd: Node<'t>) -> ShResult<'t,()> {
let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!()
};
for redir in cmd.redirs {
self.io_stack.push_to_frame(redir);
}
let exec_args = ExecArgs::new(prepare_argv(argv));
let io_frame = self.io_stack.pop_frame();
run_fork(
io_frame,
exec_args,
def_child_action,
def_parent_action
)?;
Ok(())
}
}
pub fn prepare_argv(argv: Vec<Tk>) -> Vec<String> {
let mut args = vec![];
for arg in argv {
let flags = arg.flags;
let span = arg.span.clone();
let expanded = arg.expand(span, flags);
args.extend(expanded.get_words());
}
args
}
pub fn run_fork<'t,C,P>(
io_frame: IoFrame,
exec_args: ExecArgs,
child_action: C,
parent_action: P,
) -> ShResult<'t,()>
where
C: Fn(IoFrame,ExecArgs),
P: Fn(IoFrame,Pid) -> ShResult<'t,()>
{
match unsafe { fork()? } {
ForkResult::Child => {
child_action(io_frame,exec_args);
exit(1);
}
ForkResult::Parent { child } => {
parent_action(io_frame,child)
}
}
}
/// The default behavior for the child process after forking
pub fn def_child_action<'t>(mut io_frame: IoFrame, exec_args: ExecArgs) {
io_frame.redirect().unwrap();
execvpe(&exec_args.cmd, &exec_args.argv, &exec_args.envp).unwrap();
}
/// The default behavior for the parent process after forking
pub fn def_parent_action<'t>(io_frame: IoFrame, child: Pid) -> ShResult<'t,()> {
let status = waitpid(child, Some(WaitPidFlag::WSTOPPED))?;
match status {
WaitStatus::Exited(_, status) => state::set_status(status),
_ => unimplemented!()
}
Ok(())
}
/// Initialize the pipes for a pipeline
/// The first command gets `(None, WPipe)`
/// The last command gets `(RPipe, None)`
/// Commands inbetween get `(RPipe, WPipe)`
/// If there is only one command, it gets `(None, None)`
pub fn get_pipe_stack(num_cmds: usize) -> Vec<(Option<Redir>,Option<Redir>)> {
let mut stack = Vec::with_capacity(num_cmds);
let mut prev_read: Option<Redir> = None;
for i in 0..num_cmds {
if i == num_cmds - 1 {
stack.push((prev_read.take(), None));
} else {
let (rpipe,wpipe) = IoPipe::get_pipes();
let r_redir = Redir::new(Box::new(rpipe), RedirType::Input);
let w_redir = Redir::new(Box::new(wpipe), RedirType::Output);
// Push (prev_read, Some(w_redir)) and set prev_read to r_redir
stack.push((prev_read.take(), Some(w_redir)));
prev_read = Some(r_redir);
}
}
stack
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,180 +1,35 @@
pub use std::{
io::{
// Standard Library Common IO and FS Abstractions
pub use std::io::{
self,
BufRead,
BufReader,
BufWriter,
Error,
ErrorKind,
Read,
Write
},
cell::RefCell,
rc::Rc,
os::fd::{
OwnedFd,
BorrowedFd,
RawFd,
FromRawFd
},
collections::{
VecDeque,
HashMap,
},
ffi::{
CStr,
CString
},
path::{
Path,
PathBuf,
},
process::{
exit
},
Seek,
SeekFrom,
Write,
};
pub use bitflags::bitflags;
pub use std::fs::{ self, File, OpenOptions };
pub use std::path::{ Path, PathBuf };
pub use std::ffi::{ CStr, CString };
pub use std::process::exit;
// Unix-specific IO abstractions
pub use std::os::unix::io::{ AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd, };
// Nix crate for POSIX APIs
pub use nix::{
fcntl::{
open,
OFlag,
},
sys::{
signal::{
killpg,
kill,
signal,
pthread_sigmask,
SigmaskHow,
SigSet,
SigHandler,
Signal
},
wait::{
waitpid,
WaitStatus as WtStat,
WaitPidFlag as WtFlag
},
stat::Mode,
memfd::memfd_create,
},
errno::Errno,
unistd::{
Pid,
ForkResult::*,
fork,
getppid,
getpid,
getpgid,
getpgrp,
geteuid,
read,
write,
isatty,
tcgetpgrp,
tcsetpgrp,
dup,
dup2,
close,
},
libc,
};
pub use crate::{
libsh::{
term::{
Style,
style_text
},
utils::{
LogLevel::*,
ArgVec,
Redir,
RedirType,
RedirBldr,
StrOps,
RedirTarget,
CmdRedirs,
borrow_fd,
check_expansion,
clean_string
},
collections::{
VecDequeAliases
},
fcntl::{ open, OFlag },
sys::{
self,
get_path_cmds,
get_bin_path,
sh_quit,
read_to_string,
write_err,
write_out,
c_pipe,
execvpe
signal::{ self, kill, SigHandler, Signal },
stat::Mode,
wait::{ waitpid, WaitStatus },
},
error::{
ResultExt,
Blame,
ShErrKind,
ShErr,
ShResult
},
},
builtin::{
echo::echo,
cd::cd,
pwd::pwd,
read::read_builtin,
alias::alias,
control_flow::sh_flow,
export::export,
source::source,
jobctl::{
continue_job,
jobs
},
BUILTINS
},
expand::{
expand_argv,
expand_token,
prompt::expand_prompt,
alias::expand_aliases
},
shellenv::{
self,
dispatch_job,
log_level,
attach_tty,
term_ctlr,
take_term,
jobs::{
JobTab,
JobID,
write_jobs,
read_jobs
},
exec_ctx::ExecFlags,
shenv::ShEnv
},
execute::{
exec_input,
Executor,
},
parse::{
SynTree,
LoopKind,
Node,
CmdGuard,
NdFlag,
NdRule,
Parser,
ParseRule,
lex::{
EXPANSIONS,
Span,
Token,
TkRule,
Lexer,
LexRule
},
},
log,
test,
bp,
libc::{ STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO },
unistd::{ dup, read, write, close, dup2, execvpe, fork, pipe, Pid, ForkResult },
};
// Additional utilities, if needed, can be added here

252
src/procio.rs Normal file
View File

@@ -0,0 +1,252 @@
use std::{fmt::Debug, ops::{Deref, DerefMut}};
use crate::{libsh::error::ShResult, parse::Redir, prelude::*};
// Credit to fish-shell for many of the implementation ideas present in this module
// https://fishshell.com/
pub enum IoMode {
Fd,
File,
Pipe,
}
pub trait IoInfo: Read {
fn mode(&self) -> IoMode;
/// The fildesc that is replaced by src_fd in dup2()
/// e.g. `dup2(src_fd, tgt_fd)`
fn tgt_fd(&self) -> RawFd;
/// The fildesc that replaces tgt_fd in dup2()
/// e.g. `dup2(src_fd, tgt_fd)`
fn src_fd(&self) -> RawFd;
fn print(&self) -> String;
fn close(&mut self) -> ShResult<()>;
}
macro_rules! read_impl {
($type:path) => {
impl Read for $type {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let src_fd = self.src_fd();
Ok(read(src_fd, buf)?)
}
}
};
}
read_impl!(IoPipe);
read_impl!(IoFile);
read_impl!(IoFd);
// TODO: implement this
impl Debug for Box<dyn IoInfo> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f,"{}",self.print())
}
}
#[derive(Debug)]
pub struct IoFd {
tgt_fd: RawFd,
src_fd: RawFd
}
impl IoFd {
pub fn new(tgt_fd: RawFd, src_fd: RawFd) -> Self {
Self { tgt_fd, src_fd }
}
}
impl IoInfo for IoFd {
fn mode(&self) -> IoMode {
IoMode::Fd
}
fn tgt_fd(&self) -> RawFd {
self.tgt_fd
}
fn src_fd(&self) -> RawFd {
self.src_fd
}
fn close(&mut self) -> ShResult<()> {
if self.src_fd == -1 {
return Ok(())
}
close(self.src_fd)?;
self.src_fd = -1;
Ok(())
}
fn print(&self) -> String {
format!("{:?}",self)
}
}
#[derive(Debug)]
pub struct IoFile {
tgt_fd: RawFd,
file: File
}
impl IoFile {
pub fn new(tgt_fd: RawFd, file: File) -> Self {
Self { tgt_fd, file }
}
}
impl IoInfo for IoFile {
fn mode(&self) -> IoMode {
IoMode::File
}
fn tgt_fd(&self) -> RawFd {
self.tgt_fd
}
fn src_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
fn close(&mut self) -> ShResult<()> {
// Closes on it's own when it's dropped
Ok(())
}
fn print(&self) -> String {
format!("{:?}",self)
}
}
#[derive(Debug)]
pub struct IoPipe {
tgt_fd: RawFd,
pipe_fd: OwnedFd
}
impl IoPipe {
pub fn new(tgt_fd: RawFd, pipe_fd: OwnedFd) -> Self {
Self { tgt_fd, pipe_fd }
}
pub fn get_pipes() -> (Self, Self) {
let (rpipe,wpipe) = pipe().unwrap();
let r_iopipe = Self::new(STDIN_FILENO, rpipe);
let w_iopipe = Self::new(STDOUT_FILENO, wpipe);
(r_iopipe,w_iopipe)
}
}
impl IoInfo for IoPipe {
fn mode(&self) -> IoMode {
IoMode::Pipe
}
fn tgt_fd(&self) -> RawFd {
self.tgt_fd
}
fn src_fd(&self) -> RawFd {
self.pipe_fd.as_raw_fd()
}
fn close(&mut self) -> ShResult<()> {
// Closes on it's own
Ok(())
}
fn print(&self) -> String {
format!("{:?}",self)
}
}
#[derive(Debug)]
pub struct IoGroup(OwnedFd,OwnedFd,OwnedFd);
#[derive(Default,Debug)]
pub struct IoFrame {
redirs: Vec<Redir>,
saved_io: Option<IoGroup>,
}
impl<'e> IoFrame {
pub fn new() -> Self {
Default::default()
}
pub fn save(&'e mut self) {
unsafe {
let saved_in = OwnedFd::from_raw_fd(dup(STDIN_FILENO).unwrap());
let saved_out = OwnedFd::from_raw_fd(dup(STDOUT_FILENO).unwrap());
let saved_err = OwnedFd::from_raw_fd(dup(STDERR_FILENO).unwrap());
self.saved_io = Some(IoGroup(saved_in,saved_out,saved_err));
}
}
pub fn redirect(&'e mut self) -> ShResult<'e,()> {
self.save();
for redir in &mut self.redirs {
let io_info = &mut redir.io_info;
let tgt_fd = io_info.tgt_fd();
let src_fd = io_info.src_fd();
dup2(src_fd, tgt_fd)?;
io_info.close()?;
}
Ok(())
}
pub fn restore(&'e mut self) -> ShResult<'e,()> {
while let Some(mut redir) = self.pop() {
redir.io_info.close().ok();
}
if let Some(saved) = self.saved_io.take() {
dbg!(&saved);
dup2(saved.0.as_raw_fd(), STDIN_FILENO)?;
dup2(saved.1.as_raw_fd(), STDOUT_FILENO)?;
dup2(saved.2.as_raw_fd(), STDERR_FILENO)?;
}
Ok(())
}
}
impl Deref for IoFrame {
type Target = Vec<Redir>;
fn deref(&self) -> &Self::Target {
&self.redirs
}
}
impl DerefMut for IoFrame {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.redirs
}
}
#[derive(Default)]
pub struct IoStack {
stack: Vec<IoFrame>,
}
impl<'e> IoStack {
pub fn new() -> Self {
Self {
stack: vec![IoFrame::new()],
}
}
pub fn curr_frame(&self) -> &IoFrame {
self.stack.last().unwrap()
}
pub fn curr_frame_mut(&mut self) -> &mut IoFrame {
self.stack.last_mut().unwrap()
}
pub fn push_to_frame(&mut self, redir: Redir) {
self.curr_frame_mut().push(redir)
}
pub fn pop_frame(&mut self) -> IoFrame {
if self.stack.len() > 1 {
self.stack.pop().unwrap()
} else {
std::mem::take(self.curr_frame_mut())
}
}
}
impl Deref for IoStack {
type Target = Vec<IoFrame>;
fn deref(&self) -> &Self::Target {
&self.stack
}
}
impl DerefMut for IoStack {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.stack
}
}

View File

@@ -1,86 +0,0 @@
use rustyline::completion::{Candidate, Completer};
use crate::{expand::cmdsub::expand_cmdsub_string, parse::lex::KEYWORDS, prelude::*};
use super::readline::SynHelper;
impl<'a> Completer for SynHelper<'a> {
type Candidate = String;
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
let mut shenv = self.shenv.clone();
let mut comps = vec![];
shenv.new_input(line);
let mut token_stream = Lexer::new(line.to_string(), &mut shenv).lex();
if let Some(comp_token) = token_stream.pop() {
let raw = comp_token.as_raw(&mut shenv);
let is_cmd = if let Some(token) = token_stream.pop() {
match token.rule() {
TkRule::Sep => true,
_ if KEYWORDS.contains(&token.rule()) => true,
_ => false
}
} else {
true
};
if let TkRule::Ident | TkRule::Whitespace = comp_token.rule() {
if is_cmd {
let cmds = shenv.meta().path_cmds();
comps.extend(cmds.iter().map(|cmd| cmd.to_string()));
comps.retain(|cmd| cmd.starts_with(&raw));
if !comps.is_empty() && comps.len() > 1 {
if get_bin_path("fzf", &self.shenv).is_some() {
if let Some(mut selection) = fzf_comp(&comps, &mut shenv) {
while selection.starts_with(&raw) {
selection = selection.strip_prefix(&raw).unwrap().to_string();
}
comps = vec![selection];
}
}
} else if let Some(mut comp) = comps.pop() {
while comp.starts_with(&raw) {
comp = comp.strip_prefix(&raw).unwrap().to_string();
}
comps = vec![comp];
}
return Ok((pos,comps))
} else {
let (start, matches) = self.file_comp.complete(line, pos, ctx)?;
comps.extend(matches.iter().map(|c| c.display().to_string()));
if !comps.is_empty() && comps.len() > 1 {
if get_bin_path("fzf", &self.shenv).is_some() {
if let Some(selection) = fzf_comp(&comps, &mut shenv) {
return Ok((start, vec![selection]))
} else {
return Ok((start, vec![]))
}
} else {
return Ok((start, comps))
}
} else if let Some(comp) = comps.pop() {
// Slice off the already typed bit
return Ok((start, vec![comp]))
}
}
}
}
Ok((pos,comps))
}
}
pub fn fzf_comp(comps: &[String], shenv: &mut ShEnv) -> Option<String> {
// All of the fzf wrapper libraries suck
// So we gotta do this now
let echo_args = comps.join("\n");
let echo = format!("echo \"{echo_args}\"");
let fzf = "fzf --height=~30% --layout=reverse --border --border-label=completion";
let command = format!("{echo} | {fzf}");
shenv.ctx_mut().set_flag(ExecFlags::NO_EXPAND); // Prevent any pesky shell injections with filenames like '$(rm -rf /)'
let selection = expand_cmdsub_string(&command, shenv).ok()?;
if selection.is_empty() {
None
} else {
Some(selection.trim().to_string())
}
}

View File

@@ -1,189 +0,0 @@
use rustyline::highlight::Highlighter;
use sys::get_bin_path;
use crate::{parse::lex::KEYWORDS, prelude::*};
use super::readline::SynHelper;
impl<'a> Highlighter for SynHelper<'a> {
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
let mut shenv_clone = self.shenv.clone();
shenv_clone.new_input(line);
let mut result = String::new();
let mut tokens = Lexer::new(line.to_string(),&mut shenv_clone).lex().into_iter();
let mut is_command = true;
let mut in_array = false;
let mut in_case = false;
while let Some(token) = tokens.next() {
let raw = token.as_raw(&mut shenv_clone);
match token.rule() {
TkRule::Comment => {
let styled = &raw.styled(Style::BrightBlack);
result.push_str(&styled);
}
TkRule::ErrPipeOp |
TkRule::OrOp |
TkRule::AndOp |
TkRule::PipeOp |
TkRule::RedirOp |
TkRule::BgOp => {
is_command = true;
let styled = &raw.styled(Style::Cyan);
result.push_str(&styled);
}
TkRule::CasePat => {
let pat = raw.trim_end_matches(')');
let len_delta = raw.len().saturating_sub(pat.len());
let parens = ")".repeat(len_delta);
let styled = pat.styled(Style::Magenta);
let rebuilt = format!("{styled}{parens}");
result.push_str(&rebuilt);
}
TkRule::FuncName => {
let name = raw.strip_suffix("()").unwrap_or(&raw);
let styled = name.styled(Style::Cyan);
let rebuilt = format!("{styled}()");
result.push_str(&rebuilt);
}
TkRule::DQuote | TkRule::SQuote => {
let styled = raw.styled(Style::BrightYellow);
result.push_str(&styled);
}
_ if KEYWORDS.contains(&token.rule()) => {
if in_array || in_case {
if &raw == "in" {
let styled = &raw.styled(Style::Yellow);
result.push_str(&styled);
if in_case { in_case = false };
} else {
let styled = &raw.styled(Style::Magenta);
result.push_str(&styled);
}
} else {
if &raw == "for" {
in_array = true;
}
if &raw == "case" {
in_case = true;
}
let styled = &raw.styled(Style::Yellow);
result.push_str(&styled);
}
}
TkRule::BraceGrp => {
let body = &raw[1..raw.len() - 1];
let highlighted = self.highlight(body, 0).to_string();
let styled_o_brace = "{".styled(Style::BrightBlue);
let styled_c_brace = "}".styled(Style::BrightBlue);
let rebuilt = format!("{styled_o_brace}{highlighted}{styled_c_brace}");
is_command = false;
result.push_str(&rebuilt);
}
TkRule::CmdSub => {
let body = &raw[2..raw.len() - 1];
let highlighted = self.highlight(body, 0).to_string();
let styled_o_paren = "$(".styled(Style::BrightBlue);
let styled_c_paren = ")".styled(Style::BrightBlue);
let rebuilt = format!("{styled_o_paren}{highlighted}{styled_c_paren}");
is_command = false;
result.push_str(&rebuilt);
}
TkRule::Subshell => {
let body = &raw[1..raw.len() - 1];
let highlighted = self.highlight(body, 0).to_string();
let styled_o_paren = "(".styled(Style::BrightBlue);
let styled_c_paren = ")".styled(Style::BrightBlue);
let rebuilt = format!("{styled_o_paren}{highlighted}{styled_c_paren}");
is_command = false;
result.push_str(&rebuilt);
}
TkRule::VarSub => {
let styled = raw.styled(Style::Magenta);
result.push_str(&styled);
}
TkRule::Ident => {
if in_array || in_case {
if &raw == "in" {
let styled = &raw.styled(Style::Yellow);
result.push_str(&styled);
if in_case { in_case = false };
} else {
let styled = &raw.styled(Style::Magenta);
result.push_str(&styled);
}
} else if let Some((var,val)) = raw.split_once('=') {
let var_styled = var.styled(Style::Magenta);
let val_styled = val.styled(Style::Cyan);
let rebuilt = vec![var_styled,val_styled].join("=");
result.push_str(&rebuilt);
} else if raw.starts_with(['"','\'']) {
let styled = &raw.styled(Style::BrightYellow);
result.push_str(&styled);
} else if &raw == "{" || &raw == "}" {
result.push_str(&raw);
} else if is_command {
if get_bin_path(&token.as_raw(&mut shenv_clone), self.shenv).is_some() ||
self.shenv.logic().get_alias(&raw).is_some() ||
self.shenv.logic().get_function(&raw).is_some() ||
BUILTINS.contains(&raw.as_str()) {
let styled = &raw.styled(Style::Green);
result.push_str(&styled);
} else {
let styled = &raw.styled(Style::Red | Style::Bold);
result.push_str(&styled);
}
is_command = false;
} else {
result.push_str(&raw);
}
}
TkRule::Sep => {
is_command = true;
in_array = false;
result.push_str(&raw);
}
_ => {
result.push_str(&raw);
}
}
}
std::borrow::Cow::Owned(result)
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
) -> std::borrow::Cow<'b, str> {
let _ = default;
std::borrow::Cow::Borrowed(prompt)
}
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
std::borrow::Cow::Borrowed(hint)
}
fn highlight_candidate<'c>(
&self,
candidate: &'c str, // FIXME should be Completer::Candidate
completion: rustyline::CompletionType,
) -> std::borrow::Cow<'c, str> {
let _ = completion;
std::borrow::Cow::Borrowed(candidate)
}
fn highlight_char(&self, line: &str, pos: usize, kind: rustyline::highlight::CmdKind) -> bool {
let _ = (line, pos, kind);
true
}
}

260
src/prompt/history.rs Normal file
View File

@@ -0,0 +1,260 @@
use std::{fs::{File, OpenOptions}, ops::{Deref, DerefMut}, path::PathBuf};
use bitflags::bitflags;
use rustyline::history::{History, SearchResult};
use serde::{Deserialize, Serialize};
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prelude::*};
#[derive(Deserialize,Serialize,Debug)]
pub struct HistEntry {
body: String,
id: usize
}
impl HistEntry {
pub fn new(body: String, id: usize) -> Self {
Self { body, id }
}
pub fn cmd(&self) -> &str {
&self.body
}
pub fn id(&self) -> usize {
self.id
}
}
#[derive(Deserialize,Serialize,Default)]
pub struct HistEntries {
entries: Vec<HistEntry>
}
impl HistEntries {
pub fn new() -> Self {
Self { entries: vec![] }
}
}
impl Deref for HistEntries {
type Target = Vec<HistEntry>;
fn deref(&self) -> &Self::Target {
&self.entries
}
}
impl DerefMut for HistEntries {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.entries
}
}
pub struct FernHist {
file_path: Option<PathBuf>,
entries: HistEntries,
max_len: usize,
pub flags: HistFlags
}
bitflags! {
pub struct HistFlags: u32 {
const NO_DUPES = 0b0000001;
const IGNORE_SPACE = 0b0000010;
}
}
impl<'e> FernHist {
pub fn new() -> Self {
Self { file_path: None, entries: HistEntries::new(), max_len: 1000, flags: HistFlags::empty() }
}
pub fn from_path(file_path: PathBuf) -> ShResult<'e,Self> {
let mut new_hist = FernHist::new();
new_hist.file_path = Some(file_path);
new_hist.load_hist()?;
Ok(new_hist)
}
pub fn create_entry(&self, body: &str) -> HistEntry {
let id = self.len() + 1;
HistEntry::new(body.to_string(), id)
}
pub fn init_hist_file(&mut self) -> ShResult<'e,()> {
let Some(path) = self.file_path.clone() else {
return Ok(());
};
self.save(&path)?;
Ok(())
}
pub fn load_hist(&mut self) -> ShResult<'e,()> {
let Some(file_path) = self.file_path.clone() else {
return Err(
ShErr::simple(
ShErrKind::InternalErr,
"History file not set"
)
)
};
if !file_path.is_file() {
self.init_hist_file()?;
}
let hist_file = File::open(&file_path)?;
self.entries = serde_yaml::from_reader(hist_file).unwrap_or_default();
Ok(())
}
}
impl Default for FernHist {
fn default() -> Self {
let home = std::env::var("HOME").unwrap();
let file_path = PathBuf::from(&format!("{home}/.fernhist"));
Self::from_path(file_path).unwrap()
}
}
impl History for FernHist {
fn add(&mut self, line: &str) -> rustyline::Result<bool> {
let new_entry = self.create_entry(line);
if self.flags.contains(HistFlags::NO_DUPES) {
let most_recent = self.get(self.len(), rustyline::history::SearchDirection::Reverse)?.unwrap();
dbg!(&most_recent);
if new_entry.body == most_recent.entry.to_string() {
return Ok(false)
}
}
self.entries.push(new_entry);
Ok(true)
}
fn get(&self, index: usize, dir: rustyline::history::SearchDirection) -> rustyline::Result<Option<rustyline::history::SearchResult>> {
Ok(self.entries.iter().find(|ent| ent.id() == index).map(|ent| {
SearchResult { entry: ent.cmd().to_string().into(), idx: index, pos: 0 }
}))
}
fn add_owned(&mut self, line: String) -> rustyline::Result<bool> {
todo!()
}
fn len(&self) -> usize {
self.entries.len()
}
fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn set_max_len(&mut self, len: usize) -> rustyline::Result<()> {
self.max_len = len;
Ok(())
}
fn ignore_dups(&mut self, yes: bool) -> rustyline::Result<()> {
if yes {
self.flags |= HistFlags::NO_DUPES;
} else {
self.flags &= !HistFlags::NO_DUPES;
}
Ok(())
}
fn ignore_space(&mut self, yes: bool) {
if yes {
self.flags |= HistFlags::IGNORE_SPACE;
} else {
self.flags &= !HistFlags::IGNORE_SPACE;
}
}
fn save(&mut self, path: &std::path::Path) -> rustyline::Result<()> {
let hist_file = File::create(path)?;
serde_yaml::to_writer(hist_file, &self.entries).unwrap();
Ok(())
}
fn append(&mut self, path: &std::path::Path) -> rustyline::Result<()> {
todo!()
}
fn load(&mut self, path: &std::path::Path) -> rustyline::Result<()> {
let path = path.to_path_buf();
self.file_path = Some(path);
self.load_hist().map_err(|_| rustyline::error::ReadlineError::Io(std::io::Error::last_os_error()))
}
fn clear(&mut self) -> rustyline::Result<()> {
self.entries.clear();
if self.file_path.is_some() {
self.save(&self.file_path.clone().unwrap())?;
}
Ok(())
}
fn search(
&self,
term: &str,
start: usize,
dir: rustyline::history::SearchDirection,
) -> rustyline::Result<Option<rustyline::history::SearchResult>> {
if term.is_empty() {
return Ok(None)
}
let mut matches: Vec<&HistEntry> = self.entries.iter()
.filter(|ent| is_subsequence(&ent.body, term))
.collect();
matches.sort_by(|ent_a, ent_b| {
let ent_a_rank = fuzzy_rank(term, &ent_a.body);
let ent_b_rank = fuzzy_rank(term, &ent_b.body);
ent_a_rank.cmp(&ent_b_rank)
.then(ent_a.id().cmp(&ent_b.id()))
});
Ok(matches.last().map(|ent| {
SearchResult {
entry: ent.body.clone().into(),
idx: ent.id(),
pos: start
}
}))
}
fn starts_with(
&self,
term: &str,
start: usize,
dir: rustyline::history::SearchDirection,
) -> rustyline::Result<Option<rustyline::history::SearchResult>> {
let mut matches: Vec<&HistEntry> = self.entries.iter()
.filter(|ent| ent.body.starts_with(term))
.collect();
matches.sort_by(|ent_a, ent_b| ent_a.id().cmp(&ent_b.id()));
dbg!(&matches);
Ok(matches.first().map(|ent| {
SearchResult {
entry: ent.body.clone().into(),
idx: ent.id(),
pos: start
}
}))
}
}
fn fuzzy_rank(search_term: &str, search_result: &str) -> u8 {
if search_result == search_term {
4
} else if search_result.starts_with(search_term) {
3
} else if search_result.contains(search_term) {
2
} else if is_subsequence(search_result, search_term) {
1
} else {
0
}
}
// Check if a search term is a subsequence of the body (characters in order but not necessarily adjacent)
fn is_subsequence(search_result: &str, search_term: &str) -> bool {
let mut result_chars = search_result.chars();
search_term.chars().all(|ch| result_chars.any(|c| c == ch))
}

View File

@@ -1,59 +1,38 @@
use crate::prelude::*;
use readline::SynHelper;
use rustyline::{config::Configurer, history::{DefaultHistory, History}, ColorMode, CompletionType, Config, EditMode, Editor};
pub mod history;
pub mod readline;
pub mod highlight;
pub mod validate;
pub mod comp;
fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
let hist_path = std::env::var("FERN_HIST").unwrap_or_default();
let config = Config::builder()
.max_history_size(1000).unwrap()
.history_ignore_dups(true).unwrap()
.completion_prompt_limit(100)
.edit_mode(EditMode::Vi)
.color_mode(ColorMode::Enabled)
.tab_stop(2)
.build();
use std::path::Path;
let mut editor = Editor::with_config(config).unwrap();
editor.set_completion_type(CompletionType::List);
editor.set_helper(Some(SynHelper::new(shenv)));
if !hist_path.is_empty() {
editor.load_history(&PathBuf::from(hist_path)).unwrap();
}
editor
use history::FernHist;
use readline::FernReadline;
use rustyline::{error::ReadlineError, history::{FileHistory, History}, Config, Editor};
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*};
fn init_rl<'s>() -> ShResult<'s,Editor<FernReadline,FernHist>> {
let hist = FernHist::default();
let rl = FernReadline::new();
let config = Config::default();
let mut editor = Editor::with_history(config,hist)?;
editor.set_helper(Some(rl));
Ok(editor)
}
pub fn read_line(shenv: &mut ShEnv) -> ShResult<String> {
log!(TRACE, "Entering prompt");
shenv.meta_mut().stop_timer();
let ps1 = std::env::var("PS1").unwrap_or("\\$ ".styled(Style::Green | Style::Bold));
let prompt = expand_prompt(&ps1,shenv)?;
let mut editor = init_rl(shenv);
pub fn read_line<'s>() -> ShResult<'s,String> {
let mut editor = init_rl()?;
let prompt = "$ ".styled(Style::Green | Style::Bold);
match editor.readline(&prompt) {
Ok(line) => {
if !line.is_empty() {
let hist_path = std::env::var("FERN_HIST").ok();
editor.history_mut().add(&line).unwrap();
if let Some(path) = hist_path {
editor.history_mut().save(&PathBuf::from(path)).unwrap();
}
editor.add_history_entry(&line)?;
editor.save_history(&Path::new("/home/pagedmov/.fernhist"))?;
}
Ok(line)
},
Err(rustyline::error::ReadlineError::Eof) => {
kill(Pid::this(), Signal::SIGQUIT)?;
Ok(String::new())
}
Err(rustyline::error::ReadlineError::Interrupted) => {
Ok(String::new())
}
Err(ReadlineError::Eof) => std::process::exit(0),
Err(ReadlineError::Interrupted) => Ok(String::new()),
Err(e) => {
log!(ERROR, e);
Err(e.into())
return Err(e.into())
}
}
}

View File

@@ -1,76 +1,66 @@
use rustyline::{completion::{Candidate, Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper};
use std::borrow::Cow;
use crate::prelude::*;
use rustyline::{completion::Completer, highlight::Highlighter, hint::{Hint, Hinter}, validate::{ValidationResult, Validator}, Helper};
pub struct SynHelper<'a> {
pub file_comp: FilenameCompleter,
pub shenv: &'a mut ShEnv,
use crate::{libsh::term::{Style, Styled}, prelude::*};
pub struct FernReadline {
}
impl<'a> Helper for SynHelper<'a> {}
impl<'a> SynHelper<'a> {
pub fn new(shenv: &'a mut ShEnv) -> Self {
Self {
file_comp: FilenameCompleter::new(),
shenv,
impl FernReadline {
pub fn new() -> Self {
Self { }
}
}
pub fn hist_search(&self, term: &str, hist: &dyn History) -> Option<String> {
let limit = hist.len();
let mut latest_match = None;
for i in 0..limit {
if let Some(hist_entry) = hist.get(i, SearchDirection::Forward).ok()? {
if hist_entry.entry.starts_with(term) {
latest_match = Some(hist_entry.entry.into_owned())
}
}
}
latest_match
}
impl Helper for FernReadline {}
impl Completer for FernReadline {
type Candidate = String;
}
pub struct SynHint {
text: String,
pub struct FernHint {
raw: String,
styled: String
}
impl SynHint {
pub fn new(text: String) -> Self {
let styled = (&text).styled(Style::BrightBlack);
Self { text, styled }
}
pub fn empty() -> Self {
Self { text: String::new(), styled: String::new() }
impl FernHint {
pub fn new(raw: String) -> Self {
let styled = (&raw).styled(Style::Dim | Style::BrightBlack);
Self { raw, styled }
}
}
impl Hint for SynHint {
impl Hint for FernHint {
fn display(&self) -> &str {
&self.styled
}
fn completion(&self) -> Option<&str> {
if !self.text.is_empty() {
Some(&self.text)
if !self.raw.is_empty() {
Some(&self.raw)
} else {
None
}
}
}
impl<'a> Hinter for SynHelper<'a> {
type Hint = SynHint;
impl Hinter for FernReadline {
type Hint = FernHint;
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
if line.is_empty() {
return None
}
let history = ctx.history();
let result = self.hist_search(line, history)?;
let window = result[line.len()..].trim_end().to_string();
Some(SynHint::new(window))
let ent = ctx.history().search(line, pos, rustyline::history::SearchDirection::Reverse).ok()??;
let entry_raw = ent.entry.get(pos..)?.to_string();
Some(FernHint::new(entry_raw))
}
}
impl Highlighter for FernReadline {
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
Cow::Owned(line.to_string())
}
}
impl Validator for FernReadline {
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
Ok(ValidationResult::Valid(None))
}
}

View File

@@ -1,62 +0,0 @@
use rustyline::validate::{ValidationResult, Validator};
use crate::prelude::*;
use super::readline::SynHelper;
pub fn check_delims(line: &str) -> bool {
let mut delim_stack = vec![];
let mut chars = line.chars();
let mut case_depth: u64 = 0;
let mut case_check = String::new();
let mut in_quote = None; // Tracks which quote type is open (`'` or `"`)
while let Some(ch) = chars.next() {
case_check.push(ch);
if case_check.ends_with("case") {
case_depth += 1;
}
if case_check.ends_with("esac") {
case_depth = case_depth.saturating_sub(1);
}
match ch {
'{' | '(' | '[' if in_quote.is_none() => delim_stack.push(ch),
'}' if in_quote.is_none() && delim_stack.pop() != Some('{') => return false,
')' if in_quote.is_none() && delim_stack.pop() != Some('(') => {
if case_depth == 0 {
return false
}
}
']' if in_quote.is_none() && delim_stack.pop() != Some('[') => return false,
'"' | '\'' => {
if in_quote == Some(ch) {
in_quote = None;
} else if in_quote.is_none() {
in_quote = Some(ch);
}
}
'\\' => { chars.next(); } // Skip next character if escaped
_ => {}
}
}
delim_stack.is_empty() && in_quote.is_none()
}
pub fn check_keywords(line: &str, shenv: &mut ShEnv) -> bool {
shenv.new_input(line);
let tokens = Lexer::new(line.to_string(),shenv).lex();
Parser::new(tokens, shenv).parse().is_ok()
}
impl<'a> Validator for SynHelper<'a> {
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
let input = ctx.input();
let mut shenv_clone = self.shenv.clone();
match check_delims(input) && check_keywords(input, &mut shenv_clone) {
true => Ok(ValidationResult::Valid(None)),
false => Ok(ValidationResult::Incomplete),
}
}
}

View File

@@ -1,154 +0,0 @@
use crate::prelude::*;
bitflags! {
#[derive(Copy,Clone,Debug,PartialEq,PartialOrd)]
pub struct ExecFlags: u32 {
const NO_FORK = 0b00000001;
const IN_FUNC = 0b00000010;
const NO_EXPAND = 0b00000100;
}
}
#[derive(Clone,Debug)]
pub struct ExecCtx {
redirs: Vec<Redir>,
depth: usize,
max_depth: usize,
flags: ExecFlags,
io_masks: IoMasks,
saved_io: Option<SavedIo>
}
impl ExecCtx {
pub fn new() -> Self {
Self {
redirs: vec![],
depth: 0,
max_depth: 1500,
flags: ExecFlags::empty(),
io_masks: IoMasks::new(),
saved_io: None
}
}
pub fn as_cond(&self) -> Self {
let mut clone = self.clone();
let (cond_redirs,_) = self.sort_redirs();
clone.redirs = cond_redirs;
clone
}
pub fn as_body(&self) -> Self {
let mut clone = self.clone();
let (_,body_redirs) = self.sort_redirs();
clone.redirs = body_redirs;
clone
}
pub fn redirs(&self) -> &Vec<Redir> {
&self.redirs
}
pub fn clear_redirs(&mut self) {
self.redirs.clear()
}
pub fn sort_redirs(&self) -> (Vec<Redir>,Vec<Redir>) {
let mut cond_redirs = vec![];
let mut body_redirs = vec![];
for redir in self.redirs.clone() {
match redir.op {
RedirType::Input |
RedirType::HereString |
RedirType::HereDoc => cond_redirs.push(redir),
RedirType::Output |
RedirType::Append => body_redirs.push(redir)
}
}
(cond_redirs,body_redirs)
}
pub fn masks(&self) -> &IoMasks {
&self.io_masks
}
pub fn push_rdr(&mut self, redir: Redir) {
self.redirs.push(redir)
}
pub fn saved_io(&mut self) -> &mut Option<SavedIo> {
&mut self.saved_io
}
pub fn activate_rdrs(&mut self) -> ShResult<()> {
let mut redirs = CmdRedirs::new(core::mem::take(&mut self.redirs));
self.redirs = vec![];
redirs.activate()?;
Ok(())
}
pub fn flags(&self) -> ExecFlags {
self.flags
}
pub fn set_flag(&mut self, flag: ExecFlags) {
self.flags |= flag
}
pub fn unset_flag(&mut self, flag: ExecFlags) {
self.flags &= !flag
}
}
#[derive(Debug,Clone)]
pub struct SavedIo {
pub stdin: RawFd,
pub stdout: RawFd,
pub stderr: RawFd
}
impl SavedIo {
pub fn save(stdin: RawFd, stdout: RawFd, stderr: RawFd) -> Self {
Self { stdin, stdout, stderr }
}
}
#[derive(Debug,Clone)]
pub struct IoMask {
default: RawFd,
mask: Option<RawFd>
}
impl IoMask {
pub fn new(default: RawFd) -> Self {
Self { default, mask: None }
}
pub fn new_mask(&mut self, mask: RawFd) {
self.mask = Some(mask)
}
pub fn unmask(&mut self) {
self.mask = None
}
pub fn get_fd(&self) -> RawFd {
if let Some(fd) = self.mask {
fd
} else {
self.default
}
}
}
#[derive(Clone,Debug)]
/// Necessary for when process file descriptors are permanently redirected using `exec`
pub struct IoMasks {
stdin: IoMask,
stdout: IoMask,
stderr: IoMask
}
impl IoMasks {
pub fn new() -> Self {
Self {
stdin: IoMask::new(0),
stdout: IoMask::new(1),
stderr: IoMask::new(2),
}
}
pub fn stdin(&self) -> &IoMask {
&self.stdin
}
pub fn stdout(&self) -> &IoMask {
&self.stdout
}
pub fn stderr(&self) -> &IoMask {
&self.stderr
}
}

View File

@@ -1,114 +0,0 @@
use crate::prelude::*;
#[derive(Clone,Debug,PartialEq)]
pub struct SavedSpan {
pointer: Rc<RefCell<Span>>,
start: usize,
end: usize,
expanded: bool
}
impl SavedSpan {
pub fn from_span(pointer: Rc<RefCell<Span>>) -> Self {
let expanded = pointer.borrow().expanded;
let start = pointer.borrow().start();
let end = pointer.borrow().end();
Self { pointer, start, end, expanded }
}
pub fn restore(&self) {
let mut deref = self.pointer.borrow_mut();
deref.set_start(self.start);
deref.set_end(self.end);
deref.expanded = self.expanded
}
pub fn into_span(self) -> Rc<RefCell<Span>> {
self.pointer
}
}
#[derive(Clone,Debug,PartialEq)]
pub struct InputMan {
input: Option<String>,
spans: Vec<Rc<RefCell<Span>>>,
saved_states: Vec<(String,Vec<SavedSpan>)>,
}
impl InputMan {
pub fn new() -> Self {
Self { input: None, spans: vec![], saved_states: vec![] }
}
pub fn clear(&mut self) {
*self = Self::new();
}
pub fn new_input(&mut self, input: &str) {
self.input = Some(input.to_string())
}
pub fn get_input(&self) -> Option<&String> {
self.input.as_ref()
}
pub fn get_input_mut(&mut self) -> Option<&mut String> {
self.input.as_mut()
}
pub fn push_state(&mut self) {
if let Some(input) = &self.input {
let saved_input = input.clone();
let mut saved_spans = vec![];
for span in &self.spans {
let saved_span = SavedSpan::from_span(span.clone());
saved_spans.push(saved_span);
}
self.saved_states.push((saved_input,saved_spans));
}
}
pub fn pop_state(&mut self) {
if let Some((saved_input, saved_spans)) = self.saved_states.pop() {
self.input = Some(saved_input);
let mut restored_spans = vec![];
for saved_span in saved_spans.into_iter() {
saved_span.restore();
let span = saved_span.into_span();
restored_spans.push(span);
}
self.spans = restored_spans;
}
}
pub fn new_span(&mut self, start: usize, end: usize) -> Rc<RefCell<Span>> {
if let Some(_input) = &self.input {
let span = Rc::new(RefCell::new(Span::new(start, end)));
self.spans.push(span.clone());
span
} else {
Rc::new(RefCell::new(Span::new(0,0)))
}
}
pub fn remove_span(&mut self, span: Rc<RefCell<Span>>) {
if let Some(idx) = self.spans.iter().position(|iter_span| *iter_span == span) {
self.spans.remove(idx);
}
}
pub fn spans_mut(&mut self) -> &mut Vec<Rc<RefCell<Span>>> {
&mut self.spans
}
pub fn clamp(&self, span: Rc<RefCell<Span>>) {
let mut span = span.borrow_mut();
if let Some(input) = &self.input {
span.clamp_start(input.len());
span.clamp_end(input.len());
}
}
pub fn clamp_all(&self) {
for span in &self.spans {
self.clamp(span.clone());
}
}
pub fn get_slice(&self, span: Rc<RefCell<Span>>) -> Option<&str> {
let span = span.borrow();
let mut start = span.start();
let end = span.end();
if start > end {
start = end;
}
self.input.as_ref().map(|s| &s[start..end])
}
}

View File

@@ -1,576 +0,0 @@
use std::{fmt, sync::{Arc, LazyLock, RwLock}};
use nix::unistd::setpgid;
use crate::prelude::*;
bitflags! {
#[derive(Debug, Copy, Clone)]
pub struct JobCmdFlags: u8 {
const LONG = 0b0000_0001; // 0x01
const PIDS = 0b0000_0010; // 0x02
const NEW_ONLY = 0b0000_0100; // 0x04
const RUNNING = 0b0000_1000; // 0x08
const STOPPED = 0b0001_0000; // 0x10
const INIT = 0b0010_0000; // 0x20
}
}
#[derive(Debug)]
pub struct DisplayWaitStatus(pub WtStat);
impl fmt::Display for DisplayWaitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
WtStat::Exited(_, code) => {
match code {
0 => write!(f, "done"),
_ => write!(f, "failed: {}", code),
}
}
WtStat::Signaled(_, signal, _) => {
write!(f, "signaled: {:?}", signal)
}
WtStat::Stopped(_, signal) => {
write!(f, "stopped: {:?}", signal)
}
WtStat::PtraceEvent(_, signal, _) => {
write!(f, "ptrace event: {:?}", signal)
}
WtStat::PtraceSyscall(_) => {
write!(f, "ptrace syscall")
}
WtStat::Continued(_) => {
write!(f, "continued")
}
WtStat::StillAlive => {
write!(f, "running")
}
}
}
}
/// The job table
pub static JOBS: LazyLock<Arc<RwLock<JobTab>>> = LazyLock::new(|| {
Arc::new(
RwLock::new(
JobTab::new()
)
)
});
pub fn write_jobs<'a,T,F: FnOnce(&mut JobTab) -> T>(operation: F) -> T {
unsafe {
let mut jobs = JOBS.write().unwrap();
operation(&mut jobs)
}
}
pub fn read_jobs<'a,T,F: FnOnce(&JobTab) -> T>(operation: F) -> T {
unsafe {
let jobs = JOBS.read().unwrap();
operation(&jobs)
}
}
#[derive(Clone,Debug)]
pub enum JobID {
Pgid(Pid),
Pid(Pid),
TableID(usize),
Command(String)
}
#[derive(Debug,Clone)]
pub struct ChildProc {
pgid: Pid,
pid: Pid,
command: Option<String>,
stat: WtStat
}
impl<'a> ChildProc {
pub fn new(pid: Pid, command: Option<&str>, pgid: Option<Pid>) -> ShResult<Self> {
let command = command.map(|str| str.to_string());
let stat = if kill(pid,None).is_ok() {
WtStat::StillAlive
} else {
WtStat::Exited(pid, 0)
};
let mut child = Self { pgid: pid, pid, command, stat };
if let Some(pgid) = pgid {
child.set_pgid(pgid).ok();
}
Ok(child)
}
pub fn pid(&self) -> Pid {
self.pid
}
pub fn pgid(&self) -> Pid {
self.pgid
}
pub fn cmd(&self) -> Option<&str> {
self.command.as_ref().map(|cmd| cmd.as_str())
}
pub fn stat(&self) -> WtStat {
self.stat
}
pub fn wait(&mut self, flags: Option<WtFlag>) -> Result<WtStat,Errno> {
let result = waitpid(self.pid, flags);
if let Ok(stat) = result {
self.stat = stat
}
result
}
pub fn kill<T: Into<Option<Signal>>>(&self, sig: T) -> ShResult<()> {
Ok(kill(self.pid, sig)?)
}
pub fn set_pgid(&mut self, pgid: Pid) -> ShResult<()> {
unsafe { setpgid(self.pid, pgid)? };
self.pgid = pgid;
Ok(())
}
pub fn set_stat(&mut self, stat: WtStat) {
self.stat = stat
}
pub fn is_alive(&self) -> bool {
self.stat == WtStat::StillAlive
}
pub fn is_stopped(&self) -> bool {
matches!(self.stat,WtStat::Stopped(..))
}
pub fn exited(&self) -> bool {
matches!(self.stat,WtStat::Exited(..))
}
}
pub struct JobBldr {
table_id: Option<usize>,
pgid: Option<Pid>,
children: Vec<ChildProc>
}
impl Default for JobBldr {
fn default() -> Self {
Self::new()
}
}
impl JobBldr {
pub fn new() -> Self {
Self { table_id: None, pgid: None, children: vec![] }
}
pub fn with_id(self, id: usize) -> Self {
Self {
table_id: Some(id),
pgid: self.pgid,
children: self.children
}
}
pub fn with_pgid(self, pgid: Pid) -> Self {
Self {
table_id: self.table_id,
pgid: Some(pgid),
children: self.children
}
}
pub fn with_children(self, children: Vec<ChildProc>) -> Self {
Self {
table_id: self.table_id,
pgid: self.pgid,
children
}
}
pub fn build(self) -> Job {
Job {
table_id: self.table_id,
pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
children: self.children
}
}
}
#[derive(Debug,Clone)]
pub struct Job {
table_id: Option<usize>,
pgid: Pid,
children: Vec<ChildProc>
}
impl Job {
pub fn set_tabid(&mut self, id: usize) {
self.table_id = Some(id)
}
pub fn running(&self) -> bool {
!self.children.iter().all(|chld| chld.exited())
}
pub fn tabid(&self) -> Option<usize> {
self.table_id
}
pub fn pgid(&self) -> Pid {
self.pgid
}
pub fn get_cmds(&self) -> Vec<&str> {
let mut cmds = vec![];
for child in &self.children {
cmds.push(child.cmd().unwrap_or_default())
}
cmds
}
pub fn set_stats(&mut self, stat: WtStat) {
for child in self.children.iter_mut() {
child.set_stat(stat);
}
}
pub fn get_stats(&self) -> Vec<WtStat> {
self.children
.iter()
.map(|chld| chld.stat())
.collect::<Vec<WtStat>>()
}
pub fn get_pids(&self) -> Vec<Pid> {
self.children
.iter()
.map(|chld| chld.pid())
.collect::<Vec<Pid>>()
}
pub fn children(&self) -> &[ChildProc] {
&self.children
}
pub fn children_mut(&mut self) -> &mut Vec<ChildProc> {
&mut self.children
}
pub fn killpg(&mut self, sig: Signal) -> ShResult<()> {
let stat = match sig {
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
Signal::SIGCONT => WtStat::Continued(self.pgid),
Signal::SIGTERM => WtStat::Signaled(self.pgid, Signal::SIGTERM, false),
_ => unimplemented!("{}",sig)
};
self.set_stats(stat);
Ok(killpg(self.pgid, sig)?)
}
pub fn wait_pgrp<'a>(&mut self) -> ShResult<Vec<WtStat>> {
let mut stats = vec![];
for child in self.children.iter_mut() {
let result = child.wait(Some(WtFlag::WUNTRACED));
match result {
Ok(stat) => {
stats.push(stat);
}
Err(Errno::ECHILD) => break,
Err(e) => return Err(e.into())
}
}
Ok(stats)
}
pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> {
match id {
JobID::Pid(pid) => {
let query_result = self.children.iter_mut().find(|chld| chld.pid == pid);
if let Some(child) = query_result {
child.set_stat(stat);
}
}
JobID::Command(cmd) => {
let query_result = self.children
.iter_mut()
.find(|chld| chld
.cmd()
.is_some_and(|chld_cmd| chld_cmd.contains(&cmd))
);
if let Some(child) = query_result {
child.set_stat(stat);
}
}
JobID::TableID(tid) => {
if self.table_id.is_some_and(|tblid| tblid == tid) {
for child in self.children.iter_mut() {
child.set_stat(stat);
}
}
}
JobID::Pgid(pgid) => {
if pgid == self.pgid {
for child in self.children.iter_mut() {
child.set_stat(stat);
}
}
}
}
Ok(())
}
pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String {
let long = flags.contains(JobCmdFlags::LONG);
let init = flags.contains(JobCmdFlags::INIT);
let pids = flags.contains(JobCmdFlags::PIDS);
let current = job_order.last();
let prev = if job_order.len() > 2 {
job_order.get(job_order.len() - 2)
} else {
None
};
let id = self.table_id.unwrap();
let symbol = if current == self.table_id.as_ref() {
"+"
} else if prev == self.table_id.as_ref() {
"-"
} else {
" "
};
let padding_count = symbol.len() + id.to_string().len() + 3;
let padding = " ".repeat(padding_count);
let mut output = format!("[{}]{}\t", id + 1, symbol);
for (i, cmd) in self.get_cmds().iter().enumerate() {
let pid = if pids || init {
let mut pid = self.get_pids().get(i).unwrap().to_string();
pid.push(' ');
pid
} else {
"".to_string()
};
let job_stat = *self.get_stats().get(i).unwrap();
let fmt_stat = DisplayWaitStatus(job_stat).to_string();
let mut stat_line = if init {
"".to_string()
} else {
fmt_stat.clone()
};
stat_line = format!("{}{} ",pid,stat_line);
stat_line = format!("{} {}", stat_line, cmd);
stat_line = match job_stat {
WtStat::Stopped(..) | WtStat::Signaled(..) => stat_line.styled(Style::Magenta),
WtStat::Exited(_, code) => {
match code {
0 => stat_line.styled(Style::Green),
_ => stat_line.styled(Style::Red),
}
}
_ => stat_line.styled(Style::Cyan)
};
if i != self.get_cmds().len() - 1 {
stat_line = format!("{} |",stat_line);
}
let stat_final = if long {
format!(
"{}{} {}",
if i != 0 { &padding } else { "" },
self.get_pids().get(i).unwrap(),
stat_line
)
} else {
format!(
"{}{}",
if i != 0 { &padding } else { "" },
stat_line
)
};
output.push_str(&stat_final);
output.push('\n');
}
output
}
}
pub struct JobTab {
fg: Option<Job>,
order: Vec<usize>,
new_updates: Vec<usize>,
jobs: Vec<Option<Job>>
}
impl JobTab {
pub fn new() -> Self {
Self { fg: None, order: vec![], new_updates: vec![], jobs: vec![] }
}
pub fn take_fg(&mut self) -> Option<Job> {
self.fg.take()
}
fn next_open_pos(&self) -> usize {
if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) {
position
} else {
self.jobs.len()
}
}
pub fn jobs(&self) -> &Vec<Option<Job>> {
&self.jobs
}
pub fn jobs_mut(&mut self) -> &mut Vec<Option<Job>> {
&mut self.jobs
}
pub fn curr_job(&self) -> Option<usize> {
self.order.last().copied()
}
pub fn prev_job(&self) -> Option<usize> {
self.order.last().copied()
}
fn prune_jobs(&mut self) {
while let Some(job) = self.jobs.last() {
if job.is_none() {
self.jobs.pop();
} else {
break
}
}
}
pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult<usize> {
self.prune_jobs();
let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() };
job.set_tabid(tab_pos);
self.order.push(tab_pos);
if !silent {
write(borrow_fd(1),format!("{}", job.display(&self.order, JobCmdFlags::INIT)).as_bytes())?;
}
if tab_pos == self.jobs.len() {
self.jobs.push(Some(job))
} else {
self.jobs[tab_pos] = Some(job);
}
Ok(tab_pos)
}
pub fn order(&self) -> &[usize] {
&self.order
}
pub fn query(&self, identifier: JobID) -> Option<&Job> {
match identifier {
// Match by process group ID
JobID::Pgid(pgid) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| j.pgid == pgid)
})
}
// Match by process ID
JobID::Pid(pid) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| j.children.iter().any(|child| child.pid == pid))
})
}
// Match by table ID (index in the job table)
JobID::TableID(id) => {
self.jobs.get(id).and_then(|job| job.as_ref())
}
// Match by command name (partial match)
JobID::Command(cmd) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| {
j.children.iter().any(|child| {
child.command.as_ref().is_some_and(|c| c.contains(&cmd))
})
})
})
}
}
}
pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> {
match identifier {
// Match by process group ID
JobID::Pgid(pgid) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| j.pgid == pgid)
})
}
// Match by process ID
JobID::Pid(pid) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| j.children.iter().any(|child| child.pid == pid))
})
}
// Match by table ID (index in the job table)
JobID::TableID(id) => {
self.jobs.get_mut(id).and_then(|job| job.as_mut())
}
// Match by command name (partial match)
JobID::Command(cmd) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| {
j.children.iter().any(|child| {
child.command.as_ref().is_some_and(|c| c.contains(&cmd))
})
})
})
}
}
}
pub fn get_fg(&self) -> Option<&Job> {
self.fg.as_ref()
}
pub fn get_fg_mut(&mut self) -> Option<&mut Job> {
self.fg.as_mut()
}
pub fn new_fg<'a>(&mut self, job: Job) -> ShResult<Vec<WtStat>> {
let pgid = job.pgid();
self.fg = Some(job);
attach_tty(pgid)?;
let statuses = self.fg.as_mut().unwrap().wait_pgrp()?;
attach_tty(getpgrp())?;
Ok(statuses)
}
pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> {
if self.fg.is_none() {
return Ok(())
}
take_term()?;
let fg = std::mem::take(&mut self.fg);
if let Some(mut job) = fg {
job.set_stats(stat);
self.insert_job(job, false)?;
}
Ok(())
}
pub fn bg_to_fg(&mut self, shenv: &mut ShEnv, id: JobID) -> ShResult<()> {
let job = self.remove_job(id);
if let Some(job) = job {
super::wait_fg(job, shenv)?;
}
Ok(())
}
pub fn remove_job(&mut self, id: JobID) -> Option<Job> {
let tabid = self.query(id).map(|job| job.tabid().unwrap());
if let Some(tabid) = tabid {
self.jobs.get_mut(tabid).and_then(Option::take)
} else {
None
}
}
pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> {
let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) {
&self.jobs
.iter()
.filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap())))
.map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>()
} else {
&self.jobs
.iter()
.map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>()
};
let mut jobs_to_remove = vec![];
for job in jobs.iter().flatten() {
// Skip foreground job
let id = job.tabid().unwrap();
// Filter jobs based on flags
if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) {
continue;
}
if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) {
continue;
}
// Print the job in the selected format
write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?;
if job.get_stats().iter().all(|stat| matches!(stat,WtStat::Exited(_, _))) {
jobs_to_remove.push(JobID::TableID(id));
}
}
for id in jobs_to_remove {
self.remove_job(id);
}
Ok(())
}
}

View File

@@ -1,28 +0,0 @@
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct LogTab {
aliases: HashMap<String,String>,
functions: HashMap<String,String>
}
impl LogTab {
pub fn new() -> Self {
Self {
aliases: HashMap::new(),
functions: HashMap::new()
}
}
pub fn get_alias(&self,name: &str) -> Option<&str> {
self.aliases.get(name).map(|a| a.as_str())
}
pub fn set_alias(&mut self, name: &str, body: &str) {
self.aliases.insert(name.to_string(),body.trim().to_string());
}
pub fn get_function(&self,name: &str) -> Option<&str> {
self.functions.get(name).map(|a| a.as_str())
}
pub fn set_function(&mut self, name: &str, body: &str) {
self.functions.insert(name.to_string(),body.trim().to_string());
}
}

View File

@@ -1,32 +0,0 @@
use std::time::{Duration, Instant};
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct MetaTab {
timer_start: Instant,
last_runtime: Option<Duration>,
path_cmds: Vec<String> // Used for command completion
}
impl MetaTab {
pub fn new() -> Self {
let path_cmds = get_path_cmds().unwrap_or_default();
Self {
timer_start: Instant::now(),
last_runtime: None,
path_cmds
}
}
pub fn start_timer(&mut self) {
self.timer_start = Instant::now();
}
pub fn stop_timer(&mut self) {
self.last_runtime = Some(self.timer_start.elapsed());
}
pub fn get_runtime(&self) -> Option<Duration> {
self.last_runtime
}
pub fn path_cmds(&self) -> &[String] {
&self.path_cmds
}
}

View File

@@ -1,126 +0,0 @@
use std::env;
use jobs::Job;
use crate::prelude::*;
pub mod jobs;
pub mod logic;
pub mod exec_ctx;
pub mod meta;
pub mod shenv;
pub mod vars;
pub mod input;
pub mod shopt;
/// Calls attach_tty() on the shell's process group to retake control of the terminal
pub fn take_term() -> ShResult<()> {
attach_tty(getpgrp())?;
Ok(())
}
pub fn disable_reaping() -> ShResult<()> {
log!(TRACE, "Disabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::ignore_sigchld)) }?;
Ok(())
}
/// Waits on the current foreground job and updates the shell's last status code
pub fn wait_fg(job: Job, shenv: &mut ShEnv) -> ShResult<()> {
log!(TRACE, "Waiting on foreground job");
let mut code = 0;
attach_tty(job.pgid())?;
disable_reaping()?;
let statuses = write_jobs(|j| j.new_fg(job))?;
for status in statuses {
match status {
WtStat::Exited(_, exit_code) => {
code = exit_code;
}
WtStat::Stopped(pid, sig) => {
write_jobs(|j| j.fg_to_bg(status))?;
code = sys::SIG_EXIT_OFFSET + sig as i32;
},
WtStat::Signaled(pid, sig, _) => {
if sig == Signal::SIGTSTP {
write_jobs(|j| j.fg_to_bg(status))?;
}
code = sys::SIG_EXIT_OFFSET + sig as i32;
},
_ => { /* Do nothing */ }
}
}
take_term()?;
shenv.set_code(code);
log!(TRACE, "exit code: {}", code);
enable_reaping()?;
Ok(())
}
pub fn dispatch_job(job: Job, is_bg: bool, shenv: &mut ShEnv) -> ShResult<()> {
if is_bg {
write_jobs(|j| {
j.insert_job(job, false)
})?;
} else {
wait_fg(job, shenv)?;
}
Ok(())
}
pub fn log_level() -> crate::libsh::utils::LogLevel {
let level = env::var("FERN_LOG_LEVEL").unwrap_or_default();
match level.to_lowercase().as_str() {
"error" => ERROR,
"warn" => WARN,
"info" => INFO,
"debug" => DEBUG,
"trace" => TRACE,
_ => NULL
}
}
pub fn enable_reaping() -> ShResult<()> {
log!(TRACE, "Enabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::handle_sigchld)) }.unwrap();
Ok(())
}
pub fn attach_tty(pgid: Pid) -> ShResult<()> {
// If we aren't attached to a terminal, the pgid already controls it, or the process group does not exist
// Then return ok
if !isatty(0).unwrap_or(false) || pgid == term_ctlr() || killpg(pgid, None).is_err() {
return Ok(())
}
log!(TRACE, "Attaching tty to pgid: {}",pgid);
if pgid == getpgrp() && term_ctlr() != getpgrp() {
kill(term_ctlr(), Signal::SIGTTOU).ok();
}
let mut new_mask = SigSet::empty();
let mut mask_bkup = SigSet::empty();
new_mask.add(Signal::SIGTSTP);
new_mask.add(Signal::SIGTTIN);
new_mask.add(Signal::SIGTTOU);
pthread_sigmask(SigmaskHow::SIG_BLOCK, Some(&mut new_mask), Some(&mut mask_bkup))?;
let result = unsafe { tcsetpgrp(borrow_fd(0), pgid) };
pthread_sigmask(SigmaskHow::SIG_SETMASK, Some(&mut mask_bkup), Some(&mut new_mask))?;
match result {
Ok(_) => return Ok(()),
Err(e) => {
log!(ERROR, "error while switching term control: {}",e);
unsafe { tcsetpgrp(borrow_fd(0), getpgrp())? };
Ok(())
}
}
}
pub fn term_ctlr() -> Pid {
unsafe { tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp()) }
}

View File

@@ -1,196 +0,0 @@
use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO};
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct ShEnv {
vars: shellenv::vars::VarTab,
logic: shellenv::logic::LogTab,
meta: shellenv::meta::MetaTab,
input_man: shellenv::input::InputMan,
ctx: shellenv::exec_ctx::ExecCtx
}
impl ShEnv {
pub fn new() -> Self {
Self {
vars: shellenv::vars::VarTab::new(),
logic: shellenv::logic::LogTab::new(),
meta: shellenv::meta::MetaTab::new(),
input_man: shellenv::input::InputMan::new(),
ctx: shellenv::exec_ctx::ExecCtx::new(),
}
}
pub fn vars(&self) -> &shellenv::vars::VarTab {
&self.vars
}
pub fn vars_mut(&mut self) -> &mut shellenv::vars::VarTab {
&mut self.vars
}
pub fn meta(&self) -> &shellenv::meta::MetaTab {
&self.meta
}
pub fn input_slice(&self, span: Rc<RefCell<Span>>) -> &str {
&self.input_man.get_slice(span).unwrap_or_default()
}
pub fn source_file(&mut self, path: PathBuf) -> ShResult<()> {
if path.is_file() {
let mut file = std::fs::File::open(path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
exec_input(buf, self)?;
}
Ok(())
}
pub fn source_rc(&mut self) -> ShResult<()> {
let path_raw = std::env::var("FERN_RC")?;
let path = PathBuf::from(path_raw);
self.source_file(path)?;
Ok(())
}
pub fn expand_input(&mut self, new: &str, repl_span: Rc<RefCell<Span>>) -> Vec<Token> {
if repl_span.borrow().expanded {
return vec![];
}
repl_span.borrow_mut().expanded = true;
let saved_spans = self.input_man.spans_mut().clone();
let mut new_tokens = Lexer::new(new.to_string(), self).lex();
*self.input_man.spans_mut() = saved_spans;
let offset = repl_span.borrow().start();
for token in new_tokens.iter_mut() {
token.span().borrow_mut().shift(offset as isize);
}
let repl_start = repl_span.borrow().start();
let repl_end = repl_span.borrow().end();
let range = repl_start..repl_end;
if let Some(input) = self.input_man.get_input_mut() {
let old = &input[range.clone()];
let delta: isize = new.len() as isize - old.len() as isize;
input.replace_range(range, new);
for span in self.input_man.spans_mut() {
let mut span_mut = span.borrow_mut();
if span_mut.start() > repl_start {
span_mut.shift(delta);
}
}
for token in &new_tokens {
self.input_man.spans_mut().push(token.span());
}
}
self.input_man.clamp_all();
if new_tokens.is_empty() {
let empty = Token::new(
TkRule::Ident,
self.inputman_mut().new_span(repl_start, repl_start)
);
vec![empty]
} else {
new_tokens
}
}
/// Executes a group of command lists, and only uses redirections that operate on input
/// For instance:
/// `if cat; then echo foo; fi < file.txt > otherfile.txt`
/// `cat` will be executed as a condition, meaning the input from file.txt will be the only
/// redirection used.
pub fn exec_as_cond(&mut self, nodes: Vec<Node>) -> ShResult<i32> {
let saved = self.ctx().clone();
self.ctx = self.ctx().as_cond();
let ast = SynTree::from_vec(nodes);
Executor::new(ast, self).walk()?;
self.ctx = saved;
Ok(self.get_code())
}
/// Executes a group of command lists, and only uses redirections that operate on output
/// For instance:
/// `if cat; then echo foo; fi < file.txt > otherfile.txt`
/// `echo foo` will be executed as a body, meaning the output to otherfile.txt will be the only
/// redirection used.
pub fn exec_as_body(&mut self, nodes: Vec<Node>) -> ShResult<i32> {
let saved = self.ctx().clone();
self.ctx = self.ctx().as_body();
let ast = SynTree::from_vec(nodes);
Executor::new(ast, self).walk()?;
self.ctx = saved;
Ok(self.get_code())
}
pub fn new_input(&mut self, input: &str) {
self.input_man.clear();
self.input_man.new_input(input);
}
pub fn get_input(&self) -> String {
let input = self.input_man.get_input().map(|s| s.to_string()).unwrap_or_default();
log!(TRACE, input);
input
}
pub fn inputman(&self) -> &shellenv::input::InputMan {
&self.input_man
}
pub fn inputman_mut(&mut self) -> &mut shellenv::input::InputMan {
&mut self.input_man
}
pub fn meta_mut(&mut self) -> &mut shellenv::meta::MetaTab {
&mut self.meta
}
pub fn logic(&self) -> &shellenv::logic::LogTab {
&self.logic
}
pub fn logic_mut(&mut self) -> &mut shellenv::logic::LogTab {
&mut self.logic
}
pub fn save_io(&mut self) -> ShResult<()> {
if self.ctx_mut().saved_io().is_none() {
let ctx = self.ctx_mut();
let stdin = ctx.masks().stdin().get_fd();
let stdout = ctx.masks().stdout().get_fd();
let stderr = ctx.masks().stderr().get_fd();
let saved_in = dup(stdin)?;
let saved_out = dup(stdout)?;
let saved_err = dup(stderr)?;
let saved_io = shellenv::exec_ctx::SavedIo::save(saved_in, saved_out, saved_err);
*ctx.saved_io() = Some(saved_io);
}
Ok(())
}
pub fn reset_io(&mut self) -> ShResult<()> {
let ctx = self.ctx_mut();
ctx.clear_redirs();
if let Some(saved) = ctx.saved_io().take() {
let saved_in = saved.stdin;
let saved_out = saved.stdout;
let saved_err = saved.stderr;
dup2(saved_in,STDIN_FILENO)?;
close(saved_in).ok();
dup2(saved_out,STDOUT_FILENO)?;
close(saved_out).ok();
dup2(saved_err,STDERR_FILENO)?;
close(saved_err).ok();
}
Ok(())
}
pub fn collect_redirs(&mut self, mut redirs: Vec<Redir>) {
let ctx = self.ctx_mut();
while let Some(redir) = redirs.pop() {
ctx.push_rdr(redir);
}
}
pub fn set_code(&mut self, code: i32) {
self.vars_mut().set_param("?", &code.to_string());
}
pub fn get_code(&self) -> i32 {
self.vars().get_param("?").parse::<i32>().unwrap_or(0)
}
pub fn ctx(&self) -> &shellenv::exec_ctx::ExecCtx {
&self.ctx
}
pub fn ctx_mut(&mut self) -> &mut shellenv::exec_ctx::ExecCtx {
&mut self.ctx
}
}

View File

@@ -1,11 +0,0 @@
pub struct ShOpts {
}
pub struct CoreOpts {
}
pub struct PromptOpts {
}

View File

@@ -1,167 +0,0 @@
use std::env;
use nix::unistd::{gethostname, User};
use crate::prelude::*;
#[derive(Clone,Debug)]
pub struct VarTab {
env: HashMap<String,String>,
params: HashMap<String,String>,
pos_params: VecDeque<String>,
vars: HashMap<String,String>
}
impl VarTab {
pub fn new() -> Self {
let (params,pos_params) = Self::init_params();
Self {
env: Self::init_env(),
params,
pos_params,
vars: HashMap::new(),
}
}
pub fn init_params() -> (HashMap<String,String>, VecDeque<String>) {
let mut args = std::env::args().collect::<Vec<String>>();
let mut params = HashMap::new();
let mut pos_params = VecDeque::new();
params.insert("@".to_string(), args.join(" "));
params.insert("#".to_string(), args.len().to_string());
while let Some(arg) = args.pop() {
pos_params.fpush(arg);
}
(params,pos_params)
}
pub fn init_env() -> HashMap<String,String> {
let pathbuf_to_string = |pb: Result<PathBuf, std::io::Error>| pb.unwrap_or_default().to_string_lossy().to_string();
// First, inherit any env vars from the parent process
let mut env_vars = std::env::vars().collect::<HashMap<String,String>>();
let term = {
if isatty(1).unwrap() {
if let Ok(term) = std::env::var("TERM") {
term
} else {
"linux".to_string()
}
} else {
"xterm-256color".to_string()
}
};
let home;
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_vars.insert("IFS".into(), " \t\n".into());
env::set_var("IFS", " \t\n");
env_vars.insert("HOSTNAME".into(), hostname.clone());
env::set_var("HOSTNAME", hostname);
env_vars.insert("UID".into(), uid.to_string());
env::set_var("UID", uid.to_string());
env_vars.insert("PPID".into(), getppid().to_string());
env::set_var("PPID", getppid().to_string());
env_vars.insert("TMPDIR".into(), "/tmp".into());
env::set_var("TMPDIR", "/tmp");
env_vars.insert("TERM".into(), term.clone());
env::set_var("TERM", term);
env_vars.insert("LANG".into(), "en_US.UTF-8".into());
env::set_var("LANG", "en_US.UTF-8");
env_vars.insert("USER".into(), username.clone());
env::set_var("USER", username.clone());
env_vars.insert("LOGNAME".into(), username.clone());
env::set_var("LOGNAME", username);
env_vars.insert("PWD".into(), pathbuf_to_string(std::env::current_dir()));
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
env_vars.insert("OLDPWD".into(), pathbuf_to_string(std::env::current_dir()));
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
env_vars.insert("HOME".into(), home.clone());
env::set_var("HOME", home.clone());
env_vars.insert("SHELL".into(), pathbuf_to_string(std::env::current_exe()));
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env_vars.insert("FERN_HIST".into(),format!("{}/.fern_hist",home));
env::set_var("FERN_HIST",format!("{}/.fern_hist",home));
env_vars.insert("FERN_RC".into(),format!("{}/.fernrc",home));
env::set_var("FERN_RC",format!("{}/.fernrc",home));
env_vars
}
pub fn env(&self) -> &HashMap<String,String> {
&self.env
}
pub fn env_mut(&mut self) -> &mut HashMap<String,String> {
&mut self.env
}
pub fn reset_params(&mut self) {
self.params.clear();
}
pub fn unset_param(&mut self, key: &str) {
self.params.remove(key);
}
pub fn set_param(&mut self, key: &str, val: &str) {
self.params.insert(key.to_string(), val.to_string());
}
pub fn get_param(&self, key: &str) -> &str {
self.params.get(key).map(|s| s.as_str()).unwrap_or_default()
}
/// Push an arg to the back of the positional parameter deque
pub fn bpush_arg(&mut self, arg: &str) {
self.pos_params.bpush(arg.to_string());
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
}
/// Pop an arg from the back of the positional parameter deque
pub fn bpop_arg(&mut self) -> Option<String> {
let item = self.pos_params.bpop();
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
item
}
/// Push an arg to the front of the positional parameter deque
pub fn fpush_arg(&mut self, arg: &str) {
self.pos_params.fpush(arg.to_string());
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
}
/// Pop an arg from the front of the positional parameter deque
pub fn fpop_arg(&mut self) -> Option<String> {
let item = self.pos_params.fpop();
self.set_param("@", &self.pos_params.clone().to_vec().join(" "));
self.set_param("#", &self.pos_params.len().to_string());
item
}
pub fn get_var(&self, var: &str) -> &str {
if let Ok(idx) = var.parse::<usize>() {
self.pos_params.get(idx).map(|p| p.as_str()).unwrap_or_default()
} else if let Some(var) = self.env.get(var) {
var.as_str()
} else if let Some(param) = self.params.get(var) {
param.as_str()
} else {
self.vars.get(var).map(|v| v.as_str()).unwrap_or_default()
}
}
pub fn set_var(&mut self, var: &str, val: &str) {
self.vars.insert(var.to_string(), val.to_string());
}
pub fn unset_var(&mut self, var: &str) {
self.vars.remove(var);
}
pub fn export(&mut self, var: &str, val: &str) {
self.env.insert(var.to_string(),val.to_string());
std::env::set_var(var, val);
}
}

View File

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

118
src/state.rs Normal file
View File

@@ -0,0 +1,118 @@
use std::{collections::HashMap, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}};
use crate::prelude::*;
pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new()));
pub static VAR_TABLE: LazyLock<RwLock<VarTab>> = LazyLock::new(|| RwLock::new(VarTab::new()));
pub static ENV_TABLE: LazyLock<RwLock<EnvTab>> = LazyLock::new(|| RwLock::new(EnvTab::new()));
pub struct JobTab {
}
impl JobTab {
pub fn new() -> Self {
Self {}
}
}
pub struct VarTab {
vars: HashMap<String,String>,
params: HashMap<char,String>,
}
impl VarTab {
pub fn new() -> Self {
let vars = HashMap::new();
let params = Self::init_params();
Self { vars, params }
}
fn init_params() -> HashMap<char, String> {
let mut params = HashMap::new();
params.insert('?', "0".into()); // Last command exit status
params.insert('#', "0".into()); // Number of positional parameters
params.insert('0', std::env::current_exe().unwrap().to_str().unwrap().to_string()); // Name of the shell
params.insert('$', Pid::this().to_string()); // PID of the shell
params.insert('!', "".into()); // PID of the last background job (if any)
params
}
pub fn vars(&self) -> &HashMap<String,String> {
&self.vars
}
pub fn vars_mut(&mut self) -> &mut HashMap<String,String> {
&mut self.vars
}
pub fn params(&self) -> &HashMap<char,String> {
&self.params
}
pub fn params_mut(&mut self) -> &mut HashMap<char,String> {
&mut self.params
}
pub fn get_var(&self, var: &str) -> String {
self.vars.get(var).map(|s| s.to_string()).unwrap_or_default()
}
pub fn new_var(&mut self, var: &str, val: &str) {
self.vars.insert(var.to_string(), val.to_string());
}
pub fn set_param(&mut self, param: char, val: &str) {
self.params.insert(param,val.to_string());
}
pub fn get_param(&self, param: char) -> String {
self.params.get(&param).map(|s| s.to_string()).unwrap_or("0".to_string())
}
}
pub struct EnvTab {
}
impl EnvTab {
pub fn new() -> Self {
Self {}
}
}
/// Read from the job table
pub fn read_jobs<T, F: FnOnce(RwLockReadGuard<JobTab>) -> T>(f: F) -> T {
let lock = JOB_TABLE.read().unwrap();
f(lock)
}
/// Write to the job table
pub fn write_jobs<T, F: FnOnce(&mut RwLockWriteGuard<JobTab>) -> T>(f: F) -> T {
let lock = &mut JOB_TABLE.write().unwrap();
f(lock)
}
/// Read from the variable table
pub fn read_vars<T, F: FnOnce(RwLockReadGuard<VarTab>) -> T>(f: F) -> T {
let lock = VAR_TABLE.read().unwrap();
f(lock)
}
/// Write to the variable table
pub fn write_vars<T, F: FnOnce(&mut RwLockWriteGuard<VarTab>) -> T>(f: F) -> T {
let lock = &mut VAR_TABLE.write().unwrap();
f(lock)
}
/// Read from the environment table
pub fn read_env<T, F: FnOnce(RwLockReadGuard<EnvTab>) -> T>(f: F) -> T {
let lock = ENV_TABLE.read().unwrap();
f(lock)
}
/// Write to the environment table
pub fn write_env<T, F: FnOnce(&mut RwLockWriteGuard<EnvTab>) -> T>(f: F) -> T {
let lock = &mut ENV_TABLE.write().unwrap();
f(lock)
}
pub fn get_status() -> i32 {
read_vars(|v| v.get_param('?')).parse::<i32>().unwrap()
}
pub fn set_status(code: i32) {
write_vars(|v| v.set_param('?', &code.to_string()))
}

View File

@@ -1,7 +0,0 @@
pub mod lex_test {
}
pub mod parse_test {
}

19
src/tests/expand.rs Normal file
View File

@@ -0,0 +1,19 @@
use parse::lex::{Tk, TkFlags, TkRule};
use state::write_vars;
use super::super::*;
#[test]
fn simple_expansion() {
let varsub = "$foo";
write_vars(|v| v.new_var("foo", "this is the value of the variable".into()));
let mut tokens: Vec<Tk> = LexStream::new(varsub, LexFlags::empty())
.filter(|tk| !matches!(tk.class, TkRule::EOI | TkRule::SOI))
.collect();
let var_tk = tokens.pop().unwrap();
let var_span = var_tk.span.clone();
let exp_tk = var_tk.expand(var_span, TkFlags::empty());
write_vars(|v| v.vars_mut().clear());
insta::assert_debug_snapshot!(exp_tk.get_words())
}

36
src/tests/lexer.rs Normal file
View File

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

4
src/tests/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod lexer;
pub mod parser;
pub mod expand;
pub mod term;

37
src/tests/parser.rs Normal file
View File

@@ -0,0 +1,37 @@
use super::super::*;
#[test]
fn parse_simple() {
let input = "echo hello world";
let tk_stream: Vec<_> = LexStream::new(input, LexFlags::empty()).collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_pipeline() {
let input = "echo foo | sed s/foo/bar";
let tk_stream: Vec<_> = LexStream::new(input, LexFlags::empty()).collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
fn parse_conjunction() {
let input = "echo foo && echo bar";
let tk_stream: Vec<_> = LexStream::new(input, LexFlags::empty()).collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}
#[test]
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 tk_stream: Vec<_> = LexStream::new(input, LexFlags::empty()).collect();
let nodes: Vec<_> = ParseStream::new(tk_stream).collect();
insta::assert_debug_snapshot!(nodes)
}

View File

@@ -0,0 +1,13 @@
---
source: src/tests/expand.rs
expression: exp_tk.get_words()
---
[
"this",
"is",
"the",
"value",
"of",
"the",
"variable",
]

View File

@@ -0,0 +1,78 @@
---
source: src/tests/lexer.rs
expression: tokens
---
[
Tk {
class: SOI,
err_span: None,
err: Null,
span: Span {
range: 0..0,
source: "echo \"foo bar\" biz baz",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo \"foo bar\" biz baz",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..14,
source: "echo \"foo bar\" biz baz",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..18,
source: "echo \"foo bar\" biz baz",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 19..22,
source: "echo \"foo bar\" biz baz",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: EOI,
err_span: None,
err: Null,
span: Span {
range: 22..22,
source: "echo \"foo bar\" biz baz",
},
flags: TkFlags(
0x0,
),
},
]

View File

@@ -0,0 +1,78 @@
---
source: src/tests/lexer.rs
expression: tokens
---
[
Tk {
class: SOI,
err_span: None,
err: Null,
span: Span {
range: 0..0,
source: "echo foo > bar.txt",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo > bar.txt",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo > bar.txt",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Redir,
err_span: None,
err: Null,
span: Span {
range: 9..10,
source: "echo foo > bar.txt",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..18,
source: "echo foo > bar.txt",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: EOI,
err_span: None,
err: Null,
span: Span {
range: 18..18,
source: "echo foo > bar.txt",
},
flags: TkFlags(
0x0,
),
},
]

View File

@@ -0,0 +1,66 @@
---
source: src/tests/lexer.rs
expression: tokens
---
[
Tk {
class: SOI,
err_span: None,
err: Null,
span: Span {
range: 0..0,
source: "echo foo 1>&2",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo 1>&2",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo 1>&2",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Redir,
err_span: None,
err: Null,
span: Span {
range: 9..13,
source: "echo foo 1>&2",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: EOI,
err_span: None,
err: Null,
span: Span {
range: 13..13,
source: "echo foo 1>&2",
},
flags: TkFlags(
0x0,
),
},
]

View File

@@ -0,0 +1,66 @@
---
source: src/tests/lexer.rs
expression: tokens
---
[
Tk {
class: SOI,
err_span: None,
err: Null,
span: Span {
range: 0..0,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo hello world",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..10,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..16,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: EOI,
err_span: None,
err: Null,
span: Span {
range: 16..16,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
]

View File

@@ -0,0 +1,126 @@
---
source: src/tests/lexer.rs
expression: tokens
---
[
Tk {
class: SOI,
err_span: None,
err: Null,
span: Span {
range: 0..0,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..2,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
KEYWORD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 3..7,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Sep,
err_span: None,
err: Null,
span: Span {
range: 7..9,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 9..13,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
KEYWORD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 14..18,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 19..22,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Sep,
err_span: None,
err: Null,
span: Span {
range: 22..24,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 24..26,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
KEYWORD,
),
},
Tk {
class: EOI,
err_span: None,
err: Null,
span: Span {
range: 26..26,
source: "if true; then echo foo; fi",
},
flags: TkFlags(
0x0,
),
},
]

View File

@@ -0,0 +1,282 @@
---
source: src/tests/parser.rs
expression: nodes
---
[
Match(
Node {
class: CmdList {
elements: [
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: And,
},
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 12..16,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 17..20,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 12..16,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 17..20,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 12..16,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 17..20,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: Null,
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: And,
err_span: None,
err: Null,
span: Span {
range: 9..11,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 12..16,
source: "echo foo && echo bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 17..20,
source: "echo foo && echo bar",
},
flags: TkFlags(
0x0,
),
},
],
},
),
]

View File

@@ -0,0 +1,962 @@
---
source: src/tests/parser.rs
expression: nodes
---
[
Match(
Node {
class: CmdList {
elements: [
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 9..10,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: And,
},
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 28..32,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 33..36,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 28..32,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 33..36,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 39..42,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 43..52,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 39..42,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 43..52,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 28..32,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 33..36,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 37..38,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 39..42,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 43..52,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: Or,
},
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 56..60,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 61..64,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 65..68,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 56..60,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 61..64,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 65..68,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 71..74,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 75..80,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 81..88,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 89..93,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 71..74,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 75..80,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 81..88,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 89..93,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 56..60,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 61..64,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 65..68,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 69..70,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 71..74,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 75..80,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 81..88,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 89..93,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: Null,
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 9..10,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: And,
err_span: None,
err: Null,
span: Span {
range: 25..27,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 28..32,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 33..36,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 37..38,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 39..42,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 43..52,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Or,
err_span: None,
err: Null,
span: Span {
range: 53..55,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 56..60,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 61..64,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 65..68,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 69..70,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 71..74,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 75..80,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 81..88,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 89..93,
source: "echo foo | sed s/foo/bar && echo bar | sed s/bar/foo || echo foo bar | sed s/foo bar/bar foo/",
},
flags: TkFlags(
0x0,
),
},
],
},
),
]

View File

@@ -0,0 +1,278 @@
---
source: src/tests/parser.rs
expression: nodes
---
[
Match(
Node {
class: CmdList {
elements: [
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
],
},
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 9..10,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: Null,
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..8,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Pipe,
err_span: None,
err: Null,
span: Span {
range: 9..10,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..14,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 15..24,
source: "echo foo | sed s/foo/bar",
},
flags: TkFlags(
0x0,
),
},
],
},
),
]

View File

@@ -0,0 +1,193 @@
---
source: src/tests/parser.rs
expression: nodes
---
[
Match(
Node {
class: CmdList {
elements: [
ConjunctNode {
cmd: Node {
class: Pipeline {
cmds: [
Node {
class: Command {
assignments: [],
argv: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo hello world",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..10,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..16,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo hello world",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..10,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..16,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
],
},
],
pipe_err: false,
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo hello world",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..10,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..16,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
],
},
operator: Null,
},
],
},
flags: NdFlags(
0x0,
),
redirs: [],
tokens: [
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 0..4,
source: "echo hello world",
},
flags: TkFlags(
IS_CMD,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 5..10,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
Tk {
class: Str,
err_span: None,
err: Null,
span: Span {
range: 11..16,
source: "echo hello world",
},
flags: TkFlags(
0x0,
),
},
],
},
),
]

View File

@@ -0,0 +1,5 @@
---
source: src/tests/term.rs
expression: styled
---
text with background

View File

@@ -0,0 +1,5 @@
---
source: src/tests/term.rs
expression: styled
---
styled text

View File

@@ -0,0 +1,5 @@
---
source: src/tests/term.rs
expression: styled
---
reset test

View File

@@ -0,0 +1,5 @@
---
source: src/tests/term.rs
expression: styled
---
RGB styled text

View File

@@ -0,0 +1,5 @@
---
source: src/tests/term.rs
expression: styled
---
multi-style text

View File

@@ -0,0 +1,5 @@
---
source: src/tests/term.rs
expression: styled
---
hello world

41
src/tests/term.rs Normal file
View File

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