Compare commits

...

2 Commits

31 changed files with 455 additions and 425 deletions

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state::{self, read_logic, write_logic}, state::{self, read_logic, write_logic},
}; };

View File

@@ -3,12 +3,12 @@ use std::sync::LazyLock;
use crate::{ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
expand::expand_prompt, expand::expand_prompt,
getopt::{Opt, OptSpec, get_opts_from_tokens}, getopt::{get_opts_from_tokens, Opt, OptSpec},
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state, state,
}; };

View File

@@ -4,7 +4,7 @@ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
jobs::JobBldr, jobs::JobBldr,
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node, execute::exec_input}, parse::{execute::exec_input, NdRule, Node},
procio::IoStack, procio::IoStack,
state, state,
}; };
@@ -25,10 +25,11 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
return Ok(()); return Ok(());
} }
let joined_argv = expanded_argv.into_iter() let joined_argv = expanded_argv
.map(|(s, _)| s) .into_iter()
.collect::<Vec<_>>() .map(|(s, _)| s)
.join(" "); .collect::<Vec<_>>()
.join(" ");
exec_input(joined_argv, None, false) exec_input(joined_argv, None, false)
} }

View File

@@ -4,7 +4,7 @@ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, execute::ExecArgs}, parse::{execute::ExecArgs, NdRule, Node},
procio::IoStack, procio::IoStack,
state, state,
}; };
@@ -40,11 +40,7 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh
// execvpe only returns on error // execvpe only returns on error
let cmd_str = cmd.to_str().unwrap().to_string(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => { Errno::ENOENT => Err(ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span)),
Err(ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span)) _ => Err(ShErr::full(ShErrKind::Errno(e), format!("{e}"), span)),
}
_ => {
Err(ShErr::full(ShErrKind::Errno(e), format!("{e}"), span))
}
} }
} }

View File

@@ -3,8 +3,8 @@ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state::{self, VarFlags, read_vars, write_vars}, state::{self, read_vars, write_vars, VarFlags},
}; };
use super::setup_builtin; use super::setup_builtin;
@@ -35,10 +35,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
for (arg, _) in argv { for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') { if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like
// 'foo=bar' // 'foo=bar'
} else { } else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any // any
} }
} }
} }
@@ -59,16 +59,17 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
if argv.is_empty() { if argv.is_empty() {
// Display the local variables // Display the local variables
let vars_output = read_vars(|v| { let vars_output = read_vars(|v| {
let mut vars = v.flatten_vars() let mut vars = v
.into_iter() .flatten_vars()
.map(|(k, v)| format!("{}={}", k, v)) .into_iter()
.collect::<Vec<String>>(); .map(|(k, v)| format!("{}={}", k, v))
vars.sort(); .collect::<Vec<String>>();
let mut vars_joined = vars.join("\n"); vars.sort();
vars_joined.push('\n'); let mut vars_joined = vars.join("\n");
vars_joined vars_joined.push('\n');
}); vars_joined
});
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, vars_output.as_bytes())?; // Write it write(stdout, vars_output.as_bytes())?; // Write it

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, execute::prepare_argv}, parse::{execute::prepare_argv, NdRule, Node},
prelude::*, prelude::*,
}; };

View File

@@ -1,9 +1,9 @@
use crate::{ use crate::{
jobs::{JobBldr, JobCmdFlags, JobID}, jobs::{JobBldr, JobCmdFlags, JobID},
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, lex::Span}, parse::{lex::Span, NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state::{self, read_jobs, write_jobs}, state::{self, read_jobs, write_jobs},
}; };
@@ -181,7 +181,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
} }
pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -196,29 +196,33 @@ pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id id
} else { } else {
return Err(ShErr::full(ShErrKind::ExecFail, "disown: No jobs to disown", blame)); return Err(ShErr::full(
ShErrKind::ExecFail,
"disown: No jobs to disown",
blame,
));
}; };
let mut tabid = curr_job_id; let mut tabid = curr_job_id;
let mut nohup = false; let mut nohup = false;
let mut disown_all = false; let mut disown_all = false;
while let Some((arg, span)) = argv.next() { while let Some((arg, span)) = argv.next() {
match arg.as_str() { match arg.as_str() {
"-h" => nohup = true, "-h" => nohup = true,
"-a" => disown_all = true, "-a" => disown_all = true,
_ => { _ => {
tabid = parse_job_id(&arg, span.clone())?; tabid = parse_job_id(&arg, span.clone())?;
} }
} }
} }
if disown_all { if disown_all {
write_jobs(|j| j.disown_all(nohup))?; write_jobs(|j| j.disown_all(nohup))?;
} else { } else {
write_jobs(|j| j.disown(JobID::TableID(tabid), nohup))?; write_jobs(|j| j.disown(JobID::TableID(tabid), nohup))?;
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state, state,
}; };

View File

@@ -7,13 +7,13 @@ use nix::{
use crate::{ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
getopt::{Opt, OptSpec, get_opts_from_tokens}, getopt::{get_opts_from_tokens, Opt, OptSpec},
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
prompt::readline::term::RawModeGuard, prompt::readline::term::RawModeGuard,
state::{self, VarFlags, read_vars, write_vars}, state::{self, read_vars, write_vars, VarFlags},
}; };
pub const READ_OPTS: [OptSpec; 7] = [ pub const READ_OPTS: [OptSpec; 7] = [

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::{ShResult, ShResultExt}, libsh::error::{ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state::write_shopts, state::write_shopts,
}; };

View File

@@ -8,7 +8,7 @@ use regex::Regex;
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase}, parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
prelude::*, prelude::*,
}; };

View File

@@ -11,7 +11,7 @@ use crate::{
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node},
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
state::{self, read_logic, write_logic}, state::{self, read_logic, write_logic},
}; };

View File

@@ -1,12 +1,12 @@
use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock}; use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock};
use crate::{ use crate::{
getopt::{Opt, OptSet, OptSpec, get_opts_from_tokens}, getopt::{get_opts_from_tokens, Opt, OptSet, OptSpec},
jobs::JobBldr, jobs::JobBldr,
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{borrow_fd, IoStack},
}; };
use super::setup_builtin; use super::setup_builtin;

View File

@@ -4,7 +4,7 @@ use crate::{
term::{Style, Styled}, term::{Style, Styled},
}, },
prelude::*, prelude::*,
procio::{IoMode, borrow_fd}, procio::{borrow_fd, IoMode},
signal::{disable_reaping, enable_reaping}, signal::{disable_reaping, enable_reaping},
state::{self, read_jobs, set_status, write_jobs}, state::{self, read_jobs, set_status, write_jobs},
}; };
@@ -384,40 +384,40 @@ impl JobTab {
Ok(()) Ok(())
} }
pub fn hang_up(&mut self) { pub fn hang_up(&mut self) {
for job in self.jobs_mut().iter_mut().flatten() { for job in self.jobs_mut().iter_mut().flatten() {
if job.send_hup { if job.send_hup {
job.killpg(Signal::SIGHUP).ok(); job.killpg(Signal::SIGHUP).ok();
} }
} }
} }
pub fn disown(&mut self, id: JobID, nohup: bool) -> ShResult<()> { pub fn disown(&mut self, id: JobID, nohup: bool) -> ShResult<()> {
if let Some(job) = self.query_mut(id.clone()) { if let Some(job) = self.query_mut(id.clone()) {
if nohup { if nohup {
job.no_hup(); job.no_hup();
} else { } else {
self.remove_job(id); self.remove_job(id);
} }
} }
Ok(()) Ok(())
} }
pub fn disown_all(&mut self, nohup: bool) -> ShResult<()> { pub fn disown_all(&mut self, nohup: bool) -> ShResult<()> {
let mut ids_to_remove = vec![]; let mut ids_to_remove = vec![];
for job in self.jobs_mut().iter_mut().flatten() { for job in self.jobs_mut().iter_mut().flatten() {
if nohup { if nohup {
job.no_hup(); job.no_hup();
} else { } else {
ids_to_remove.push(JobID::TableID(job.tabid().unwrap())); ids_to_remove.push(JobID::TableID(job.tabid().unwrap()));
} }
} }
for id in ids_to_remove { for id in ids_to_remove {
self.remove_job(id); self.remove_job(id);
} }
Ok(()) Ok(())
} }
} }
#[derive(Debug)] #[derive(Debug)]
@@ -425,7 +425,7 @@ pub struct JobBldr {
table_id: Option<usize>, table_id: Option<usize>,
pgid: Option<Pid>, pgid: Option<Pid>,
children: Vec<ChildProc>, children: Vec<ChildProc>,
send_hup: bool, send_hup: bool,
} }
impl Default for JobBldr { impl Default for JobBldr {
@@ -440,7 +440,7 @@ impl JobBldr {
table_id: None, table_id: None,
pgid: None, pgid: None,
children: vec![], children: vec![],
send_hup: true, send_hup: true,
} }
} }
pub fn with_id(self, id: usize) -> Self { pub fn with_id(self, id: usize) -> Self {
@@ -448,7 +448,7 @@ impl JobBldr {
table_id: Some(id), table_id: Some(id),
pgid: self.pgid, pgid: self.pgid,
children: self.children, children: self.children,
send_hup: self.send_hup, send_hup: self.send_hup,
} }
} }
pub fn with_pgid(self, pgid: Pid) -> Self { pub fn with_pgid(self, pgid: Pid) -> Self {
@@ -456,7 +456,7 @@ impl JobBldr {
table_id: self.table_id, table_id: self.table_id,
pgid: Some(pgid), pgid: Some(pgid),
children: self.children, children: self.children,
send_hup: self.send_hup, send_hup: self.send_hup,
} }
} }
pub fn set_pgid(&mut self, pgid: Pid) { pub fn set_pgid(&mut self, pgid: Pid) {
@@ -465,16 +465,16 @@ impl JobBldr {
pub fn pgid(&self) -> Option<Pid> { pub fn pgid(&self) -> Option<Pid> {
self.pgid self.pgid
} }
pub fn no_hup(mut self) -> Self { pub fn no_hup(mut self) -> Self {
self.send_hup = false; self.send_hup = false;
self self
} }
pub fn with_children(self, children: Vec<ChildProc>) -> Self { pub fn with_children(self, children: Vec<ChildProc>) -> Self {
Self { Self {
table_id: self.table_id, table_id: self.table_id,
pgid: self.pgid, pgid: self.pgid,
children, children,
send_hup: self.send_hup, send_hup: self.send_hup,
} }
} }
pub fn push_child(&mut self, child: ChildProc) { pub fn push_child(&mut self, child: ChildProc) {
@@ -485,7 +485,7 @@ impl JobBldr {
table_id: self.table_id, table_id: self.table_id,
pgid: self.pgid.unwrap_or(Pid::from_raw(0)), pgid: self.pgid.unwrap_or(Pid::from_raw(0)),
children: self.children, children: self.children,
send_hup: self.send_hup, send_hup: self.send_hup,
} }
} }
} }
@@ -514,16 +514,16 @@ pub struct Job {
table_id: Option<usize>, table_id: Option<usize>,
pgid: Pid, pgid: Pid,
children: Vec<ChildProc>, children: Vec<ChildProc>,
send_hup: bool, send_hup: bool,
} }
impl Job { impl Job {
pub fn set_tabid(&mut self, id: usize) { pub fn set_tabid(&mut self, id: usize) {
self.table_id = Some(id) self.table_id = Some(id)
} }
pub fn no_hup(&mut self) { pub fn no_hup(&mut self) {
self.send_hup = false; self.send_hup = false;
} }
pub fn running(&self) -> bool { pub fn running(&self) -> bool {
!self.children.iter().all(|chld| chld.exited()) !self.children.iter().all(|chld| chld.exited())
} }

View File

@@ -34,8 +34,7 @@ use crate::prelude::*;
pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None; pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| { pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
open("/dev/tty", OFlag::O_RDWR, Mode::empty()) open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty")
.expect("Failed to open /dev/tty")
}); });
#[derive(Debug)] #[derive(Debug)]

View File

@@ -15,8 +15,8 @@ impl<T: Display> Styled for T {}
pub enum Style { pub enum Style {
// Undoes all styles // Undoes all styles
Reset, Reset,
ResetFg, ResetFg,
ResetBg, ResetBg,
// Foreground Colors // Foreground Colors
Black, Black,
Red, Red,
@@ -68,8 +68,8 @@ impl Display for Style {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Style::Reset => write!(f, "\x1b[0m"), Style::Reset => write!(f, "\x1b[0m"),
Style::ResetFg => write!(f, "\x1b[39m"), Style::ResetFg => write!(f, "\x1b[39m"),
Style::ResetBg => write!(f, "\x1b[49m"), Style::ResetBg => write!(f, "\x1b[49m"),
// Foreground colors // Foreground colors
Style::Black => write!(f, "\x1b[30m"), Style::Black => write!(f, "\x1b[30m"),
@@ -131,13 +131,13 @@ impl StyleSet {
Self { styles: vec![] } Self { styles: vec![] }
} }
pub fn styles(&self) -> &[Style] { pub fn styles(&self) -> &[Style] {
&self.styles &self.styles
} }
pub fn styles_mut(&mut self) -> &mut Vec<Style> { pub fn styles_mut(&mut self) -> &mut Vec<Style> {
&mut self.styles &mut self.styles
} }
pub fn add_style(mut self, style: Style) -> Self { pub fn add_style(mut self, style: Style) -> Self {
if !self.styles.contains(&style) { if !self.styles.contains(&style) {

View File

@@ -1,7 +1,7 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -23,7 +23,6 @@ use std::process::ExitCode;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use nix::errno::Errno; use nix::errno::Errno;
use nix::libc::STDIN_FILENO;
use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::unistd::read; use nix::unistd::read;
@@ -33,7 +32,7 @@ use crate::libsh::sys::TTY_FILENO;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::prompt::get_prompt; use crate::prompt::get_prompt;
use crate::prompt::readline::term::raw_mode; use crate::prompt::readline::term::{RawModeGuard, raw_mode};
use crate::prompt::readline::{FernVi, ReadlineEvent}; use crate::prompt::readline::{FernVi, ReadlineEvent};
use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{read_logic, source_rc, write_jobs, write_meta}; use crate::state::{read_logic, source_rc, write_jobs, write_meta};
@@ -42,16 +41,16 @@ use state::{read_vars, write_vars};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FernArgs { struct FernArgs {
script: Option<String>, script: Option<String>,
#[arg(short)] #[arg(short)]
command: Option<String>, command: Option<String>,
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool, version: bool,
#[arg(short)] #[arg(short)]
interactive: bool, interactive: bool,
@@ -71,7 +70,7 @@ struct FernArgs {
/// closure, which forces access to the variable table and causes its `LazyLock` /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run. /// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
/// We need to make sure that even if we panic, our child processes get sighup /// We need to make sure that even if we panic, our child processes get sighup
@@ -89,196 +88,192 @@ fn setup_panic_handler() {
} }
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
setup_panic_handler(); setup_panic_handler();
let mut args = FernArgs::parse(); let mut args = FernArgs::parse();
if env::args().next().is_some_and(|a| a.starts_with('-')) { if env::args().next().is_some_and(|a| a.starts_with('-')) {
// first arg is '-fern' // first arg is '-fern'
// meaning we are in a login shell // meaning we are in a login shell
args.login_shell = true; args.login_shell = true;
} }
if args.version { if args.version {
println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS); println!("fern {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS);
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false) exec_input(cmd, None, false)
} else { } else {
fern_interactive() fern_interactive()
} { } {
eprintln!("fern: {e}"); eprintln!("fern: {e}");
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false) && let Err(e) = exec_input(trap, None, false) {
{ eprintln!("fern: error running EXIT trap: {e}");
eprintln!("fern: error running EXIT trap: {e}"); }
}
write_jobs(|j| j.hang_up()); write_jobs(|j| j.hang_up());
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display()); eprintln!("fern: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"input file not found", "input file not found",
)); ));
} }
let Ok(input) = fs::read_to_string(path) else { let Ok(input) = fs::read_to_string(path) else {
eprintln!("fern: Failed to read input file: {}", path.display()); eprintln!("fern: Failed to read input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"failed to read input file", "failed to read input file",
)); ));
}; };
write_vars(|v| { write_vars(|v| {
v.cur_scope_mut() v.cur_scope_mut()
.bpush_arg(path.to_string_lossy().to_string()) .bpush_arg(path.to_string_lossy().to_string())
}); });
for arg in args { for arg in args {
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
} }
exec_input(input, None, false) exec_input(input, None, false)
} }
fn fern_interactive() -> ShResult<()> { fn fern_interactive() -> ShResult<()> {
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
// Create readline instance with initial prompt // Create readline instance with initial prompt
let mut readline = match FernVi::new(get_prompt().ok(), *TTY_FILENO) { let mut readline = match FernVi::new(get_prompt().ok(), *TTY_FILENO) {
Ok(rl) => rl, Ok(rl) => rl,
Err(e) => { Err(e) => {
eprintln!("Failed to initialize readline: {e}"); eprintln!("Failed to initialize readline: {e}");
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"readline initialization failed", "readline initialization failed",
)); ));
} }
}; };
// Main poll loop // Main poll loop
loop { loop {
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt // Ctrl+C - clear current input and show new prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
} }
readline.print_line()?; readline.print_line()?;
// Poll for stdin input // Poll for stdin input
let mut fds = [PollFd::new( let mut fds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
PollFlags::POLLIN, PollFlags::POLLIN,
)]; )];
match poll(&mut fds, PollTimeout::MAX) { match poll(&mut fds, PollTimeout::MAX) {
Ok(_) => {} Ok(_) => {}
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted by signal, loop back to handle it // Interrupted by signal, loop back to handle it
continue; continue;
} }
Err(e) => { Err(e) => {
eprintln!("poll error: {e}"); eprintln!("poll error: {e}");
break; break;
} }
} }
// Check if stdin has data // Check if stdin has data
if fds[0] if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
.revents() let mut buffer = [0u8; 1024];
.is_some_and(|r| r.contains(PollFlags::POLLIN)) match read(*TTY_FILENO, &mut buffer) {
{ Ok(0) => {
let mut buffer = [0u8; 1024]; // EOF
match read(*TTY_FILENO, &mut buffer) { break;
Ok(0) => { }
// EOF Ok(n) => {
break; readline.feed_bytes(&buffer[..n]);
} }
Ok(n) => { Err(Errno::EINTR) => {
readline.feed_bytes(&buffer[..n]); // Interrupted, continue to handle signals
} continue;
Err(Errno::EINTR) => { }
// Interrupted, continue to handle signals Err(e) => {
continue; eprintln!("read error: {e}");
} break;
Err(e) => { }
eprintln!("read error: {e}"); }
break; }
}
}
}
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = exec_input(input, None, true) { if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
let command_run_time = start.elapsed(); let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time); log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer()); write_meta(|m| m.stop_timer());
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
let real_end = start.elapsed(); let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end); log::info!("Total round trip time: {:.2?}", real_end);
} }
Ok(ReadlineEvent::Eof) => { Ok(ReadlineEvent::Eof) => {
// Ctrl+D on empty line // Ctrl+D on empty line
QUIT_CODE.store(0, Ordering::SeqCst); QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
Ok(ReadlineEvent::Pending) => { Ok(ReadlineEvent::Pending) => {
// No complete input yet, keep polling // No complete input yet, keep polling
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
}, },
} }
} }
Ok(()) Ok(())
} }

View File

@@ -155,7 +155,9 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
return Ok(()); return Ok(());
} }
let mut dispatcher = Dispatcher::new(parser.extract_nodes(), interactive); let nodes = parser.extract_nodes();
let mut dispatcher = Dispatcher::new(nodes, interactive);
if let Some(mut stack) = io_stack { if let Some(mut stack) = io_stack {
dispatcher.io_stack.extend(stack.drain(..)); dispatcher.io_stack.extend(stack.drain(..));
} }

View File

@@ -659,6 +659,9 @@ impl LexStream {
} }
_ if is_cmd_sub(text) => { _ if is_cmd_sub(text) => {
new_tk.mark(TkFlags::IS_CMDSUB); new_tk.mark(TkFlags::IS_CMDSUB);
if self.next_is_cmd() {
new_tk.mark(TkFlags::IS_CMD);
}
self.set_next_is_cmd(false); self.set_next_is_cmd(false);
} }
_ => { _ => {
@@ -846,11 +849,26 @@ pub fn is_field_sep(ch: char) -> bool {
} }
pub fn is_keyword(slice: &str) -> bool { pub fn is_keyword(slice: &str) -> bool {
KEYWORDS.contains(&slice) || (slice.ends_with("()") && !slice.ends_with("\\()")) KEYWORDS.contains(&slice) || ends_with_unescaped(slice, "()")
} }
pub fn is_cmd_sub(slice: &str) -> bool { pub fn is_cmd_sub(slice: &str) -> bool {
(slice.starts_with("$(") && slice.ends_with(')')) && !slice.ends_with("\\)") slice.starts_with("$(") && ends_with_unescaped(slice,")")
}
pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool {
slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len())
}
pub fn pos_is_escaped(slice: &str, pos: usize) -> bool {
let bytes = slice.as_bytes();
let mut escaped = false;
let mut i = pos;
while i > 0 && bytes[i - 1] == b'\\' {
escaped = !escaped;
i -= 1;
}
escaped
} }
pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> { pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> {

View File

@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
pub use bitflags::bitflags; pub use bitflags::bitflags;
pub use nix::{ pub use nix::{
errno::Errno, errno::Errno,
fcntl::{OFlag, open}, fcntl::{open, OFlag},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{ sys::{
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal}, signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
stat::Mode, stat::Mode,
termios::{self}, termios::{self},
wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid}, wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
}, },
unistd::{ unistd::{
ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
setpgid, tcgetpgrp, tcsetpgrp, write, tcsetpgrp, write, ForkResult, Pid,
}, },
}; };

View File

@@ -9,7 +9,7 @@ use crate::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
utils::RedirVecUtils, utils::RedirVecUtils,
}, },
parse::{Redir, RedirType, get_redir_file}, parse::{get_redir_file, Redir, RedirType},
prelude::*, prelude::*,
}; };
@@ -39,9 +39,9 @@ pub enum IoMode {
buf: String, buf: String,
pipe: Arc<OwnedFd>, pipe: Arc<OwnedFd>,
}, },
Close { Close {
tgt_fd: RawFd, tgt_fd: RawFd,
} },
} }
impl IoMode { impl IoMode {
@@ -79,7 +79,7 @@ impl IoMode {
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string(); let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
// multiple // multiple
let expanded_pathbuf = PathBuf::from(expanded_path); let expanded_pathbuf = PathBuf::from(expanded_path);
@@ -157,17 +157,17 @@ impl<R: Read> IoBuf<R> {
pub struct RedirGuard(IoFrame); pub struct RedirGuard(IoFrame);
impl RedirGuard { impl RedirGuard {
pub fn persist(mut self) { pub fn persist(mut self) {
if let Some(saved) = self.0.saved_io.take() { if let Some(saved) = self.0.saved_io.take() {
close(saved.0).ok(); close(saved.0).ok();
close(saved.1).ok(); close(saved.1).ok();
close(saved.2).ok(); close(saved.2).ok();
} }
// the guard is dropped here // the guard is dropped here
// but since we took the saved fds // but since we took the saved fds
// the drop does not restore them // the drop does not restore them
} }
} }
impl Drop for RedirGuard { impl Drop for RedirGuard {

View File

@@ -184,14 +184,16 @@ impl Highlighter {
input_chars.next(); input_chars.next();
} }
let inner_clean = Self::strip_markers(&inner);
// Determine prefix from content (handles both <( and >( for proc subs) // Determine prefix from content (handles both <( and >( for proc subs)
let prefix = match ch { let prefix = match ch {
markers::CMD_SUB => "$(", markers::CMD_SUB => "$(",
markers::SUBSH => "(", markers::SUBSH => "(",
markers::PROC_SUB => { markers::PROC_SUB => {
if inner.starts_with("<(") { if inner_clean.starts_with("<(") {
"<(" "<("
} else if inner.starts_with(">(") { } else if inner_clean.starts_with(">(") {
">(" ">("
} else { } else {
"<(" "<("
@@ -199,14 +201,13 @@ impl Highlighter {
} }
_ => unreachable!(), _ => unreachable!(),
}; };
let inner_content = if incomplete { let inner_content = if incomplete {
inner.strip_prefix(prefix).unwrap_or(&inner) inner_clean.strip_prefix(prefix).unwrap_or(&inner_clean)
} else { } else {
inner inner_clean
.strip_prefix(prefix) .strip_prefix(prefix)
.and_then(|s| s.strip_suffix(")")) .and_then(|s| s.strip_suffix(")"))
.unwrap_or(&inner) .unwrap_or(&inner_clean)
}; };
let mut recursive_highlighter = Self::new(); let mut recursive_highlighter = Self::new();

View File

@@ -9,8 +9,11 @@ use std::{
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
}; };
use crate::{libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::linebuf::LineBuf};
use crate::prelude::*; use crate::prelude::*;
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
prompt::readline::linebuf::LineBuf,
};
use super::vicmd::Direction; // surprisingly useful use super::vicmd::Direction; // surprisingly useful
@@ -271,23 +274,23 @@ impl History {
} }
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) { pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
let cursor_pos = if let Some(pending) = &self.pending { let cursor_pos = if let Some(pending) = &self.pending {
pending.cursor.get() pending.cursor.get()
} else { } else {
buf.1 buf.1
}; };
let cmd = buf.0.to_string(); let cmd = buf.0.to_string();
let constraint = SearchConstraint { let constraint = SearchConstraint {
kind: SearchKind::Prefix, kind: SearchKind::Prefix,
term: cmd.clone(), term: cmd.clone(),
}; };
if let Some(pending) = &mut self.pending { if let Some(pending) = &mut self.pending {
pending.set_buffer(cmd); pending.set_buffer(cmd);
pending.cursor.set(cursor_pos); pending.cursor.set(cursor_pos);
} else { } else {
self.pending = Some(LineBuf::new().with_initial(&cmd, cursor_pos)); self.pending = Some(LineBuf::new().with_initial(&cmd, cursor_pos));
} }
self.constrain_entries(constraint); self.constrain_entries(constraint);
} }

View File

@@ -18,7 +18,13 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform}; use vte::{Parser, Perform};
use crate::{ use crate::{
libsh::{error::{ShErr, ShErrKind, ShResult}, sys::TTY_FILENO}, prompt::readline::keys::{KeyCode, ModKeys}, shopt::FernBellStyle, state::read_shopts libsh::{
error::{ShErr, ShErrKind, ShResult},
sys::TTY_FILENO,
},
prompt::readline::keys::{KeyCode, ModKeys},
shopt::FernBellStyle,
state::read_shopts,
}; };
use crate::{ use crate::{
prelude::*, prelude::*,
@@ -107,7 +113,8 @@ fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> {
Ok(()) Ok(())
} }
/// Check if a string ends with a newline, ignoring any trailing ANSI escape sequences. /// Check if a string ends with a newline, ignoring any trailing ANSI escape
/// sequences.
fn ends_with_newline(s: &str) -> bool { fn ends_with_newline(s: &str) -> bool {
let bytes = s.as_bytes(); let bytes = s.as_bytes();
let mut i = bytes.len(); let mut i = bytes.len();
@@ -208,7 +215,7 @@ pub trait LineWriter {
fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>; fn clear_rows(&mut self, layout: &Layout) -> ShResult<()>;
fn redraw(&mut self, prompt: &str, line: &str, new_layout: &Layout) -> ShResult<()>; fn redraw(&mut self, prompt: &str, line: &str, new_layout: &Layout) -> ShResult<()>;
fn flush_write(&mut self, buf: &str) -> ShResult<()>; fn flush_write(&mut self, buf: &str) -> ShResult<()>;
fn send_bell(&mut self) -> ShResult<()>; fn send_bell(&mut self) -> ShResult<()>;
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@@ -757,12 +764,7 @@ impl Layout {
end: Pos::default(), end: Pos::default(),
} }
} }
pub fn from_parts( pub fn from_parts(term_width: u16, prompt: &str, to_cursor: &str, to_end: &str) -> Self {
term_width: u16,
prompt: &str,
to_cursor: &str,
to_end: &str,
) -> Self {
let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 }); let prompt_end = Self::calc_pos(term_width, prompt, Pos { col: 0, row: 0 });
let cursor = Self::calc_pos(term_width, to_cursor, prompt_end); let cursor = Self::calc_pos(term_width, to_cursor, prompt_end);
let end = Self::calc_pos(term_width, to_end, prompt_end); let end = Self::calc_pos(term_width, to_end, prompt_end);
@@ -998,11 +1000,10 @@ impl LineWriter for TermWriter {
Ok(()) Ok(())
} }
fn send_bell(&mut self) -> ShResult<()> { fn send_bell(&mut self) -> ShResult<()> {
if read_shopts(|o| o.core.bell_enabled) { if read_shopts(|o| o.core.bell_enabled) {
self.flush_write("\x07")?; self.flush_write("\x07")?;
} }
Ok(()) Ok(())
} }
} }

View File

@@ -212,12 +212,10 @@ impl ShOptCore {
} }
"bell_enabled" => { "bell_enabled" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, "shopt: expected 'true' or 'false' for bell_enabled value",
"shopt: expected 'true' or 'false' for bell_enabled value", ));
),
);
}; };
self.bell_enabled = val; self.bell_enabled = val;
} }
@@ -371,7 +369,7 @@ pub struct ShOptPrompt {
pub edit_mode: FernEditMode, pub edit_mode: FernEditMode,
pub comp_limit: usize, pub comp_limit: usize,
pub highlight: bool, pub highlight: bool,
pub auto_indent: bool pub auto_indent: bool,
} }
impl ShOptPrompt { impl ShOptPrompt {
@@ -413,15 +411,15 @@ impl ShOptPrompt {
}; };
self.highlight = val; self.highlight = val;
} }
"auto_indent" => { "auto_indent" => {
let Ok(val) = val.parse::<bool>() else { let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for auto_indent value", "shopt: expected 'true' or 'false' for auto_indent value",
)); ));
}; };
self.auto_indent = val; self.auto_indent = val;
} }
"custom" => { "custom" => {
todo!() todo!()
} }
@@ -480,11 +478,12 @@ impl ShOptPrompt {
output.push_str(&format!("{}", self.highlight)); output.push_str(&format!("{}", self.highlight));
Ok(Some(output)) Ok(Some(output))
} }
"auto_indent" => { "auto_indent" => {
let mut output = String::from("Whether to automatically indent new lines in multiline commands\n"); let mut output =
output.push_str(&format!("{}", self.auto_indent)); String::from("Whether to automatically indent new lines in multiline commands\n");
Ok(Some(output)) output.push_str(&format!("{}", self.auto_indent));
} Ok(Some(output))
}
_ => Err( _ => Err(
ShErr::simple( ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
@@ -499,7 +498,7 @@ impl ShOptPrompt {
"edit_mode", "edit_mode",
"comp_limit", "comp_limit",
"highlight", "highlight",
"auto_indent", "auto_indent",
]), ]),
), ),
), ),
@@ -515,7 +514,7 @@ impl Display for ShOptPrompt {
output.push(format!("edit_mode = {}", self.edit_mode)); output.push(format!("edit_mode = {}", self.edit_mode));
output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("comp_limit = {}", self.comp_limit));
output.push(format!("highlight = {}", self.highlight)); output.push(format!("highlight = {}", self.highlight));
output.push(format!("auto_indent = {}", self.auto_indent)); output.push(format!("auto_indent = {}", self.auto_indent));
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -530,7 +529,7 @@ impl Default for ShOptPrompt {
edit_mode: FernEditMode::Vi, edit_mode: FernEditMode::Vi,
comp_limit: 100, comp_limit: 100,
highlight: true, highlight: true,
auto_indent: true auto_indent: true,
} }
} }
} }

View File

@@ -6,7 +6,7 @@ use tempfile::TempDir;
use crate::prompt::readline::complete::Completer; use crate::prompt::readline::complete::Completer;
use crate::prompt::readline::markers; use crate::prompt::readline::markers;
use crate::state::{VarFlags, write_logic, write_vars}; use crate::state::{write_logic, write_vars, VarFlags};
use super::*; use super::*;
@@ -192,12 +192,10 @@ fn complete_filename_with_slash() {
// Should complete files in subdir/ // Should complete files in subdir/
if result.is_some() { if result.is_some() {
assert!( assert!(completer
completer .candidates
.candidates .iter()
.iter() .any(|c| c.contains("nested.txt")));
.any(|c| c.contains("nested.txt"))
);
} }
} }
@@ -704,12 +702,10 @@ fn complete_special_characters_in_filename() {
if result.is_some() { if result.is_some() {
// Should handle special chars in filenames // Should handle special chars in filenames
assert!( assert!(completer
completer .candidates
.candidates .iter()
.iter() .any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore")));
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))
);
} }
} }

View File

@@ -1,6 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion}; use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
use crate::state::VarFlags; use crate::state::VarFlags;
use super::*; use super::*;

View File

@@ -4,9 +4,8 @@ use super::*;
use crate::expand::{expand_aliases, unescape_str}; use crate::expand::{expand_aliases, unescape_str};
use crate::libsh::error::{Note, ShErr, ShErrKind}; use crate::libsh::error::{Note, ShErr, ShErrKind};
use crate::parse::{ use crate::parse::{
NdRule, Node, ParseStream,
lex::{LexFlags, LexStream, Tk, TkRule}, lex::{LexFlags, LexStream, Tk, TkRule},
node_operation, node_operation, NdRule, Node, ParseStream,
}; };
use crate::state::{write_logic, write_vars}; use crate::state::{write_logic, write_vars};

View File

@@ -6,12 +6,12 @@ use crate::{
term::{Style, Styled}, term::{Style, Styled},
}, },
prompt::readline::{ prompt::readline::{
FernVi,
history::History, history::History,
keys::{KeyCode, KeyEvent, ModKeys}, keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf, linebuf::LineBuf,
term::{KeyReader, LineWriter, raw_mode}, term::{raw_mode, KeyReader, LineWriter},
vimode::{ViInsert, ViMode, ViNormal}, vimode::{ViInsert, ViMode, ViNormal},
FernVi,
}, },
}; };
@@ -175,9 +175,9 @@ impl LineWriter for TestWriter {
Ok(()) Ok(())
} }
fn send_bell(&mut self) -> ShResult<()> { fn send_bell(&mut self) -> ShResult<()> {
Ok(()) Ok(())
} }
} }
// NOTE: FernVi structure has changed significantly and readline() method no // NOTE: FernVi structure has changed significantly and readline() method no
@@ -605,31 +605,27 @@ fn editor_delete_line_up() {
#[test] #[test]
fn editor_insert_at_line_start() { fn editor_insert_at_line_start() {
// I should move cursor to position 0 when line starts with non-whitespace // I should move cursor to position 0 when line starts with non-whitespace
assert_eq!( assert_eq!(normal_cmd("I", "hello world", 5), ("hello world".into(), 0));
normal_cmd("I", "hello world", 5),
("hello world".into(), 0)
);
// I should skip leading whitespace // I should skip leading whitespace
assert_eq!( assert_eq!(
normal_cmd("I", " hello world", 8), normal_cmd("I", " hello world", 8),
(" hello world".into(), 2) (" hello world".into(), 2)
); );
// I should move to the first non-whitespace on the current line in a multiline buffer // I should move to the first non-whitespace on the current line in a multiline
// buffer
assert_eq!( assert_eq!(
normal_cmd("I", "first line\nsecond line", 14), normal_cmd("I", "first line\nsecond line", 14),
("first line\nsecond line".into(), 11) ("first line\nsecond line".into(), 11)
); );
// I should land on position 0 when cursor is already at 0 // I should land on position 0 when cursor is already at 0
assert_eq!( assert_eq!(normal_cmd("I", "hello", 0), ("hello".into(), 0));
normal_cmd("I", "hello", 0),
("hello".into(), 0)
);
} }
#[test] #[test]
fn editor_f_char_from_position_zero() { fn editor_f_char_from_position_zero() {
// f<char> at position 0 should skip the cursor and find the next occurrence // f<char> at position 0 should skip the cursor and find the next occurrence
// Regression: previously at pos 0, f would match the char under the cursor itself // Regression: previously at pos 0, f would match the char under the cursor
// itself
assert_eq!( assert_eq!(
normal_cmd("fa", "abcaef", 0), normal_cmd("fa", "abcaef", 0),
("abcaef".into(), 3) // should find second 'a', not the 'a' at position 0 ("abcaef".into(), 3) // should find second 'a', not the 'a' at position 0

View File

@@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use crate::parse::{ use crate::parse::{
NdRule, Node, ParseStream, Redir, RedirType,
lex::{LexFlags, LexStream}, lex::{LexFlags, LexStream},
NdRule, Node, ParseStream, Redir, RedirType,
}; };
use crate::procio::{IoFrame, IoMode, IoStack}; use crate::procio::{IoFrame, IoMode, IoStack};

View File

@@ -1,5 +1,5 @@
use std::path::PathBuf;
use crate::state::{LogTab, MetaTab, ScopeStack, ShellParam, VarFlags, VarTab}; use crate::state::{LogTab, MetaTab, ScopeStack, ShellParam, VarFlags, VarTab};
use std::path::PathBuf;
// ============================================================================ // ============================================================================
// ScopeStack Tests - Variable Scoping // ScopeStack Tests - Variable Scoping
@@ -11,8 +11,8 @@ fn scopestack_new() {
// Should start with one global scope // Should start with one global scope
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
// it doesn't // it doesn't
// panic // panic
} }
#[test] #[test]
@@ -296,13 +296,21 @@ fn scopestack_local_var_mutation() {
// `foo="bar"` — reassign without LOCAL flag (plain assignment) // `foo="bar"` — reassign without LOCAL flag (plain assignment)
stack.set_var("foo", "bar", VarFlags::NONE); stack.set_var("foo", "bar", VarFlags::NONE);
assert_eq!(stack.get_var("foo"), "bar", "Local var should be mutated in place"); assert_eq!(
stack.get_var("foo"),
"bar",
"Local var should be mutated in place"
);
// Ascend back to global // Ascend back to global
stack.ascend(); stack.ascend();
// foo should not exist in global scope // foo should not exist in global scope
assert_eq!(stack.get_var("foo"), "", "Local var should not leak to global scope"); assert_eq!(
stack.get_var("foo"),
"",
"Local var should not leak to global scope"
);
} }
#[test] #[test]
@@ -318,13 +326,21 @@ fn scopestack_local_var_uninitialized() {
// `foo="bar"` — assign a value later // `foo="bar"` — assign a value later
stack.set_var("foo", "bar", VarFlags::NONE); stack.set_var("foo", "bar", VarFlags::NONE);
assert_eq!(stack.get_var("foo"), "bar", "Uninitialized local should be assignable"); assert_eq!(
stack.get_var("foo"),
"bar",
"Uninitialized local should be assignable"
);
// Ascend back to global // Ascend back to global
stack.ascend(); stack.ascend();
// foo should not exist in global scope // foo should not exist in global scope
assert_eq!(stack.get_var("foo"), "", "Local var should not leak to global scope"); assert_eq!(
stack.get_var("foo"),
"",
"Local var should not leak to global scope"
);
} }
// ============================================================================ // ============================================================================
@@ -740,11 +756,14 @@ fn dirstack_pushd_rotation_with_cwd() {
assert_eq!(new_cwd, Some(PathBuf::from("/var"))); assert_eq!(new_cwd, Some(PathBuf::from("/var")));
let remaining: Vec<_> = meta.dirs().iter().collect(); let remaining: Vec<_> = meta.dirs().iter().collect();
assert_eq!(remaining, vec![ assert_eq!(
&PathBuf::from("/etc"), remaining,
&PathBuf::from("/home"), vec![
&PathBuf::from("/tmp"), &PathBuf::from("/etc"),
]); &PathBuf::from("/home"),
&PathBuf::from("/tmp"),
]
);
} }
#[test] #[test]
@@ -821,10 +840,10 @@ fn dirstack_popd_plus_n_offset() {
assert_eq!(removed, Some(PathBuf::from("/var"))); assert_eq!(removed, Some(PathBuf::from("/var")));
let remaining: Vec<_> = meta.dirs().iter().collect(); let remaining: Vec<_> = meta.dirs().iter().collect();
assert_eq!(remaining, vec![ assert_eq!(
&PathBuf::from("/tmp"), remaining,
&PathBuf::from("/etc"), vec![&PathBuf::from("/tmp"), &PathBuf::from("/etc"),]
]); );
} }
#[test] #[test]