Initial commit for fern

This commit is contained in:
2025-03-02 16:26:28 -05:00
parent 56917524c3
commit a9a9642a2a
40 changed files with 5281 additions and 0 deletions

36
src/libsh/collections.rs Normal file
View File

@@ -0,0 +1,36 @@
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
}
}

195
src/libsh/error.rs Normal file
View File

@@ -0,0 +1,195 @@
use std::fmt::Display;
use crate::parse::lex::Span;
use crate::prelude::*;
pub type ShResult<T> = Result<T,ShErr>;
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, span: 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, span: Span) -> Self;
}
impl From<std::io::Error> for ShErr {
fn from(_: std::io::Error) -> Self {
ShErr::io()
}
}
impl From<std::env::VarError> for ShErr {
fn from(value: std::env::VarError) -> Self {
ShErr::simple(ShErrKind::InternalErr, &value.to_string())
}
}
impl From<rustyline::error::ReadlineError> for ShErr {
fn from(value: rustyline::error::ReadlineError) -> Self {
ShErr::simple(ShErrKind::ParseErr, &value.to_string())
}
}
impl From<Errno> for ShErr {
fn from(value: Errno) -> Self {
ShErr::simple(ShErrKind::Errno, &value.to_string())
}
}
impl<T> Blame for Result<T,ShErr> {
fn blame(self, span: Span) -> Self {
if let Err(mut e) = self {
e.blame(span);
Err(e)
} else {
self
}
}
fn try_blame(self, span: Span) -> Self {
if let Err(mut e) = self {
e.try_blame(span);
Err(e)
} else {
self
}
}
}
#[derive(Debug,Clone)]
pub enum ShErrKind {
IoErr,
SyntaxErr,
ParseErr,
InternalErr,
ExecFail,
Errno,
CmdNotFound,
CleanExit,
FuncReturn,
LoopContinue,
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, span: Span },
}
impl ShErr {
pub fn simple(kind: ShErrKind, message: &str) -> Self {
Self::Simple { kind, message: message.to_string() }
}
pub fn io() -> Self {
io::Error::last_os_error().into()
}
pub fn full(kind: ShErrKind, message: String, span: Span) -> Self {
Self::Full { kind, message, span }
}
pub fn try_blame(&mut self, blame: 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), span: blame }
}
}
}
pub fn blame(&mut self, blame: Span) {
match self {
Self::Full { kind: _, message: _, span } => {
*span = blame;
}
Self::Simple { kind, message } => {
*self = Self::Full { kind: core::mem::take(kind), message: core::mem::take(message), span: blame }
}
}
}
pub fn with_msg(&mut self, new_message: String) {
match self {
Self::Full { kind: _, message, span: _ } => {
*message = new_message
}
Self::Simple { kind: _, message } => {
*message = new_message
}
}
}
pub fn with_kind(&mut self, new_kind: ShErrKind) {
match self {
Self::Full { kind, message: _, span: _ } => {
*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: _, span: _ } => {
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_window(&self) -> String {
if let ShErr::Full { kind: _, message: _, span } = self {
let window = span.get_slice();
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, span } => {
let (offset,line_no,line_text) = span.get_line();
let dist = span.end() - span.start();
let padding = " ".repeat(offset);
let line_inner = "~".repeat(dist.saturating_sub(2));
let err_kind = style_text(&self.display_kind(), 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)
}
}

6
src/libsh/mod.rs Normal file
View File

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

51
src/libsh/sys.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::fmt::Display;
use crate::prelude::*;
pub const SIG_EXIT_OFFSET: i32 = 128;
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(':');
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 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(())
}

108
src/libsh/term.rs Normal file
View File

@@ -0,0 +1,108 @@
use std::{fmt::Display, ops::BitOr};
/// Enum representing a single ANSI style
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Style {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Bold,
Italic,
Underline,
Reversed,
}
impl Style {
pub fn as_str(&self) -> &'static str {
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",
}
}
}
/// Struct representing a **set** of styles
#[derive(Debug, Default, Clone)]
pub struct StyleSet {
styles: Vec<Style>,
}
impl StyleSet {
pub fn new() -> Self {
Self { styles: Vec::new() }
}
pub fn add(mut self, style: Style) -> Self {
if !self.styles.contains(&style) {
self.styles.push(style);
}
self
}
pub fn as_str(&self) -> String {
self.styles.iter().map(|s| s.as_str()).collect::<String>()
}
}
/// Allow OR (`|`) operator to combine multiple `Style` values into a `StyleSet`
impl BitOr for Style {
type Output = StyleSet;
fn bitor(self, rhs: Self) -> StyleSet {
StyleSet::new().add(self).add(rhs)
}
}
/// Allow OR (`|`) operator to combine `StyleSet` with `Style`
impl BitOr<Style> for StyleSet {
type Output = StyleSet;
fn bitor(self, rhs: Style) -> StyleSet {
self.add(rhs)
}
}
impl From<Style> for StyleSet {
fn from(style: Style) -> Self {
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())
}

340
src/libsh/utils.rs Normal file
View File

@@ -0,0 +1,340 @@
use core::{arch::asm, fmt::{self, Debug, Display, Write}, ops::Deref};
use std::{os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}, str::FromStr};
use nix::libc::getpgrp;
use crate::{expand::{expand_vars::{expand_dquote, expand_var}, tilde::expand_tilde}, prelude::*};
pub trait StrOps {
fn trim_quotes(&self) -> String;
}
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.
/// This function also performs token expansion.
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() {
match arg.rule() {
TkRule::VarSub => {
let mut tokens = expand_var(arg, shenv).into_iter();
while let Some(token) = tokens.next() {
argv_processed.push(token.to_string())
}
}
TkRule::TildeSub => {
let expanded = expand_tilde(arg);
argv_processed.push(expanded);
}
TkRule::DQuote => {
let expanded = expand_dquote(arg, shenv);
argv_processed.push(expanded)
}
_ => {
argv_processed.push(arg.to_string())
}
}
}
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,"{}",style_text("ERROR", Style::Red | Style::Bold)),
WARN => write!(f,"{}",style_text("WARN", Style::Yellow | Style::Bold)),
INFO => write!(f,"{}",style_text("INFO", Style::Green | Style::Bold)),
DEBUG => write!(f,"{}",style_text("DEBUG", Style::Magenta | Style::Bold)),
TRACE => write!(f,"{}",style_text("TRACE", 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 = style_text(file,Style::Cyan);
let line = line!();
let line_styled = style_text(line,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 = style_text(file, Style::Cyan);
let line = line!();
let line_styled = style_text(line, 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 = style_text(file, Style::Cyan);
let line = line!();
let line_styled = style_text(line, 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,Copy)]
pub enum RedirType {
Input,
Output,
Append,
HereDoc,
HereString
}
#[derive(Debug,Clone)]
pub enum RedirTarget {
Fd(i32),
File(PathBuf),
}
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);
} else {
redir_bldr = redir_bldr.with_op(RedirType::HereDoc);
}
} else {
redir_bldr = redir_bldr.with_op(RedirType::Input);
}
}
'>' => {
if chars.peek() == Some(&'>') {
chars.next();
redir_bldr = redir_bldr.with_op(RedirType::Append);
} else {
redir_bldr = redir_bldr.with_op(RedirType::Output);
}
}
'&' => {
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 }
}
}
#[derive(Debug,Clone)]
pub struct CmdRedirs {
open: Vec<RawFd>,
targets_fd: Vec<Redir>,
targets_file: Vec<Redir>
}
impl CmdRedirs {
pub fn new(mut redirs: Vec<Redir>) -> Self {
let mut targets_fd = vec![];
let mut targets_file = 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)
}
}
Self { open: vec![], targets_fd, targets_file }
}
pub fn close_all(&mut self) -> ShResult<()> {
while let Some(fd) = self.open.pop() {
if let Err(e) = close(fd) {
self.open.push(fd);
return Err(e.into())
}
}
Ok(())
}
pub fn activate(&mut self) -> ShResult<()> {
self.open_file_tgts()?;
self.open_fd_tgts()?;
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 mut 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 mut 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 trim_quotes(s: impl ToString) -> String {
let s = s.to_string();
if s.starts_with('"') && s.ends_with('"') {
s.trim_matches('"').to_string()
} else if s.starts_with('\'') && s.ends_with('\'') {
s.trim_matches('\'').to_string()
} else {
s
}
}