From dd24cc2aec9ece8214ec1a4eff4abd26d00ea083 Mon Sep 17 00:00:00 2001 From: Jonas Maier Date: Sat, 9 May 2026 11:30:28 +0200 Subject: simple script file test --- src/lib.rs | 509 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 485 +----------------------------------------- src/run/builtin.rs | 4 +- src/run/mod.rs | 37 ++-- test-cases/fun0/script.sh | 5 + test-cases/fun0/stdout.txt | 1 + test-cases/generate.sh | 21 ++ tests/common.rs | 19 ++ tests/scripts.rs | 8 + 9 files changed, 596 insertions(+), 493 deletions(-) create mode 100644 src/lib.rs create mode 100644 test-cases/fun0/script.sh create mode 100644 test-cases/fun0/stdout.txt create mode 100644 test-cases/generate.sh create mode 100644 tests/common.rs create mode 100644 tests/scripts.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cf57cee --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,509 @@ +#![feature( + unix_socket_ancillary_data, + peer_credentials_unix_socket, + associated_type_defaults +)] +#![allow(clippy::needless_range_loop)] + +use std::collections::HashMap; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::hash::Hash; +use std::io::{self, Read, Write}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::io::AsRawFd; +use std::path::Path; +use std::process::Command; +use std::sync::{Arc, Mutex}; + +pub mod ansi; +pub mod basedir; +pub mod completion; +pub mod ctrlc; +pub mod cursor; +pub mod date; +pub mod defer; +pub mod export_fun; +pub mod history; +pub mod linebuf; +pub mod panic; +pub mod parse; +pub mod raw; +pub mod reload; +pub mod run; +pub mod rw; +pub mod serialization; +pub mod wait; + +use linebuf::LineBuf; +use raw::*; + +use crate::completion::{PathCache, completion}; +use crate::ctrlc::CtrlC; +use crate::cursor::{Direction, move_cursor}; +use crate::history::HistoryEntry; +use crate::parse::{Block, ExpString, Parse, PostExpansion}; + +macro_rules! print { + ($($x:tt)*) => {{ + let res = write!(io::stdout(), $($x)*); + res.unwrap(); + io::stdout().flush().unwrap(); + }} +} + +macro_rules! println { + () => {{ + println!("") + }}; + ($($x:tt)*) => { + { + let res = write!(io::stdout(), $($x)*); + res.unwrap(); + let res = write!(io::stdout(), "\r\n"); + res.unwrap(); + io::stdout().flush().unwrap(); + } + }; +} + +fn completely_clear_screen() { + print!("\x1B[2J\x1B[1;1H"); +} + +fn clear_screen() { + completely_clear_screen(); +} + +type BString = Vec; +#[allow(non_camel_case_types)] +type bstr = [u8]; + +pub struct Session { + raw: Option, + line: LineBuf, + history: Vec, + prev_path: BString, + builtins: HashMap, + vars: HashMap, + funs: HashMap, + aliases: run::Aliases, + socket_running: Option, + path_cache: PathCache, + ctrlc: CtrlC, + + /// terminfo identifier to command invocation + ti_keybinds: HashMap>, + /// byte literals to command invocation + ascii_keybinds: HashMap>, + + debug_keystrokes: bool, + loud: bool, + + /// n before end of history.len() + /// 0 == not checking history + history_visit: usize, +} + +impl Session { + pub fn new_noninteractive() -> Self { + Self { + raw: None, + line: LineBuf::new(), + history: Vec::new(), + prev_path: b".".into(), + builtins: HashMap::new(), + vars: HashMap::new(), + funs: HashMap::new(), + aliases: run::Aliases::new(), + socket_running: None, + path_cache: Default::default(), + ctrlc: Default::default(), + ti_keybinds: HashMap::new(), + ascii_keybinds: HashMap::new(), + debug_keystrokes: false, + loud: false, + history_visit: 0, + } + } +} + +/// relative path -- in case it is a proper subpath the result starts with a slash `/` +fn relative_path(root: &Path, target: &Path) -> Option { + let root = root.to_string_lossy(); + let mut target = target.to_string_lossy().to_string(); + if !target.ends_with("/") { + target += "/"; + } + if let Some(("", leaf)) = target.split_once(&*root) { + Some(leaf.into()) + } else { + None + } +} + +impl Session { + fn pretty_cwd_res(&self) -> io::Result { + let dir = std::env::current_dir()?; + let mut s = if let Some(home_dir) = std::env::home_dir() { + if let Some(rela) = relative_path(&home_dir, &dir) { + format!("~{rela}") + } else { + dir.to_string_lossy().to_string() + } + } else { + dir.to_string_lossy().to_string() + }; + while s.ends_with("/") && s.len() > 1 { + s.remove(s.len() - 1); + } + Ok(s) + } + + fn pretty_cwd(&self) -> String { + self.pretty_cwd_res().unwrap_or_else(|_| String::new()) + } + + // TODO: prompt should be BString as well + fn prompt(&self) -> String { + #[cfg(debug_assertions)] + let dev = "dev "; + #[cfg(not(debug_assertions))] + let dev = ""; + format!("{dev}[{}]# ", self.pretty_cwd()) + } + + fn clear_prompt(&mut self) { + cursor::move_cursor(Direction::Right, self.line.distance_from_right_end()); + for _ in 0..self.line.len() { + print!("\x08 \x08"); + } + io::stdout().lock().flush().unwrap(); + self.line.clear(); + } + + fn prompt_clear(&mut self) { + self.clear_prompt(); + self.history_visit = 0; + } + + fn reprint_prompt(&self) { + print!("{}", self.prompt()); + self.line.display_pre(); + self.line.display_post(b""); + } + + fn display_historic_entry(&mut self) { + self.clear_prompt(); + let new = if self.history_visit == 0 { + Vec::new() + } else { + self.history[self.history.len() - self.history_visit] + .cmd + .clone() + }; + io::stdout().write_all(&new).unwrap(); + io::stdout().flush().unwrap(); + self.line.set_content(new); + } + + fn history_up(&mut self) { + if self.history_visit < self.history.len() { + self.history_visit += 1; + self.display_historic_entry(); + } + } + + fn history_down(&mut self) { + if self.history_visit > 0 { + self.history_visit -= 1; + self.display_historic_entry(); + } + } + + fn type_byte(&mut self, b: u8) { + self.line.add(b); + io::stdout().lock().write_all(&[b]).unwrap(); + self.line.display_post(b""); + } + + fn type_bytes(&mut self, bs: &[u8]) { + for b in bs.iter() { + self.type_byte(*b); + } + } + + fn del_left(&mut self) { + if self.line.del_left().is_some() { + cursor::move_cursor(Direction::Left, 1); + self.line.display_post(b" "); + } + } + + fn del_right(&mut self) { + self.line.del_right(); + self.line.display_post(b" "); + } + + fn del_left_or_previous(&mut self) { + if self.line.is_empty() && !self.line.is_dirty() && !self.history.is_empty() { + // take previous command for editing + let cmd = self.history[self.history.len() - 1].cmd.clone(); + self.type_bytes(&cmd); + } else { + self.del_left(); + } + } + + fn move_to_begin(&mut self) { + cursor::move_cursor(Direction::Left, self.line.all_left()); + io::stdout().flush().unwrap(); + } + + fn move_to_end(&mut self) { + cursor::move_cursor(Direction::Right, self.line.all_right()); + io::stdout().flush().unwrap(); + } + + fn del_left_word(&mut self) { + let mut del = 0; + while let Some(x) = self.line.get_left() + && x == b' ' + { + self.line.del_left(); + del += 1; + } + while let Some(x) = self.line.get_left() + && x != b' ' + { + self.line.del_left(); + del += 1; + } + cursor::move_cursor(Direction::Left, del); + self.line.display_post(&vec![b' '; del]); + } + + fn del_right_word(&mut self) { + let mut del = 0; + while let Some(x) = self.line.get_right() + && x == b' ' + { + self.line.del_right(); + del += 1; + } + while let Some(x) = self.line.get_right() + && x != b' ' + { + self.line.del_right(); + del += 1; + } + self.line.display_post(&vec![b' '; del]); + } + + fn cursor_left(&mut self) { + if self.line.left() { + move_cursor(Direction::Left, 1); + io::stdout().lock().flush().unwrap(); + } + } + + fn cursor_right(&mut self) { + if self.line.right() { + move_cursor(Direction::Right, 1); + io::stdout().lock().flush().unwrap(); + } + } + + // move to next + fn cursor_left_word(&mut self) { + let mut i = 0; + + // find word + while let Some(b' ') = self.line.get_left() { + self.line.left(); + i += 1; + } + + // skip it + while let Some(x) = self.line.get_left() + && !x.is_ascii_whitespace() + { + self.line.left(); + i += 1; + } + + cursor::move_cursor(Direction::Left, i); + io::stdout().flush().unwrap(); + } + + fn cursor_right_word(&mut self) { + let mut i = 0; + + // find word + while let Some(b' ') = self.line.get_right() { + self.line.right(); + i += 1; + } + + // skip it + while let Some(x) = self.line.get_right() + && !x.is_ascii_whitespace() + { + self.line.right(); + i += 1; + } + + cursor::move_cursor(Direction::Right, i); + io::stdout().flush().unwrap(); + } + + fn complete(session: Arc>) { + let cmd = session.lock().unwrap().line.pre().to_vec(); + + let comp = completion(session.clone(), &cmd); + + let mut se = session.lock().unwrap(); + + se.type_bytes(&comp.shared_prefix); + + if comp.suggestions.len() > 1 { + print!("\r\n"); + for s in comp.suggestions { + io::stdout().lock().write_all(&s.display).unwrap(); + println!(); + } + se.reprint_prompt(); + } + } + + fn try_submit_command(session: Arc>) { + let mut se = session.lock().unwrap(); + let line = se.line.into_bytes(); + + if !line.is_empty() { + let parsed = match parse::do_parse(&line) { + Ok(p) => p, + Err(_) => { + se.line.add(b'\n'); + print!("\r\n> "); + return; + } + }; + print!("\r\n"); + let entry = HistoryEntry::new(line.clone()); + history::persist(&entry); + se.history.push(entry); + se.history_visit = 0; + se.line.dump(); + drop(se); + run::run(session.clone(), parsed); + } + } + + fn screen_clear(&mut self) { + clear_screen(); + self.reprint_prompt(); + } + + fn raw_enable(&self) { + if let Some(r) = &self.raw { + r.enable(); + } + } + + fn raw_disable(&self) { + if let Some(r) = &self.raw { + r.disable(); + } + } +} + +fn exec_rc_file(se: Arc>) { + let _ = run::source( + se, + basedir::config_dir().join(".pishrc").as_os_str().as_bytes(), + ); +} + +pub fn event_loop() { + history::setup(); + ansi::setup(); + + let stdin = io::stdin(); + + let fd = stdin.as_raw_fd(); + let raw = ScopedRawMode::on_fd(fd); + raw.enable(); + + fs::create_dir_all(basedir::config_dir()).unwrap(); + fs::create_dir_all(basedir::data_dir()).unwrap(); + + let se = Session { + raw: Some(raw), + line: LineBuf::new(), + history: Vec::new(), + builtins: run::builtin_map(), + prev_path: vec![b'.'], + history_visit: 0, + socket_running: None, + vars: HashMap::new(), + funs: HashMap::new(), + aliases: run::Aliases::new(), + path_cache: Default::default(), + ctrlc: Default::default(), + debug_keystrokes: false, + loud: false, + ti_keybinds: HashMap::new(), + ascii_keybinds: HashMap::new(), + }; + + let session = Arc::new(Mutex::new(se)); + exec_rc_file(session.clone()); + + session.lock().unwrap().loud = true; + + print!("{}", session.lock().unwrap().prompt()); + + completion::populate_path_cache(session.clone()); + + let _sock_dropper = export_fun::listen(session.clone()); + let _ctrlc = ctrlc::setup(session.clone()); + + 'repl: loop { + let mut se = session.lock().unwrap(); + + let Some(key) = ansi::read(se.debug_keystrokes) else { + break; + }; + + if se.debug_keystrokes { + println!("{key:?}"); + } + + if let Some(cmd) = se.ascii_keybinds.get(key.as_bytes()) { + let cmd = cmd.clone(); + drop(se); + // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. + let _ = run::run_quiet(session.clone(), cmd); + continue 'repl; + } + + match key { + ansi::KbInput::Key([x]) => se.type_byte(x), + ansi::KbInput::Escape(escape) => { + for terminfo_key in escape.keys.iter() { + if let Some(cmd) = se.ti_keybinds.get(terminfo_key.as_bytes()) { + let cmd = cmd.clone(); + drop(se); + // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. + let _ = run::run_quiet(session.clone(), cmd); + continue 'repl; + } + } + } + ansi::KbInput::InvalidEscape(_) => continue, + } + } + + session.lock().unwrap().raw_disable(); +} diff --git a/src/main.rs b/src/main.rs index a4a6f00..fc44fa5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,478 +1,5 @@ -#![feature( - unix_socket_ancillary_data, - peer_credentials_unix_socket, - associated_type_defaults -)] -#![allow(clippy::needless_range_loop)] - -use std::collections::HashMap; -use std::ffi::OsStr; -use std::fs::{self, File}; -use std::io::{self, IsTerminal, Read, Write}; -use std::os::unix::ffi::OsStrExt; -use std::os::unix::io::AsRawFd; -use std::path::Path; -use std::process::Command; -use std::sync::{Arc, Mutex}; -use std::thread::sleep; -use std::time::Duration; - -pub mod ansi; -pub mod basedir; -pub mod completion; -pub mod ctrlc; -pub mod cursor; -pub mod date; -pub mod defer; -pub mod export_fun; -pub mod history; -pub mod linebuf; -pub mod panic; -pub mod parse; -pub mod raw; -pub mod reload; -pub mod run; -pub mod rw; -pub mod serialization; -pub mod wait; - -use linebuf::LineBuf; -use raw::*; - -use crate::completion::{PathCache, completion}; -use crate::ctrlc::CtrlC; -use crate::cursor::{Direction, move_cursor}; -use crate::history::HistoryEntry; -use crate::parse::{Block, ExpString, Parse, PostExpansion}; - -macro_rules! print { - ($($x:tt)*) => {{ - let res = write!(io::stdout(), $($x)*); - res.unwrap(); - io::stdout().flush().unwrap(); - }} -} - -macro_rules! println { - () => {{ - println!("") - }}; - ($($x:tt)*) => { - { - let res = write!(io::stdout(), $($x)*); - res.unwrap(); - let res = write!(io::stdout(), "\r\n"); - res.unwrap(); - io::stdout().flush().unwrap(); - } - }; -} - -fn completely_clear_screen() { - print!("\x1B[2J\x1B[1;1H"); -} - -fn clear_screen() { - completely_clear_screen(); -} - -type BString = Vec; -#[allow(non_camel_case_types)] -type bstr = [u8]; - -pub struct Session { - raw: ScopedRawMode, - line: LineBuf, - history: Vec, - prev_path: BString, - builtins: HashMap, - vars: HashMap, - funs: HashMap, - aliases: run::Aliases, - socket_running: Option, - path_cache: PathCache, - ctrlc: CtrlC, - - /// terminfo identifier to command invocation - ti_keybinds: HashMap>, - /// byte literals to command invocation - ascii_keybinds: HashMap>, - - debug_keystrokes: bool, - loud: bool, - - /// n before end of history.len() - /// 0 == not checking history - history_visit: usize, -} - -/// relative path -- in case it is a proper subpath the result starts with a slash `/` -fn relative_path(root: &Path, target: &Path) -> Option { - let root = root.to_string_lossy(); - let mut target = target.to_string_lossy().to_string(); - if !target.ends_with("/") { - target += "/"; - } - if let Some(("", leaf)) = target.split_once(&*root) { - Some(leaf.into()) - } else { - None - } -} - -impl Session { - fn pretty_cwd_res(&self) -> io::Result { - let dir = std::env::current_dir()?; - let mut s = if let Some(home_dir) = std::env::home_dir() { - if let Some(rela) = relative_path(&home_dir, &dir) { - format!("~{rela}") - } else { - dir.to_string_lossy().to_string() - } - } else { - dir.to_string_lossy().to_string() - }; - while s.ends_with("/") && s.len() > 1 { - s.remove(s.len() - 1); - } - Ok(s) - } - - fn pretty_cwd(&self) -> String { - self.pretty_cwd_res().unwrap_or_else(|_| String::new()) - } - - // TODO: prompt should be BString as well - fn prompt(&self) -> String { - #[cfg(debug_assertions)] - let dev = "dev "; - #[cfg(not(debug_assertions))] - let dev = ""; - format!("{dev}[{}]# ", self.pretty_cwd()) - } - - fn clear_prompt(&mut self) { - cursor::move_cursor(Direction::Right, self.line.distance_from_right_end()); - for _ in 0..self.line.len() { - print!("\x08 \x08"); - } - io::stdout().lock().flush().unwrap(); - self.line.clear(); - } - - fn prompt_clear(&mut self) { - self.clear_prompt(); - self.history_visit = 0; - } - - fn reprint_prompt(&self) { - print!("{}", self.prompt()); - self.line.display_pre(); - self.line.display_post(b""); - } - - fn display_historic_entry(&mut self) { - self.clear_prompt(); - let new = if self.history_visit == 0 { - Vec::new() - } else { - self.history[self.history.len() - self.history_visit] - .cmd - .clone() - }; - io::stdout().write_all(&new).unwrap(); - io::stdout().flush().unwrap(); - self.line.set_content(new); - } - - fn history_up(&mut self) { - if self.history_visit < self.history.len() { - self.history_visit += 1; - self.display_historic_entry(); - } - } - - fn history_down(&mut self) { - if self.history_visit > 0 { - self.history_visit -= 1; - self.display_historic_entry(); - } - } - - fn type_byte(&mut self, b: u8) { - self.line.add(b); - io::stdout().lock().write_all(&[b]).unwrap(); - self.line.display_post(b""); - } - - fn type_bytes(&mut self, bs: &[u8]) { - for b in bs.iter() { - self.type_byte(*b); - } - } - - fn del_left(&mut self) { - if self.line.del_left().is_some() { - cursor::move_cursor(Direction::Left, 1); - self.line.display_post(b" "); - } - } - - fn del_right(&mut self) { - self.line.del_right(); - self.line.display_post(b" "); - } - - fn del_left_or_previous(&mut self) { - if self.line.is_empty() && !self.line.is_dirty() && !self.history.is_empty() { - // take previous command for editing - let cmd = self.history[self.history.len() - 1].cmd.clone(); - self.type_bytes(&cmd); - } else { - self.del_left(); - } - } - - fn move_to_begin(&mut self) { - cursor::move_cursor(Direction::Left, self.line.all_left()); - io::stdout().flush().unwrap(); - } - - fn move_to_end(&mut self) { - cursor::move_cursor(Direction::Right, self.line.all_right()); - io::stdout().flush().unwrap(); - } - - fn del_left_word(&mut self) { - let mut del = 0; - while let Some(x) = self.line.get_left() - && x == b' ' - { - self.line.del_left(); - del += 1; - } - while let Some(x) = self.line.get_left() - && x != b' ' - { - self.line.del_left(); - del += 1; - } - cursor::move_cursor(Direction::Left, del); - self.line.display_post(&vec![b' '; del]); - } - - fn del_right_word(&mut self) { - let mut del = 0; - while let Some(x) = self.line.get_right() - && x == b' ' - { - self.line.del_right(); - del += 1; - } - while let Some(x) = self.line.get_right() - && x != b' ' - { - self.line.del_right(); - del += 1; - } - self.line.display_post(&vec![b' '; del]); - } - - fn cursor_left(&mut self) { - if self.line.left() { - move_cursor(Direction::Left, 1); - io::stdout().lock().flush().unwrap(); - } - } - - fn cursor_right(&mut self) { - if self.line.right() { - move_cursor(Direction::Right, 1); - io::stdout().lock().flush().unwrap(); - } - } - - // move to next - fn cursor_left_word(&mut self) { - let mut i = 0; - - // find word - while let Some(b' ') = self.line.get_left() { - self.line.left(); - i += 1; - } - - // skip it - while let Some(x) = self.line.get_left() - && !x.is_ascii_whitespace() - { - self.line.left(); - i += 1; - } - - cursor::move_cursor(Direction::Left, i); - io::stdout().flush().unwrap(); - } - - fn cursor_right_word(&mut self) { - let mut i = 0; - - // find word - while let Some(b' ') = self.line.get_right() { - self.line.right(); - i += 1; - } - - // skip it - while let Some(x) = self.line.get_right() - && !x.is_ascii_whitespace() - { - self.line.right(); - i += 1; - } - - cursor::move_cursor(Direction::Right, i); - io::stdout().flush().unwrap(); - } - - fn complete(session: Arc>) { - let cmd = session.lock().unwrap().line.pre().to_vec(); - - let comp = completion(session.clone(), &cmd); - - let mut se = session.lock().unwrap(); - - se.type_bytes(&comp.shared_prefix); - - if comp.suggestions.len() > 1 { - print!("\r\n"); - for s in comp.suggestions { - io::stdout().lock().write_all(&s.display).unwrap(); - println!(); - } - se.reprint_prompt(); - } - } - - fn try_submit_command(session: Arc>) { - let mut se = session.lock().unwrap(); - let line = se.line.into_bytes(); - - if !line.is_empty() { - let parsed = match parse::do_parse(&line) { - Ok(p) => p, - Err(_) => { - se.line.add(b'\n'); - print!("\r\n> "); - return; - } - }; - print!("\r\n"); - let entry = HistoryEntry::new(line.clone()); - history::persist(&entry); - se.history.push(entry); - se.history_visit = 0; - se.line.dump(); - drop(se); - run::run(session.clone(), parsed); - } - } - - fn screen_clear(&mut self) { - clear_screen(); - self.reprint_prompt(); - } -} - -fn exec_rc_file(se: Arc>) { - let _ = run::source( - se, - basedir::config_dir().join(".pishrc").as_os_str().as_bytes(), - ); -} - -fn event_loop() { - history::setup(); - ansi::setup(); - - let stdin = io::stdin(); - - let fd = stdin.as_raw_fd(); - let raw = ScopedRawMode::on_fd(fd); - raw.enable(); - - fs::create_dir_all(basedir::config_dir()).unwrap(); - fs::create_dir_all(basedir::data_dir()).unwrap(); - - let se = Session { - raw, - line: LineBuf::new(), - history: Vec::new(), - builtins: run::builtin_map(), - prev_path: vec![b'.'], - history_visit: 0, - socket_running: None, - vars: HashMap::new(), - funs: HashMap::new(), - aliases: run::Aliases::new(), - path_cache: Default::default(), - ctrlc: Default::default(), - debug_keystrokes: false, - loud: false, - ti_keybinds: HashMap::new(), - ascii_keybinds: HashMap::new(), - }; - - let session = Arc::new(Mutex::new(se)); - exec_rc_file(session.clone()); - - session.lock().unwrap().loud = true; - - print!("{}", session.lock().unwrap().prompt()); - - completion::populate_path_cache(session.clone()); - - let _sock_dropper = export_fun::listen(session.clone()); - let _ctrlc = ctrlc::setup(session.clone()); - - 'repl: loop { - let mut se = session.lock().unwrap(); - - let Some(key) = ansi::read(se.debug_keystrokes) else { - break; - }; - - if se.debug_keystrokes { - println!("{key:?}"); - } - - if let Some(cmd) = se.ascii_keybinds.get(key.as_bytes()) { - let cmd = cmd.clone(); - drop(se); - // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. - let _ = run::run_quiet(session.clone(), cmd); - continue 'repl; - } - - match key { - ansi::KbInput::Key([x]) => se.type_byte(x), - ansi::KbInput::Escape(escape) => { - for terminfo_key in escape.keys.iter() { - if let Some(cmd) = se.ti_keybinds.get(terminfo_key.as_bytes()) { - let cmd = cmd.clone(); - drop(se); - // not sure if/how to report this error - would be strange to print something to console every time a keybind command returns nonzero exit code. - let _ = run::run_quiet(session.clone(), cmd); - continue 'repl; - } - } - } - ansi::KbInput::InvalidEscape(_) => continue, - } - } - - session.lock().unwrap().raw.disable(); -} +use pish::export_fun; +use std::io::{self, IsTerminal}; fn main() { export_fun::maybe_run_defined_function(); @@ -481,23 +8,23 @@ fn main() { return; } - crate::panic::hook(); + pish::panic::hook(); // it is quite annoying when the terminal window closes due to a crash, so let's just catch all panics loop { - let res = std::panic::catch_unwind(event_loop); + let res = std::panic::catch_unwind(pish::event_loop); match res { Ok(_) => break, Err(_) => { #[cfg(debug_assertions)] unsafe { - reload::continue_reload() + pish::reload::continue_reload() } } } // prevent incredibly fast panic loops - sleep(Duration::from_secs(1)); + std::thread::sleep(std::time::Duration::from_secs(1)); } println!("bye"); diff --git a/src/run/builtin.rs b/src/run/builtin.rs index cfaef1e..f085005 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -120,9 +120,9 @@ impl Builtin for re { } fn special(&mut self, session: Arc>, _args: &[BString]) { - session.lock().unwrap().raw.disable(); + session.lock().unwrap().raw_disable(); crate::reload::begin_reload(); - session.lock().unwrap().raw.enable(); // something went wrong, let's restore raw mode + session.lock().unwrap().raw_enable(); // something went wrong, let's restore raw mode } fn io( diff --git a/src/run/mod.rs b/src/run/mod.rs index 885261b..7e575ce 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -95,12 +95,12 @@ pub struct Executor { expand_commands: bool, } -struct SpawnedPipeline { +pub struct SpawnedPipeline { executors: Vec, cancel: Vec, } -enum SpawnedCmd { +pub enum SpawnedCmd { Builtin(ThreadWaiter>), Fun(ThreadWaiter>), Child(ChildWaiter), @@ -135,7 +135,7 @@ impl IsSuccessful for Result<(), ExecError> { } impl SpawnedCmd { - fn join(self) -> Result<(), ExecError> { + pub fn join(self) -> Result<(), ExecError> { match self { SpawnedCmd::Builtin(handle) => { handle.into_inner().join().map_err(|_| ExecError::Panic)?? @@ -171,7 +171,7 @@ impl SpawnedCmd { } /// returns whether the spawned command is already joined - fn join_timeout(&mut self, timeout_ms: u16) -> bool { + pub fn join_timeout(&mut self, timeout_ms: u16) -> bool { match self { SpawnedCmd::Builtin(tw) => tw.try_join(timeout_ms), SpawnedCmd::Fun(tw) => tw.try_join(timeout_ms), @@ -185,7 +185,7 @@ impl SpawnedCmd { } } - fn cancel(&mut self) { + pub fn cancel(&mut self) { match self { SpawnedCmd::Pipeline(pipes) => { for c in pipes.cancel.iter_mut() { @@ -226,6 +226,14 @@ impl Executor { } } + pub fn new(se: Arc>) -> Self { + Self { + se, + args: None, + expand_commands: true, + } + } + fn spawn_cmd( &mut self, cmd: CommandKind, @@ -436,6 +444,15 @@ impl Executor { }); SpawnedCmd::Fun(handle) } + + pub fn execute_script( + &mut self, + s: parse::Script, + stdin: InputReader, + stdout: OutputWriter, + ) -> SpawnedCmd { + self.execute_block(parse::Block { commands: s.stmts }, stdin, stdout) + } } impl parse::Expander for Executor { @@ -559,11 +576,7 @@ impl Aliases { } fn exec(se: Arc>, ast: Ast) -> Result<(), ExecError> { - let mut exec = Executor { - se: se.clone(), - args: None, - expand_commands: true, - }; + let mut exec = Executor::new(se.clone()); let ast = ast.expand(&mut exec)?; let (stdin, c1) = InputReader::new(Input::Stdin); let (stdout, c2) = OutputWriter::new(Output::Stdout); @@ -587,9 +600,9 @@ pub fn run_quiet( } pub fn run(se: Arc>, parsed: Ast) { - se.lock().unwrap().raw.disable(); + se.lock().unwrap().raw_disable(); let result = exec(se.clone(), parsed); - se.lock().unwrap().raw.enable(); + se.lock().unwrap().raw_enable(); if se.lock().unwrap().loud { let status_string = match result { diff --git a/test-cases/fun0/script.sh b/test-cases/fun0/script.sh new file mode 100644 index 0000000..3e6ef96 --- /dev/null +++ b/test-cases/fun0/script.sh @@ -0,0 +1,5 @@ +fun foo { + echo foo +} + +foo | sed 's/f/z/g' diff --git a/test-cases/fun0/stdout.txt b/test-cases/fun0/stdout.txt new file mode 100644 index 0000000..d15dc45 --- /dev/null +++ b/test-cases/fun0/stdout.txt @@ -0,0 +1 @@ +zoo diff --git a/test-cases/generate.sh b/test-cases/generate.sh new file mode 100644 index 0000000..d39abe7 --- /dev/null +++ b/test-cases/generate.sh @@ -0,0 +1,21 @@ +#! /bin/bash + +cd "$(dirname "$0")" + +( +cat <<'EOF' +//! This file is generated by running `bash test-cases/generate.sh` - do not manually adjust. +mod common; + +EOF +for d in */; do +dir="$(printf '%s\n' "$d" | sed 's|/||')" +cat < ../tests/scripts.rs diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..210b92a --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,19 @@ +#[cfg(test)] +pub fn test_case(_name: &str, script: &[u8], expected_output: &[u8]) { + use pish::parse::Parse; + use pish::rw::*; + use std::io::Read; + use std::sync::{Arc, Mutex}; + + let session = Arc::new(Mutex::new(pish::Session::new_noninteractive())); + let script = pish::parse::Script::parse_from_bytes(script).unwrap(); + let (stdin, _c1) = InputReader::new(Input::Null); + let (mut pr, pw) = std::io::pipe().unwrap(); + let (stdout, _c2) = OutputWriter::new(Output::Pipe(pw)); + let mut exec = pish::run::Executor::new(session.clone()); + let mut spawned = exec.execute_script(script, stdin, stdout); + let _res = spawned.join_timeout(1000); + let mut actual_output = Vec::new(); + pr.read_to_end(&mut actual_output).unwrap(); + assert_eq!(expected_output, actual_output); +} diff --git a/tests/scripts.rs b/tests/scripts.rs new file mode 100644 index 0000000..9b82ed1 --- /dev/null +++ b/tests/scripts.rs @@ -0,0 +1,8 @@ +//! This file is generated by running `bash test-cases/generate.sh` - do not manually adjust. +mod common; + +#[test] +fn fun0() { + common::test_case("fun0", include_bytes!("../test-cases/fun0/script.sh"), include_bytes!("../test-cases/fun0/stdout.txt")); +} + -- cgit v1.2.3